From 91e5b2e5985e27a2d75f865a95cae187ab33e943 Mon Sep 17 00:00:00 2001 From: hyochan Date: Sat, 25 Apr 2026 14:15:59 +0900 Subject: [PATCH 01/41] docs: split apis & types into flat per-symbol pages with platform folders Restructures the docs site so every API function and every type has its own dedicated page with field tables, deprecated markers, and References sections that link back to the canonical OpenIAP GraphQL schema. - Flatten /docs/apis and /docs/types: one page per symbol, platform- specific symbols moved under apis/ios, apis/android, types/ios, types/android - Move Validation, Refund, Debugging out of APIs into Features, drop the IAPKit explainer + provider-based verifyPurchase walkthrough - Add nested MenuDropdown groups so the sidebar surfaces every symbol while keeping iOS/Android subgroups collapsible - Audit every field cell against the GraphQL schema: backfill missing fields (subscriptionOffers, discountOffers, RenewalInfoIOS.json Representation, ActiveSubscription deferred/parent/receipt fields, BillingProgram availability, ExternalPurchase notice token, etc.) and link non-primitive type cells to their reference page - Mark deprecated fields and types with strikethrough + "Deprecated." hint and rename surfaced labels to canonical schema names (Product Subscription* and ProductAndroidOneTimePurchaseOfferDetail) - Sidebar UX: sticky positioning fix via overflow-x: clip, horizontal scroll for long identifiers, right padding so labels do not collide with the scrollbar, and remove the Types definition-zip downloader - Update routes, searchData, navigation, and every cross-page reference to the new URLs; legacy /docs/apis/#anchor links redirect through the new flat paths Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/docs/src/components/MenuDropdown.tsx | 145 +- packages/docs/src/lib/searchData.ts | 199 ++- packages/docs/src/pages/docs/apis/android.tsx | 630 -------- .../android/acknowledge-purchase-android.tsx | 49 + ...ternative-billing-availability-android.tsx | 40 + .../apis/android/consume-purchase-android.tsx | 49 + ...eate-alternative-billing-token-android.tsx | 40 + ...ling-program-reporting-details-android.tsx | 43 + .../enable-billing-program-android.tsx | 40 + .../is-billing-program-available-android.tsx | 42 + .../android/launch-external-link-android.tsx | 49 + ...how-alternative-billing-dialog-android.tsx | 40 + .../docs/src/pages/docs/apis/connection.tsx | 223 --- .../docs/apis/deep-link-to-subscriptions.tsx | 108 ++ .../src/pages/docs/apis/end-connection.tsx | 85 + .../src/pages/docs/apis/fetch-products.tsx | 109 ++ .../pages/docs/apis/finish-transaction.tsx | 92 ++ .../docs/apis/get-active-subscriptions.tsx | 88 + .../docs/apis/get-available-purchases.tsx | 93 ++ .../src/pages/docs/apis/get-storefront.tsx | 79 + .../docs/apis/has-active-subscriptions.tsx | 74 + packages/docs/src/pages/docs/apis/index.tsx | 643 +++++--- .../src/pages/docs/apis/init-connection.tsx | 122 ++ packages/docs/src/pages/docs/apis/ios.tsx | 346 ---- .../apis/ios/begin-refund-request-ios.tsx | 43 + ...n-present-external-purchase-notice-ios.tsx | 37 + .../docs/apis/ios/clear-transaction-ios.tsx | 35 + .../docs/apis/ios/current-entitlement-ios.tsx | 35 + .../apis/ios/get-all-transactions-ios.tsx | 39 + .../docs/apis/ios/get-app-transaction-ios.tsx | 46 + .../apis/ios/get-pending-transactions-ios.tsx | 35 + .../apis/ios/get-promoted-product-ios.tsx | 35 + .../docs/apis/ios/get-receipt-data-ios.tsx | 35 + .../docs/apis/ios/get-storefront-ios.tsx | 44 + .../docs/apis/ios/get-transaction-jws-ios.tsx | 35 + .../ios/is-eligible-for-intro-offer-ios.tsx | 38 + .../apis/ios/is-transaction-verified-ios.tsx | 35 + .../docs/apis/ios/latest-transaction-ios.tsx | 35 + .../ios/present-code-redemption-sheet-ios.tsx | 43 + .../present-external-purchase-link-ios.tsx | 40 + ...ent-external-purchase-notice-sheet-ios.tsx | 40 + ...quest-purchase-on-promoted-product-ios.tsx | 48 + .../ios/show-manage-subscriptions-ios.tsx | 39 + .../docs/apis/ios/subscription-status-ios.tsx | 35 + .../docs/src/pages/docs/apis/ios/sync-ios.tsx | 34 + .../docs/apis/ios/validate-receipt-ios.tsx | 45 + .../docs/src/pages/docs/apis/products.tsx | 272 ---- .../docs/src/pages/docs/apis/purchase.tsx | 490 ------ .../src/pages/docs/apis/request-purchase.tsx | 142 ++ .../src/pages/docs/apis/restore-purchases.tsx | 83 + .../docs/src/pages/docs/apis/subscription.tsx | 372 ----- packages/docs/src/pages/docs/errors.tsx | 3 +- packages/docs/src/pages/docs/events.tsx | 35 +- .../docs/{apis => features}/debugging.tsx | 42 +- .../docs/src/pages/docs/features/discount.tsx | 7 +- .../pages/docs/features/external-purchase.tsx | 28 +- .../docs/features/offer-code-redemption.tsx | 2 +- .../docs/src/pages/docs/features/purchase.tsx | 15 +- .../docs/src/pages/docs/features/refund.tsx | 470 ++++++ .../features/subscription-billing-issue.tsx | 23 +- .../docs/features/subscription/index.tsx | 67 +- .../subscription/upgrade-downgrade.tsx | 12 +- .../docs/{apis => features}/validation.tsx | 106 +- packages/docs/src/pages/docs/index.tsx | 698 +++++++- packages/docs/src/pages/docs/ios-setup.tsx | 8 +- .../docs/src/pages/docs/lifecycle/index.tsx | 16 +- .../src/pages/docs/lifecycle/subscription.tsx | 56 +- .../src/pages/docs/setup/react-native.tsx | 16 +- .../pages/docs/types/active-subscription.tsx | 311 ++++ .../docs/types/alternative-billing-types.tsx | 663 ++++++++ .../docs/src/pages/docs/types/alternative.tsx | 1415 ----------------- .../docs/src/pages/docs/types/android.tsx | 688 -------- ...one-time-purchase-offer-detail-android.tsx | 240 +++ .../types/android/pricing-phase-android.tsx | 139 ++ .../android/subscription-offer-android.tsx | 114 ++ .../src/pages/docs/types/billing-programs.tsx | 512 ++++++ .../src/pages/docs/types/discount-offer.tsx | 404 +++++ .../docs/types/external-purchase-link.tsx | 499 ++++++ packages/docs/src/pages/docs/types/index.tsx | 588 +++---- packages/docs/src/pages/docs/types/ios.tsx | 685 -------- .../docs/types/ios/app-transaction-ios.tsx | 334 ++++ .../src/pages/docs/types/ios/discount-ios.tsx | 91 ++ .../docs/types/ios/discount-offer-ios.tsx | 76 + .../pages/docs/types/ios/payment-mode-ios.tsx | 55 + .../types/ios/subscription-period-ios.tsx | 44 + .../types/ios/subscription-status-ios.tsx | 160 ++ packages/docs/src/pages/docs/types/offer.tsx | 1239 --------------- .../src/pages/docs/types/product-request.tsx | 158 ++ .../docs/src/pages/docs/types/product.tsx | 380 +---- .../docs/src/pages/docs/types/purchase.tsx | 327 +--- ...request.tsx => request-purchase-props.tsx} | 345 ++-- .../docs/src/pages/docs/types/storefront.tsx | 63 + .../pages/docs/types/subscription-offer.tsx | 539 +++++++ .../pages/docs/types/subscription-product.tsx | 340 ++++ .../src/pages/docs/types/verification.tsx | 800 ---------- .../verify-purchase-with-provider-props.tsx | 176 ++ .../verify-purchase-with-provider-result.tsx | 419 +++++ .../src/pages/docs/types/verify-purchase.tsx | 293 ++++ .../src/pages/docs/updates/announcements.tsx | 2 +- .../docs/src/pages/docs/updates/releases.tsx | 199 ++- packages/docs/src/pages/home.tsx | 27 +- packages/docs/src/pages/introduction.tsx | 32 +- packages/docs/src/styles/base.css | 6 +- packages/docs/src/styles/documentation.css | 65 +- packages/docs/src/styles/responsive.css | 2 +- 105 files changed, 10703 insertions(+), 8969 deletions(-) delete mode 100644 packages/docs/src/pages/docs/apis/android.tsx create mode 100644 packages/docs/src/pages/docs/apis/android/acknowledge-purchase-android.tsx create mode 100644 packages/docs/src/pages/docs/apis/android/check-alternative-billing-availability-android.tsx create mode 100644 packages/docs/src/pages/docs/apis/android/consume-purchase-android.tsx create mode 100644 packages/docs/src/pages/docs/apis/android/create-alternative-billing-token-android.tsx create mode 100644 packages/docs/src/pages/docs/apis/android/create-billing-program-reporting-details-android.tsx create mode 100644 packages/docs/src/pages/docs/apis/android/enable-billing-program-android.tsx create mode 100644 packages/docs/src/pages/docs/apis/android/is-billing-program-available-android.tsx create mode 100644 packages/docs/src/pages/docs/apis/android/launch-external-link-android.tsx create mode 100644 packages/docs/src/pages/docs/apis/android/show-alternative-billing-dialog-android.tsx delete mode 100644 packages/docs/src/pages/docs/apis/connection.tsx create mode 100644 packages/docs/src/pages/docs/apis/deep-link-to-subscriptions.tsx create mode 100644 packages/docs/src/pages/docs/apis/end-connection.tsx create mode 100644 packages/docs/src/pages/docs/apis/fetch-products.tsx create mode 100644 packages/docs/src/pages/docs/apis/finish-transaction.tsx create mode 100644 packages/docs/src/pages/docs/apis/get-active-subscriptions.tsx create mode 100644 packages/docs/src/pages/docs/apis/get-available-purchases.tsx create mode 100644 packages/docs/src/pages/docs/apis/get-storefront.tsx create mode 100644 packages/docs/src/pages/docs/apis/has-active-subscriptions.tsx create mode 100644 packages/docs/src/pages/docs/apis/init-connection.tsx delete mode 100644 packages/docs/src/pages/docs/apis/ios.tsx create mode 100644 packages/docs/src/pages/docs/apis/ios/begin-refund-request-ios.tsx create mode 100644 packages/docs/src/pages/docs/apis/ios/can-present-external-purchase-notice-ios.tsx create mode 100644 packages/docs/src/pages/docs/apis/ios/clear-transaction-ios.tsx create mode 100644 packages/docs/src/pages/docs/apis/ios/current-entitlement-ios.tsx create mode 100644 packages/docs/src/pages/docs/apis/ios/get-all-transactions-ios.tsx create mode 100644 packages/docs/src/pages/docs/apis/ios/get-app-transaction-ios.tsx create mode 100644 packages/docs/src/pages/docs/apis/ios/get-pending-transactions-ios.tsx create mode 100644 packages/docs/src/pages/docs/apis/ios/get-promoted-product-ios.tsx create mode 100644 packages/docs/src/pages/docs/apis/ios/get-receipt-data-ios.tsx create mode 100644 packages/docs/src/pages/docs/apis/ios/get-storefront-ios.tsx create mode 100644 packages/docs/src/pages/docs/apis/ios/get-transaction-jws-ios.tsx create mode 100644 packages/docs/src/pages/docs/apis/ios/is-eligible-for-intro-offer-ios.tsx create mode 100644 packages/docs/src/pages/docs/apis/ios/is-transaction-verified-ios.tsx create mode 100644 packages/docs/src/pages/docs/apis/ios/latest-transaction-ios.tsx create mode 100644 packages/docs/src/pages/docs/apis/ios/present-code-redemption-sheet-ios.tsx create mode 100644 packages/docs/src/pages/docs/apis/ios/present-external-purchase-link-ios.tsx create mode 100644 packages/docs/src/pages/docs/apis/ios/present-external-purchase-notice-sheet-ios.tsx create mode 100644 packages/docs/src/pages/docs/apis/ios/request-purchase-on-promoted-product-ios.tsx create mode 100644 packages/docs/src/pages/docs/apis/ios/show-manage-subscriptions-ios.tsx create mode 100644 packages/docs/src/pages/docs/apis/ios/subscription-status-ios.tsx create mode 100644 packages/docs/src/pages/docs/apis/ios/sync-ios.tsx create mode 100644 packages/docs/src/pages/docs/apis/ios/validate-receipt-ios.tsx delete mode 100644 packages/docs/src/pages/docs/apis/products.tsx delete mode 100644 packages/docs/src/pages/docs/apis/purchase.tsx create mode 100644 packages/docs/src/pages/docs/apis/request-purchase.tsx create mode 100644 packages/docs/src/pages/docs/apis/restore-purchases.tsx delete mode 100644 packages/docs/src/pages/docs/apis/subscription.tsx rename packages/docs/src/pages/docs/{apis => features}/debugging.tsx (87%) create mode 100644 packages/docs/src/pages/docs/features/refund.tsx rename packages/docs/src/pages/docs/{apis => features}/validation.tsx (79%) create mode 100644 packages/docs/src/pages/docs/types/active-subscription.tsx create mode 100644 packages/docs/src/pages/docs/types/alternative-billing-types.tsx delete mode 100644 packages/docs/src/pages/docs/types/alternative.tsx delete mode 100644 packages/docs/src/pages/docs/types/android.tsx create mode 100644 packages/docs/src/pages/docs/types/android/one-time-purchase-offer-detail-android.tsx create mode 100644 packages/docs/src/pages/docs/types/android/pricing-phase-android.tsx create mode 100644 packages/docs/src/pages/docs/types/android/subscription-offer-android.tsx create mode 100644 packages/docs/src/pages/docs/types/billing-programs.tsx create mode 100644 packages/docs/src/pages/docs/types/discount-offer.tsx create mode 100644 packages/docs/src/pages/docs/types/external-purchase-link.tsx delete mode 100644 packages/docs/src/pages/docs/types/ios.tsx create mode 100644 packages/docs/src/pages/docs/types/ios/app-transaction-ios.tsx create mode 100644 packages/docs/src/pages/docs/types/ios/discount-ios.tsx create mode 100644 packages/docs/src/pages/docs/types/ios/discount-offer-ios.tsx create mode 100644 packages/docs/src/pages/docs/types/ios/payment-mode-ios.tsx create mode 100644 packages/docs/src/pages/docs/types/ios/subscription-period-ios.tsx create mode 100644 packages/docs/src/pages/docs/types/ios/subscription-status-ios.tsx delete mode 100644 packages/docs/src/pages/docs/types/offer.tsx create mode 100644 packages/docs/src/pages/docs/types/product-request.tsx rename packages/docs/src/pages/docs/types/{request.tsx => request-purchase-props.tsx} (68%) create mode 100644 packages/docs/src/pages/docs/types/storefront.tsx create mode 100644 packages/docs/src/pages/docs/types/subscription-offer.tsx create mode 100644 packages/docs/src/pages/docs/types/subscription-product.tsx delete mode 100644 packages/docs/src/pages/docs/types/verification.tsx create mode 100644 packages/docs/src/pages/docs/types/verify-purchase-with-provider-props.tsx create mode 100644 packages/docs/src/pages/docs/types/verify-purchase-with-provider-result.tsx create mode 100644 packages/docs/src/pages/docs/types/verify-purchase.tsx diff --git a/packages/docs/src/components/MenuDropdown.tsx b/packages/docs/src/components/MenuDropdown.tsx index 9403c4eb..5036299d 100644 --- a/packages/docs/src/components/MenuDropdown.tsx +++ b/packages/docs/src/components/MenuDropdown.tsx @@ -1,18 +1,110 @@ import { useState, useRef, 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 (entry as MenuGroup).items !== undefined; +} + interface MenuDropdownProps { title: string; titleTo: string; - items: MenuItem[]; + items: MenuEntry[]; + onItemClick?: () => void; +} + +interface SubMenuProps { + group: MenuGroup; onItemClick?: () => void; } +function SubMenu({ group, onItemClick }: SubMenuProps) { + const [isExpanded, setIsExpanded] = useState(false); + const contentRef = useRef(null); + const [height, setHeight] = useState(0); + const location = useLocation(); + + const isAnyChildActive = group.items.some( + (item) => location.pathname === item.to + ); + + useEffect(() => { + if (contentRef.current) { + setHeight(isExpanded ? contentRef.current.scrollHeight : 0); + } + }, [isExpanded, group.items.length]); + + useEffect(() => { + if (isAnyChildActive) { + setIsExpanded(true); + } + }, [isAnyChildActive]); + + const toggleExpanded = () => { + setIsExpanded(!isExpanded); + }; + + return ( +
  • +
    + + +
    +
    +
      + {group.items.map((item) => ( +
    • + + `menu-dropdown-item ${isActive ? 'active' : ''}` + } + onClick={onItemClick} + > + + {item.label} + +
    • + ))} +
    +
    +
  • + ); +} + export function MenuDropdown({ title, titleTo, @@ -27,24 +119,26 @@ export function MenuDropdown({ const navigate = useNavigate(); 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]); + }, [isExpanded, items.length]); useEffect(() => { - // Auto-expand when navigating to the title page or any child if (isGroupActive) { setIsExpanded(true); } }, [isGroupActive]); const handleTitleClick = () => { - // Always navigate and expand — never collapse from title click setIsExpanded(true); navigate(titleTo); onItemClick?.(); @@ -86,24 +180,33 @@ export function MenuDropdown({ ref={contentRef} className="menu-dropdown-content" style={{ - maxHeight: `${height}px`, + maxHeight: isExpanded ? 'none' : `${height}px`, + overflow: isExpanded ? 'visible' : 'hidden', }} >
      - {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 ecabe856..08fd2080 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,7 @@ 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', }, // iOS Specific @@ -145,7 +145,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 +154,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 +163,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 +172,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 +181,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 +190,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 +199,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 +208,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 +217,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 +226,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 +235,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 +244,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 +253,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 +262,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 +271,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', @@ -280,7 +280,7 @@ export const apiData: ApiItem[] = [ description: 'Fetch the current app transaction (iOS 16+)', parameters: '', returns: 'AppTransaction', - path: '/docs/apis/ios#get-app-transaction-ios', + path: '/docs/apis/ios/get-app-transaction-ios', }, { id: 'external-purchase-ios', @@ -289,7 +289,7 @@ 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', }, // Android Specific @@ -300,7 +300,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 +309,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 +319,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 +329,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 +339,10 @@ 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', }, - // Debugging & Logging + // Debugging & Logging (moved to Features) { id: 'debugging-logging', title: 'Debugging & Logging', @@ -350,7 +350,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 +359,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 +368,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 +380,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 +468,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 +502,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 +510,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', @@ -496,7 +559,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 +574,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 +582,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 +597,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 +607,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 +615,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', }, // Alternative Billing Types (from types/alternative.tsx) @@ -568,7 +631,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 +639,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 +647,7 @@ 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', }, // Platform-Specific Request Types @@ -594,7 +657,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 +665,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 +673,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 +681,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 +703,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 @@ -685,7 +748,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 +756,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 +766,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 +774,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 56295dfb..00000000 --- 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 00000000..07aecb97 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/android/acknowledge-purchase-android.tsx @@ -0,0 +1,49 @@ +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. +

    + +

    Signature

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

    + 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 00000000..96138d87 --- /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 00000000..5fcc89f3 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/android/consume-purchase-android.tsx @@ -0,0 +1,49 @@ +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. +

    + +

    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 00000000..1160020a --- /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 00000000..7cf943d8 --- /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 00000000..6fc70209 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/android/enable-billing-program-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 EnableBillingProgramAndroid() { + useScrollToHash(); + + return ( +
    + +

    + Android{' '} + enableBillingProgramAndroid +

    +

    + Step 0 of Billing Programs API. Enable a billing program before + initConnection() (Billing Library 8.2.0+). +

    + +

    Signature

    + + {{ + kotlin: ( + {`// Call BEFORE initConnection() +// program: BillingProgramAndroid.ExternalOffer or BillingProgramAndroid.ExternalContentLink +fun enableBillingProgram(program: BillingProgramAndroid)`} + ), + }} + +
    + ); +} + +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 00000000..caee40b3 --- /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 00000000..135c448b --- /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 00000000..143c5ba8 --- /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 17cc5e20..00000000 --- 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 00000000..fdcfd440 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/deep-link-to-subscriptions.tsx @@ -0,0 +1,108 @@ +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)`} + ), + 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'; + +await deepLinkToSubscriptions({ + skuAndroid: 'com.app.premium', + packageNameAndroid: 'com.yourcompany.app', +});`} + ), + swift: ( + {`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: ( + {`var options = DeepLinkOptions.new() +options.sku_android = "com.app.premium" +options.package_name_android = "com.yourcompany.app" +await iap.deep_link_to_subscriptions(options)`} + ), + }} + + +

    + See:{' '} + + showManageSubscriptionsIOS + +

    +
    + ); +} + +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 00000000..91c7ff92 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/end-connection.tsx @@ -0,0 +1,85 @@ +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: ( + {`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 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 00000000..196a4120 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/fetch-products.tsx @@ -0,0 +1,109 @@ +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 FetchProducts() { + useScrollToHash(); + + return ( +
    + +

    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', +});`} + ), + 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: ( + {`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,{' '} + SubscriptionProduct +

    +
    + ); +} + +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 00000000..c290ebfc --- /dev/null +++ b/packages/docs/src/pages/docs/apis/finish-transaction.tsx @@ -0,0 +1,92 @@ +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(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`} + ), + }} + + +

    Example

    + + {{ + typescript: ( + {`import { finishTransaction, purchaseUpdatedListener } from 'expo-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); +});`} + ), + 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: ( + {`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 00000000..8ec19f1f --- /dev/null +++ b/packages/docs/src/pages/docs/apis/get-active-subscriptions.tsx @@ -0,0 +1,88 @@ +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: ( + {`import { getActiveSubscriptions } from 'expo-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'); + } +}`} + ), + 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 00000000..8e011cd0 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/get-available-purchases.tsx @@ -0,0 +1,93 @@ +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: ( + {`import { getAvailablePurchases, finishTransaction } from 'expo-iap'; + +const purchases = await getAvailablePurchases(); + +for (const purchase of purchases) { + 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: ( + {`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 00000000..e249a159 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/get-storefront.tsx @@ -0,0 +1,79 @@ +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: ( + {`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()`} + ), + }} + + +

    + 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 00000000..27e8429e --- /dev/null +++ b/packages/docs/src/pages/docs/apis/has-active-subscriptions.tsx @@ -0,0 +1,74 @@ +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: ( + {`import { hasActiveSubscriptions } from 'expo-iap'; + +const isPremium = await hasActiveSubscriptions(); +const hasProPlan = await hasActiveSubscriptions(['pro_monthly', 'pro_yearly']);`} + ), + 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 001170a5..7dce7fc3 100644 --- a/packages/docs/src/pages/docs/apis/index.tsx +++ b/packages/docs/src/pages/docs/apis/index.tsx @@ -1,100 +1,9 @@ -import { useEffect } from 'react'; -import { Link, useLocation, useNavigate } from 'react-router-dom'; +import { Link } 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', - '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', - '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', - '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', - 'present-code-redemption-sheet-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', - '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 - 'acknowledge-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', - '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 - 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', -}; - function APIsIndex() { - 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 }); - } - }, [location.hash, navigate]); - useScrollToHash(); return ( @@ -107,152 +16,441 @@ 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.
    +
    -

    Core APIs

    -

    Essential APIs used in every IAP implementation.

    -
    - - - - -
    + + Products + + + + + + + + + + + + + + + + + + +
    FunctionDescription
    + + fetchProducts + + Fetch products or subscriptions from the store.
    + + getAvailablePurchases + + List active purchases for the current user.
    -

    Advanced APIs

    -

    Additional APIs for validation and debugging.

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

    Platform-Specific APIs

    -

    - APIs available only on specific platforms. Use these for - platform-specific features. -

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

    API Naming Convention

    -

    OpenIAP follows a consistent naming pattern:

    + + iOS Functions + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FunctionDescription
    + + syncIOS + + Force sync transactions with the App Store.
    + + getStorefrontIOS + + Get the iOS storefront country code.
    + + 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.
    + + 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.
    + + presentExternalPurchaseNoticeSheetIOS + + Present the external purchase notice sheet.
    + + presentExternalPurchaseLinkIOS + + Present an external purchase link (StoreKit External).
    + + validateReceiptIOS + + Validate a receipt against the App Store (legacy path).
    +
    + +
    + + 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.
    +
    + +
    + + 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.

    @@ -313,13 +511,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 00000000..36c5ed93 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/init-connection.tsx @@ -0,0 +1,122 @@ +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 + +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 + +

    +
    + ); +} + +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 2795b7b5..00000000 --- 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 00000000..fa812d6f --- /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 00000000..4f62def5 --- /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 00000000..632179e3 --- /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 00000000..7d0834a7 --- /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 00000000..b236cd20 --- /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 00000000..a1a00645 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/ios/get-app-transaction-ios.tsx @@ -0,0 +1,46 @@ +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 -> 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? +}`} + ), + }} + +
    + ); +} + +export default GetAppTransactionIOS; 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 00000000..25cc0898 --- /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 00000000..38a8070a --- /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 00000000..3c8d9646 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/ios/get-receipt-data-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 GetReceiptDataIOS() { + useScrollToHash(); + + return ( +
    + +

    + iOS{' '} + getReceiptDataIOS +

    +

    Get base64-encoded receipt data for legacy validation.

    + +

    Signature

    + + {{ + swift: ( + {`func getReceiptDataIOS() async throws -> String?`} + ), + }} + +
    + ); +} + +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 00000000..1bd3f26e --- /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 00000000..52cd0c4e --- /dev/null +++ b/packages/docs/src/pages/docs/apis/ios/get-transaction-jws-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 GetTransactionJwsIOS() { + useScrollToHash(); + + return ( +
    + +

    + iOS{' '} + getTransactionJwsIOS +

    +

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

    + +

    Signature

    + + {{ + swift: ( + {`func getTransactionJwsIOS(sku: String) async throws -> String?`} + ), + }} + +
    + ); +} + +export default GetTransactionJwsIOS; 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 00000000..0bc4e4fe --- /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 00000000..2eeee108 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/ios/is-transaction-verified-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 IsTransactionVerifiedIOS() { + useScrollToHash(); + + return ( +
    + +

    + iOS{' '} + isTransactionVerifiedIOS +

    +

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

    + +

    Signature

    + + {{ + swift: ( + {`func isTransactionVerifiedIOS(sku: String) async throws -> Bool`} + ), + }} + +
    + ); +} + +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 00000000..1e96ec05 --- /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 00000000..6feaff79 --- /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 00000000..74e4f15d --- /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 00000000..01ee6d89 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/ios/present-external-purchase-notice-sheet-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 PresentExternalPurchaseNoticeSheetIOS() { + useScrollToHash(); + + return ( +
    + +

    + iOS{' '} + presentExternalPurchaseNoticeSheetIOS +

    +

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

    + +

    Signature

    + + {{ + swift: ( + {`func presentExternalPurchaseNoticeSheetIOS() async throws -> ExternalPurchaseNoticeResultIOS + +struct ExternalPurchaseNoticeResultIOS { + let error: String? + let result: ExternalPurchaseNoticeAction +}`} + ), + }} + +
    + ); +} + +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 00000000..2886ea4f --- /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-manage-subscriptions-ios.tsx b/packages/docs/src/pages/docs/apis/ios/show-manage-subscriptions-ios.tsx new file mode 100644 index 00000000..eda7db09 --- /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 00000000..ef9e13b5 --- /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 -> [SubscriptionStatus]`} + ), + }} + +
    + ); +} + +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 00000000..ff9a0770 --- /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 00000000..8f7014d7 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/ios/validate-receipt-ios.tsx @@ -0,0 +1,45 @@ +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: PurchaseVerificationProps) async throws -> PurchaseVerificationResult`} + ), + }} + +
    + ); +} + +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 8cd0d90b..00000000 --- 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 9c484d51..00000000 --- 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 00000000..4583628c --- /dev/null +++ b/packages/docs/src/pages/docs/apis/request-purchase.tsx @@ -0,0 +1,142 @@ +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. +

    + +

    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'; + +// One-time product +await requestPurchase({ + request: { + apple: { sku: 'com.app.premium' }, + google: { skus: ['com.app.premium'] }, + }, + type: 'inapp', +}); + +// Subscription +await requestPurchase({ + request: { + apple: { sku: 'com.app.monthly' }, + google: { + skus: ['com.app.monthly'], + subscriptionOffers: [{ sku: 'com.app.monthly', offerToken: 'offer-token' }], + }, + }, + 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: ( + {`kmpIAP.requestPurchase( + RequestPurchaseProps( + request = RequestPurchasePropsByPlatforms( + google = RequestPurchaseAndroidProps(skus = listOf("com.app.premium")) + ), + type = ProductQueryType.InApp + ) +)`} + ), + dart: ( + {`await FlutterInappPurchase.instance.requestPurchase('com.app.premium');`} + ), + gdscript: ( + {`var props = RequestPurchaseProps.new() +props.request = RequestPurchasePropsByPlatforms.new() +props.request.apple = RequestPurchaseIosProps.new() +props.request.apple.sku = "com.app.premium" +props.type = ProductQueryType.IN_APP +await iap.request_purchase(props)`} + ), + }} + + +
    +

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

    +
    + +

    + See:{' '} + + RequestPurchaseProps + +

    +
    + ); +} + +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 00000000..875cd0e0 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/restore-purchases.tsx @@ -0,0 +1,83 @@ +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: ( + {`import { restorePurchases, getAvailablePurchases } from 'expo-iap'; + +const handleRestore = async () => { + await restorePurchases(); + const purchases = await getAvailablePurchases(); + + for (const purchase of purchases) { + await grantProduct(purchase.productId); + } +};`} + ), + swift: ( + {`try await OpenIapModule.shared.restorePurchases()`} + ), + kotlin: ( + {`openIapStore.restorePurchases()`} + ), + kmp: ( + {`kmpIAP.restorePurchases()`} + ), + dart: ( + {`await FlutterInappPurchase.instance.restorePurchases();`} + ), + gdscript: ( + {`await iap.restore_purchases()`} + ), + }} + +
    + ); +} + +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 49efc638..00000000 --- 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/errors.tsx b/packages/docs/src/pages/docs/errors.tsx index 0ec39532..17dac54b 100644 --- a/packages/docs/src/pages/docs/errors.tsx +++ b/packages/docs/src/pages/docs/errors.tsx @@ -22,8 +22,7 @@ function Errors() {

    Error Structure

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

    {{ diff --git a/packages/docs/src/pages/docs/events.tsx b/packages/docs/src/pages/docs/events.tsx index 8c796a04..bc21e4e6 100644 --- a/packages/docs/src/pages/docs/events.tsx +++ b/packages/docs/src/pages/docs/events.tsx @@ -263,23 +263,21 @@ func _exit_tree():

    Event Payload

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

    Purchase Update Flow

    1. - Receive Purchase object via + Receive Purchase object via listener
    2. Validate receipt with backend service
    3. Deliver purchased content to user
    4. Finish transaction with{' '} - - finishTransaction - {' '} + finishTransaction{' '} (handles acknowledgment on both platforms)
    5. Update application state
    6. @@ -551,11 +549,14 @@ val subscriptionBillingIssueListener: Flow`} }}

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

      @@ -717,20 +718,17 @@ subscription.cancel();`}

    7. Receive product SKU via listener
    8. Fetch product details using{' '} - fetchProducts + fetchProducts
    9. Display product information to user
    10. - Call{' '} - - requestPurchase - {' '} + Call requestPurchase{' '} with the received SKU if user confirms

    Also check{' '} - + getPromotedProductIOS {' '} on app launch for pending promoted products. @@ -738,7 +736,10 @@ subscription.cancel();`}

    Note: In StoreKit 2, promoted products can be - purchased directly via the standard requestPurchase(){' '} + purchased directly via the standard{' '} + + requestPurchase() + {' '} flow. The deprecated{' '} requestPurchaseOnPromotedProductIOS() diff --git a/packages/docs/src/pages/docs/apis/debugging.tsx b/packages/docs/src/pages/docs/features/debugging.tsx similarity index 87% rename from packages/docs/src/pages/docs/apis/debugging.tsx rename to packages/docs/src/pages/docs/features/debugging.tsx index 266da691..25747afb 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 @@ -278,4 +286,4 @@ console.log('Actual basePlanId:', basePlanId);`} ); } -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 2bdd0fd5..996f1b11 100644 --- a/packages/docs/src/pages/docs/features/discount.tsx +++ b/packages/docs/src/pages/docs/features/discount.tsx @@ -33,10 +33,9 @@ function Discount() {

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

    diff --git a/packages/docs/src/pages/docs/features/external-purchase.tsx b/packages/docs/src/pages/docs/features/external-purchase.tsx index 82906034..8133f072 100644 --- a/packages/docs/src/pages/docs/features/external-purchase.tsx +++ b/packages/docs/src/pages/docs/features/external-purchase.tsx @@ -1703,7 +1703,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 +2434,9 @@ func _ready_user_choice() -> void: 1 - canPresentExternalPurchaseNoticeIOS() + + canPresentExternalPurchaseNoticeIOS() + Check if device supports external purchase notice sheet @@ -2441,7 +2445,9 @@ func _ready_user_choice() -> void: 2 - presentExternalPurchaseNoticeSheetIOS() + + presentExternalPurchaseNoticeSheetIOS() + Show Apple's notice sheet informing user about external @@ -2791,25 +2797,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
    • 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 65e3a402..24140124 100644 --- a/packages/docs/src/pages/docs/features/offer-code-redemption.tsx +++ b/packages/docs/src/pages/docs/features/offer-code-redemption.tsx @@ -567,7 +567,7 @@ func redeem_with_code(code: String) -> void:
      • - + presentCodeRedemptionSheetIOS API Reference
      • diff --git a/packages/docs/src/pages/docs/features/purchase.tsx b/packages/docs/src/pages/docs/features/purchase.tsx index d7aa8801..6181f3b8 100644 --- a/packages/docs/src/pages/docs/features/purchase.tsx +++ b/packages/docs/src/pages/docs/features/purchase.tsx @@ -356,8 +356,7 @@ func _exit_tree() -> void: purchase results — instead, listen for events through{' '} purchaseUpdatedListener or{' '} purchaseErrorListener. See{' '} - API Terminology{' '} - for details. + API Terminology for details.

    @@ -920,7 +919,7 @@ Future verifyWithIapkit(ProductPurchase purchase) async { ℹ️ Endpoint: Requests are sent to{' '} https://kit.openiap.dev/v1/purchase/verify with{' '} Authorization: Bearer <apiKey>. See the{' '} - + PurchaseVerificationProvider {' '} type reference for the full response shape. @@ -1672,7 +1671,11 @@ func _exit_tree() -> void: Purchase replays on launch Transaction not finished - Call finishTransaction() after verification + Call{' '} + + finishTransaction() + {' '} + after verification @@ -1685,7 +1688,9 @@ func _exit_tree() -> void: Not consumed Pass isConsumable: true to{' '} - finishTransaction() + + finishTransaction() + diff --git a/packages/docs/src/pages/docs/features/refund.tsx b/packages/docs/src/pages/docs/features/refund.tsx new file mode 100644 index 00000000..19882d94 --- /dev/null +++ b/packages/docs/src/pages/docs/features/refund.tsx @@ -0,0 +1,470 @@ +import { Link } from 'react-router-dom'; +import AnchorLink from '../../../components/AnchorLink'; +import CodeBlock from '../../../components/CodeBlock'; +import LanguageTabs from '../../../components/LanguageTabs'; +import PlatformTabs from '../../../components/PlatformTabs'; +import SEO from '../../../components/SEO'; +import TLDRBox from '../../../components/TLDRBox'; +import { useScrollToHash } from '../../../hooks/useScrollToHash'; + +function Refund() { + useScrollToHash(); + + return ( +
    + +

    Refund

    +

    + Handle refunds initiated by users or store-side actions. iOS supports + in-app refund requests via StoreKit 2, while Android refunds are + store-driven and require server-side detection. +

    + + +
      +
    • + iOS 15+: Use{' '} + + beginRefundRequestIOS + {' '} + to present an in-app refund sheet +
    • +
    • + Android: No client-side refund API. Auto-refunded + after 3 days if not acknowledged +
    • +
    • + Server-side: Subscribe to App Store Server + Notifications V2 (Apple) and Real-time Developer Notifications + (Google) to react to refunds +
    • +
    • + Critical: Always revoke entitlements when a refund + is detected +
    • +
    +
    + +
    + + Platform Differences + + + + + + + + + + + + + + + + + + + + + +
    PlatformClient APIDetection
    iOS + beginRefundRequestIOS (iOS 15+) + + App Store Server Notifications V2 (REFUND,{' '} + REVOKE) +
    AndroidNone — store-driven + Real-time Developer Notifications (REFUND) + + purchase state polling +
    +
    + +
    + + Platform Implementation + + + + {{ + ios: ( + <> + + Overview + +

    + iOS lets users request refunds directly from inside your app + using StoreKit 2's refund sheet. The system handles the refund + flow; your app receives the result via the returned status + string. +

    +
      +
    • Requires iOS 15+
    • +
    • Not available on tvOS
    • +
    • + The actual refund decision is made by Apple — the API only + initiates the request +
    • +
    • + For detection of approved refunds, use App Store Server + Notifications V2 +
    • +
    + + + beginRefundRequestIOS + +

    + Present the refund request sheet for a previously purchased + product. +

    + + {{ + typescript: ( + {`import { beginRefundRequestIOS } from 'expo-iap'; + +const status = await beginRefundRequestIOS(purchase.productId); + +switch (status) { + case 'success': + console.log('Refund request submitted'); + break; + case 'userCancelled': + console.log('User cancelled refund flow'); + break; + default: + console.log('Refund request status:', status); +}`} + ), + swift: ( + {`import OpenIap + +let status = try await OpenIapModule.shared.beginRefundRequestIOS(sku: purchase.productId) + +switch status { +case "success": + print("Refund request submitted") +case "userCancelled": + print("User cancelled refund flow") +default: + print("Refund request status: \\(status ?? "nil")") +}`} + ), + kotlin: ( + {`// KMP iOS target +val status = openIapStore.beginRefundRequestIOS(sku = purchase.productId) + +when (status) { + "success" -> println("Refund request submitted") + "userCancelled" -> println("User cancelled refund flow") + else -> println("Refund request status: \$status") +}`} + ), + kmp: ( + {`import io.github.hyochan.kmpiap.KmpIAP + +val kmpIAP = KmpIAP() +val status = kmpIAP.beginRefundRequestIOS(sku = purchase.productId) + +when (status) { + "success" -> println("Refund request submitted") + "userCancelled" -> println("User cancelled refund flow") + else -> println("Refund request status: \$status") +}`} + ), + dart: ( + {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart'; + +final status = await FlutterInappPurchase.instance + .beginRefundRequestIOS(purchase.productId); + +switch (status) { + case 'success': + print('Refund request submitted'); + break; + case 'userCancelled': + print('User cancelled refund flow'); + break; + default: + print('Refund request status: \$status'); +}`} + ), + gdscript: ( + {`var status = await iap.begin_refund_request_ios(purchase.product_id) + +match status: + "success": + print("Refund request submitted") + "userCancelled": + print("User cancelled refund flow") + _: + print("Refund request status: ", status)`} + ), + }} + + + + App Store Server Notifications V2 + +

    + Apple sends a server-to-server notification when a refund is + approved. Subscribe to handle revocation reliably — the in-app + status is just the request, not the final outcome. +

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    notificationTypeMeaning
    + REFUND + Apple refunded the user
    + REFUND_DECLINED + Refund was declined
    + REVOKE + + Family Sharing access revoked (treat like a refund) +
    + CONSUMPTION_REQUEST + + Apple wants consumption data to decide on a refund — + respond within 12 hours +
    + {`// Server webhook handler (Node.js) +app.post('/webhooks/apple', async (req, res) => { + const { signedPayload } = req.body; + const decoded = await verifyAndDecodeJWS(signedPayload); + + if (decoded.notificationType === 'REFUND' || decoded.notificationType === 'REVOKE') { + const transactionId = decoded.data.signedTransactionInfo.transactionId; + await revokeEntitlement(transactionId); + } + + res.sendStatus(200); +});`} + + + Testing + +
      +
    • + beginRefundRequestIOS can be exercised in + sandbox but the sheet may show limited UI +
    • +
    • + Use StoreKit's "Refund" command in Xcode StoreKit + Configuration to simulate refunds +
    • +
    • + Configure the sandbox URL for App Store Server Notifications + in App Store Connect +
    • +
    + + ), + android: ( + <> + + Overview + +

    + Google Play does not expose an in-app refund API. Refunds + originate from the Play Store, support requests, or + developer-initiated actions in the Play Console. Two paths + trigger an effective refund: +

    +
      +
    • + Auto-refund after 3 days — Google + automatically refunds purchases that aren't acknowledged + within 3 days. Always call finishTransaction{' '} + immediately after server verification. +
    • +
    • + User or Play Console refund — initiated via + Play Store, Google support, or your Play Console +
    • +
    + +
    +

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

    +
    + + + Real-time Developer Notifications + +

    + Subscribe to Pub/Sub-backed Real-time Developer Notifications + (RTDN) to receive refund events. There is no client API to + query refund state directly. +

    + + + + + + + + + + + + + + + + + + + + + +
    NotificationMeaning
    + oneTimeProductNotification.REFUNDED (12) + One-time product refunded
    + subscriptionNotification.REVOKED (12) + Subscription was revoked (refund or chargeback)
    + voidedPurchaseNotification + + Voided purchase event (refund, chargeback, or other) +
    + {`// Server webhook handler (Node.js) +app.post('/webhooks/google', async (req, res) => { + const message = JSON.parse( + Buffer.from(req.body.message.data, 'base64').toString() + ); + + if (message.voidedPurchaseNotification) { + const purchaseToken = message.voidedPurchaseNotification.purchaseToken; + await revokeEntitlementByToken(purchaseToken); + } + + if (message.subscriptionNotification?.notificationType === 12) { + // SUBSCRIPTION_REVOKED + const purchaseToken = message.subscriptionNotification.purchaseToken; + await revokeEntitlementByToken(purchaseToken); + } + + res.sendStatus(200); +});`} + + + Voided Purchases API (fallback) + +

    + If you cannot run a webhook, poll the{' '} + + Google Play Voided Purchases API + + . It returns purchases voided in the last 30 days (or 60 days + with extended access). +

    + + + Testing + +
      +
    • + Use license testers and the "Refund" action in Play Console + to simulate refunds +
    • +
    • RTDN works in test tracks once Pub/Sub is wired up
    • +
    • + To test the 3-day auto-refund, intentionally skip{' '} + finishTransaction and wait +
    • +
    + + ), + }} +
    +
    + +
    + + Revoking Entitlements + +

    + When a refund is detected — through a webhook, polling, or a manual + process — revoke the user's entitlement and clean up downstream state. +

    + {`async function revokeEntitlement(transactionId: string) { + // 1. Mark entitlement inactive in your database + await db.entitlements.update({ + where: { transactionId }, + data: { status: 'refunded', revokedAt: new Date() }, + }); + + // 2. Restrict access in any cached/session state + await invalidateUserSessionsForTransaction(transactionId); + + // 3. (Optional) Notify the user out-of-band + await sendRefundConfirmationEmail(transactionId); +}`} +
    + +
    + + Related + +
      +
    • + + beginRefundRequestIOS API Reference + +
    • +
    • + + Purchase Flow & Acknowledgment + +
    • +
    • + Subscription Lifecycle +
    • +
    • + Server-side Validation +
    • +
    • + Events & Listeners +
    • +
    +
    +
    + ); +} + +export default Refund; diff --git a/packages/docs/src/pages/docs/features/subscription-billing-issue.tsx b/packages/docs/src/pages/docs/features/subscription-billing-issue.tsx index f082a505..f810c994 100644 --- a/packages/docs/src/pages/docs/features/subscription-billing-issue.tsx +++ b/packages/docs/src/pages/docs/features/subscription-billing-issue.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'; @@ -115,11 +116,14 @@ function SubscriptionBillingIssue() {

    When this event fires, route the user to the platform subscription - center via deepLinkToSubscriptions() so they can update - their payment method. Do not re-grant entitlements on - the assumption the subscription is still active — Play suspends - entitlement for these purchases, and iOS will re-emit the message - until the billing issue is resolved. + center via{' '} + + deepLinkToSubscriptions() + {' '} + so they can update their payment method. Do not{' '} + re-grant entitlements on the assumption the subscription is still + active — Play suspends entitlement for these purchases, and iOS will + re-emit the message until the billing issue is resolved.

    @@ -206,9 +210,12 @@ kmpIapInstance.subscriptionBillingIssueListener On Android, the native SDK tracks emitted purchase tokens per session so the event fires once per affected purchase even if the app polls getAvailablePurchases repeatedly. The dedupe set is - only cleared on endConnection() or app restart — a - purchase that exits suspension and re-enters within the same session - will + only cleared on{' '} + + endConnection() + {' '} + or app restart — a purchase that exits suspension and re-enters within + the same session will not re-emit until the next reconnect or process restart.

    diff --git a/packages/docs/src/pages/docs/features/subscription/index.tsx b/packages/docs/src/pages/docs/features/subscription/index.tsx index 3e187f74..5aea88e6 100644 --- a/packages/docs/src/pages/docs/features/subscription/index.tsx +++ b/packages/docs/src/pages/docs/features/subscription/index.tsx @@ -69,7 +69,11 @@ function Subscription() { Subscription offers are required when purchasing. You must pass subscriptionOffers with - offer tokens from fetchProducts(). + offer tokens from{' '} + + fetchProducts() + + . @@ -741,7 +745,10 @@ suspend fun purchaseSubscription(subscriptionId: String) { Android requires explicit specification of subscription offers when purchasing. Each offer is identified by an{' '} offerToken obtained from{' '} - fetchProducts(). + + fetchProducts() + + .

    @@ -1233,10 +1240,12 @@ suspend fun purchaseSubscription(subscriptionId: String) { {' '} returned by Google Play Billing does NOT{' '} include basePlanId. This means{' '} - getActiveSubscriptions() and purchase callbacks - cannot reliably determine which specific plan was purchased - within a subscription group. See{' '} - + + getActiveSubscriptions() + {' '} + and purchase callbacks cannot reliably determine which + specific plan was purchased within a subscription group. See{' '} + detailed limitation and solutions . @@ -1253,15 +1262,21 @@ suspend fun purchaseSubscription(subscriptionId: String) { basePlanId
  • - Purchase object (from purchase callbacks) - ❌ - Does NOT contain basePlanId + + Purchase + {' '} + object (from purchase callbacks) - ❌ Does NOT contain{' '} + basePlanId
  • When a subscription group has multiple base plans (weekly, monthly, yearly), there is no way to determine which specific - plan was purchased from the client-side Purchase{' '} + plan was purchased from the client-side{' '} + + Purchase + {' '} object alone.

    @@ -1551,7 +1566,7 @@ func _on_purchase_success(purchase: PurchaseAndroid) -> void: IAPKit {' '} which handles all the complexity for you. Use{' '} - + verifyPurchaseWithProvider {' '} to verify purchases with minimal setup. @@ -1984,7 +1999,10 @@ func purchase_with_offer(subscription_id: String, offer_type: int) -> void: Subscription remains valid until Day 30 (end of billing period)
  • - getAvailablePurchases() still returns this purchase + + getAvailablePurchases() + {' '} + still returns this purchase
  • iOS: renewalInfo.willAutoRenew = false (client-side) @@ -2036,8 +2054,10 @@ func purchase_with_offer(subscription_id: String, offer_type: int) -> void:
  • User requests refund from Apple/Google
  • Refund is approved
  • - getAvailablePurchases() may still return the purchase - temporarily + + getAvailablePurchases() + {' '} + may still return the purchase temporarily
  • @@ -2060,7 +2080,7 @@ func purchase_with_offer(subscription_id: String, offer_type: int) -> void: IAPKit to make your life easier - it provides backend verification with minimal setup via{' '} - + verifyPurchaseWithProvider . @@ -2080,9 +2100,12 @@ func purchase_with_offer(subscription_id: String, offer_type: int) -> void:

    ⚠️ Android Limitation: While{' '} - getAvailablePurchases() can retrieve purchase history, - Android clients cannot access expiry time, cancellation status, or - refund information. For complete subscription management, consider: + + getAvailablePurchases() + {' '} + can retrieve purchase history, Android clients cannot access expiry + time, cancellation status, or refund information. For complete + subscription management, consider:

    + ); +} + +export default ActiveSubscription; diff --git a/packages/docs/src/pages/docs/types/alternative-billing-types.tsx b/packages/docs/src/pages/docs/types/alternative-billing-types.tsx new file mode 100644 index 00000000..a1636aba --- /dev/null +++ b/packages/docs/src/pages/docs/types/alternative-billing-types.tsx @@ -0,0 +1,663 @@ +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 AlternativeBillingTypes() { + useScrollToHash(); + + return ( +
    + +

    Alternative Billing Types

    +
    + + Alternative Billing Types + +

    + Types for configuring alternative billing systems, primarily used for + Android. +

    + + + AlternativeBillingModeAndroid{' '} + + (Deprecated) + + +
    + Deprecated: Use{' '} + enableBillingProgramAndroid with{' '} + + BillingProgramAndroid + {' '} + instead. +
      +
    • + USER_CHOICE →{' '} + BillingProgramAndroid.USER_CHOICE_BILLING +
    • +
    • + ALTERNATIVE_ONLY →{' '} + BillingProgramAndroid.EXTERNAL_OFFER +
    • +
    +
    +

    + Enum controlling which billing system is used during{' '} + + initConnection() + + : +

    + + + + + + + + + + + + + + + + + + + + + + +
    NameSummary
    + NONE + Standard Google Play billing (default)
    + USER_CHOICE + + User can select between Google Play or alternative billing + (requires Billing Library 7.0+) +
    + ALTERNATIVE_ONLY + + Alternative billing only, no Google Play option (requires + Billing Library 6.2+) +
    + + + InitConnectionConfig + +

    + Configuration options for{' '} + + initConnection() + + : +

    + + + + + + + + + + + + + + + + + +
    NameSummary
    + enableBillingProgramAndroid + + (Recommended) Enable a specific billing program + during connection. Use USER_CHOICE_BILLING for user + choice, EXTERNAL_OFFER for alternative only, or{' '} + EXTERNAL_PAYMENTS for Japan external payments + (8.3.0+). +
    + alternativeBillingModeAndroid + + + (Deprecated) + {' '} + Use enableBillingProgramAndroid instead. +
    + + + Basic Usage (Recommended) + + + {{ + typescript: ( + {`// Initialize with user choice billing (7.0+) +await initConnection({ + enableBillingProgramAndroid: 'user-choice-billing' +}); + +// Initialize with external offer (alternative only) +await initConnection({ + enableBillingProgramAndroid: 'external-offer' +}); + +// Initialize with external payments (Japan only, 8.3.0+) +await initConnection({ + enableBillingProgramAndroid: 'external-payments' +}); + +// Standard billing (default) +await initConnection();`} + ), + swift: ( + {`// iOS uses standard StoreKit billing +// Alternative billing is Android-only +try await OpenIapModule.shared.initConnection() + +// Check connection status +let isConnected = try await OpenIapModule.shared.initConnection()`} + ), + kotlin: ( + {`// Initialize with user choice billing (7.0+) +openIapStore.initConnection( + InitConnectionConfig( + enableBillingProgramAndroid = BillingProgramAndroid.UserChoiceBilling + ) +) + +// Initialize with external offer (alternative only) +openIapStore.initConnection( + InitConnectionConfig( + enableBillingProgramAndroid = BillingProgramAndroid.ExternalOffer + ) +) + +// Initialize with external payments (Japan only, 8.3.0+) +openIapStore.initConnection( + InitConnectionConfig( + enableBillingProgramAndroid = BillingProgramAndroid.ExternalPayments + ) +) + +// Standard billing (default) +openIapStore.initConnection()`} + ), + dart: ( + {`// Initialize with user choice billing (7.0+) +await FlutterInappPurchase.instance.initConnection( + enableBillingProgramAndroid: BillingProgramAndroid.UserChoiceBilling, +); + +// Initialize with external offer (alternative only) +await FlutterInappPurchase.instance.initConnection( + enableBillingProgramAndroid: BillingProgramAndroid.ExternalOffer, +); + +// Initialize with external payments (Japan only, 8.3.0+) +await FlutterInappPurchase.instance.initConnection( + enableBillingProgramAndroid: BillingProgramAndroid.ExternalPayments, +); + +// Standard billing (default) +await FlutterInappPurchase.instance.initConnection();`} + ), + gdscript: ( + {`# Initialize with user choice billing (7.0+) +var config = InitConnectionConfig.new() +config.enable_billing_program_android = BillingProgramAndroid.USER_CHOICE_BILLING +await iap.init_connection(config) + +# Initialize with external offer (alternative only) +config.enable_billing_program_android = BillingProgramAndroid.EXTERNAL_OFFER +await iap.init_connection(config) + +# Initialize with external payments (Japan only, 8.3.0+) +config.enable_billing_program_android = BillingProgramAndroid.EXTERNAL_PAYMENTS +await iap.init_connection(config) + +# Standard billing (default) +await iap.init_connection()`} + ), + }} + + + + User Choice Billing Complete Example + +

    + With User Choice Billing (7.0+), users see a dialog to choose between + Google Play or your alternative payment. Handle both paths: +

    + + {{ + typescript: ( + {`import { + initConnection, + userChoiceBillingListenerAndroid, + fetchProducts, + requestPurchase, + createAlternativeBillingToken, +} from 'expo-iap'; + +// Step 1: Set up listener for when user selects alternative billing +const userChoiceSubscription = userChoiceBillingListenerAndroid(async (details) => { + console.log('User chose alternative billing'); + console.log('Products:', details.products.map(p => p.productId)); + console.log('External Transaction Token:', details.externalTransactionToken); + + // Process payment with your backend using the token + const paymentResult = await yourBackend.processPayment({ + products: details.products, + token: details.externalTransactionToken, + }); + + if (paymentResult.success) { + grantUserAccess(); + } +}); + +// Step 2: Initialize with user choice billing (recommended) +await initConnection({ + enableBillingProgramAndroid: 'user-choice-billing', +}); + +// Step 3: Fetch products and purchase as normal +const products = await fetchProducts({ + request: { skus: ['premium_subscription'] }, + type: 'subs', +}); + +// Step 4: Request purchase - dialog will show both options +await requestPurchase({ + request: { + google: { skus: ['premium_subscription'] }, + }, + type: 'subs', +}); + +// If user selects Google Play → purchaseUpdatedListener fires +// If user selects alternative → userChoiceBillingListenerAndroid fires + +// Cleanup +userChoiceSubscription.remove();`} + ), + kotlin: ( + {`import dev.hyo.openiap.store.OpenIapStore +import dev.hyo.openiap.InitConnectionConfig +import dev.hyo.openiap.BillingProgramAndroid +import dev.hyo.openiap.listener.OpenIapUserChoiceBillingListener + +val iapStore = OpenIapStore(context) + +// Step 1: Set up listener for when user selects alternative billing +iapStore.addUserChoiceBillingListener(object : OpenIapUserChoiceBillingListener { + override fun onUserChoiceBilling(details: UserChoiceBillingDetails) { + Log.d("IAP", "User chose alternative billing") + Log.d("IAP", "Products: \${details.products.map { it.productId }}") + Log.d("IAP", "Token: \${details.externalTransactionToken}") + + // Process payment with your backend using the token + lifecycleScope.launch { + val paymentResult = yourBackend.processPayment( + products = details.products, + token = details.externalTransactionToken + ) + + if (paymentResult.success) { + grantUserAccess() + } + } + } +}) + +// Step 2: Initialize with user choice billing (recommended) +iapStore.initConnection( + InitConnectionConfig( + enableBillingProgramAndroid = BillingProgramAndroid.UserChoiceBilling + ) +) + +// Step 3: Fetch products and purchase as normal +val products = iapStore.fetchProducts( + skus = listOf("premium_subscription"), + type = ProductQueryType.Subs +) + +// Step 4: Request purchase - dialog will show both options +iapStore.setActivity(activity) +iapStore.requestPurchase( + RequestPurchaseProps( + request = RequestPurchaseProps.Request.Subscription( + RequestSubscriptionPropsByPlatforms( + google = RequestSubscriptionAndroidProps( + skus = listOf("premium_subscription") + ) + ) + ), + type = ProductQueryType.Subs + ) +) + +// If user selects Google Play → onPurchaseSuccess fires +// If user selects alternative → OpenIapUserChoiceBillingListener fires`} + ), + dart: ( + {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart'; + +// Step 1: Set up listener for when user selects alternative billing +final userChoiceSubscription = FlutterInappPurchase.userChoiceBillingStream + .listen((details) async { + print('User chose alternative billing'); + print('Products: \${details.products.map((p) => p.productId).toList()}'); + print('Token: \${details.externalTransactionToken}'); + + // Process payment with your backend using the token + final paymentResult = await yourBackend.processPayment( + products: details.products, + token: details.externalTransactionToken, + ); + + if (paymentResult.success) { + grantUserAccess(); + } +}); + +// Step 2: Initialize with user choice billing (recommended) +await FlutterInappPurchase.instance.initConnection( + enableBillingProgramAndroid: BillingProgramAndroid.UserChoiceBilling, +); + +// Step 3: Fetch products and purchase as normal +final products = await FlutterInappPurchase.instance.getSubscriptions( + ['premium_subscription'], +); + +// Step 4: Request purchase - dialog will show both options +await FlutterInappPurchase.instance.requestSubscription( + sku: 'premium_subscription', +); + +// If user selects Google Play → purchaseUpdatedStream fires +// If user selects alternative → userChoiceBillingStream fires + +// Cleanup +userChoiceSubscription.cancel();`} + ), + gdscript: ( + {`# Step 1: Set up listener for when user selects alternative billing +func _on_user_choice_billing(details: UserChoiceBillingDetails): + print("User chose alternative billing") + var product_ids = [] + for p in details.products: + product_ids.append(p.product_id) + print("Products: %s" % str(product_ids)) + print("Token: %s" % details.external_transaction_token) + + # Process payment with your backend using the token + var payment_result = await your_backend.process_payment( + details.products, + details.external_transaction_token + ) + + if payment_result.success: + grant_user_access() + +iap.user_choice_billing.connect(_on_user_choice_billing) + +# Step 2: Initialize with user choice billing (recommended) +var config = InitConnectionConfig.new() +config.enable_billing_program_android = BillingProgramAndroid.USER_CHOICE_BILLING +await iap.init_connection(config) + +# Step 3: Fetch products and purchase as normal +var request = ProductRequest.new() +request.skus = ["premium_subscription"] +request.type = ProductQueryType.SUBS +var products = await iap.fetch_products(request) + +# Step 4: Request purchase - dialog will show both options +var props = RequestPurchaseProps.new() +props.request = RequestSubscriptionPropsByPlatforms.new() +props.request.google = RequestSubscriptionAndroidProps.new() +props.request.google.skus = ["premium_subscription"] +props.type = ProductType.SUBS +await iap.request_purchase(props) + +# If user selects Google Play → purchase_updated signal fires +# If user selects alternative → user_choice_billing signal fires`} + ), + }} + + + + Alternative Billing Only Complete Example + +

    + With External Offer mode (replaces Alternative Only), all purchases go + through your alternative payment system. Google Play is not shown: +

    + + {{ + typescript: ( + {`import { + initConnection, + fetchProducts, + checkAlternativeBillingAvailability, + showAlternativeBillingDialog, + createAlternativeBillingToken, +} from 'expo-iap'; + +// Step 1: Initialize with external offer (recommended) +await initConnection({ + enableBillingProgramAndroid: 'external-offer', +}); + +// Step 2: Check if alternative billing is available +const availability = await checkAlternativeBillingAvailability(); +if (!availability.isAvailable) { + console.log('Alternative billing not available in this region'); + // Fall back to standard Google Play billing + return; +} + +// Step 3: Fetch products (still needed to show prices) +const products = await fetchProducts({ + request: { skus: ['premium_subscription'] }, + type: 'subs', +}); + +// Step 4: Show required Google Play disclosure dialog +const dialogResult = await showAlternativeBillingDialog(); +if (dialogResult.responseCode !== 0) { + console.log('User did not accept alternative billing'); + return; +} + +// Step 5: Create token for this transaction +const token = await createAlternativeBillingToken(products[0].id); + +// Step 6: Process purchase with your backend +const paymentResult = await yourBackend.processAlternativePurchase({ + productId: products[0].id, + price: products[0].price, + token: token, + userId: currentUserId, +}); + +if (paymentResult.success) { + // Report transaction to Google (required) + await yourBackend.reportExternalTransaction(token, paymentResult.orderId); + grantUserAccess(); +}`} + ), + kotlin: ( + {`import dev.hyo.openiap.store.OpenIapStore +import dev.hyo.openiap.InitConnectionConfig +import dev.hyo.openiap.BillingProgramAndroid + +val iapStore = OpenIapStore(context) + +// Step 1: Initialize with external offer (recommended) +iapStore.initConnection( + InitConnectionConfig( + enableBillingProgramAndroid = BillingProgramAndroid.ExternalOffer + ) +) + +// Step 2: Check if alternative billing is available +val availability = iapStore.checkAlternativeBillingAvailability() +if (!availability.isAvailable) { + Log.w("IAP", "Alternative billing not available in this region") + // Fall back to standard Google Play billing + return +} + +// Step 3: Fetch products (still needed to show prices) +val products = iapStore.fetchProducts( + skus = listOf("premium_subscription"), + type = ProductQueryType.Subs +) + +// Step 4: Show required Google Play disclosure dialog +iapStore.setActivity(activity) +val dialogResult = iapStore.showAlternativeBillingDialog() +if (dialogResult.responseCode != 0) { + Log.d("IAP", "User did not accept alternative billing") + return +} + +// Step 5: Create token for this transaction +val token = iapStore.createAlternativeBillingToken(products.first().id) + +// Step 6: Process purchase with your backend +lifecycleScope.launch { + val paymentResult = yourBackend.processAlternativePurchase( + productId = products.first().id, + price = products.first().price, + token = token, + userId = currentUserId + ) + + if (paymentResult.success) { + // Report transaction to Google (required) + yourBackend.reportExternalTransaction(token, paymentResult.orderId) + grantUserAccess() + } +}`} + ), + dart: ( + {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart'; + +final iap = FlutterInappPurchase.instance; + +// Step 1: Initialize with external offer (recommended) +await iap.initConnection( + enableBillingProgramAndroid: BillingProgramAndroid.ExternalOffer, +); + +// Step 2: Check if alternative billing is available +final availability = await iap.checkAlternativeBillingAvailability(); +if (!availability.isAvailable) { + print('Alternative billing not available in this region'); + // Fall back to standard Google Play billing + return; +} + +// Step 3: Fetch products (still needed to show prices) +final products = await iap.getSubscriptions(['premium_subscription']); + +// Step 4: Show required Google Play disclosure dialog +final dialogResult = await iap.showAlternativeBillingDialog(); +if (dialogResult.responseCode != 0) { + print('User did not accept alternative billing'); + return; +} + +// Step 5: Create token for this transaction +final token = await iap.createAlternativeBillingToken(products.first.productId); + +// Step 6: Process purchase with your backend +final paymentResult = await yourBackend.processAlternativePurchase( + productId: products.first.productId, + price: products.first.price, + token: token, + userId: currentUserId, +); + +if (paymentResult.success) { + // Report transaction to Google (required) + await yourBackend.reportExternalTransaction(token, paymentResult.orderId); + grantUserAccess(); +}`} + ), + gdscript: ( + {`# Step 1: Initialize with external offer (recommended) +var config = InitConnectionConfig.new() +config.enable_billing_program_android = BillingProgramAndroid.EXTERNAL_OFFER +await iap.init_connection(config) + +# Step 2: Check if alternative billing is available +var availability = await iap.check_alternative_billing_availability() +if not availability.is_available: + print("Alternative billing not available in this region") + # Fall back to standard Google Play billing + return + +# Step 3: Fetch products (still needed to show prices) +var request = ProductRequest.new() +request.skus = ["premium_subscription"] +request.type = ProductQueryType.SUBS +var products = await iap.fetch_products(request) + +# Step 4: Show required Google Play disclosure dialog +var dialog_result = await iap.show_alternative_billing_dialog() +if dialog_result.response_code != 0: + print("User did not accept alternative billing") + return + +# Step 5: Create token for this transaction +var token = await iap.create_alternative_billing_token(products[0].id) + +# Step 6: Process purchase with your backend +var payment_result = await your_backend.process_alternative_purchase( + products[0].id, + products[0].price, + token, + current_user_id +) + +if payment_result.success: + # Report transaction to Google (required) + await your_backend.report_external_transaction(token, payment_result.order_id) + grant_user_access()`} + ), + }} + + +
    + Reporting Requirement: +

    + For both User Choice and Alternative Only modes, you must report + completed transactions to Google Play within 24 hours using the + Google Play Developer API. Failure to report may result in account + suspension. +

    +
    +
    +
    + ); +} + +export default AlternativeBillingTypes; diff --git a/packages/docs/src/pages/docs/types/alternative.tsx b/packages/docs/src/pages/docs/types/alternative.tsx deleted file mode 100644 index 9734c5b6..00000000 --- a/packages/docs/src/pages/docs/types/alternative.tsx +++ /dev/null @@ -1,1415 +0,0 @@ -import { Link } from 'react-router-dom'; -import AnchorLink from '../../../components/AnchorLink'; -import CodeBlock from '../../../components/CodeBlock'; -import LanguageTabs from '../../../components/LanguageTabs'; -import SEO from '../../../components/SEO'; -import TLDRBox from '../../../components/TLDRBox'; -import { useScrollToHash } from '../../../hooks/useScrollToHash'; - -function TypesAlternative() { - useScrollToHash(); - - return ( -
    - -

    Alternative Billing Types

    -

    - Type definitions for alternative billing systems and external purchase - links. -

    - - -
    - - -
    - - Alternative Billing Types - -

    - Types for configuring alternative billing systems, primarily used for - Android. -

    - - - AlternativeBillingModeAndroid{' '} - - (Deprecated) - - -
    - Deprecated: Use{' '} - enableBillingProgramAndroid with{' '} - BillingProgramAndroid instead. -
      -
    • - USER_CHOICE →{' '} - BillingProgramAndroid.USER_CHOICE_BILLING -
    • -
    • - ALTERNATIVE_ONLY →{' '} - BillingProgramAndroid.EXTERNAL_OFFER -
    • -
    -
    -

    - Enum controlling which billing system is used during{' '} - initConnection(): -

    - - - - - - - - - - - - - - - - - - - - - -
    NameSummary
    - NONE - Standard Google Play billing (default)
    - USER_CHOICE - - User can select between Google Play or alternative billing - (requires Billing Library 7.0+) -
    - ALTERNATIVE_ONLY - - Alternative billing only, no Google Play option (requires - Billing Library 6.2+) -
    - - - InitConnectionConfig - -

    - Configuration options for initConnection(): -

    - - - - - - - - - - - - - - - - - -
    NameSummary
    - enableBillingProgramAndroid - - (Recommended) Enable a specific billing program - during connection. Use USER_CHOICE_BILLING for user - choice, EXTERNAL_OFFER for alternative only, or{' '} - EXTERNAL_PAYMENTS for Japan external payments - (8.3.0+). -
    - alternativeBillingModeAndroid - - - (Deprecated) - {' '} - Use enableBillingProgramAndroid instead. -
    - - - Basic Usage (Recommended) - - - {{ - typescript: ( - {`// Initialize with user choice billing (7.0+) -await initConnection({ - enableBillingProgramAndroid: 'user-choice-billing' -}); - -// Initialize with external offer (alternative only) -await initConnection({ - enableBillingProgramAndroid: 'external-offer' -}); - -// Initialize with external payments (Japan only, 8.3.0+) -await initConnection({ - enableBillingProgramAndroid: 'external-payments' -}); - -// Standard billing (default) -await initConnection();`} - ), - swift: ( - {`// iOS uses standard StoreKit billing -// Alternative billing is Android-only -try await OpenIapModule.shared.initConnection() - -// Check connection status -let isConnected = try await OpenIapModule.shared.initConnection()`} - ), - kotlin: ( - {`// Initialize with user choice billing (7.0+) -openIapStore.initConnection( - InitConnectionConfig( - enableBillingProgramAndroid = BillingProgramAndroid.UserChoiceBilling - ) -) - -// Initialize with external offer (alternative only) -openIapStore.initConnection( - InitConnectionConfig( - enableBillingProgramAndroid = BillingProgramAndroid.ExternalOffer - ) -) - -// Initialize with external payments (Japan only, 8.3.0+) -openIapStore.initConnection( - InitConnectionConfig( - enableBillingProgramAndroid = BillingProgramAndroid.ExternalPayments - ) -) - -// Standard billing (default) -openIapStore.initConnection()`} - ), - dart: ( - {`// Initialize with user choice billing (7.0+) -await FlutterInappPurchase.instance.initConnection( - enableBillingProgramAndroid: BillingProgramAndroid.UserChoiceBilling, -); - -// Initialize with external offer (alternative only) -await FlutterInappPurchase.instance.initConnection( - enableBillingProgramAndroid: BillingProgramAndroid.ExternalOffer, -); - -// Initialize with external payments (Japan only, 8.3.0+) -await FlutterInappPurchase.instance.initConnection( - enableBillingProgramAndroid: BillingProgramAndroid.ExternalPayments, -); - -// Standard billing (default) -await FlutterInappPurchase.instance.initConnection();`} - ), - gdscript: ( - {`# Initialize with user choice billing (7.0+) -var config = InitConnectionConfig.new() -config.enable_billing_program_android = BillingProgramAndroid.USER_CHOICE_BILLING -await iap.init_connection(config) - -# Initialize with external offer (alternative only) -config.enable_billing_program_android = BillingProgramAndroid.EXTERNAL_OFFER -await iap.init_connection(config) - -# Initialize with external payments (Japan only, 8.3.0+) -config.enable_billing_program_android = BillingProgramAndroid.EXTERNAL_PAYMENTS -await iap.init_connection(config) - -# Standard billing (default) -await iap.init_connection()`} - ), - }} - - - - User Choice Billing Complete Example - -

    - With User Choice Billing (7.0+), users see a dialog to choose between - Google Play or your alternative payment. Handle both paths: -

    - - {{ - typescript: ( - {`import { - initConnection, - userChoiceBillingListenerAndroid, - fetchProducts, - requestPurchase, - createAlternativeBillingToken, -} from 'expo-iap'; - -// Step 1: Set up listener for when user selects alternative billing -const userChoiceSubscription = userChoiceBillingListenerAndroid(async (details) => { - console.log('User chose alternative billing'); - console.log('Products:', details.products.map(p => p.productId)); - console.log('External Transaction Token:', details.externalTransactionToken); - - // Process payment with your backend using the token - const paymentResult = await yourBackend.processPayment({ - products: details.products, - token: details.externalTransactionToken, - }); - - if (paymentResult.success) { - grantUserAccess(); - } -}); - -// Step 2: Initialize with user choice billing (recommended) -await initConnection({ - enableBillingProgramAndroid: 'user-choice-billing', -}); - -// Step 3: Fetch products and purchase as normal -const products = await fetchProducts({ - request: { skus: ['premium_subscription'] }, - type: 'subs', -}); - -// Step 4: Request purchase - dialog will show both options -await requestPurchase({ - request: { - google: { skus: ['premium_subscription'] }, - }, - type: 'subs', -}); - -// If user selects Google Play → purchaseUpdatedListener fires -// If user selects alternative → userChoiceBillingListenerAndroid fires - -// Cleanup -userChoiceSubscription.remove();`} - ), - kotlin: ( - {`import dev.hyo.openiap.store.OpenIapStore -import dev.hyo.openiap.InitConnectionConfig -import dev.hyo.openiap.BillingProgramAndroid -import dev.hyo.openiap.listener.OpenIapUserChoiceBillingListener - -val iapStore = OpenIapStore(context) - -// Step 1: Set up listener for when user selects alternative billing -iapStore.addUserChoiceBillingListener(object : OpenIapUserChoiceBillingListener { - override fun onUserChoiceBilling(details: UserChoiceBillingDetails) { - Log.d("IAP", "User chose alternative billing") - Log.d("IAP", "Products: \${details.products.map { it.productId }}") - Log.d("IAP", "Token: \${details.externalTransactionToken}") - - // Process payment with your backend using the token - lifecycleScope.launch { - val paymentResult = yourBackend.processPayment( - products = details.products, - token = details.externalTransactionToken - ) - - if (paymentResult.success) { - grantUserAccess() - } - } - } -}) - -// Step 2: Initialize with user choice billing (recommended) -iapStore.initConnection( - InitConnectionConfig( - enableBillingProgramAndroid = BillingProgramAndroid.UserChoiceBilling - ) -) - -// Step 3: Fetch products and purchase as normal -val products = iapStore.fetchProducts( - skus = listOf("premium_subscription"), - type = ProductQueryType.Subs -) - -// Step 4: Request purchase - dialog will show both options -iapStore.setActivity(activity) -iapStore.requestPurchase( - RequestPurchaseProps( - request = RequestPurchaseProps.Request.Subscription( - RequestSubscriptionPropsByPlatforms( - google = RequestSubscriptionAndroidProps( - skus = listOf("premium_subscription") - ) - ) - ), - type = ProductQueryType.Subs - ) -) - -// If user selects Google Play → onPurchaseSuccess fires -// If user selects alternative → OpenIapUserChoiceBillingListener fires`} - ), - dart: ( - {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart'; - -// Step 1: Set up listener for when user selects alternative billing -final userChoiceSubscription = FlutterInappPurchase.userChoiceBillingStream - .listen((details) async { - print('User chose alternative billing'); - print('Products: \${details.products.map((p) => p.productId).toList()}'); - print('Token: \${details.externalTransactionToken}'); - - // Process payment with your backend using the token - final paymentResult = await yourBackend.processPayment( - products: details.products, - token: details.externalTransactionToken, - ); - - if (paymentResult.success) { - grantUserAccess(); - } -}); - -// Step 2: Initialize with user choice billing (recommended) -await FlutterInappPurchase.instance.initConnection( - enableBillingProgramAndroid: BillingProgramAndroid.UserChoiceBilling, -); - -// Step 3: Fetch products and purchase as normal -final products = await FlutterInappPurchase.instance.getSubscriptions( - ['premium_subscription'], -); - -// Step 4: Request purchase - dialog will show both options -await FlutterInappPurchase.instance.requestSubscription( - sku: 'premium_subscription', -); - -// If user selects Google Play → purchaseUpdatedStream fires -// If user selects alternative → userChoiceBillingStream fires - -// Cleanup -userChoiceSubscription.cancel();`} - ), - gdscript: ( - {`# Step 1: Set up listener for when user selects alternative billing -func _on_user_choice_billing(details: UserChoiceBillingDetails): - print("User chose alternative billing") - var product_ids = [] - for p in details.products: - product_ids.append(p.product_id) - print("Products: %s" % str(product_ids)) - print("Token: %s" % details.external_transaction_token) - - # Process payment with your backend using the token - var payment_result = await your_backend.process_payment( - details.products, - details.external_transaction_token - ) - - if payment_result.success: - grant_user_access() - -iap.user_choice_billing.connect(_on_user_choice_billing) - -# Step 2: Initialize with user choice billing (recommended) -var config = InitConnectionConfig.new() -config.enable_billing_program_android = BillingProgramAndroid.USER_CHOICE_BILLING -await iap.init_connection(config) - -# Step 3: Fetch products and purchase as normal -var request = ProductRequest.new() -request.skus = ["premium_subscription"] -request.type = ProductQueryType.SUBS -var products = await iap.fetch_products(request) - -# Step 4: Request purchase - dialog will show both options -var props = RequestPurchaseProps.new() -props.request = RequestSubscriptionPropsByPlatforms.new() -props.request.google = RequestSubscriptionAndroidProps.new() -props.request.google.skus = ["premium_subscription"] -props.type = ProductType.SUBS -await iap.request_purchase(props) - -# If user selects Google Play → purchase_updated signal fires -# If user selects alternative → user_choice_billing signal fires`} - ), - }} - - - - Alternative Billing Only Complete Example - -

    - With External Offer mode (replaces Alternative Only), all purchases go - through your alternative payment system. Google Play is not shown: -

    - - {{ - typescript: ( - {`import { - initConnection, - fetchProducts, - checkAlternativeBillingAvailability, - showAlternativeBillingDialog, - createAlternativeBillingToken, -} from 'expo-iap'; - -// Step 1: Initialize with external offer (recommended) -await initConnection({ - enableBillingProgramAndroid: 'external-offer', -}); - -// Step 2: Check if alternative billing is available -const availability = await checkAlternativeBillingAvailability(); -if (!availability.isAvailable) { - console.log('Alternative billing not available in this region'); - // Fall back to standard Google Play billing - return; -} - -// Step 3: Fetch products (still needed to show prices) -const products = await fetchProducts({ - request: { skus: ['premium_subscription'] }, - type: 'subs', -}); - -// Step 4: Show required Google Play disclosure dialog -const dialogResult = await showAlternativeBillingDialog(); -if (dialogResult.responseCode !== 0) { - console.log('User did not accept alternative billing'); - return; -} - -// Step 5: Create token for this transaction -const token = await createAlternativeBillingToken(products[0].id); - -// Step 6: Process purchase with your backend -const paymentResult = await yourBackend.processAlternativePurchase({ - productId: products[0].id, - price: products[0].price, - token: token, - userId: currentUserId, -}); - -if (paymentResult.success) { - // Report transaction to Google (required) - await yourBackend.reportExternalTransaction(token, paymentResult.orderId); - grantUserAccess(); -}`} - ), - kotlin: ( - {`import dev.hyo.openiap.store.OpenIapStore -import dev.hyo.openiap.InitConnectionConfig -import dev.hyo.openiap.BillingProgramAndroid - -val iapStore = OpenIapStore(context) - -// Step 1: Initialize with external offer (recommended) -iapStore.initConnection( - InitConnectionConfig( - enableBillingProgramAndroid = BillingProgramAndroid.ExternalOffer - ) -) - -// Step 2: Check if alternative billing is available -val availability = iapStore.checkAlternativeBillingAvailability() -if (!availability.isAvailable) { - Log.w("IAP", "Alternative billing not available in this region") - // Fall back to standard Google Play billing - return -} - -// Step 3: Fetch products (still needed to show prices) -val products = iapStore.fetchProducts( - skus = listOf("premium_subscription"), - type = ProductQueryType.Subs -) - -// Step 4: Show required Google Play disclosure dialog -iapStore.setActivity(activity) -val dialogResult = iapStore.showAlternativeBillingDialog() -if (dialogResult.responseCode != 0) { - Log.d("IAP", "User did not accept alternative billing") - return -} - -// Step 5: Create token for this transaction -val token = iapStore.createAlternativeBillingToken(products.first().id) - -// Step 6: Process purchase with your backend -lifecycleScope.launch { - val paymentResult = yourBackend.processAlternativePurchase( - productId = products.first().id, - price = products.first().price, - token = token, - userId = currentUserId - ) - - if (paymentResult.success) { - // Report transaction to Google (required) - yourBackend.reportExternalTransaction(token, paymentResult.orderId) - grantUserAccess() - } -}`} - ), - dart: ( - {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart'; - -final iap = FlutterInappPurchase.instance; - -// Step 1: Initialize with external offer (recommended) -await iap.initConnection( - enableBillingProgramAndroid: BillingProgramAndroid.ExternalOffer, -); - -// Step 2: Check if alternative billing is available -final availability = await iap.checkAlternativeBillingAvailability(); -if (!availability.isAvailable) { - print('Alternative billing not available in this region'); - // Fall back to standard Google Play billing - return; -} - -// Step 3: Fetch products (still needed to show prices) -final products = await iap.getSubscriptions(['premium_subscription']); - -// Step 4: Show required Google Play disclosure dialog -final dialogResult = await iap.showAlternativeBillingDialog(); -if (dialogResult.responseCode != 0) { - print('User did not accept alternative billing'); - return; -} - -// Step 5: Create token for this transaction -final token = await iap.createAlternativeBillingToken(products.first.productId); - -// Step 6: Process purchase with your backend -final paymentResult = await yourBackend.processAlternativePurchase( - productId: products.first.productId, - price: products.first.price, - token: token, - userId: currentUserId, -); - -if (paymentResult.success) { - // Report transaction to Google (required) - await yourBackend.reportExternalTransaction(token, paymentResult.orderId); - grantUserAccess(); -}`} - ), - gdscript: ( - {`# Step 1: Initialize with external offer (recommended) -var config = InitConnectionConfig.new() -config.enable_billing_program_android = BillingProgramAndroid.EXTERNAL_OFFER -await iap.init_connection(config) - -# Step 2: Check if alternative billing is available -var availability = await iap.check_alternative_billing_availability() -if not availability.is_available: - print("Alternative billing not available in this region") - # Fall back to standard Google Play billing - return - -# Step 3: Fetch products (still needed to show prices) -var request = ProductRequest.new() -request.skus = ["premium_subscription"] -request.type = ProductQueryType.SUBS -var products = await iap.fetch_products(request) - -# Step 4: Show required Google Play disclosure dialog -var dialog_result = await iap.show_alternative_billing_dialog() -if dialog_result.response_code != 0: - print("User did not accept alternative billing") - return - -# Step 5: Create token for this transaction -var token = await iap.create_alternative_billing_token(products[0].id) - -# Step 6: Process purchase with your backend -var payment_result = await your_backend.process_alternative_purchase( - products[0].id, - products[0].price, - token, - current_user_id -) - -if payment_result.success: - # Report transaction to Google (required) - await your_backend.report_external_transaction(token, payment_result.order_id) - grant_user_access()`} - ), - }} - - -
    - Reporting Requirement: -

    - For both User Choice and Alternative Only modes, you must report - completed transactions to Google Play within 24 hours using the - Google Play Developer API. Failure to report may result in account - suspension. -

    -
    -
    - -
    - - Billing Programs (Android 8.2.0+) - -

    - Google Play Billing Library 8.2.0+ introduces the Billing Programs - API, which provides a more structured approach to external offers and - content links. Version 8.3.0 adds External Payments for Japan. -

    - - - BillingProgramAndroid - -

    - Enum for different billing program types. Use with{' '} - enableBillingProgramAndroid in{' '} - InitConnectionConfig: -

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    NameSummaryVersion
    - USER_CHOICE_BILLING - - User can select between Google Play or alternative billing - 7.0+
    - EXTERNAL_CONTENT_LINK - - For apps that link to external content (reader apps, music - streaming) - 8.2.0+
    - EXTERNAL_OFFER - - For apps offering alternative payment options (replaces - ALTERNATIVE_ONLY) - 8.2.0+
    - EXTERNAL_PAYMENTS - - Side-by-side choice between Google Play and developer billing - (Japan only) - 8.3.0+
    - - - DeveloperBillingOptionParamsAndroid - -

    - Parameters for configuring developer billing option in purchase flow - (8.3.0+): -

    - - - - - - - - - - - - - - - - - - - - - - - - - -
    NameTypeSummary
    - billingProgram - - BillingProgramAndroid - - The billing program (usually EXTERNAL_PAYMENTS) -
    - linkUri - - String - URL where the external payment will be processed
    - launchMode - - DeveloperBillingLaunchModeAndroid - How to launch the external payment link
    - - - DeveloperBillingLaunchModeAndroid - -

    How the external payment URL is launched:

    - - - - - - - - - - - - - - - - - -
    NameSummary
    - LAUNCH_IN_EXTERNAL_BROWSER_OR_APP - - Google Play launches the link in a browser or eligible app -
    - CALLER_WILL_LAUNCH_LINK - - Your app handles launching the link after Play returns control -
    - - - DeveloperProvidedBillingDetailsAndroid - -

    Details received when user selects developer billing (8.3.0+):

    - - - - - - - - - - - - - - - -
    NameTypeSummary
    - externalTransactionToken - - String - - Token to report external transaction to Google (must report - within 24 hours) -
    - - - Usage Example - - - {{ - typescript: ( - {`import { - enableBillingProgramAndroid, - isBillingProgramAvailableAndroid, - requestPurchase, - developerProvidedBillingListenerAndroid, -} from 'expo-iap'; - -// Enable External Payments before initConnection -enableBillingProgramAndroid('EXTERNAL_PAYMENTS'); - -await initConnection(); - -// Listen for developer billing selection -developerProvidedBillingListenerAndroid((details) => { - console.log('Token:', details.externalTransactionToken); - // Report token to Google via your backend within 24 hours -}); - -// Check availability (Japan only) -const result = await isBillingProgramAvailableAndroid('EXTERNAL_PAYMENTS'); -if (result.isAvailable) { - // Purchase with developer billing option - await requestPurchase({ - google: { - skus: ['product_id'], - developerBillingOption: { - billingProgram: 'EXTERNAL_PAYMENTS', - linkUri: 'https://your-site.com/checkout', - launchMode: 'LAUNCH_IN_EXTERNAL_BROWSER_OR_APP', - }, - }, - }); -}`} - ), - kotlin: ( - {`import dev.hyo.openiap.store.OpenIapStore -import dev.hyo.openiap.* - -val iapStore = OpenIapStore(context) - -// Enable External Payments before initConnection -iapStore.enableBillingProgram(BillingProgramAndroid.ExternalPayments) - -iapStore.initConnection(null) - -// Listen for developer billing selection -iapStore.addDeveloperProvidedBillingListener { details -> - Log.d("IAP", "Token: \${details.externalTransactionToken}") - // Report token to Google via your backend within 24 hours -} - -// Check availability (Japan only) -val result = iapStore.isBillingProgramAvailable( - BillingProgramAndroid.ExternalPayments -) -if (result.isAvailable) { - // Purchase with developer billing option - val props = RequestPurchaseProps( - request = RequestPurchaseProps.Request.Purchase( - RequestPurchasePropsByPlatforms( - google = RequestPurchaseAndroidProps( - skus = listOf("product_id"), - developerBillingOption = DeveloperBillingOptionParamsAndroid( - billingProgram = BillingProgramAndroid.ExternalPayments, - linkUri = "https://your-site.com/checkout", - launchMode = DeveloperBillingLaunchModeAndroid.LaunchInExternalBrowserOrApp - ) - ) - ) - ), - type = ProductQueryType.InApp - ) - iapStore.requestPurchase(props) -}`} - ), - dart: ( - {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart'; - -// Enable External Payments before initConnection -FlutterInappPurchase.instance.enableBillingProgramAndroid( - BillingProgramAndroid.externalPayments, -); - -await FlutterInappPurchase.instance.initConnection(); - -// Listen for developer billing selection -FlutterInappPurchase.developerProvidedBillingStream.listen((details) { - print('Token: \${details.externalTransactionToken}'); - // Report token to Google via your backend within 24 hours -}); - -// Check availability (Japan only) -final result = await FlutterInappPurchase.instance - .isBillingProgramAvailableAndroid(BillingProgramAndroid.externalPayments); -if (result.isAvailable) { - // Purchase with developer billing option - await FlutterInappPurchase.instance.requestPurchase( - 'product_id', - developerBillingOption: DeveloperBillingOptionParamsAndroid( - billingProgram: BillingProgramAndroid.externalPayments, - linkUri: 'https://your-site.com/checkout', - launchMode: DeveloperBillingLaunchModeAndroid.launchInExternalBrowserOrApp, - ), - ); -}`} - ), - gdscript: ( - {`# Enable External Payments before initConnection -iap.enable_billing_program_android(BillingProgramAndroid.EXTERNAL_PAYMENTS) - -await iap.init_connection() - -# Listen for developer billing selection -func _on_developer_provided_billing(details: DeveloperProvidedBillingDetailsAndroid): - print("Token: %s" % details.external_transaction_token) - # Report token to Google via your backend within 24 hours - -iap.developer_provided_billing.connect(_on_developer_provided_billing) - -# Check availability (Japan only) -var result = await iap.is_billing_program_available_android( - BillingProgramAndroid.EXTERNAL_PAYMENTS -) -if result.is_available: - # Purchase with developer billing option - var props = RequestPurchaseProps.new() - props.request = RequestPurchasePropsByPlatforms.new() - props.request.google = RequestPurchaseAndroidProps.new() - props.request.google.skus = ["product_id"] - props.request.google.developer_billing_option = DeveloperBillingOptionParamsAndroid.new() - props.request.google.developer_billing_option.billing_program = BillingProgramAndroid.EXTERNAL_PAYMENTS - props.request.google.developer_billing_option.link_uri = "https://your-site.com/checkout" - props.request.google.developer_billing_option.launch_mode = DeveloperBillingLaunchModeAndroid.LAUNCH_IN_EXTERNAL_BROWSER_OR_APP - props.type = ProductQueryType.IN_APP - await iap.request_purchase(props)`} - ), - }} - - -
    -

    - Token Reporting: When a user completes a purchase - through developer billing, you must report the{' '} - externalTransactionToken to Google Play within 24 - hours. See{' '} - - External Payments documentation - {' '} - for complete implementation details. -

    -
    -
    - -
    - - External Purchase Link (iOS) - -

    - iOS-specific feature for redirecting users to an external website for - payment using Apple's StoreKit ExternalPurchase API. - Available from iOS 17.4+ (notice sheet) and iOS 18.2+ (custom links). -

    - -
    -

    - Important: External purchase links bypass StoreKit - completely. No purchaseUpdatedListener will fire. You - must implement deep links and server-side verification. -

    -
    - - - External Purchase APIs - - - - - - - - - - - - - - - - - - - - - - - - - - -
    APIDescriptionAvailability
    - canPresentExternalPurchaseNoticeIOS - Check if external purchase notice sheet can be presentediOS 17.4+
    - presentExternalPurchaseNoticeSheetIOS - - Present Apple's compliance notice sheet (required before - external purchase) - iOS 17.4+
    - presentExternalPurchaseLinkIOS - Open external purchase URL in SafariiOS 18.2+
    - - - Types - - - {{ - typescript: ( - {`// Result from presenting external purchase link -interface ExternalPurchaseLinkResultIOS { - error?: string; - success: boolean; -} - -// Result from presenting notice sheet -interface ExternalPurchaseNoticeResultIOS { - error?: string; - result: ExternalPurchaseNoticeAction; -} - -// User action on notice sheet -type ExternalPurchaseNoticeAction = 'continue' | 'dismissed';`} - ), - swift: ( - {`// Result from presenting external purchase link -struct ExternalPurchaseLinkResultIOS { - let error: String? - let success: Bool -} - -// Result from presenting notice sheet -struct ExternalPurchaseNoticeResultIOS { - let error: String? - let result: ExternalPurchaseNoticeAction -} - -// User action on notice sheet -enum ExternalPurchaseNoticeAction: String { - case \`continue\` = "continue" - case dismissed = "dismissed" -}`} - ), - kotlin: ( - {`// Result from presenting external purchase link (iOS-only via KMP) -data class ExternalPurchaseLinkResultIOS( - val error: String? = null, - val success: Boolean -) - -// Result from presenting notice sheet -data class ExternalPurchaseNoticeResultIOS( - val error: String? = null, - val result: ExternalPurchaseNoticeAction -) - -// User action on notice sheet -enum class ExternalPurchaseNoticeAction(val rawValue: String) { - Continue("continue"), - Dismissed("dismissed") -}`} - ), - dart: ( - {`// Result from presenting external purchase link -class ExternalPurchaseLinkResultIOS { - final String? error; - final bool success; -} - -// Result from presenting notice sheet -class ExternalPurchaseNoticeResultIOS { - final String? error; - final ExternalPurchaseNoticeAction result; -} - -// User action on notice sheet -enum ExternalPurchaseNoticeAction { - \`continue\`('continue'), - dismissed('dismissed'); -}`} - ), - gdscript: ( - {`# Result from presenting external purchase link -class_name ExternalPurchaseLinkResultIOS -var error: String # optional -var success: bool - -# Result from presenting notice sheet -class_name ExternalPurchaseNoticeResultIOS -var error: String # optional -var result: int # ExternalPurchaseNoticeAction - -# User action on notice sheet -enum ExternalPurchaseNoticeAction { - CONTINUE, - DISMISSED -}`} - ), - }} - - - - External Purchase Flow - -

    The external purchase flow requires 3 steps for Apple compliance:

    -
      -
    1. - Check availability - Verify the device supports - external purchase -
    2. -
    3. - Present notice sheet - Show Apple's required - disclosure -
    4. -
    5. - Open external link - Redirect to your payment page -
    6. -
    - - - Complete Example - - - {{ - typescript: ( - {`import { - canPresentExternalPurchaseNoticeIOS, - presentExternalPurchaseNoticeSheetIOS, - presentExternalPurchaseLinkIOS, -} from 'expo-iap'; - -async function handleExternalPurchase(externalUrl: string) { - // Step 1: Check if external purchase is available - const canPresent = await canPresentExternalPurchaseNoticeIOS(); - if (!canPresent) { - console.log('External purchase not available on this device'); - return; - } - - // Step 2: Present Apple's compliance notice sheet - const noticeResult = await presentExternalPurchaseNoticeSheetIOS(); - if (noticeResult.result === 'dismissed') { - console.log('User dismissed the notice sheet'); - return; - } - - // Step 3: Open external purchase link - const linkResult = await presentExternalPurchaseLinkIOS(externalUrl); - if (linkResult.success) { - console.log('User redirected to external payment'); - // Implement deep linking to handle return from payment - } else { - console.error('Failed:', linkResult.error); - } -}`} - ), - swift: ( - {`import OpenIap - -@available(iOS 18.2, *) -func handleExternalPurchase(externalUrl: String) async { - do { - // Step 1: Check if external purchase is available - let canPresent = try await OpenIapModule.shared.canPresentExternalPurchaseNoticeIOS() - guard canPresent else { - print("External purchase not available on this device") - return - } - - // Step 2: Present Apple's compliance notice sheet - let noticeResult = try await OpenIapModule.shared.presentExternalPurchaseNoticeSheetIOS() - guard noticeResult.result == .continue else { - print("User dismissed the notice sheet") - return - } - - // Step 3: Open external purchase link - let linkResult = try await OpenIapModule.shared.presentExternalPurchaseLinkIOS(externalUrl) - if linkResult.success { - print("User redirected to external payment") - // Implement deep linking to handle return from payment - } else if let error = linkResult.error { - print("Failed: \\(error)") - } - } catch { - print("External purchase error: \\(error)") - } -}`} - ), - kotlin: ( - {`import io.github.hyochan.kmpiap.kmpIapInstance -import io.github.hyochan.kmpiap.ExternalPurchaseNoticeAction - -// External purchase is iOS-only. For iOS targets in KMP: -suspend fun handleExternalPurchase(externalUrl: String) { - // Step 1: Check if external purchase is available - val canPresent = kmpIapInstance.canPresentExternalPurchaseNoticeIOS() - if (!canPresent) { - println("External purchase not available on this device") - return - } - - // Step 2: Present Apple's compliance notice sheet - val noticeResult = kmpIapInstance.presentExternalPurchaseNoticeSheetIOS() - if (noticeResult.result == ExternalPurchaseNoticeAction.Dismissed) { - println("User dismissed the notice sheet") - return - } - - // Step 3: Open external purchase link - val linkResult = kmpIapInstance.presentExternalPurchaseLinkIOS(externalUrl) - if (linkResult.success) { - println("User redirected to external payment") - // Implement deep linking to handle return from payment - } else { - println("Failed: \${linkResult.error}") - } -} - -// For Android: Use alternative billing APIs instead -// See: checkAlternativeBillingAvailability, showAlternativeBillingDialog`} - ), - dart: ( - {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart'; - -Future handleExternalPurchase(String externalUrl) async { - final iap = FlutterInappPurchase.instance; - - // Step 1: Check if external purchase is available - final canPresent = await iap.canPresentExternalPurchaseNoticeIOS(); - if (!canPresent) { - print('External purchase not available on this device'); - return; - } - - // Step 2: Present Apple's compliance notice sheet - final noticeResult = await iap.presentExternalPurchaseNoticeSheetIOS(); - if (noticeResult.result == ExternalPurchaseNoticeAction.dismissed) { - print('User dismissed the notice sheet'); - return; - } - - // Step 3: Open external purchase link - final linkResult = await iap.presentExternalPurchaseLinkIOS(externalUrl); - if (linkResult.success) { - print('User redirected to external payment'); - // Implement deep linking to handle return from payment - } else { - print('Failed: \${linkResult.error}'); - } -}`} - ), - gdscript: ( - {`func handle_external_purchase(external_url: String): - # Step 1: Check if external purchase is available - var can_present = await iap.can_present_external_purchase_notice_ios() - if not can_present: - print("External purchase not available on this device") - return - - # Step 2: Present Apple's compliance notice sheet - var notice_result = await iap.present_external_purchase_notice_sheet_ios() - if notice_result.result == ExternalPurchaseNoticeAction.DISMISSED: - print("User dismissed the notice sheet") - return - - # Step 3: Open external purchase link - var link_result = await iap.present_external_purchase_link_ios(external_url) - if link_result.success: - print("User redirected to external payment") - # Implement deep linking to handle return from payment - else: - print("Failed: %s" % link_result.error)`} - ), - }} - - - - Requirements - - - - - - - - - - - - - - - - - - - - - - - - - - -
    RequirementDetails
    PlatformiOS 17.4+ (notice sheet), iOS 18.2+ (custom links)
    Entitlement - App must have StoreKit external purchase entitlement from Apple -
    Deep LinkingImplement deep linking for app return flow after payment
    Verification - Handle purchase verification on your backend (no StoreKit - receipt) -
    - -
    -

    - Android alternative: For Android, use the{' '} - - alternative billing APIs - {' '} - (checkAlternativeBillingAvailability,{' '} - showAlternativeBillingDialog,{' '} - createAlternativeBillingToken) instead. -

    -
    -
    -
    - ); -} - -export default TypesAlternative; diff --git a/packages/docs/src/pages/docs/types/android.tsx b/packages/docs/src/pages/docs/types/android.tsx deleted file mode 100644 index cc3c8b2c..00000000 --- a/packages/docs/src/pages/docs/types/android.tsx +++ /dev/null @@ -1,688 +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 TypesAndroid() { - useScrollToHash(); - - return ( -
    - -

    Android Types

    -

    - Type definitions specific to Android/Google Play Billing for - subscription offers and pricing phases. -

    - - - - - -
    -

    - Deprecation Notice: The Android-specific offer types - (ProductAndroidOneTimePurchaseOfferDetail,{' '} - ProductSubscriptionAndroidOfferDetails) are deprecated. - Use the new cross-platform{' '} - DiscountOffer and SubscriptionOffer{' '} - types instead. -

    -
    - -
    - - ProductAndroidOneTimePurchaseOfferDetail - -

    - One-time purchase offer details for Android products. Available with{' '} - - Play Billing Library 7.0+ - - . For implementation examples, see the{' '} - Discounts feature guide. -

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    NameTypeSummary
    - offerId - - string | null - - Unique offer identifier. null for base offers -
    - offerToken - - string - Token required for purchase requests
    - offerTags - - string[] - Tags for categorizing offers
    - formattedPrice - - string - Localized price string (e.g., "$4.99")
    - priceAmountMicros - - string - Price in micro-units (divide by 1,000,000)
    - priceCurrencyCode - - string - ISO 4217 currency code
    - discountDisplayInfo - - DiscountDisplayInfoAndroid | null - Discount display information (percentage, badge text)
    - fullPriceMicros - - string | null - Original price before discount in micro-units
    - validTimeWindow - - - - ValidTimeWindowAndroid - {' '} - | null - - Time-limited offer validity window
    - limitedQuantityInfo - - - - LimitedQuantityInfoAndroid - {' '} - | null - - Quantity-limited offer availability
    - purchaseOptionId - - string | null - - Purchase option ID to identify which option was selected (7.0+) -
    - - - ValidTimeWindowAndroid - -

    Defines the validity period for time-limited offers:

    - - - - - - - - - - - - - - - - - - - - -
    NameTypeSummary
    - startTimeMillis - - string - Offer start time (Unix timestamp in milliseconds)
    - endTimeMillis - - string - Offer end time (Unix timestamp in milliseconds)
    - - - LimitedQuantityInfoAndroid - -

    Defines availability for quantity-limited offers:

    - - - - - - - - - - - - - - - - - - - - -
    NameTypeSummary
    - maximumQuantity - - number - Maximum number of times offer can be redeemed
    - remainingQuantity - - number - Remaining redemptions available for this user
    -
    - -
    - - SubscriptionOffer Deprecated - -

    - Deprecated: Use{' '} - SubscriptionOffer{' '} - (cross-platform) instead. -

    -

    Offer details for subscription purchases:

    - - - - - - - - - - - - - - - - - -
    NameSummary
    - sku - Product identifier
    - offerToken - Play Billing offer token (required for purchase)
    -

    - Note: The offerToken must be passed to{' '} - requestPurchase() when purchasing Android subscriptions. -

    -
    - -
    - - PricingPhase - -

    Pricing phase for Android subscriptions:

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    NameSummary
    - billingPeriod - ISO 8601 period (P1W, P1M, P1Y)
    - formattedPrice - Formatted price string
    - priceAmountMicros - Price in micro-units (divide by 1,000,000)
    - priceCurrencyCode - ISO 4217 currency code
    - billingCycleCount - Number of cycles for this phase
    - recurrenceMode - - How this phase recurs (1 = infinite, 2 = finite, 3 = - non-recurring) -
    - - - Recurrence Mode Values - - - - - - - - - - - - - - - - - - - - - - - - - - -
    ValueDescriptionUse Case
    - 1 - INFINITE_RECURRINGStandard subscription (repeats forever)
    - 2 - FINITE_RECURRINGLimited recurring (e.g., 3 months at intro price)
    - 3 - NON_RECURRINGOne-time (e.g., free trial)
    -
    - -
    - - PricingPhasesAndroid - -

    Container for pricing phases:

    - - - - - - - - - - - - - -
    NameSummary
    - pricingPhaseList - Array of PricingPhase objects
    -

    - Subscriptions typically have multiple phases. For example, a - subscription with a free trial might have: -

    -
      -
    1. - Phase 1: Free trial (7 days, recurrenceMode = 3) -
    2. -
    3. - Phase 2: Regular price (monthly, recurrenceMode = - 1) -
    4. -
    -
    - -
    - - Usage Example - -

    - When fetching Android subscription products, iterate through pricing - phases and use offerToken for purchases: -

    - - {{ - typescript: ( - {`import { fetchProducts, requestPurchase, ProductSubscription } from 'expo-iap'; - -// Fetch subscription products -const subscriptions = await fetchProducts({ - request: { skus: ['premium_monthly'] }, - type: 'subs', -}); - -const subscription = subscriptions[0]; - -// Access Android subscription offers -if ('subscriptionOfferDetailsAndroid' in subscription) { - const offers = subscription.subscriptionOfferDetailsAndroid; - - offers?.forEach((offer, idx) => { - console.log(\`Offer \${idx + 1}: \${offer.basePlanId}\`); - console.log(' Token:', offer.offerToken); - - // Iterate through pricing phases - offer.pricingPhases?.pricingPhaseList?.forEach((phase, phaseIdx) => { - console.log(\` Phase \${phaseIdx + 1}:\`); - console.log(' Period:', phase.billingPeriod); - console.log(' Price:', phase.formattedPrice); - console.log(' Recurrence:', phase.recurrenceMode); - }); - }); - - // Build subscription offers for purchase - const subscriptionOffers = offers - ?.filter((offer) => offer?.offerToken) - .map((offer) => ({ - sku: subscription.id, - offerToken: offer.offerToken, - })); - - // Purchase with offerToken (required for Android subscriptions) - await requestPurchase({ - request: { - google: { - skus: [subscription.id], - subscriptionOffers, - }, - }, - type: 'subs', - }); -}`} - ), - kotlin: ( - {`import dev.hyo.openiap.OpenIapModule -import dev.hyo.openiap.types.* - -// Fetch subscription products -val subscriptions = openIapModule.fetchProducts( - skus = listOf("premium_monthly"), - type = ProductQueryType.Subs -) - -val subscription = subscriptions.firstOrNull() ?: return - -// Access Android subscription offers -subscription.subscriptionOfferDetailsAndroid?.forEachIndexed { idx, offer -> - println("Offer \${idx + 1}: \${offer.basePlanId}") - println(" Token: \${offer.offerToken}") - - // Iterate through pricing phases - offer.pricingPhases.pricingPhaseList.forEachIndexed { phaseIdx, phase -> - println(" Phase \${phaseIdx + 1}:") - println(" Period: \${phase.billingPeriod}") - println(" Price: \${phase.formattedPrice}") - println(" Recurrence: \${phase.recurrenceMode}") - } -} - -// Build subscription offers for purchase -val subscriptionOffers = subscription.subscriptionOfferDetailsAndroid - ?.filter { it.offerToken.isNotEmpty() } - ?.map { offer -> - SubscriptionOfferAndroid( - sku = subscription.id, - offerToken = offer.offerToken - ) - } - -// Purchase with offerToken (required for Android subscriptions) -openIapModule.requestPurchase( - sku = subscription.id, - subscriptionOffers = subscriptionOffers -)`} - ), - dart: ( - {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart'; - -// Fetch subscription products -final subscriptions = await FlutterInappPurchase.instance.getSubscriptions( - ['premium_monthly'], -); - -final subscription = subscriptions.first; - -// Access Android subscription offers -if (subscription is ProductSubscriptionAndroid) { - final offers = subscription.subscriptionOfferDetailsAndroid; - - for (var i = 0; i < offers.length; i++) { - final offer = offers[i]; - print('Offer \${i + 1}: \${offer.basePlanId}'); - print(' Token: \${offer.offerToken}'); - - // Iterate through pricing phases - final phases = offer.pricingPhases.pricingPhaseList; - for (var j = 0; j < phases.length; j++) { - final phase = phases[j]; - print(' Phase \${j + 1}:'); - print(' Period: \${phase.billingPeriod}'); - print(' Price: \${phase.formattedPrice}'); - print(' Recurrence: \${phase.recurrenceMode}'); - } - } - - // Build subscription offers for purchase - final subscriptionOffers = offers - .where((offer) => offer.offerToken.isNotEmpty) - .map((offer) => SubscriptionOfferAndroid( - sku: subscription.id, - offerToken: offer.offerToken, - )) - .toList(); - - // Purchase with offerToken (required for Android subscriptions) - await FlutterInappPurchase.instance.requestSubscription( - sku: subscription.id, - subscriptionOffers: subscriptionOffers, - ); -}`} - ), - gdscript: ( - {`# Fetch subscription products -var request = ProductRequest.new() -request.skus = ["premium_monthly"] -request.type = ProductQueryType.SUBS -var subscriptions = await iap.fetch_products(request) - -var subscription = subscriptions[0] - -# Access Android subscription offers -var offers = subscription.subscription_offer_details_android - -for i in range(offers.size()): - var offer = offers[i] - print("Offer %d: %s" % [i + 1, offer.base_plan_id]) - print(" Token: %s" % offer.offer_token) - - # Iterate through pricing phases - var phases = offer.pricing_phases.pricing_phase_list - for j in range(phases.size()): - var phase = phases[j] - print(" Phase %d:" % [j + 1]) - print(" Period: %s" % phase.billing_period) - print(" Price: %s" % phase.formatted_price) - print(" Recurrence: %d" % phase.recurrence_mode) - -# Build subscription offers for purchase -var subscription_offers = [] -for offer in offers: - if offer.offer_token != "": - var sub_offer = SubscriptionOfferAndroid.new() - sub_offer.sku = subscription.id - sub_offer.offer_token = offer.offer_token - subscription_offers.append(sub_offer) - -# Purchase with offerToken (required for Android subscriptions) -var props = RequestPurchaseProps.new() -props.request = RequestSubscriptionPropsByPlatforms.new() -props.request.google = RequestSubscriptionAndroidProps.new() -props.request.google.skus = [subscription.id] -props.request.google.subscription_offers = subscription_offers -props.type = ProductType.SUBS -await iap.request_purchase(props)`} - ), - }} - -
    - -
    - - basePlanId Limitation - -

    - Important: While Google Play Console allows creating - multiple base plans for a single subscription product, the{' '} - basePlanId is not exposed by the Play Billing Library. - See{' '} - - detailed limitation and solutions - - . -

    -

    - When you have multiple base plans (e.g., monthly and yearly), each - generates separate SubscriptionOffer objects. Use the{' '} - offerToken to differentiate between them during purchase. -

    - - - - - - - - - - - - - - - - - - - - - -
    WorkaroundDescription
    - Parse billingPeriod - - Use the billing period (P1M, P1Y) to identify monthly vs yearly -
    Use tags/metadata - Add identifying info in Google Play Console that can be parsed -
    Separate product IDs - Create separate subscription products for each billing period -
    -
    -
    - ); -} - -export default TypesAndroid; diff --git a/packages/docs/src/pages/docs/types/android/one-time-purchase-offer-detail-android.tsx b/packages/docs/src/pages/docs/types/android/one-time-purchase-offer-detail-android.tsx new file mode 100644 index 00000000..2e7ba16b --- /dev/null +++ b/packages/docs/src/pages/docs/types/android/one-time-purchase-offer-detail-android.tsx @@ -0,0 +1,240 @@ +import { Link } from 'react-router-dom'; +import AnchorLink from '../../../../components/AnchorLink'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function OneTimePurchaseOfferDetailAndroid() { + useScrollToHash(); + + return ( +
    + +

    ProductAndroidOneTimePurchaseOfferDetail

    +
    + + ProductAndroidOneTimePurchaseOfferDetail{' '} + Deprecated + +

    + Deprecated. Use{' '} + + DiscountOffer + {' '} + (cross-platform) instead. +

    +

    + One-time purchase offer details for Android products. Available with{' '} + + Play Billing Library 7.0+ + + . For implementation examples, see the{' '} + Discounts feature guide. +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeSummary
    + offerId + + string | null + + Unique offer identifier. null for base offers +
    + offerToken + + string + Token required for purchase requests
    + offerTags + + string[] + Tags for categorizing offers
    + formattedPrice + + string + Localized price string (e.g., "$4.99")
    + priceAmountMicros + + string + Price in micro-units (divide by 1,000,000)
    + priceCurrencyCode + + string + ISO 4217 currency code
    + discountDisplayInfo + + DiscountDisplayInfoAndroid | null + Discount display information (percentage, badge text)
    + fullPriceMicros + + string | null + Original price before discount in micro-units
    + validTimeWindow + + + + ValidTimeWindowAndroid + {' '} + | null + + Time-limited offer validity window
    + limitedQuantityInfo + + + + LimitedQuantityInfoAndroid + {' '} + | null + + Quantity-limited offer availability
    + purchaseOptionId + + string | null + + Purchase option ID to identify which option was selected (7.0+) +
    + + + ValidTimeWindowAndroid + +

    Defines the validity period for time-limited offers:

    + + + + + + + + + + + + + + + + + + + + +
    NameTypeSummary
    + startTimeMillis + + string + Offer start time (Unix timestamp in milliseconds)
    + endTimeMillis + + string + Offer end time (Unix timestamp in milliseconds)
    + + + LimitedQuantityInfoAndroid + +

    Defines availability for quantity-limited offers:

    + + + + + + + + + + + + + + + + + + + + +
    NameTypeSummary
    + maximumQuantity + + number + Maximum number of times offer can be redeemed
    + remainingQuantity + + number + Remaining redemptions available for this user
    +
    +
    + ); +} + +export default OneTimePurchaseOfferDetailAndroid; diff --git a/packages/docs/src/pages/docs/types/android/pricing-phase-android.tsx b/packages/docs/src/pages/docs/types/android/pricing-phase-android.tsx new file mode 100644 index 00000000..5765c991 --- /dev/null +++ b/packages/docs/src/pages/docs/types/android/pricing-phase-android.tsx @@ -0,0 +1,139 @@ +import AnchorLink from '../../../../components/AnchorLink'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function PricingPhaseAndroid() { + useScrollToHash(); + + return ( +
    + +

    PricingPhaseAndroid

    +
    + + PricingPhase + +

    Pricing phase for Android subscriptions:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameSummary
    + billingPeriod + ISO 8601 period (P1W, P1M, P1Y)
    + formattedPrice + Formatted price string
    + priceAmountMicros + Price in micro-units (divide by 1,000,000)
    + priceCurrencyCode + ISO 4217 currency code
    + billingCycleCount + Number of cycles for this phase
    + recurrenceMode + + How this phase recurs (1 = infinite, 2 = finite, 3 = + non-recurring) +
    + + + Recurrence Mode Values + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ValueDescriptionUse Case
    + 1 + INFINITE_RECURRINGStandard subscription (repeats forever)
    + 2 + FINITE_RECURRINGLimited recurring (e.g., 3 months at intro price)
    + 3 + NON_RECURRINGOne-time (e.g., free trial)
    + + + PricingPhasesAndroid + +

    Container wrapping the list of pricing phases for an offer.

    + + + + + + + + + + + + + + + +
    NameTypeSummary
    + pricingPhaseList + + PricingPhaseAndroid[] + + Ordered list of pricing phases (intro → trial → standard). +
    +
    +
    + ); +} + +export default PricingPhaseAndroid; diff --git a/packages/docs/src/pages/docs/types/android/subscription-offer-android.tsx b/packages/docs/src/pages/docs/types/android/subscription-offer-android.tsx new file mode 100644 index 00000000..19f0a29d --- /dev/null +++ b/packages/docs/src/pages/docs/types/android/subscription-offer-android.tsx @@ -0,0 +1,114 @@ +import { Link } from 'react-router-dom'; +import AnchorLink from '../../../../components/AnchorLink'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function SubscriptionOfferAndroid() { + useScrollToHash(); + + return ( +
    + +

    ProductSubscriptionAndroidOfferDetails

    +
    + + ProductSubscriptionAndroidOfferDetails{' '} + Deprecated + +

    + Deprecated: Use{' '} + + SubscriptionOffer + {' '} + (cross-platform) instead. +

    +

    Offer details for subscription purchases:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeSummary
    + basePlanId + + string + Base plan identifier (e.g., "monthly", "yearly")
    + offerId + + string? + Offer identifier (null for the base plan)
    + offerToken + + string + Play Billing offer token (required for purchase)
    + offerTags + + string[] + Tags for categorizing offers
    + pricingPhases + + + PricingPhasesAndroid + + Pricing phase list for the offer
    + installmentPlanDetails + + InstallmentPlanDetailsAndroid? + + Installment plan details (Play Billing 7.0+, null for + non-installment plans) +
    +

    + Note: The offerToken must be passed to{' '} + + requestPurchase() + {' '} + when purchasing Android subscriptions. +

    +
    +
    + ); +} + +export default SubscriptionOfferAndroid; diff --git a/packages/docs/src/pages/docs/types/billing-programs.tsx b/packages/docs/src/pages/docs/types/billing-programs.tsx new file mode 100644 index 00000000..3be58b2b --- /dev/null +++ b/packages/docs/src/pages/docs/types/billing-programs.tsx @@ -0,0 +1,512 @@ +import { Link } from 'react-router-dom'; +import AnchorLink from '../../../components/AnchorLink'; +import CodeBlock from '../../../components/CodeBlock'; +import LanguageTabs from '../../../components/LanguageTabs'; +import SEO from '../../../components/SEO'; +import { useScrollToHash } from '../../../hooks/useScrollToHash'; + +function BillingPrograms() { + useScrollToHash(); + + return ( +
    + +

    Billing Programs Types

    +
    + + Billing Programs (Android 8.2.0+) + +

    + Google Play Billing Library 8.2.0+ introduces the Billing Programs + API, which provides a more structured approach to external offers and + content links. Version 8.3.0 adds External Payments for Japan. +

    + + + BillingProgramAndroid + +

    + Enum for different billing program types. Use with{' '} + enableBillingProgramAndroid in{' '} + + InitConnectionConfig + + : +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameSummaryVersion
    + USER_CHOICE_BILLING + + User can select between Google Play or alternative billing + 7.0+
    + EXTERNAL_CONTENT_LINK + + For apps that link to external content (reader apps, music + streaming) + 8.2.0+
    + EXTERNAL_OFFER + + For apps offering alternative payment options (replaces + ALTERNATIVE_ONLY) + 8.2.0+
    + EXTERNAL_PAYMENTS + + Side-by-side choice between Google Play and developer billing + (Japan only) + 8.3.0+
    + + + BillingProgramAvailabilityResultAndroid + +

    + Result of{' '} + + isBillingProgramAvailableAndroid() + + : +

    + + + + + + + + + + + + + + + + + + + + +
    NameTypeSummary
    + isAvailable + + boolean + Whether the billing program is available for the user
    + billingProgram + + + BillingProgramAndroid + + The billing program that was checked
    + + + BillingProgramReportingDetailsAndroid + +

    + Result of{' '} + + createBillingProgramReportingDetailsAndroid() + + : +

    + + + + + + + + + + + + + + + + + + + + +
    NameTypeSummary
    + billingProgram + + + BillingProgramAndroid + + The billing program associated with these details
    + externalTransactionToken + + string + + Token to report external transactions to Google (must report + within 24 hours) +
    + + + LaunchExternalLinkParamsAndroid + +

    + Parameters for{' '} + + launchExternalLinkAndroid() + + : +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeSummary
    + billingProgram + + + BillingProgramAndroid + + + The billing program (EXTERNAL_CONTENT_LINK or{' '} + EXTERNAL_OFFER) +
    + launchMode + + ExternalLinkLaunchModeAndroid + How the external link is launched
    + linkType + + ExternalLinkTypeAndroid + The type of the external link
    + linkUri + + string + The URI where the external content will be accessed
    + + + DeveloperBillingOptionParamsAndroid + +

    + Parameters for configuring developer billing option in purchase flow + (8.3.0+): +

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeSummary
    + billingProgram + + BillingProgramAndroid + + The billing program (usually EXTERNAL_PAYMENTS) +
    + linkUri + + String + URL where the external payment will be processed
    + launchMode + + DeveloperBillingLaunchModeAndroid + How to launch the external payment link
    + + + DeveloperBillingLaunchModeAndroid + +

    How the external payment URL is launched:

    + + + + + + + + + + + + + + + + + +
    NameSummary
    + LAUNCH_IN_EXTERNAL_BROWSER_OR_APP + + Google Play launches the link in a browser or eligible app +
    + CALLER_WILL_LAUNCH_LINK + + Your app handles launching the link after Play returns control +
    + + + DeveloperProvidedBillingDetailsAndroid + +

    Details received when user selects developer billing (8.3.0+):

    + + + + + + + + + + + + + + + +
    NameTypeSummary
    + externalTransactionToken + + String + + Token to report external transaction to Google (must report + within 24 hours) +
    + + + Usage Example + + + {{ + typescript: ( + {`import { + enableBillingProgramAndroid, + isBillingProgramAvailableAndroid, + requestPurchase, + developerProvidedBillingListenerAndroid, +} from 'expo-iap'; + +// Enable External Payments before initConnection +enableBillingProgramAndroid('EXTERNAL_PAYMENTS'); + +await initConnection(); + +// Listen for developer billing selection +developerProvidedBillingListenerAndroid((details) => { + console.log('Token:', details.externalTransactionToken); + // Report token to Google via your backend within 24 hours +}); + +// Check availability (Japan only) +const result = await isBillingProgramAvailableAndroid('EXTERNAL_PAYMENTS'); +if (result.isAvailable) { + // Purchase with developer billing option + await requestPurchase({ + google: { + skus: ['product_id'], + developerBillingOption: { + billingProgram: 'EXTERNAL_PAYMENTS', + linkUri: 'https://your-site.com/checkout', + launchMode: 'LAUNCH_IN_EXTERNAL_BROWSER_OR_APP', + }, + }, + }); +}`} + ), + kotlin: ( + {`import dev.hyo.openiap.store.OpenIapStore +import dev.hyo.openiap.* + +val iapStore = OpenIapStore(context) + +// Enable External Payments before initConnection +iapStore.enableBillingProgram(BillingProgramAndroid.ExternalPayments) + +iapStore.initConnection(null) + +// Listen for developer billing selection +iapStore.addDeveloperProvidedBillingListener { details -> + Log.d("IAP", "Token: \${details.externalTransactionToken}") + // Report token to Google via your backend within 24 hours +} + +// Check availability (Japan only) +val result = iapStore.isBillingProgramAvailable( + BillingProgramAndroid.ExternalPayments +) +if (result.isAvailable) { + // Purchase with developer billing option + val props = RequestPurchaseProps( + request = RequestPurchaseProps.Request.Purchase( + RequestPurchasePropsByPlatforms( + google = RequestPurchaseAndroidProps( + skus = listOf("product_id"), + developerBillingOption = DeveloperBillingOptionParamsAndroid( + billingProgram = BillingProgramAndroid.ExternalPayments, + linkUri = "https://your-site.com/checkout", + launchMode = DeveloperBillingLaunchModeAndroid.LaunchInExternalBrowserOrApp + ) + ) + ) + ), + type = ProductQueryType.InApp + ) + iapStore.requestPurchase(props) +}`} + ), + dart: ( + {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart'; + +// Enable External Payments before initConnection +FlutterInappPurchase.instance.enableBillingProgramAndroid( + BillingProgramAndroid.externalPayments, +); + +await FlutterInappPurchase.instance.initConnection(); + +// Listen for developer billing selection +FlutterInappPurchase.developerProvidedBillingStream.listen((details) { + print('Token: \${details.externalTransactionToken}'); + // Report token to Google via your backend within 24 hours +}); + +// Check availability (Japan only) +final result = await FlutterInappPurchase.instance + .isBillingProgramAvailableAndroid(BillingProgramAndroid.externalPayments); +if (result.isAvailable) { + // Purchase with developer billing option + await FlutterInappPurchase.instance.requestPurchase( + 'product_id', + developerBillingOption: DeveloperBillingOptionParamsAndroid( + billingProgram: BillingProgramAndroid.externalPayments, + linkUri: 'https://your-site.com/checkout', + launchMode: DeveloperBillingLaunchModeAndroid.launchInExternalBrowserOrApp, + ), + ); +}`} + ), + gdscript: ( + {`# Enable External Payments before initConnection +iap.enable_billing_program_android(BillingProgramAndroid.EXTERNAL_PAYMENTS) + +await iap.init_connection() + +# Listen for developer billing selection +func _on_developer_provided_billing(details: DeveloperProvidedBillingDetailsAndroid): + print("Token: %s" % details.external_transaction_token) + # Report token to Google via your backend within 24 hours + +iap.developer_provided_billing.connect(_on_developer_provided_billing) + +# Check availability (Japan only) +var result = await iap.is_billing_program_available_android( + BillingProgramAndroid.EXTERNAL_PAYMENTS +) +if result.is_available: + # Purchase with developer billing option + var props = RequestPurchaseProps.new() + props.request = RequestPurchasePropsByPlatforms.new() + props.request.google = RequestPurchaseAndroidProps.new() + props.request.google.skus = ["product_id"] + props.request.google.developer_billing_option = DeveloperBillingOptionParamsAndroid.new() + props.request.google.developer_billing_option.billing_program = BillingProgramAndroid.EXTERNAL_PAYMENTS + props.request.google.developer_billing_option.link_uri = "https://your-site.com/checkout" + props.request.google.developer_billing_option.launch_mode = DeveloperBillingLaunchModeAndroid.LAUNCH_IN_EXTERNAL_BROWSER_OR_APP + props.type = ProductQueryType.IN_APP + await iap.request_purchase(props)`} + ), + }} + + +
    +

    + Token Reporting: When a user completes a purchase + through developer billing, you must report the{' '} + externalTransactionToken to Google Play within 24 + hours. See{' '} + + External Payments documentation + {' '} + for complete implementation details. +

    +
    +
    +
    + ); +} + +export default BillingPrograms; diff --git a/packages/docs/src/pages/docs/types/discount-offer.tsx b/packages/docs/src/pages/docs/types/discount-offer.tsx new file mode 100644 index 00000000..8b27bc0b --- /dev/null +++ b/packages/docs/src/pages/docs/types/discount-offer.tsx @@ -0,0 +1,404 @@ +import { Link } from 'react-router-dom'; +import AnchorLink from '../../../components/AnchorLink'; +import CodeBlock from '../../../components/CodeBlock'; +import LanguageTabs from '../../../components/LanguageTabs'; +import SEO from '../../../components/SEO'; +import { useScrollToHash } from '../../../hooks/useScrollToHash'; + +function DiscountOffer() { + useScrollToHash(); + + return ( +
    + +

    DiscountOffer

    +
    + + DiscountOffer + +

    + 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 +}`} + ), + }} + +
    +
    + ); +} + +export default DiscountOffer; diff --git a/packages/docs/src/pages/docs/types/external-purchase-link.tsx b/packages/docs/src/pages/docs/types/external-purchase-link.tsx new file mode 100644 index 00000000..ad283d6e --- /dev/null +++ b/packages/docs/src/pages/docs/types/external-purchase-link.tsx @@ -0,0 +1,499 @@ +import { Link } from 'react-router-dom'; +import AnchorLink from '../../../components/AnchorLink'; +import CodeBlock from '../../../components/CodeBlock'; +import LanguageTabs from '../../../components/LanguageTabs'; +import SEO from '../../../components/SEO'; +import { useScrollToHash } from '../../../hooks/useScrollToHash'; + +function ExternalPurchaseLink() { + useScrollToHash(); + + return ( +
    + +

    External Purchase Link Types

    +
    + + External Purchase Link (iOS) + +

    + iOS-specific feature for redirecting users to an external website for + payment using Apple's StoreKit ExternalPurchase API. + Available from iOS 17.4+ (notice sheet) and iOS 18.2+ (custom links). +

    + +
    +

    + Important: External purchase links bypass StoreKit + completely. No purchaseUpdatedListener will fire. You + must implement deep links and server-side verification. +

    +
    + + + External Purchase APIs + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    APIDescriptionAvailability
    + canPresentExternalPurchaseNoticeIOS + Check if external purchase notice sheet can be presentediOS 17.4+
    + presentExternalPurchaseNoticeSheetIOS + + Present Apple's compliance notice sheet (required before + external purchase) + iOS 17.4+
    + presentExternalPurchaseLinkIOS + Open external purchase URL in SafariiOS 18.2+
    + + + Types + + +

    ExternalPurchaseNoticeResultIOS

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeSummary
    + result + + ExternalPurchaseNoticeAction + + User action on the notice sheet (continue or{' '} + dismissed) +
    + error + + string? + Optional error message if presentation failed
    + externalPurchaseToken + + string? + + External purchase token returned when{' '} + result === 'continue'. Report to Apple's External + Purchase Server API. +
    + +

    ExternalPurchaseLinkResultIOS

    + + + + + + + + + + + + + + + + + + + + +
    NameTypeSummary
    + success + + boolean + Whether the user completed the external purchase flow
    + error + + string? + Optional error message if presentation failed
    + + + {{ + typescript: ( + {`// Result from presenting external purchase link +interface ExternalPurchaseLinkResultIOS { + success: boolean; + error?: string; +} + +// Result from presenting notice sheet (iOS 17.4+) +interface ExternalPurchaseNoticeResultIOS { + result: ExternalPurchaseNoticeAction; + error?: string; + // External purchase token returned when result === 'continue' + // (must be reported to Apple's External Purchase Server API) + externalPurchaseToken?: string; +} + +// User action on notice sheet +type ExternalPurchaseNoticeAction = 'continue' | 'dismissed';`} + ), + swift: ( + {`// Result from presenting external purchase link +struct ExternalPurchaseLinkResultIOS { + let success: Bool + let error: String? +} + +// Result from presenting notice sheet (iOS 17.4+) +struct ExternalPurchaseNoticeResultIOS { + let result: ExternalPurchaseNoticeAction + let error: String? + // External purchase token returned when result == .continue + let externalPurchaseToken: String? +} + +// User action on notice sheet +enum ExternalPurchaseNoticeAction: String { + case \`continue\` = "continue" + case dismissed = "dismissed" +}`} + ), + kotlin: ( + {`// Result from presenting external purchase link (iOS-only via KMP) +data class ExternalPurchaseLinkResultIOS( + val success: Boolean, + val error: String? = null +) + +// Result from presenting notice sheet (iOS 17.4+) +data class ExternalPurchaseNoticeResultIOS( + val result: ExternalPurchaseNoticeAction, + val error: String? = null, + // External purchase token returned when result == Continue + val externalPurchaseToken: String? = null +) + +// User action on notice sheet +enum class ExternalPurchaseNoticeAction(val rawValue: String) { + Continue("continue"), + Dismissed("dismissed") +}`} + ), + dart: ( + {`// Result from presenting external purchase link +class ExternalPurchaseLinkResultIOS { + final bool success; + final String? error; +} + +// Result from presenting notice sheet (iOS 17.4+) +class ExternalPurchaseNoticeResultIOS { + final ExternalPurchaseNoticeAction result; + final String? error; + // External purchase token returned when result == continue + final String? externalPurchaseToken; +} + +// User action on notice sheet +enum ExternalPurchaseNoticeAction { + \`continue\`('continue'), + dismissed('dismissed'); +}`} + ), + gdscript: ( + {`# Result from presenting external purchase link +class_name ExternalPurchaseLinkResultIOS +var success: bool +var error: String # optional + +# Result from presenting notice sheet (iOS 17.4+) +class_name ExternalPurchaseNoticeResultIOS +var result: int # ExternalPurchaseNoticeAction +var error: String # optional +var external_purchase_token: String # optional + +# User action on notice sheet +enum ExternalPurchaseNoticeAction { + CONTINUE, + DISMISSED +}`} + ), + }} + + + + External Purchase Flow + +

    The external purchase flow requires 3 steps for Apple compliance:

    +
      +
    1. + Check availability - Verify the device supports + external purchase +
    2. +
    3. + Present notice sheet - Show Apple's required + disclosure +
    4. +
    5. + Open external link - Redirect to your payment page +
    6. +
    + + + Complete Example + + + {{ + typescript: ( + {`import { + canPresentExternalPurchaseNoticeIOS, + presentExternalPurchaseNoticeSheetIOS, + presentExternalPurchaseLinkIOS, +} from 'expo-iap'; + +async function handleExternalPurchase(externalUrl: string) { + // Step 1: Check if external purchase is available + const canPresent = await canPresentExternalPurchaseNoticeIOS(); + if (!canPresent) { + console.log('External purchase not available on this device'); + return; + } + + // Step 2: Present Apple's compliance notice sheet + const noticeResult = await presentExternalPurchaseNoticeSheetIOS(); + if (noticeResult.result === 'dismissed') { + console.log('User dismissed the notice sheet'); + return; + } + + // Step 3: Open external purchase link + const linkResult = await presentExternalPurchaseLinkIOS(externalUrl); + if (linkResult.success) { + console.log('User redirected to external payment'); + // Implement deep linking to handle return from payment + } else { + console.error('Failed:', linkResult.error); + } +}`} + ), + swift: ( + {`import OpenIap + +@available(iOS 18.2, *) +func handleExternalPurchase(externalUrl: String) async { + do { + // Step 1: Check if external purchase is available + let canPresent = try await OpenIapModule.shared.canPresentExternalPurchaseNoticeIOS() + guard canPresent else { + print("External purchase not available on this device") + return + } + + // Step 2: Present Apple's compliance notice sheet + let noticeResult = try await OpenIapModule.shared.presentExternalPurchaseNoticeSheetIOS() + guard noticeResult.result == .continue else { + print("User dismissed the notice sheet") + return + } + + // Step 3: Open external purchase link + let linkResult = try await OpenIapModule.shared.presentExternalPurchaseLinkIOS(externalUrl) + if linkResult.success { + print("User redirected to external payment") + // Implement deep linking to handle return from payment + } else if let error = linkResult.error { + print("Failed: \\(error)") + } + } catch { + print("External purchase error: \\(error)") + } +}`} + ), + kotlin: ( + {`import io.github.hyochan.kmpiap.kmpIapInstance +import io.github.hyochan.kmpiap.ExternalPurchaseNoticeAction + +// External purchase is iOS-only. For iOS targets in KMP: +suspend fun handleExternalPurchase(externalUrl: String) { + // Step 1: Check if external purchase is available + val canPresent = kmpIapInstance.canPresentExternalPurchaseNoticeIOS() + if (!canPresent) { + println("External purchase not available on this device") + return + } + + // Step 2: Present Apple's compliance notice sheet + val noticeResult = kmpIapInstance.presentExternalPurchaseNoticeSheetIOS() + if (noticeResult.result == ExternalPurchaseNoticeAction.Dismissed) { + println("User dismissed the notice sheet") + return + } + + // Step 3: Open external purchase link + val linkResult = kmpIapInstance.presentExternalPurchaseLinkIOS(externalUrl) + if (linkResult.success) { + println("User redirected to external payment") + // Implement deep linking to handle return from payment + } else { + println("Failed: \${linkResult.error}") + } +} + +// For Android: Use alternative billing APIs instead +// See: checkAlternativeBillingAvailability, showAlternativeBillingDialog`} + ), + dart: ( + {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart'; + +Future handleExternalPurchase(String externalUrl) async { + final iap = FlutterInappPurchase.instance; + + // Step 1: Check if external purchase is available + final canPresent = await iap.canPresentExternalPurchaseNoticeIOS(); + if (!canPresent) { + print('External purchase not available on this device'); + return; + } + + // Step 2: Present Apple's compliance notice sheet + final noticeResult = await iap.presentExternalPurchaseNoticeSheetIOS(); + if (noticeResult.result == ExternalPurchaseNoticeAction.dismissed) { + print('User dismissed the notice sheet'); + return; + } + + // Step 3: Open external purchase link + final linkResult = await iap.presentExternalPurchaseLinkIOS(externalUrl); + if (linkResult.success) { + print('User redirected to external payment'); + // Implement deep linking to handle return from payment + } else { + print('Failed: \${linkResult.error}'); + } +}`} + ), + gdscript: ( + {`func handle_external_purchase(external_url: String): + # Step 1: Check if external purchase is available + var can_present = await iap.can_present_external_purchase_notice_ios() + if not can_present: + print("External purchase not available on this device") + return + + # Step 2: Present Apple's compliance notice sheet + var notice_result = await iap.present_external_purchase_notice_sheet_ios() + if notice_result.result == ExternalPurchaseNoticeAction.DISMISSED: + print("User dismissed the notice sheet") + return + + # Step 3: Open external purchase link + var link_result = await iap.present_external_purchase_link_ios(external_url) + if link_result.success: + print("User redirected to external payment") + # Implement deep linking to handle return from payment + else: + print("Failed: %s" % link_result.error)`} + ), + }} + + + + Requirements + + + + + + + + + + + + + + + + + + + + + + + + + + +
    RequirementDetails
    PlatformiOS 17.4+ (notice sheet), iOS 18.2+ (custom links)
    Entitlement + App must have StoreKit external purchase entitlement from Apple +
    Deep LinkingImplement deep linking for app return flow after payment
    Verification + Handle purchase verification on your backend (no StoreKit + receipt) +
    + +
    +

    + Android alternative: For Android, use the + alternative billing APIs ( + + checkAlternativeBillingAvailabilityAndroid + + ,{' '} + + showAlternativeBillingDialogAndroid + + ,{' '} + + createAlternativeBillingTokenAndroid + + ) instead. +

    +
    +
    +
    + ); +} + +export default ExternalPurchaseLink; diff --git a/packages/docs/src/pages/docs/types/index.tsx b/packages/docs/src/pages/docs/types/index.tsx index 03e9c464..b5cb9003 100644 --- a/packages/docs/src/pages/docs/types/index.tsx +++ b/packages/docs/src/pages/docs/types/index.tsx @@ -1,448 +1,224 @@ -import { useState, useEffect, type ChangeEvent } from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; -import { toast } from 'react-toastify'; -import APICard from '../../../components/APICard'; +import { Link } from 'react-router-dom'; +import AnchorLink from '../../../components/AnchorLink'; import SEO from '../../../components/SEO'; -import TLDRBox from '../../../components/TLDRBox'; -import { GQL_RELEASE } from '../../../lib/versioning'; +import { useScrollToHash } from '../../../hooks/useScrollToHash'; -const RELEASE_VERSION = GQL_RELEASE.tag; -const RELEASE_PAGE_URL = GQL_RELEASE.pageUrl; -const RELEASE_DOWNLOAD_PREFIX = GQL_RELEASE.downloadPrefix; - -const SELECT_CARET_ICON = - 'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2212%22%20height%3D%228%22%20viewBox%3D%220%200%2012%208%22%20fill%3D%22none%22%3E%3Cpath%20d%3D%22M1%201l5%205%205-5%22%20stroke%3D%22%23a2a9b0%22%20stroke-width%3D%221.5%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22/%3E%3C/svg%3E'; +interface TypeRow { + to: string; + name: string; + description: string; +} -const SPEC_ARCHIVES = [ +const COMMON_TYPES: TypeRow[] = [ + { + to: '/docs/types/product', + name: 'Product', + description: 'Base product type with platform-specific extensions.', + }, + { + to: '/docs/types/subscription-product', + name: 'SubscriptionProduct', + description: 'Subscription product with billing periods and offers.', + }, + { + to: '/docs/types/storefront', + name: 'Storefront', + description: 'Store region info from getStorefront().', + }, + { + to: '/docs/types/purchase', + name: 'Purchase', + description: 'Transaction record from a successful purchase.', + }, { - filename: 'openiap-typescript.zip', - label: 'TypeScript definitions', - size: '4.71 KB', - sha256: '0dfce08584584ce6d6c1106640338c8519b459bd5e304a16d6967afda3fe01b7', + to: '/docs/types/active-subscription', + name: 'ActiveSubscription', + description: 'Active subscription with renewal status.', }, { - filename: 'openiap-swift.zip', - label: 'Swift definitions', - size: '4.95 KB', - sha256: 'f4895459f08293e362bec537db56d584784540808c9af1abe8d77e59bf05f2da', + to: '/docs/types/product-request', + name: 'ProductRequest', + description: 'Input for fetchProducts (skus + type).', }, { - filename: 'openiap-kotlin.zip', - label: 'Kotlin definitions', - size: '10.3 KB', - sha256: 'a002b634270c029b52401c3a529b7d989d57df875310f077ae42328c7312813f', + to: '/docs/types/request-purchase-props', + name: 'RequestPurchaseProps', + description: 'Discriminated union for one-time purchases or subscriptions.', }, { - filename: 'openiap-dart.zip', - label: 'Dart definitions', - size: '11.9 KB', - sha256: 'ee89e90a7a8aee3400bd9cc97bf1e9c5bef93a93b27a8da03490f1625b0d125e', + to: '/docs/types/discount-offer', + name: 'DiscountOffer', + description: 'Cross-platform discount offer details.', }, { - filename: 'openiap-gdscript.zip', - label: 'GDScript definitions', - size: '36.9 KB', - sha256: '', + to: '/docs/types/subscription-offer', + name: 'SubscriptionOffer', + description: 'Cross-platform subscription offer details.', }, ]; -// Redirect map for legacy anchor links -const legacyAnchorRedirects: Record = { - // Product types - product: '/docs/types/product#product', - 'product-common': '/docs/types/product#product-common', - 'product-platform': '/docs/types/product#product-platform', - 'product-subscription': '/docs/types/product#product-subscription', - 'subscription-product-common': - '/docs/types/product#subscription-product-common', - 'subscription-product-platform': - '/docs/types/product#subscription-product-platform', - 'unified-platform-types': '/docs/types/product#unified-platform-types', - 'store-discriminators': '/docs/types/product#store-discriminators', - 'union-types': '/docs/types/product#union-types', - storefront: '/docs/types/product#storefront', - // Purchase types - purchase: '/docs/types/purchase#purchase', - 'purchase-state': '/docs/types/purchase#purchase-state', - 'purchase-common': '/docs/types/purchase#purchase-common', - 'purchase-platform': '/docs/types/purchase#purchase-platform', - 'renewal-info-ios': '/docs/types/purchase#renewal-info-ios', - 'active-subscription': '/docs/types/purchase#active-subscription', - 'active-subscription-common': - '/docs/types/purchase#active-subscription-common', - 'active-subscription-platform': - '/docs/types/purchase#active-subscription-platform', - 'active-subscription-example': - '/docs/types/purchase#active-subscription-example', - // Request types - 'product-request': '/docs/types/request#product-request', - 'product-request-fields': '/docs/types/request#product-request-fields', - 'product-request-example': '/docs/types/request#product-request-example', - 'request-types': '/docs/types/request#request-types', - 'request-purchase-props': '/docs/types/request#request-purchase-props', - 'request-purchase-example': '/docs/types/request#request-purchase-example', - 'request-purchase-props-by-platforms': - '/docs/types/request#request-purchase-props-by-platforms', - 'request-subscription-props-by-platforms': - '/docs/types/request#request-subscription-props-by-platforms', - 'platform-specific-request-props': - '/docs/types/request#platform-specific-request-props', - 'subscription-request-props': - '/docs/types/request#subscription-request-props', - // Alternative billing types - 'alternative-billing-types': - '/docs/types/alternative#alternative-billing-types', - 'alternative-billing-mode-android': - '/docs/types/alternative#alternative-billing-mode-android', - 'init-connection-config': '/docs/types/alternative#init-connection-config', - 'init-connection-example': '/docs/types/alternative#init-connection-example', - 'external-purchase-link': '/docs/types/alternative#external-purchase-link', - 'external-purchase-apis': '/docs/types/alternative#external-purchase-apis', - 'external-purchase-types': '/docs/types/alternative#external-purchase-types', - 'external-purchase-flow': '/docs/types/alternative#external-purchase-flow', - 'external-purchase-example': - '/docs/types/alternative#external-purchase-example', - 'external-purchase-requirements': - '/docs/types/alternative#external-purchase-requirements', - // Verification types - 'purchase-verification-types': - '/docs/types/verification#purchase-verification-types', - 'verify-purchase-props': '/docs/types/verification#verify-purchase-props', - 'verify-purchase-result': '/docs/types/verification#verify-purchase-result', - 'verify-purchase-with-provider-props': - '/docs/types/verification#verify-purchase-with-provider-props', - 'request-verify-purchase-with-iapkit-props': - '/docs/types/verification#request-verify-purchase-with-iapkit-props', - 'request-verify-purchase-with-iapkit-apple-props': - '/docs/types/verification#request-verify-purchase-with-iapkit-apple-props', - 'request-verify-purchase-with-iapkit-google-props': - '/docs/types/verification#request-verify-purchase-with-iapkit-google-props', - 'verify-purchase-with-provider-result': - '/docs/types/verification#verify-purchase-with-provider-result', - 'request-verify-purchase-with-iapkit-result': - '/docs/types/verification#request-verify-purchase-with-iapkit-result', - 'iapkit-purchase-state': '/docs/types/verification#iapkit-purchase-state', - 'iapkit-store': '/docs/types/verification#iapkit-store', - 'purchase-verification-provider': - '/docs/types/verification#purchase-verification-provider', - 'verify-purchase-with-provider-example': - '/docs/types/verification#verify-purchase-with-provider-example', - // Offer types (new standardized types) - 'discount-offer': '/docs/types/offer#discount-offer', - 'subscription-offer': '/docs/types/offer#subscription-offer', - 'discount-offer-type': '/docs/types/offer#discount-offer', - 'subscription-offer-type': '/docs/types/offer#subscription-offer', - // iOS types - 'platform-specific-types': '/docs/types/offer#discount-offer', - discount: '/docs/types/ios#discount', - 'subscription-period-ios': '/docs/types/ios#subscription-period-ios', - 'payment-mode': '/docs/types/ios#payment-mode', - 'subscription-status-ios': '/docs/types/ios#subscription-status-ios', - 'subscription-state-values': '/docs/types/ios#subscription-state-values', - 'expiration-reasons': '/docs/types/ios#expiration-reasons', - 'app-transaction': '/docs/types/ios#app-transaction', - 'app-transaction-fields': '/docs/types/ios#app-transaction-fields', - 'app-transaction-type-definition': - '/docs/types/ios#app-transaction-type-definition', - 'app-transaction-example': '/docs/types/ios#app-transaction-example', - // Android types (subscription-offer redirects to standardized offer page above) - 'subscription-offer-android': '/docs/types/android#subscription-offer', - 'pricing-phase': '/docs/types/android#pricing-phase', - 'recurrence-mode-values': '/docs/types/android#recurrence-mode-values', - 'pricing-phases-android': '/docs/types/android#pricing-phases-android', - 'android-type-example': '/docs/types/android#android-type-example', - 'base-plan-limitation': '/docs/types/android#base-plan-limitation', -}; - -function TypesIndex() { - const location = useLocation(); - const navigate = useNavigate(); - const [selectedArchive, setSelectedArchive] = useState(SPEC_ARCHIVES[0]); +const VALIDATION_TYPES: TypeRow[] = [ + { + to: '/docs/types/verify-purchase', + name: 'VerifyPurchase Types', + description: 'VerifyPurchaseProps and VerifyPurchaseResult definitions.', + }, + { + to: '/docs/types/verify-purchase-with-provider-props', + name: 'VerifyPurchaseWithProviderProps', + description: 'Provider-based verification input (e.g., IAPKit).', + }, + { + to: '/docs/types/verify-purchase-with-provider-result', + name: 'VerifyPurchaseWithProviderResult', + description: 'Provider-based verification response.', + }, +]; - // Redirect legacy anchor links to new paths - useEffect(() => { - const hash = location.hash.slice(1); // Remove '#' - if (hash && legacyAnchorRedirects[hash]) { - navigate(legacyAnchorRedirects[hash], { replace: true }); - } - }, [location.hash, navigate]); +const ALT_BILLING_TYPES: TypeRow[] = [ + { + to: '/docs/types/alternative-billing-types', + name: 'Alternative Billing', + description: 'AlternativeBillingModeAndroid and InitConnectionConfig.', + }, + { + to: '/docs/types/billing-programs', + name: 'Billing Programs', + description: 'Google Play Billing Programs API types (8.2.0+).', + }, + { + to: '/docs/types/external-purchase-link', + name: 'External Purchase Link', + description: 'iOS external purchase link result types.', + }, +]; - const handleChangeArchive = (event: ChangeEvent) => { - const archive = SPEC_ARCHIVES.find( - (item) => item.filename === event.target.value - ); +const IOS_TYPES: TypeRow[] = [ + { + to: '/docs/types/ios/discount-offer-ios', + name: 'DiscountOfferIOS', + description: 'iOS-specific discount offer payload.', + }, + { + to: '/docs/types/ios/discount-ios', + name: 'DiscountIOS', + description: 'iOS discount details (type, payment mode, period).', + }, + { + to: '/docs/types/ios/subscription-period-ios', + name: 'SubscriptionPeriodIOS', + description: 'iOS subscription period units (Day/Week/Month/Year).', + }, + { + to: '/docs/types/ios/payment-mode-ios', + name: 'PaymentMode', + description: 'iOS payment modes for promotional offers.', + }, + { + to: '/docs/types/ios/subscription-status-ios', + name: 'SubscriptionStatusIOS', + description: 'StoreKit 2 subscription status values.', + }, + { + to: '/docs/types/ios/app-transaction-ios', + name: 'AppTransaction', + description: 'iOS 16+ app transaction info.', + }, +]; - if (archive) { - setSelectedArchive(archive); - } - }; +const ANDROID_TYPES: TypeRow[] = [ + { + to: '/docs/types/android/one-time-purchase-offer-detail-android', + name: 'OneTimePurchaseOfferDetailAndroid', + description: 'Android one-time purchase offer details.', + }, + { + to: '/docs/types/android/subscription-offer-android', + name: 'SubscriptionOfferAndroid', + description: 'Android subscription offer (Play Billing).', + }, + { + to: '/docs/types/android/pricing-phase-android', + name: 'PricingPhaseAndroid', + description: 'Android pricing phases (intro / paid / free trial).', + }, +]; - const handleDownloadSelected = () => { - const link = document.createElement('a'); - link.href = `${RELEASE_DOWNLOAD_PREFIX}${selectedArchive.filename}`; - link.download = selectedArchive.filename; - link.rel = 'noreferrer'; - link.style.display = 'none'; - document.body.append(link); - link.click(); - link.remove(); +function TypeTable({ rows }: { rows: TypeRow[] }) { + return ( + + + + + + + + + {rows.map((row) => ( + + + + + ))} + +
    TypeDescription
    + + {row.name} + + {row.description}
    + ); +} - toast.info( - () => ( -
    - - Downloading {selectedArchive.label} ({selectedArchive.size}) from - openiap-gql {RELEASE_VERSION}. - - -
    - ), - { icon: '⬇️' } - ); - }; +function TypesIndex() { + useScrollToHash(); 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 b81f4c0a..00000000 --- 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 00000000..b902322f --- /dev/null +++ b/packages/docs/src/pages/docs/types/ios/app-transaction-ios.tsx @@ -0,0 +1,334 @@ +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 ( +
    + +

    AppTransaction

    +
    + + 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 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 00000000..ba9d20fb --- /dev/null +++ b/packages/docs/src/pages/docs/types/ios/discount-ios.tsx @@ -0,0 +1,91 @@ +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:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    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 00000000..43dcd9ea --- /dev/null +++ b/packages/docs/src/pages/docs/types/ios/discount-offer-ios.tsx @@ -0,0 +1,76 @@ +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. +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    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 00000000..4a3b8169 --- /dev/null +++ b/packages/docs/src/pages/docs/types/ios/payment-mode-ios.tsx @@ -0,0 +1,55 @@ +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:

    + + + + + + + + + + + + + + + + + + + + + +
    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/subscription-period-ios.tsx b/packages/docs/src/pages/docs/types/ios/subscription-period-ios.tsx new file mode 100644 index 00000000..cffa441f --- /dev/null +++ b/packages/docs/src/pages/docs/types/ios/subscription-period-ios.tsx @@ -0,0 +1,44 @@ +import AnchorLink from '../../../../components/AnchorLink'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function SubscriptionPeriodIos() { + useScrollToHash(); + + return ( +
    + +

    SubscriptionPeriodIOS

    +
    + + SubscriptionPeriodIOS + +

    Subscription period units:

    + + + + + + + + + + + + + +
    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 00000000..6862e349 --- /dev/null +++ b/packages/docs/src/pages/docs/types/ios/subscription-status-ios.tsx @@ -0,0 +1,160 @@ +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. +

    + + + + + + + + + + + + + + + + + + +
    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 ed51e222..00000000 --- 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 00000000..06e01dcf --- /dev/null +++ b/packages/docs/src/pages/docs/types/product-request.tsx @@ -0,0 +1,158 @@ +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() + + . +

    + + + 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 e1e6a535..a73b9a7d 100644 --- a/packages/docs/src/pages/docs/types/product.tsx +++ b/packages/docs/src/pages/docs/types/product.tsx @@ -1,55 +1,21 @@ +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 @@ -103,6 +69,12 @@ function TypesProduct() { subscriptions + + + displayName + + Display-friendly product name (optional) + displayPrice @@ -123,6 +95,12 @@ function TypesProduct() { Numeric price value (e.g., 9.99) + + + debugDescription + + Debug-friendly description (optional) + store @@ -134,15 +112,10 @@ function TypesProduct() { - platform{' '} - - (deprecated) - + platform - Use store instead + Deprecated. Use store instead. @@ -202,6 +175,24 @@ function TypesProduct() { promotionalOffers + + + subscriptionOffers + + + Cross-platform array of{' '} + + SubscriptionOffer + {' '} + — unified across iOS/Android. + + + + + jsonRepresentationIOS + + Raw StoreKit 2 JWS payload as a JSON string. + @@ -280,155 +271,28 @@ function TypesProduct() { - - - - ), - }} - -
    - -
    - - 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) -
    - subscriptionPeriodNumberIOS + Cross-platform array of{' '} + + DiscountOffer + {' '} + — unified discount metadata. Number of units in a subscription period
    - subscriptionPeriodUnitIOS - Period unit (Day, Week, Month, Year)
    - - ), - android: ( - <> - - SubscriptionProductAndroid - -

    Additional fields available on Android subscriptions:

    - - - - - - - @@ -438,150 +302,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 + subscriptionOffers - 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 17074155..006d470c 100644 --- a/packages/docs/src/pages/docs/types/purchase.tsx +++ b/packages/docs/src/pages/docs/types/purchase.tsx @@ -1,56 +1,20 @@ 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 @@ -65,6 +29,7 @@ function TypesPurchase() { PurchaseState

    Enum representing the current state of a purchase:

    + @@ -107,7 +72,7 @@ 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 +137,10 @@ function TypesPurchase() { @@ -210,7 +170,7 @@ function TypesPurchase() { "premium"). On iOS: productId (e.g., "com.example.premium_monthly"). ⚠️ Android: May be inaccurate for multi-plan subscriptions. See{' '} - + limitation . @@ -323,6 +283,20 @@ 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 @@ -487,6 +461,15 @@ function TypesPurchase() { "WIN_BACK"
    + jsonRepresentation + + Raw JWS representation of the StoreKit renewal info — + useful for server-side validation. +
    @@ -745,250 +728,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 68% rename from packages/docs/src/pages/docs/types/request.tsx rename to packages/docs/src/pages/docs/types/request-purchase-props.tsx index 60b0beeb..bc2fd957 100644 --- a/packages/docs/src/pages/docs/types/request.tsx +++ b/packages/docs/src/pages/docs/types/request-purchase-props.tsx @@ -1,212 +1,63 @@ +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() + + .

    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 +66,27 @@ var all_products = await iap.fetch_products(all_request)`}type + + + + + + @@ -229,7 +100,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 +109,7 @@ await requestPurchase({ // Subscription purchase await requestPurchase({ - params: { + request: { apple: { sku: 'monthly_sub' }, google: { skus: ['monthly_sub'] } }, @@ -362,28 +233,18 @@ await iap.request_purchase(subs_props)`} @@ -419,28 +280,18 @@ await iap.request_purchase(subs_props)`} @@ -585,6 +436,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 +467,46 @@ 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 +534,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 +553,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 +586,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 00000000..48713d3a --- /dev/null +++ b/packages/docs/src/pages/docs/types/storefront.tsx @@ -0,0 +1,63 @@ +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 + +

    + 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 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 00000000..543c38e5 --- /dev/null +++ b/packages/docs/src/pages/docs/types/subscription-offer.tsx @@ -0,0 +1,539 @@ +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). +

    + + + 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 00000000..04f3c7ba --- /dev/null +++ b/packages/docs/src/pages/docs/types/subscription-product.tsx @@ -0,0 +1,340 @@ +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. +

    + + + Common Fields + +

    + Inherits all{' '} + + Product common fields + + . +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeSummary
    + id + + string + Unique product identifier
    + title + + string + Localized product title
    + description + + string + Localized description
    + type + + "subs" + + Always "subs" for subscription products +
    + displayName + + string? + Display-friendly product name (optional)
    + displayPrice + + string + Formatted price with currency symbol
    + currency + + string + ISO 4217 currency code
    + price + + number? + Numeric price value
    + debugDescription + + string? + Debug-friendly description (optional)
    + platform + + IapPlatform + + Deprecated. Use store instead. +
    + + + Platform-Specific Fields + + + {{ + ios: ( + <> + + ProductSubscriptionIOS + +

    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 + + 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 (e.g.,{' '} + auto-renewable-subscription) +
    + displayNameIOS + iOS-specific display name
    + isFamilyShareableIOS + Whether the subscription supports Family Sharing
    + jsonRepresentationIOS + Raw StoreKit 2 JWS payload
    + subscriptionOffers + + Cross-platform array of{' '} + + SubscriptionOffer + +
    + + ), + 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+ +
    + discountOffers + + Cross-platform array of{' '} + + DiscountOffer + +
    + subscriptionOffers + + Cross-platform array of{' '} + + SubscriptionOffer + +
    + 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 c49a96cb..00000000 --- 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 00000000..0fc53fa3 --- /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 00000000..d1d94d03 --- /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 00000000..aa76db22 --- /dev/null +++ b/packages/docs/src/pages/docs/types/verify-purchase.tsx @@ -0,0 +1,293 @@ +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. +

    + + + 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 f06ce285..22aa98ea 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 52e6b899..9d847974 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 fb251994..d155ac0d 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() @@ -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 87b43eed..bcc70a23 100644 --- a/packages/docs/src/pages/introduction.tsx +++ b/packages/docs/src/pages/introduction.tsx @@ -75,19 +75,19 @@ function Introduction() {
    ); diff --git a/packages/docs/src/pages/docs/types/billing-programs.tsx b/packages/docs/src/pages/docs/types/billing-programs.tsx index 3be58b2b..d7fa7bfc 100644 --- a/packages/docs/src/pages/docs/types/billing-programs.tsx +++ b/packages/docs/src/pages/docs/types/billing-programs.tsx @@ -354,16 +354,16 @@ function BillingPrograms() { {{ typescript: ( {`import { - enableBillingProgramAndroid, + initConnection, isBillingProgramAvailableAndroid, requestPurchase, developerProvidedBillingListenerAndroid, } from 'expo-iap'; -// Enable External Payments before initConnection -enableBillingProgramAndroid('EXTERNAL_PAYMENTS'); - -await initConnection(); +// Enable External Payments via InitConnectionConfig +await initConnection({ + enableBillingProgramAndroid: 'external-payments', +}); // Listen for developer billing selection developerProvidedBillingListenerAndroid((details) => { @@ -393,10 +393,12 @@ import dev.hyo.openiap.* val iapStore = OpenIapStore(context) -// Enable External Payments before initConnection -iapStore.enableBillingProgram(BillingProgramAndroid.ExternalPayments) - -iapStore.initConnection(null) +// Enable External Payments via InitConnectionConfig +iapStore.initConnection( + InitConnectionConfig( + enableBillingProgramAndroid = BillingProgramAndroid.ExternalPayments + ) +) // Listen for developer billing selection iapStore.addDeveloperProvidedBillingListener { details -> @@ -431,13 +433,13 @@ if (result.isAvailable) { dart: ( {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart'; -// Enable External Payments before initConnection -FlutterInappPurchase.instance.enableBillingProgramAndroid( - BillingProgramAndroid.externalPayments, +// Enable External Payments via InitConnectionConfig +await FlutterInappPurchase.instance.initConnection( + config: InitConnectionConfig( + enableBillingProgramAndroid: BillingProgramAndroid.externalPayments, + ), ); -await FlutterInappPurchase.instance.initConnection(); - // Listen for developer billing selection FlutterInappPurchase.developerProvidedBillingStream.listen((details) { print('Token: \${details.externalTransactionToken}'); @@ -460,10 +462,10 @@ if (result.isAvailable) { }`} ), gdscript: ( - {`# Enable External Payments before initConnection -iap.enable_billing_program_android(BillingProgramAndroid.EXTERNAL_PAYMENTS) - -await iap.init_connection() + {`# Enable External Payments via InitConnectionConfig +var config = InitConnectionConfig.new() +config.enable_billing_program_android = BillingProgramAndroid.EXTERNAL_PAYMENTS +await iap.init_connection(config) # Listen for developer billing selection func _on_developer_provided_billing(details: DeveloperProvidedBillingDetailsAndroid): diff --git a/packages/docs/src/pages/docs/types/discount-offer.tsx b/packages/docs/src/pages/docs/types/discount-offer.tsx index 8b27bc0b..d73317f3 100644 --- a/packages/docs/src/pages/docs/types/discount-offer.tsx +++ b/packages/docs/src/pages/docs/types/discount-offer.tsx @@ -188,7 +188,7 @@ function DiscountOffer() { preorderDetailsAndroid - + PreorderDetailsAndroid @@ -199,7 +199,7 @@ function DiscountOffer() { rentalDetailsAndroid - + RentalDetailsAndroid diff --git a/packages/docs/src/pages/docs/types/ios/discount-ios.tsx b/packages/docs/src/pages/docs/types/ios/discount-ios.tsx index ba9d20fb..1535f4ca 100644 --- a/packages/docs/src/pages/docs/types/ios/discount-ios.tsx +++ b/packages/docs/src/pages/docs/types/ios/discount-ios.tsx @@ -1,3 +1,4 @@ +import { Link } from 'react-router-dom'; import AnchorLink from '../../../../components/AnchorLink'; import SEO from '../../../../components/SEO'; import { useScrollToHash } from '../../../../hooks/useScrollToHash'; @@ -20,7 +21,7 @@ function DiscountIos() {

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

    Discount info returned as part of product details:

    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 index 43dcd9ea..e44f73c9 100644 --- a/packages/docs/src/pages/docs/types/ios/discount-offer-ios.tsx +++ b/packages/docs/src/pages/docs/types/ios/discount-offer-ios.tsx @@ -1,3 +1,4 @@ +import { Link } from 'react-router-dom'; import AnchorLink from '../../../../components/AnchorLink'; import SEO from '../../../../components/SEO'; import { useScrollToHash } from '../../../../hooks/useScrollToHash'; @@ -20,7 +21,7 @@ function DiscountOfferIos() {

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

    diff --git a/packages/docs/src/pages/docs/types/product.tsx b/packages/docs/src/pages/docs/types/product.tsx index a73b9a7d..1f1332b4 100644 --- a/packages/docs/src/pages/docs/types/product.tsx +++ b/packages/docs/src/pages/docs/types/product.tsx @@ -232,7 +232,7 @@ function Product() { limitedQuantityInfo,{' '} preorderDetailsAndroid,{' '} rentalDetailsAndroid. See{' '} - Discounts. + Discounts. Requires{' '} Transaction objects on successful purchases, so iOS purchases always have{' '} Purchased state. See{' '} - + release notes - {' '} + {' '} for details.

    @@ -170,9 +171,9 @@ function Purchase() { "premium"). On iOS: productId (e.g., "com.example.premium_monthly"). ⚠️ Android: May be inaccurate for multi-plan subscriptions. See{' '} - + limitation - + . diff --git a/packages/docs/src/pages/docs/types/storefront.tsx b/packages/docs/src/pages/docs/types/storefront.tsx index 48713d3a..047668d2 100644 --- a/packages/docs/src/pages/docs/types/storefront.tsx +++ b/packages/docs/src/pages/docs/types/storefront.tsx @@ -9,10 +9,10 @@ function Storefront() { return (

    Storefront

    @@ -20,34 +20,39 @@ function Storefront() { Storefront

    - Represents the user's App Store or Play Store region, returned by{' '} + 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() .

    +

    Return shape

    - + + -
    NameType Summary
    - StorefrontCode + String! + + ISO 3166-1 alpha-2 country code (e.g. "US",{' '} + "KR", "JP"). Empty string when the + storefront cannot be determined. 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 From 0f66b353d01abde0d98290398033d27668feea2b Mon Sep 17 00:00:00 2001 From: hyochan Date: Sat, 25 Apr 2026 15:29:25 +0900 Subject: [PATCH 03/41] =?UTF-8?q?docs(review):=20round=202=20=E2=80=94=20C?= =?UTF-8?q?opilot=20follow-up=20nits?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - apis/init-connection.tsx: stop redefining InitConnectionConfig inline with a stale shape; link to the canonical type page and use the enableBillingProgramAndroid example so the page is no longer the source of truth for InitConnectionConfig fields - apis/fetch-products.tsx + features/purchase.tsx + features/discount.tsx + setup/{expo,react-native}.tsx + apis/request-purchase.tsx + types/request-purchase-props.tsx: align all 'inapp' literals on the generated TS union ('in-app') so copy/paste examples don't fail - types/subscription-product.tsx: drop the duplicated Product common fields table — link to the Product page for inherited fields and only document the 'subs' override on this page (avoids schema drift) - components/MenuDropdown.tsx: replace maxHeight: 'none' with a numeric cap (9999px) so CSS open/close transitions interpolate cleanly even with nested submenus, and add aria-expanded / aria-controls / type= "button" to both the parent and SubMenu toggle buttons - features/discount.tsx: convert remaining raw internal link to from react-router-dom for SPA-consistent navigation Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/docs/src/components/MenuDropdown.tsx | 24 ++++- .../src/pages/docs/apis/init-connection.tsx | 17 ++-- .../src/pages/docs/apis/request-purchase.tsx | 4 +- .../docs/src/pages/docs/features/discount.tsx | 3 +- .../docs/src/pages/docs/features/purchase.tsx | 4 +- packages/docs/src/pages/docs/setup/expo.tsx | 2 +- .../src/pages/docs/setup/react-native.tsx | 2 +- .../pages/docs/types/subscription-product.tsx | 99 +++---------------- packages/docs/src/pages/introduction.tsx | 4 +- 9 files changed, 51 insertions(+), 108 deletions(-) diff --git a/packages/docs/src/components/MenuDropdown.tsx b/packages/docs/src/components/MenuDropdown.tsx index b7d1c14e..2cc202a5 100644 --- a/packages/docs/src/components/MenuDropdown.tsx +++ b/packages/docs/src/components/MenuDropdown.tsx @@ -1,4 +1,4 @@ -import { useState, useRef, useEffect } from 'react'; +import { useId, useState, useRef, useEffect } from 'react'; import { NavLink, useLocation, useNavigate } from 'react-router-dom'; export interface MenuItem { @@ -34,6 +34,7 @@ function SubMenu({ group, onItemClick }: SubMenuProps) { const contentRef = useRef(null); const [height, setHeight] = useState(0); const location = useLocation(); + const submenuContentId = useId(); const isAnyChildActive = group.items.some( (item) => location.pathname === item.to @@ -61,23 +62,30 @@ function SubMenu({ group, onItemClick }: SubMenuProps) { className={`menu-dropdown-header ${isAnyChildActive ? 'group-active' : ''}`} >

    @@ -154,6 +163,7 @@ export function MenuDropdown({ className={`menu-dropdown-header ${isTitleActive ? 'active' : isChildActive ? 'group-active' : ''}`} >
    diff --git a/packages/docs/src/pages/docs/apis/init-connection.tsx b/packages/docs/src/pages/docs/apis/init-connection.tsx index 36c5ed93..b4fe9a6d 100644 --- a/packages/docs/src/pages/docs/apis/init-connection.tsx +++ b/packages/docs/src/pages/docs/apis/init-connection.tsx @@ -25,11 +25,7 @@ function InitConnection() { {{ typescript: ( - {`initConnection(config?: InitConnectionConfig): Promise - -interface InitConnectionConfig { - alternativeBillingModeAndroid?: 'user-choice' | 'alternative-only'; -}`} + {`initConnection(config?: InitConnectionConfig): Promise`} ), swift: ( {`func initConnection() async throws -> Bool`} @@ -58,9 +54,9 @@ interface InitConnectionConfig { // Standard connection await initConnection(); -// Android with user choice billing +// Android with a billing program (preferred — see InitConnectionConfig) await initConnection({ - alternativeBillingModeAndroid: 'user-choice' + enableBillingProgramAndroid: 'external-offer', });`} ), swift: ( @@ -110,10 +106,13 @@ var success = await iap.init_connection(config)`}

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

    ); diff --git a/packages/docs/src/pages/docs/apis/request-purchase.tsx b/packages/docs/src/pages/docs/apis/request-purchase.tsx index 4583628c..d574e48d 100644 --- a/packages/docs/src/pages/docs/apis/request-purchase.tsx +++ b/packages/docs/src/pages/docs/apis/request-purchase.tsx @@ -28,7 +28,7 @@ function RequestPurchase() { {`requestPurchase(props: RequestPurchaseProps): Promise type RequestPurchaseProps = - | { request: RequestPurchasePropsByPlatforms; type: 'inapp' } + | { request: RequestPurchasePropsByPlatforms; type: 'in-app' } | { request: RequestSubscriptionPropsByPlatforms; type: 'subs' }`} ), swift: ( @@ -61,7 +61,7 @@ await requestPurchase({ apple: { sku: 'com.app.premium' }, google: { skus: ['com.app.premium'] }, }, - type: 'inapp', + type: 'in-app', }); // Subscription diff --git a/packages/docs/src/pages/docs/features/discount.tsx b/packages/docs/src/pages/docs/features/discount.tsx index 996f1b11..bff69f84 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,7 +34,7 @@ function Discount() {

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

    diff --git a/packages/docs/src/pages/docs/features/purchase.tsx b/packages/docs/src/pages/docs/features/purchase.tsx index 6181f3b8..cd657e07 100644 --- a/packages/docs/src/pages/docs/features/purchase.tsx +++ b/packages/docs/src/pages/docs/features/purchase.tsx @@ -376,7 +376,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) { @@ -1199,7 +1199,7 @@ function PurchaseProvider({ children }: { children: React.ReactNode }) { // Fetch products const items = await fetchProducts({ skus: PRODUCT_IDS, - type: 'inapp', + type: 'in-app', }); setProducts(items); diff --git a/packages/docs/src/pages/docs/setup/expo.tsx b/packages/docs/src/pages/docs/setup/expo.tsx index 6e6b4578..19387201 100644 --- a/packages/docs/src/pages/docs/setup/expo.tsx +++ b/packages/docs/src/pages/docs/setup/expo.tsx @@ -399,7 +399,7 @@ function Store() { apple: { sku: product.productId }, google: { skus: [product.productId] }, }, - type: 'inapp', + type: 'in-app', }) } /> diff --git a/packages/docs/src/pages/docs/setup/react-native.tsx b/packages/docs/src/pages/docs/setup/react-native.tsx index b3b04fce..8bb4c98b 100644 --- a/packages/docs/src/pages/docs/setup/react-native.tsx +++ b/packages/docs/src/pages/docs/setup/react-native.tsx @@ -250,7 +250,7 @@ function Store() { apple: { sku: item.productId }, google: { skus: [item.productId] }, }, - type: 'inapp', + type: 'in-app', }) } /> diff --git a/packages/docs/src/pages/docs/types/subscription-product.tsx b/packages/docs/src/pages/docs/types/subscription-product.tsx index 04f3c7ba..ab9203ba 100644 --- a/packages/docs/src/pages/docs/types/subscription-product.tsx +++ b/packages/docs/src/pages/docs/types/subscription-product.tsx @@ -30,11 +30,16 @@ function SubscriptionProduct() { Common Fields

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

    @@ -46,33 +51,6 @@ function SubscriptionProduct() { - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/docs/src/pages/introduction.tsx b/packages/docs/src/pages/introduction.tsx index bcc70a23..381068b8 100644 --- a/packages/docs/src/pages/introduction.tsx +++ b/packages/docs/src/pages/introduction.tsx @@ -347,7 +347,7 @@ const subscription = purchaseUpdatedListener(async (purchase) => { // 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' }, ], }); @@ -358,7 +358,7 @@ await requestPurchase({ apple: { sku: 'com.app.premium' }, google: { skus: ['com.app.premium'] }, }, - type: 'inapp', + type: 'in-app', }); // 8. Cleanup on unmount From 58f23b526b31df6e15c6781bec8dd748146a60e2 Mon Sep 17 00:00:00 2001 From: hyochan Date: Sat, 25 Apr 2026 15:44:23 +0900 Subject: [PATCH 04/41] =?UTF-8?q?docs(review):=20round=203=20=E2=80=94=20C?= =?UTF-8?q?odeRabbit=20nits=20on=20PR=20#106?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - apis/fetch-products.tsx: Dart example now awaits FetchProductsResult and unwraps via switch pattern on FetchProductsResultProducts (matches the sealed-class signature) - apis/request-purchase.tsx: Swift type uses canonical .inApp camelCase enum case; Dart example replaces the legacy positional-string call with the proper RequestPurchaseProps shape so it matches the documented signature and the Swift/Kotlin samples - pages/docs/index.tsx: sidebar labels for the iOS group now carry the IOS suffix (PaymentModeIOS, AppTransactionIOS) so they line up with every other entry and the canonical type identifiers - types/billing-programs.tsx: * SEO title/description/keywords cleaned up — "Billing Programs" with a real keyword set, no double-space duplication * TypeScript example uses the kebab-case BillingProgramAndroid / DeveloperBillingLaunchModeAndroid string literals exposed by the generated TS types ('external-payments', 'launch-in-external-browser-or-app') instead of SCREAMING_SNAKE_CASE - types/ios/discount-ios.tsx: SEO keywords no longer ship the awkward "Discount I O S" PascalCase-splitter artifact - types/subscription-product.tsx: typeIOS example uses canonical PascalCase (AutoRenewableSubscription / NonRenewingSubscription) so it matches the GraphQL enum and product.tsx Co-Authored-By: Claude Opus 4.7 (1M context) --- .../docs/src/pages/docs/apis/fetch-products.tsx | 10 ++++++++-- .../docs/src/pages/docs/apis/request-purchase.tsx | 12 ++++++++++-- packages/docs/src/pages/docs/index.tsx | 4 ++-- .../docs/src/pages/docs/types/billing-programs.tsx | 14 +++++++------- .../docs/src/pages/docs/types/ios/discount-ios.tsx | 2 +- .../src/pages/docs/types/subscription-product.tsx | 5 +++-- 6 files changed, 31 insertions(+), 16 deletions(-) diff --git a/packages/docs/src/pages/docs/apis/fetch-products.tsx b/packages/docs/src/pages/docs/apis/fetch-products.tsx index ce34b6fb..4aef10bf 100644 --- a/packages/docs/src/pages/docs/apis/fetch-products.tsx +++ b/packages/docs/src/pages/docs/apis/fetch-products.tsx @@ -88,10 +88,16 @@ val products = kmpIAP.fetchProducts( )`} ), dart: ( - {`final List products = await FlutterInappPurchase.instance.fetchProducts( + {`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() diff --git a/packages/docs/src/pages/docs/apis/request-purchase.tsx b/packages/docs/src/pages/docs/apis/request-purchase.tsx index d574e48d..658ef3e8 100644 --- a/packages/docs/src/pages/docs/apis/request-purchase.tsx +++ b/packages/docs/src/pages/docs/apis/request-purchase.tsx @@ -82,7 +82,7 @@ await requestPurchase({ request: RequestPurchasePropsByPlatforms( apple: RequestPurchaseIosProps(sku: "com.app.premium") ), - type: .inapp + type: .inApp ) )`} ), @@ -107,7 +107,15 @@ await requestPurchase({ )`} ), dart: ( - {`await FlutterInappPurchase.instance.requestPurchase('com.app.premium');`} + {`await FlutterInappPurchase.instance.requestPurchase( + RequestPurchaseProps( + request: RequestPurchasePropsByPlatforms( + apple: RequestPurchaseIosProps(sku: 'com.app.premium'), + google: RequestPurchaseAndroidProps(skus: ['com.app.premium']), + ), + type: ProductQueryType.inApp, + ), +);`} ), gdscript: ( {`var props = RequestPurchaseProps.new() diff --git a/packages/docs/src/pages/docs/index.tsx b/packages/docs/src/pages/docs/index.tsx index 4ebd935d..626fe92d 100644 --- a/packages/docs/src/pages/docs/index.tsx +++ b/packages/docs/src/pages/docs/index.tsx @@ -243,7 +243,7 @@ function Docs() { }, { to: '/docs/types/ios/payment-mode-ios', - label: 'PaymentMode', + label: 'PaymentModeIOS', }, { to: '/docs/types/ios/subscription-status-ios', @@ -251,7 +251,7 @@ function Docs() { }, { to: '/docs/types/ios/app-transaction-ios', - label: 'AppTransaction', + label: 'AppTransactionIOS', }, ], }, diff --git a/packages/docs/src/pages/docs/types/billing-programs.tsx b/packages/docs/src/pages/docs/types/billing-programs.tsx index d7fa7bfc..210669ed 100644 --- a/packages/docs/src/pages/docs/types/billing-programs.tsx +++ b/packages/docs/src/pages/docs/types/billing-programs.tsx @@ -11,12 +11,12 @@ function BillingPrograms() { return (
    -

    Billing Programs Types

    +

    Billing Programs

    Billing Programs (Android 8.2.0+) @@ -372,16 +372,16 @@ developerProvidedBillingListenerAndroid((details) => { }); // Check availability (Japan only) -const result = await isBillingProgramAvailableAndroid('EXTERNAL_PAYMENTS'); +const result = await isBillingProgramAvailableAndroid('external-payments'); if (result.isAvailable) { // Purchase with developer billing option await requestPurchase({ google: { skus: ['product_id'], developerBillingOption: { - billingProgram: 'EXTERNAL_PAYMENTS', + billingProgram: 'external-payments', linkUri: 'https://your-site.com/checkout', - launchMode: 'LAUNCH_IN_EXTERNAL_BROWSER_OR_APP', + launchMode: 'launch-in-external-browser-or-app', }, }, }); diff --git a/packages/docs/src/pages/docs/types/ios/discount-ios.tsx b/packages/docs/src/pages/docs/types/ios/discount-ios.tsx index 1535f4ca..1aeef0e4 100644 --- a/packages/docs/src/pages/docs/types/ios/discount-ios.tsx +++ b/packages/docs/src/pages/docs/types/ios/discount-ios.tsx @@ -12,7 +12,7 @@ function DiscountIos() { title="DiscountIOS" description="DiscountIOS type definition and field reference." path="/docs/types/ios/discount-ios" - keywords="DiscountIOS, OpenIAP types, Discount I O S" + keywords="DiscountIOS, OpenIAP types, iOS Discount" />

    DiscountIOS

    diff --git a/packages/docs/src/pages/docs/types/subscription-product.tsx b/packages/docs/src/pages/docs/types/subscription-product.tsx index ab9203ba..6993ec3a 100644 --- a/packages/docs/src/pages/docs/types/subscription-product.tsx +++ b/packages/docs/src/pages/docs/types/subscription-product.tsx @@ -149,8 +149,9 @@ function SubscriptionProduct() { typeIOS
    From 9456ad02f6fc91d88d1d4c9eda98d4938a5bb77d Mon Sep 17 00:00:00 2001 From: hyochan Date: Sat, 25 Apr 2026 21:08:53 +0900 Subject: [PATCH 05/41] docs: add Getting Started landing page and Framework Setup overview Splits Framework Setup from React Native into its own overview page, and introduces a top-level Getting Started walkthrough above Ecosystem. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../docs/src/pages/docs/getting-started.tsx | 299 ++++++++++++++++++ packages/docs/src/pages/docs/index.tsx | 20 +- packages/docs/src/pages/docs/setup/index.tsx | 159 ++++++++++ 3 files changed, 476 insertions(+), 2 deletions(-) create mode 100644 packages/docs/src/pages/docs/getting-started.tsx create mode 100644 packages/docs/src/pages/docs/setup/index.tsx diff --git a/packages/docs/src/pages/docs/getting-started.tsx b/packages/docs/src/pages/docs/getting-started.tsx new file mode 100644 index 00000000..4798eaef --- /dev/null +++ b/packages/docs/src/pages/docs/getting-started.tsx @@ -0,0 +1,299 @@ +import { Link } from 'react-router-dom'; +import AnchorLink from '../../components/AnchorLink'; +import CodeBlock from '../../components/CodeBlock'; +import LanguageTabs from '../../components/LanguageTabs'; +import SEO from '../../components/SEO'; +import { useScrollToHash } from '../../hooks/useScrollToHash'; + +function GettingStarted() { + useScrollToHash(); + + return ( +
    + +

    Getting Started

    +

    + OpenIAP is a unified spec for in-app purchases on Apple, Google, and + Meta Horizon. One GraphQL schema generates type-safe SDKs for + TypeScript, Swift, Kotlin, Dart, and GDScript — so the same purchase + flow works across every framework you ship in. +

    + +

    + This page is a five-minute walkthrough. If you'd rather jump straight + into your stack, head to{' '} + Framework Setup. +

    + +
    + + 1. Configure the store + +

    + Every framework wraps the same store APIs, so the platform setup + comes first. Finish these before installing any SDK: +

    +
      +
    • + iOS Setup — App Store Connect + agreement, capability, sandbox testers +
    • +
    • + Android Setup — Play Console + account, license testers, billing permission +
    • +
    • + Horizon OS Setup — Meta + Horizon Store dashboard +
    • +
    +
    + +
    + + 2. Pick a framework + +

    + OpenIAP ships official SDKs for five frameworks. Pick the one your + app uses — the API surface is identical across all of them. +

    +
      +
    • + react-native-iap — bare + React Native CLI projects (RN 0.79+) +
    • +
    • + expo-iap — Expo SDK projects +
    • +
    • + flutter_inapp_purchase — + Flutter / Dart +
    • +
    • + godot-iap — Godot 4.x +
    • +
    • + kmp-iap — Kotlin Multiplatform / + Compose Multiplatform +
    • +
    +
    + +
    + + 3. Your first purchase flow + +

    + The four-step flow below is the same on every framework — only the + imports differ. Read{' '} + Features → Purchase for a + full walkthrough with verification, error handling, and + consumable/non-consumable nuances. +

    + + + {{ + typescript: ( + {`import { + initConnection, + fetchProducts, + requestPurchase, + finishTransaction, + purchaseUpdatedListener, + verifyPurchase, +} from 'expo-iap'; + +// 1. Open the store connection on app start. +await initConnection(); + +// 2. Fetch products by SKU. +const products = await fetchProducts({ + skus: ['com.app.premium'], + type: 'in-app', +}); + +// 3. Listen for purchase results — requestPurchase is event-based. +purchaseUpdatedListener(async (purchase) => { + const { isValid } = await verifyPurchase({ + purchase, + serverUrl: 'https://your-server.com/api/verify', + }); + if (!isValid) return; + + await grantEntitlement(purchase.productId); + await finishTransaction(purchase, /* isConsumable */ false); +}); + +// 4. Initiate a purchase. +await requestPurchase({ + request: { + apple: { sku: 'com.app.premium' }, + google: { skus: ['com.app.premium'] }, + }, + type: 'in-app', +});`} + ), + swift: ( + {`import OpenIap + +let store = OpenIapModule.shared + +// 1. Open the store connection on app start. +try await store.initConnection() + +// 2. Fetch products by SKU. +let products = try await store.fetchProducts( + ProductRequest(skus: ["com.app.premium"], type: .inApp) +) + +// 3. Listen for purchase results — requestPurchase is event-based. +store.onPurchaseSuccess = { purchase in + Task { + // Verify on your backend, grant entitlement, then finish. + try await store.finishTransaction(purchase, isConsumable: false) + } +} + +// 4. Initiate a purchase. +try await store.requestPurchase( + RequestPurchaseProps( + request: RequestPurchasePropsByPlatforms( + apple: RequestPurchaseIosProps(sku: "com.app.premium") + ), + type: .inApp + ) +)`} + ), + kotlin: ( + {`import dev.hyo.openiap.store.OpenIapStore +import dev.hyo.openiap.* + +val store = OpenIapStore(context) + +// 1. Open the store connection on app start. +store.initConnection(null) + +// 2. Fetch products by SKU. +val products = store.fetchProducts( + ProductRequest( + skus = listOf("com.app.premium"), + type = ProductQueryType.InApp + ) +) + +// 3. Listen for purchase results. +scope.launch { + store.purchaseFlow.collect { purchase -> + // Verify on your backend, grant entitlement, then finish. + store.finishTransaction(purchase, isConsumable = false) + } +} + +// 4. Initiate a purchase. +store.requestPurchase( + RequestPurchaseProps( + request = RequestPurchasePropsByPlatforms( + google = RequestPurchaseAndroidProps(skus = listOf("com.app.premium")) + ), + type = ProductQueryType.InApp + ) +)`} + ), + dart: ( + {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart'; + +final iap = FlutterInappPurchase.instance; + +// 1. Open the store connection on app start. +await iap.initConnection(); + +// 2. Fetch products by SKU. +final FetchProductsResult result = await iap.fetchProducts( + skus: ['com.app.premium'], + type: ProductQueryType.InApp, +); + +// 3. Listen for purchase results. +FlutterInappPurchase.purchaseUpdatedStream.listen((purchase) async { + if (purchase == null) return; + // Verify on your backend, grant entitlement, then finish. + await iap.finishTransaction(purchase, isConsumable: false); +}); + +// 4. Initiate a purchase. +await iap.requestPurchase( + RequestPurchaseProps( + request: RequestPurchasePropsByPlatforms( + apple: RequestPurchaseIosProps(sku: 'com.app.premium'), + google: RequestPurchaseAndroidProps(skus: ['com.app.premium']), + ), + type: ProductQueryType.inApp, + ), +);`} + ), + gdscript: ( + {`# 1. Open the store connection on app start. +await iap.init_connection() + +# 2. Fetch products by SKU. +var request = ProductRequest.new() +request.skus = ["com.app.premium"] +request.type = ProductQueryType.IN_APP +var products = await iap.fetch_products(request) + +# 3. Listen for purchase results. +iap.purchase_updated.connect(func(purchase): + # Verify on your backend, grant entitlement, then finish. + await iap.finish_transaction(purchase, false) +) + +# 4. Initiate a purchase. +var props = RequestPurchaseProps.new() +props.request = RequestPurchasePropsByPlatforms.new() +props.request.apple = RequestPurchaseIosProps.new() +props.request.apple.sku = "com.app.premium" +props.type = ProductQueryType.IN_APP +await iap.request_purchase(props)`} + ), + }} + +
    + +
    + + 4. Where to go next + +
      +
    • + Purchase,{' '} + Subscription,{' '} + Refund — full feature + walkthroughs +
    • +
    • + Validation — + server-side verification (your own backend or IAPKit) +
    • +
    • + API Reference — every function with + cross-platform signatures +
    • +
    • + Types — every type with field tables +
    • +
    • + Errors — unified{' '} + PurchaseError codes +
    • +
    +
    +
    + ); +} + +export default GettingStarted; diff --git a/packages/docs/src/pages/docs/index.tsx b/packages/docs/src/pages/docs/index.tsx index 626fe92d..8243960a 100644 --- a/packages/docs/src/pages/docs/index.tsx +++ b/packages/docs/src/pages/docs/index.tsx @@ -1,6 +1,7 @@ import { useState, useEffect } from 'react'; import { Route, Routes, Navigate, NavLink } from 'react-router-dom'; import { MenuDropdown } from '../../components/MenuDropdown'; +import GettingStarted from './getting-started'; import Ecosystem from './ecosystem'; import LifeCycle from './lifecycle'; import Subscription from './lifecycle/subscription'; @@ -89,6 +90,7 @@ import AlternativeMarketplaceOnside from './features/alternative-marketplace/ons import IOSSetup from './ios-setup'; import AndroidSetup from './android-setup'; import HorizonSetup from './horizon-setup'; +import SetupIndex from './setup/index'; import ReactNativeSetup from './setup/react-native'; import ExpoSetup from './setup/expo'; import FlutterSetup from './setup/flutter'; @@ -154,6 +156,15 @@ function Docs() {
    - id - - string - Unique product identifier
    - title - - string - Localized product title
    - description - - string - Localized description
    type @@ -81,63 +59,8 @@ function SubscriptionProduct() { "subs" - Always "subs" for subscription products -
    - displayName - - string? - Display-friendly product name (optional)
    - displayPrice - - string - Formatted price with currency symbol
    - currency - - string - ISO 4217 currency code
    - price - - number? - Numeric price value
    - debugDescription - - string? - Debug-friendly description (optional)
    - platform - - IapPlatform - - Deprecated. Use store instead. + Always "subs" for subscription products (overrides + the parent type discriminator).
    - Detailed product type (e.g.,{' '} - auto-renewable-subscription) + Detailed product type — for subscriptions this is almost + always AutoRenewableSubscription (or{' '} + NonRenewingSubscription).
    + + + + + + + + + {FRAMEWORKS.map((row) => ( + + + + + + ))} + +
    FrameworkLanguageDescription
    + + {row.name} + + + {row.language} + {row.description}
    + + +
    + + Before You Start + +

    + Each framework guide assumes you've already finished the platform + store configuration. Complete those first: +

    +
      +
    • + iOS Setup — App Store Connect + agreement, capabilities, sandbox testers +
    • +
    • + Android Setup — Play Console + account, license testers, billing permission +
    • +
    • + Horizon OS Setup — Meta Quest + developer dashboard for the Horizon Store +
    • +
    +
    + +
    + + Cross-Cutting Topics + +

    + These pages apply regardless of which framework you pick — read them + once, then jump back to your framework guide: +

    +
      +
    • + API Reference — every function, + organized by symbol +
    • +
    • + Type Definitions — every type with + field tables and cross-links +
    • +
    • + Events & Listeners — purchase / + error / promoted-product event patterns +
    • +
    • + Validation — server + verification, IAPKit integration +
    • +
    • + Error Handling — unified{' '} + PurchaseError codes +
    • +
    +
    +
    + ); +} + +export default SetupIndex; From b1787a06f00a08d5896154f5fdef993cd7818412 Mon Sep 17 00:00:00 2001 From: hyochan Date: Sat, 25 Apr 2026 21:20:55 +0900 Subject: [PATCH 06/41] docs: restore sidebar collapse animation, scrollable tables, KMP tab - MenuDropdown uses direct DOM maxHeight transitions for smooth open AND close animation; SVG chevron replaces unicode glyph; clicking the title on its own active route now collapses - Tables become block-level with overflow-x:auto so horizontal scroll appears only when row content exceeds the container, instead of squashing the last column - Sidebar drops horizontal scroll; long identifiers wrap to a second line so the whole nav doesn't scroll just because one item is long - Getting Started gains a Kotlin (KMP) code sample - request-purchase carries the canonical Request APIs Important callout right after the page summary - Discounts (Android) moves to the bottom of the Features section alongside other Android-only items Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/docs/src/components/MenuDropdown.tsx | 97 +++++++++++-------- .../src/pages/docs/apis/request-purchase.tsx | 39 ++++++++ .../docs/src/pages/docs/getting-started.tsx | 34 +++++++ packages/docs/src/pages/docs/index.tsx | 18 ++-- packages/docs/src/styles/documentation.css | 45 +++++---- 5 files changed, 162 insertions(+), 71 deletions(-) diff --git a/packages/docs/src/components/MenuDropdown.tsx b/packages/docs/src/components/MenuDropdown.tsx index 2cc202a5..c83d3804 100644 --- a/packages/docs/src/components/MenuDropdown.tsx +++ b/packages/docs/src/components/MenuDropdown.tsx @@ -29,10 +29,54 @@ interface SubMenuProps { onItemClick?: () => void; } +function Chevron({ isExpanded }: { isExpanded: boolean }) { + return ( + + ); +} + +function useCollapse(isExpanded: boolean) { + const ref = useRef(null); + + useEffect(() => { + const el = ref.current; + if (!el) return; + + if (isExpanded) { + // Expanding: 0 → scrollHeight → none + el.style.maxHeight = `${el.scrollHeight}px`; + const handleEnd = (e: TransitionEvent) => { + if (e.propertyName === 'max-height') { + el.style.maxHeight = 'none'; + } + }; + el.addEventListener('transitionend', handleEnd, { once: true }); + return () => el.removeEventListener('transitionend', handleEnd); + } + + // Collapsing: snap from 'none' to scrollHeight, force reflow, then 0 + el.style.maxHeight = `${el.scrollHeight}px`; + void el.offsetHeight; + el.style.maxHeight = '0px'; + }, [isExpanded]); + + return ref; +} + function SubMenu({ group, onItemClick }: SubMenuProps) { const [isExpanded, setIsExpanded] = useState(false); - const contentRef = useRef(null); - const [height, setHeight] = useState(0); const location = useLocation(); const submenuContentId = useId(); @@ -40,11 +84,7 @@ function SubMenu({ group, onItemClick }: SubMenuProps) { (item) => location.pathname === item.to ); - useEffect(() => { - if (contentRef.current) { - setHeight(isExpanded ? contentRef.current.scrollHeight : 0); - } - }, [isExpanded, group.items.length]); + const contentRef = useCollapse(isExpanded); useEffect(() => { if (isAnyChildActive) { @@ -52,9 +92,7 @@ function SubMenu({ group, onItemClick }: SubMenuProps) { } }, [isAnyChildActive]); - const toggleExpanded = () => { - setIsExpanded(!isExpanded); - }; + const toggleExpanded = () => setIsExpanded((v) => !v); return (
  • @@ -74,23 +112,18 @@ function SubMenu({ group, onItemClick }: SubMenuProps) { type="button" onClick={toggleExpanded} className="menu-dropdown-toggle" - style={{ - transform: isExpanded ? 'rotate(90deg)' : 'rotate(0deg)', - }} aria-label={`Toggle ${group.label} submenu`} aria-expanded={isExpanded} aria-controls={submenuContentId} > - ▶ +
      {group.items.map((item) => ( @@ -121,8 +154,6 @@ export function MenuDropdown({ }: 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(); @@ -135,11 +166,7 @@ export function MenuDropdown({ ); const isGroupActive = isTitleActive || isChildActive; - useEffect(() => { - if (contentRef.current) { - setHeight(isExpanded ? contentRef.current.scrollHeight : 0); - } - }, [isExpanded, items.length]); + const contentRef = useCollapse(isExpanded); useEffect(() => { if (isGroupActive) { @@ -148,14 +175,16 @@ export function MenuDropdown({ }, [isGroupActive]); const handleTitleClick = () => { + if (isExpanded && isTitleActive) { + setIsExpanded(false); + return; + } setIsExpanded(true); navigate(titleTo); onItemClick?.(); }; - const toggleExpanded = () => { - setIsExpanded(!isExpanded); - }; + const toggleExpanded = () => setIsExpanded((v) => !v); return (
    • @@ -181,28 +210,18 @@ export function MenuDropdown({ type="button" onClick={toggleExpanded} className="menu-dropdown-toggle" - style={{ - transform: isExpanded ? 'rotate(90deg)' : 'rotate(0deg)', - }} aria-label={`Toggle ${title} submenu`} aria-expanded={isExpanded} aria-controls={contentId} > - ▶ +
      {items.map((entry, index) => diff --git a/packages/docs/src/pages/docs/apis/request-purchase.tsx b/packages/docs/src/pages/docs/apis/request-purchase.tsx index 658ef3e8..054027f0 100644 --- a/packages/docs/src/pages/docs/apis/request-purchase.tsx +++ b/packages/docs/src/pages/docs/apis/request-purchase.tsx @@ -21,6 +21,45 @@ function RequestPurchase() { 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

      {{ diff --git a/packages/docs/src/pages/docs/getting-started.tsx b/packages/docs/src/pages/docs/getting-started.tsx index 4798eaef..4f83c634 100644 --- a/packages/docs/src/pages/docs/getting-started.tsx +++ b/packages/docs/src/pages/docs/getting-started.tsx @@ -202,6 +202,40 @@ store.requestPurchase( ), type = ProductQueryType.InApp ) +)`} + ), + kmp: ( + {`import io.github.hyochan.kmpiap.kmpIAP +import io.github.hyochan.kmpiap.types.* + +// 1. Open the store connection on app start. +kmpIAP.initConnection() + +// 2. Fetch products by SKU. +val products = kmpIAP.fetchProducts( + ProductRequest( + skus = listOf("com.app.premium"), + type = ProductQueryType.InApp + ) +) + +// 3. Listen for purchase results. +scope.launch { + kmpIAP.purchaseUpdatedFlow.collect { purchase -> + // Verify on your backend, grant entitlement, then finish. + kmpIAP.finishTransaction(purchase, isConsumable = false) + } +} + +// 4. Initiate a purchase. +kmpIAP.requestPurchase( + RequestPurchaseProps( + request = RequestPurchasePropsByPlatforms( + apple = RequestPurchaseIosProps(sku = "com.app.premium"), + google = RequestPurchaseAndroidProps(skus = listOf("com.app.premium")) + ), + type = ProductQueryType.InApp + ) )`} ), dart: ( diff --git a/packages/docs/src/pages/docs/index.tsx b/packages/docs/src/pages/docs/index.tsx index 8243960a..52706ba4 100644 --- a/packages/docs/src/pages/docs/index.tsx +++ b/packages/docs/src/pages/docs/index.tsx @@ -584,15 +584,6 @@ function Docs() { Refund -
    • - (isActive ? 'active' : '')} - onClick={closeSidebar} - > - Discounts (Android) - -
    • +
    • + (isActive ? 'active' : '')} + onClick={closeSidebar} + > + Discounts (Android) + +

    Foundation

      diff --git a/packages/docs/src/styles/documentation.css b/packages/docs/src/styles/documentation.css index 65e5b3b7..81f45869 100644 --- a/packages/docs/src/styles/documentation.css +++ b/packages/docs/src/styles/documentation.css @@ -15,27 +15,19 @@ top: 56px; max-height: calc(100vh - 56px); overflow-y: auto; - overflow-x: auto; + overflow-x: hidden; overscroll-behavior: contain; background: var(--bg-secondary); border-right: 1px solid var(--border-color); - /* Extra right padding so long identifiers don't sit under the scrollbar */ - padding: var(--spacing-lg) 1rem var(--spacing-lg) 0; + padding: var(--spacing-lg) 0; } +/* Long identifiers wrap to a second line instead of forcing horizontal + scroll on the whole sidebar. */ .docs-sidebar .menu-dropdown-item, .docs-sidebar .menu-dropdown-title { - white-space: nowrap; -} - -/* Let the nav content grow with the longest identifier so the sidebar - actually scrolls horizontally instead of clipping mid-word. */ -.docs-sidebar .docs-nav { - min-width: max-content; -} - -.docs-sidebar::-webkit-scrollbar:horizontal { - height: 6px; + overflow-wrap: anywhere; + word-break: break-word; } :root.dark .docs-sidebar { @@ -492,15 +484,19 @@ opacity: 1; } -/* Tables */ +/* Tables — block layout so they scroll horizontally only when content + exceeds the container. `display: block` plus `overflow-x: auto` keeps + the scrollbar hidden until a row actually overflows. */ .error-table, .doc-page table { - width: 100%; + display: block; + max-width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; border-collapse: collapse; margin: 1.5rem 0; background: var(--bg-primary); border-radius: 0.5rem; - overflow: hidden; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); } @@ -528,20 +524,23 @@ border-bottom: 1px solid var(--border-color); } -/* Non-last columns (Name, Type, etc.) keep identifiers on one line. - Last column (Summary/Description) absorbs remaining width. */ +/* Non-last columns (Name, Type, etc.) keep identifiers on one line; if the + total exceeds the container width the whole table scrolls horizontally. */ .error-table th:not(:last-child), .error-table td:not(:last-child), .doc-page table th:not(:last-child), .doc-page table td:not(:last-child) { - width: 1%; white-space: nowrap; vertical-align: top; } -.error-table td:not(:last-child) code, -.doc-page table td:not(:last-child) code { - white-space: nowrap; +/* Last column (Summary/Description) wraps naturally. */ +.error-table th:last-child, +.error-table td:last-child, +.doc-page table th:last-child, +.doc-page table td:last-child { + white-space: normal; + vertical-align: top; } .error-table tbody tr:last-child td, From 9180269d0f784f126fd59760218cb9912e1cfb0a Mon Sep 17 00:00:00 2001 From: hyochan Date: Sat, 25 Apr 2026 21:22:31 +0900 Subject: [PATCH 07/41] docs: animate sidebar dropdown collapse with rAF + reflow Force a reflow before animating to 0 so browsers transition out of maxHeight: 'none', and defer the 0px write to the next frame so the snap-to-scrollHeight commit is observed first. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/docs/src/components/MenuDropdown.tsx | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/docs/src/components/MenuDropdown.tsx b/packages/docs/src/components/MenuDropdown.tsx index c83d3804..22e98cf3 100644 --- a/packages/docs/src/components/MenuDropdown.tsx +++ b/packages/docs/src/components/MenuDropdown.tsx @@ -49,13 +49,23 @@ function Chevron({ isExpanded }: { isExpanded: boolean }) { function useCollapse(isExpanded: boolean) { const ref = useRef(null); + const isFirstRun = useRef(true); useEffect(() => { const el = ref.current; if (!el) return; + // Skip animation on first mount — set the resting state directly so + // pages don't flicker open/closed when the sidebar renders. + if (isFirstRun.current) { + isFirstRun.current = false; + el.style.maxHeight = isExpanded ? 'none' : '0px'; + return; + } + if (isExpanded) { - // Expanding: 0 → scrollHeight → none + // Expanding: from 0 → scrollHeight, then unlock to 'none' so nested + // submenus can grow freely. el.style.maxHeight = `${el.scrollHeight}px`; const handleEnd = (e: TransitionEvent) => { if (e.propertyName === 'max-height') { @@ -66,10 +76,16 @@ function useCollapse(isExpanded: boolean) { return () => el.removeEventListener('transitionend', handleEnd); } - // Collapsing: snap from 'none' to scrollHeight, force reflow, then 0 + // Collapsing: maxHeight is currently 'none' (after a previous expand) + // or some pixel value. Snap to a numeric pixel value, force a reflow + // so the browser registers it as the transition's starting point, + // then animate to 0 in the next frame. el.style.maxHeight = `${el.scrollHeight}px`; void el.offsetHeight; - el.style.maxHeight = '0px'; + const id = requestAnimationFrame(() => { + el.style.maxHeight = '0px'; + }); + return () => cancelAnimationFrame(id); }, [isExpanded]); return ref; From 252d4ade06bcb25f770030697f19fc3fdbfd65b2 Mon Sep 17 00:00:00 2001 From: hyochan Date: Sat, 25 Apr 2026 21:24:17 +0900 Subject: [PATCH 08/41] docs: surface Request APIs callout on fetch-products and Ecosystem/Life Cycle in Getting Started MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Carry forward the Request APIs Important box that lived on the original combined Purchase APIs page, and route Getting Started readers to Ecosystem and Life Cycle before they wire anything into production — the lifecycle ordering bug class is the #1 cause of stuck entitlements. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/pages/docs/apis/fetch-products.tsx | 43 +++++++++++++++++++ .../docs/src/pages/docs/getting-started.tsx | 29 ++++++++++++- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/packages/docs/src/pages/docs/apis/fetch-products.tsx b/packages/docs/src/pages/docs/apis/fetch-products.tsx index 4aef10bf..ac973865 100644 --- a/packages/docs/src/pages/docs/apis/fetch-products.tsx +++ b/packages/docs/src/pages/docs/apis/fetch-products.tsx @@ -1,4 +1,5 @@ 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'; @@ -18,6 +19,48 @@ function FetchProducts() {

      fetchProducts

      Retrieve products or subscriptions from the store by SKU.

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

      +
      +

      Signature

      {{ diff --git a/packages/docs/src/pages/docs/getting-started.tsx b/packages/docs/src/pages/docs/getting-started.tsx index 4f83c634..6c3786a7 100644 --- a/packages/docs/src/pages/docs/getting-started.tsx +++ b/packages/docs/src/pages/docs/getting-started.tsx @@ -299,8 +299,33 @@ await iap.request_purchase(props)`}
      - - 4. Where to go next + + 4. Key concepts + +

      + Two background reads make every framework guide easier to follow — + skim them before you wire anything into production. +

      +
        +
      • + Ecosystem — how OpenIAP, the + native packages (Apple / Google), and each framework SDK fit + together. Read this first if you're choosing a stack. +
      • +
      • + Life Cycle — when to call{' '} + initConnection, where to mount listeners, and when + to call finishTransaction. Getting this wrong is the + #1 cause of "purchase succeeded but the user didn't get the + entitlement" reports, so read it once even if you skip everything + else. +
      • +
      +
      + +
      + + 5. Where to go next
      • From 9598d9c3bed13d070cdab7ac61f69dfd2b7cf250 Mon Sep 17 00:00:00 2001 From: hyochan Date: Sat, 25 Apr 2026 21:25:36 +0900 Subject: [PATCH 09/41] docs: explain how the doc site is organized + run prettier Adds a "How the docs are organized" section to Getting Started so readers understand the APIs/Types flat reference vs Features task walkthroughs split before they hunt for content. Also restores prettier formatting that the CI format gate was rejecting. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/docs/src/components/MenuDropdown.tsx | 9 +- .../src/pages/docs/apis/fetch-products.tsx | 7 +- .../src/pages/docs/apis/request-purchase.tsx | 7 +- .../docs/src/pages/docs/getting-started.tsx | 102 +++++++++++++++--- packages/docs/src/pages/docs/setup/index.tsx | 8 +- 5 files changed, 107 insertions(+), 26 deletions(-) diff --git a/packages/docs/src/components/MenuDropdown.tsx b/packages/docs/src/components/MenuDropdown.tsx index 22e98cf3..cac716d1 100644 --- a/packages/docs/src/components/MenuDropdown.tsx +++ b/packages/docs/src/components/MenuDropdown.tsx @@ -42,7 +42,14 @@ function Chevron({ isExpanded }: { isExpanded: boolean }) { flexShrink: 0, }} > - + ); } diff --git a/packages/docs/src/pages/docs/apis/fetch-products.tsx b/packages/docs/src/pages/docs/apis/fetch-products.tsx index ac973865..4c10ce47 100644 --- a/packages/docs/src/pages/docs/apis/fetch-products.tsx +++ b/packages/docs/src/pages/docs/apis/fetch-products.tsx @@ -24,8 +24,8 @@ function FetchProducts() {

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

        While these APIs return values for various purposes, you should{' '} @@ -56,8 +56,7 @@ function FetchProducts() {

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

        diff --git a/packages/docs/src/pages/docs/apis/request-purchase.tsx b/packages/docs/src/pages/docs/apis/request-purchase.tsx index 054027f0..3165aaba 100644 --- a/packages/docs/src/pages/docs/apis/request-purchase.tsx +++ b/packages/docs/src/pages/docs/apis/request-purchase.tsx @@ -23,8 +23,8 @@ function RequestPurchase() {

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

        While these APIs return values for various purposes, you should{' '} @@ -55,8 +55,7 @@ function RequestPurchase() {

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

        diff --git a/packages/docs/src/pages/docs/getting-started.tsx b/packages/docs/src/pages/docs/getting-started.tsx index 6c3786a7..607d08bb 100644 --- a/packages/docs/src/pages/docs/getting-started.tsx +++ b/packages/docs/src/pages/docs/getting-started.tsx @@ -26,8 +26,7 @@ function GettingStarted() {

        This page is a five-minute walkthrough. If you'd rather jump straight - into your stack, head to{' '} - Framework Setup. + into your stack, head to Framework Setup.

        @@ -35,8 +34,8 @@ function GettingStarted() { 1. Configure the store

        - Every framework wraps the same store APIs, so the platform setup - comes first. Finish these before installing any SDK: + Every framework wraps the same store APIs, so the platform setup comes + first. Finish these before installing any SDK:

        • @@ -59,8 +58,8 @@ function GettingStarted() { 2. Pick a framework

          - OpenIAP ships official SDKs for five frameworks. Pick the one your - app uses — the API surface is identical across all of them. + OpenIAP ships official SDKs for five frameworks. Pick the one your app + uses — the API surface is identical across all of them.

          • @@ -314,9 +313,9 @@ await iap.request_purchase(props)`}
          • Life Cycle — when to call{' '} - initConnection, where to mount listeners, and when - to call finishTransaction. Getting this wrong is the - #1 cause of "purchase succeeded but the user didn't get the + initConnection, where to mount listeners, and when to + call finishTransaction. Getting this wrong is the #1 + cause of "purchase succeeded but the user didn't get the entitlement" reports, so read it once even if you skip everything else.
          • @@ -324,8 +323,85 @@ await iap.request_purchase(props)`}
        - - 5. Where to go next + + 5. How the docs are organized + +

        + The sidebar groups content by intent. Once you know which group fits + the question you're answering, navigation becomes muscle memory. +

        +
          +
        • + + APIs + {' '} + — flat reference, one page per function (initConnection + , fetchProducts, requestPurchase, …). + Cross-platform symbols live at the root; iOS- and Android-only + symbols are grouped under{' '} + iOS Specific /{' '} + Android Specific. Open + a function page when you need its exact signature, params, and a + copy-pasteable example. +
        • +
        • + + Types + {' '} + — flat reference, one page per type (Product,{' '} + Purchase, RequestPurchaseProps, …). Same + iOS / Android grouping as APIs. Field tables auto-link to related + types so you can chase a shape without leaving the docs. +
        • +
        • + + Features + {' '} + — task-oriented walkthroughs, not reference. Each page covers a real + flow end-to-end (Purchase,{' '} + Subscription,{' '} + Refund,{' '} + Validation,{' '} + + Offer Code Redemption + + , …) with verification, edge cases, and platform notes. Open a + Features page when you're shipping a flow, not just calling a + function. +
        • +
        • + + Setup Guide + {' '} + — install + native config per framework ( + React Native,{' '} + Expo,{' '} + Flutter,{' '} + Godot,{' '} + Kotlin Multiplatform) plus the + store-side configuration (iOS,{' '} + Android,{' '} + Horizon OS). +
        • +
        • + + Events &{' '} + Errors + {' '} + — listener patterns and the unified PurchaseError codes + that every SDK normalizes to. +
        • +
        +

        + Rule of thumb: "How does this function work?" → APIs. + "What does this object look like?" → Types. "How do I ship + subscription upgrades?" → Features. +

        +
        + +
        + + 6. Where to go next
        • @@ -335,8 +411,8 @@ await iap.request_purchase(props)`} walkthroughs
        • - Validation — - server-side verification (your own backend or IAPKit) + Validation — server-side + verification (your own backend or IAPKit)
        • API Reference — every function with diff --git a/packages/docs/src/pages/docs/setup/index.tsx b/packages/docs/src/pages/docs/setup/index.tsx index f667cc79..1e624de7 100644 --- a/packages/docs/src/pages/docs/setup/index.tsx +++ b/packages/docs/src/pages/docs/setup/index.tsx @@ -62,8 +62,8 @@ function SetupIndex() { />

          Framework Setup

          - Pick the framework you ship in. Every supported framework wraps the - same OpenIAP specification, so the API surface, type names, and event + Pick the framework you ship in. Every supported framework wraps the same + OpenIAP specification, so the API surface, type names, and event patterns are consistent across stacks — only the install steps differ.

          @@ -139,8 +139,8 @@ function SetupIndex() { field tables and cross-links
        • - Events & Listeners — purchase / - error / promoted-product event patterns + Events & Listeners — purchase / error + / promoted-product event patterns
        • Validation — server From 8e64d3b17b21dfd96d348ba8ecea682e18a26a06 Mon Sep 17 00:00:00 2001 From: hyochan Date: Sat, 25 Apr 2026 21:37:50 +0900 Subject: [PATCH 10/41] docs(review): address Copilot + CodeRabbit round 3 + ellipsis sidebar items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sidebar items truncate with ellipsis by default; active and hovered items expand to their full label so the page stays fixed-width but long identifiers stay reachable - Reorder Getting Started's "How the docs are organized" list (Setup Guide → Features → APIs → Types → Events & Errors) - Drop the dead status? field from FrameworkRow - fetch-products: TS return type uses FetchProductsResult union; Swift example uses ProductQueryType.inApp (canonical camelCase) - subscriptionStatusIOS signature returns [SubscriptionStatusIOS] - subscription-product: bold "Deprecated." hint on platform; the shared subscriptionOffers/discountOffers rows move to Common Fields so cross-platform fields don't appear duplicated under iOS+Android - billing-programs: link the remaining non-primitive type cells (ExternalLinkLaunchModeAndroid, ExternalLinkTypeAndroid, BillingProgramAndroid, DeveloperBillingLaunchModeAndroid) and add anchor sections for the two enums; lowercase the stray String - MenuDropdown SubMenu key drops the array index in favor of a stable titleTo+label key Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/docs/src/components/MenuDropdown.tsx | 4 +- .../src/pages/docs/apis/fetch-products.tsx | 16 +++- .../docs/apis/ios/subscription-status-ios.tsx | 2 +- .../docs/src/pages/docs/getting-started.tsx | 56 ++++++------ packages/docs/src/pages/docs/setup/index.tsx | 1 - .../src/pages/docs/types/billing-programs.tsx | 91 +++++++++++++++++-- .../pages/docs/types/subscription-product.tsx | 67 +++++++------- packages/docs/src/styles/documentation.css | 28 +++++- 8 files changed, 185 insertions(+), 80 deletions(-) diff --git a/packages/docs/src/components/MenuDropdown.tsx b/packages/docs/src/components/MenuDropdown.tsx index cac716d1..80b8668b 100644 --- a/packages/docs/src/components/MenuDropdown.tsx +++ b/packages/docs/src/components/MenuDropdown.tsx @@ -247,10 +247,10 @@ export function MenuDropdown({ style={{ maxHeight: '0px' }} >
            - {items.map((entry, index) => + {items.map((entry) => isGroup(entry) ? ( diff --git a/packages/docs/src/pages/docs/apis/fetch-products.tsx b/packages/docs/src/pages/docs/apis/fetch-products.tsx index 4c10ce47..cd7b74f2 100644 --- a/packages/docs/src/pages/docs/apis/fetch-products.tsx +++ b/packages/docs/src/pages/docs/apis/fetch-products.tsx @@ -64,15 +64,23 @@ function FetchProducts() { {{ typescript: ( - {`fetchProducts(params: ProductRequest): Promise + {`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(_ request: ProductRequest) async throws -> [Product]`} + {`func fetchProducts(_ request: ProductRequest) async throws -> FetchProductsResult`} ), kotlin: ( {`suspend fun fetchProducts(request: ProductRequest): List`} @@ -112,7 +120,7 @@ const subscriptions = await fetchProducts({ ), swift: ( {`let products = try await OpenIapModule.shared.fetchProducts( - ProductRequest(skus: ["com.app.premium"], type: .inapp) + ProductRequest(skus: ["com.app.premium"], type: .inApp) )`} ), kotlin: ( 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 index ef9e13b5..5d6fb7f8 100644 --- a/packages/docs/src/pages/docs/apis/ios/subscription-status-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/subscription-status-ios.tsx @@ -24,7 +24,7 @@ function SubscriptionStatusIOS() { {{ swift: ( - {`func subscriptionStatusIOS(sku: String) async throws -> [SubscriptionStatus]`} + {`func subscriptionStatusIOS(sku: String) async throws -> [SubscriptionStatusIOS]`} ), }} diff --git a/packages/docs/src/pages/docs/getting-started.tsx b/packages/docs/src/pages/docs/getting-started.tsx index 607d08bb..0caadccf 100644 --- a/packages/docs/src/pages/docs/getting-started.tsx +++ b/packages/docs/src/pages/docs/getting-started.tsx @@ -333,25 +333,17 @@ await iap.request_purchase(props)`}
            • - APIs - {' '} - — flat reference, one page per function (initConnection - , fetchProducts, requestPurchase, …). - Cross-platform symbols live at the root; iOS- and Android-only - symbols are grouped under{' '} - iOS Specific /{' '} - Android Specific. Open - a function page when you need its exact signature, params, and a - copy-pasteable example. -
            • -
            • - - Types + Setup Guide {' '} - — flat reference, one page per type (Product,{' '} - Purchase, RequestPurchaseProps, …). Same - iOS / Android grouping as APIs. Field tables auto-link to related - types so you can chase a shape without leaving the docs. + — install + native config per framework ( + React Native,{' '} + Expo,{' '} + Flutter,{' '} + Godot,{' '} + Kotlin Multiplatform) plus the + store-side configuration (iOS,{' '} + Android,{' '} + Horizon OS).
            • @@ -371,17 +363,25 @@ await iap.request_purchase(props)`}
            • - Setup Guide + APIs {' '} - — install + native config per framework ( - React Native,{' '} - Expo,{' '} - Flutter,{' '} - Godot,{' '} - Kotlin Multiplatform) plus the - store-side configuration (iOS,{' '} - Android,{' '} - Horizon OS). + — flat reference, one page per function (initConnection + , fetchProducts, requestPurchase, …). + Cross-platform symbols live at the root; iOS- and Android-only + symbols are grouped under{' '} + iOS Specific /{' '} + Android Specific. Open + a function page when you need its exact signature, params, and a + copy-pasteable example. +
            • +
            • + + Types + {' '} + — flat reference, one page per type (Product,{' '} + Purchase, RequestPurchaseProps, …). Same + iOS / Android grouping as APIs. Field tables auto-link to related + types so you can chase a shape without leaving the docs.
            • diff --git a/packages/docs/src/pages/docs/setup/index.tsx b/packages/docs/src/pages/docs/setup/index.tsx index 1e624de7..c373b6fb 100644 --- a/packages/docs/src/pages/docs/setup/index.tsx +++ b/packages/docs/src/pages/docs/setup/index.tsx @@ -8,7 +8,6 @@ interface FrameworkRow { name: string; language: string; description: string; - status?: string; } const FRAMEWORKS: FrameworkRow[] = [ diff --git a/packages/docs/src/pages/docs/types/billing-programs.tsx b/packages/docs/src/pages/docs/types/billing-programs.tsx index 210669ed..c247e36d 100644 --- a/packages/docs/src/pages/docs/types/billing-programs.tsx +++ b/packages/docs/src/pages/docs/types/billing-programs.tsx @@ -215,7 +215,9 @@ function BillingPrograms() { launchMode - ExternalLinkLaunchModeAndroid + + ExternalLinkLaunchModeAndroid + How the external link is launched @@ -224,7 +226,9 @@ function BillingPrograms() { linkType - ExternalLinkTypeAndroid + + ExternalLinkTypeAndroid + The type of the external link @@ -240,6 +244,77 @@ function BillingPrograms() { + + ExternalLinkLaunchModeAndroid + +

              How the external URL is launched (Play Billing Library 8.2.0+):

              + + + + + + + + + + + + + + + + + + + + + +
              NameSummary
              + UNSPECIFIED + Unspecified launch mode. Do not use.
              + LAUNCH_IN_EXTERNAL_BROWSER_OR_APP + + Play launches the URL in an external browser or eligible app. +
              + CALLER_WILL_LAUNCH_LINK + + Play does not launch the URL — the app handles launching the URL + after Play returns control. +
              + + + ExternalLinkTypeAndroid + +

              Type of external link destination (Play Billing Library 8.2.0+):

              + + + + + + + + + + + + + + + + + + + + + +
              NameSummary
              + UNSPECIFIED + Unspecified link type. Do not use.
              + LINK_TO_DIGITAL_CONTENT_OFFER + The link directs users to a digital content offer.
              + LINK_TO_APP_DOWNLOAD + The link directs users to download an app.
              + DeveloperBillingOptionParamsAndroid @@ -261,7 +336,9 @@ function BillingPrograms() { billingProgram - BillingProgramAndroid + + BillingProgramAndroid + The billing program (usually EXTERNAL_PAYMENTS) @@ -272,7 +349,7 @@ function BillingPrograms() { linkUri - String + string URL where the external payment will be processed @@ -281,7 +358,9 @@ function BillingPrograms() { launchMode - DeveloperBillingLaunchModeAndroid + + DeveloperBillingLaunchModeAndroid + How to launch the external payment link @@ -337,7 +416,7 @@ function BillingPrograms() { externalTransactionToken - String + string Token to report external transaction to Google (must report diff --git a/packages/docs/src/pages/docs/types/subscription-product.tsx b/packages/docs/src/pages/docs/types/subscription-product.tsx index 6993ec3a..4a10ead8 100644 --- a/packages/docs/src/pages/docs/types/subscription-product.tsx +++ b/packages/docs/src/pages/docs/types/subscription-product.tsx @@ -38,8 +38,9 @@ function SubscriptionProduct() { displayName, displayPrice,{' '} currency, price,{' '} debugDescription,{' '} - platform), - plus the subscription-only override below. + platform ( + Deprecated.)), plus the subscription-only override + and the cross-platform offer arrays below.

              @@ -63,6 +64,35 @@ function SubscriptionProduct() { 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. +
              @@ -172,17 +202,6 @@ function SubscriptionProduct() { Raw StoreKit 2 JWS payload - - - subscriptionOffers - - - Cross-platform array of{' '} - - SubscriptionOffer - - - @@ -217,28 +236,6 @@ function SubscriptionProduct() { , UNKNOWN) — Billing Library 8.0+ - - - discountOffers - - - Cross-platform array of{' '} - - DiscountOffer - - - - - - subscriptionOffers - - - Cross-platform array of{' '} - - SubscriptionOffer - - - subscriptionOfferDetailsAndroid diff --git a/packages/docs/src/styles/documentation.css b/packages/docs/src/styles/documentation.css index 81f45869..8927fb98 100644 --- a/packages/docs/src/styles/documentation.css +++ b/packages/docs/src/styles/documentation.css @@ -22,10 +22,32 @@ padding: var(--spacing-lg) 0; } -/* Long identifiers wrap to a second line instead of forcing horizontal - scroll on the whole sidebar. */ +/* 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 .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; } From f4b915dbf8eb2bdeece05ca1f9ce073bc2abbfca Mon Sep 17 00:00:00 2001 From: hyochan Date: Sat, 25 Apr 2026 21:53:22 +0900 Subject: [PATCH 11/41] docs(review): split RenewalInfoIOS into its own type page Gemini flagged that RenewalInfoIOS was hanging off /docs/types/purchase as an anchor while every other type follows the flat per-symbol routing pattern. Move the full RenewalInfoIOS table to /docs/types/ios/renewal-info-ios, update the active-subscription and purchase cross-links, and surface it in the sidebar's iOS Specific group. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/docs/src/pages/docs/index.tsx | 9 ++ .../pages/docs/types/active-subscription.tsx | 2 +- .../pages/docs/types/ios/renewal-info-ios.tsx | 135 ++++++++++++++++++ .../docs/src/pages/docs/types/purchase.tsx | 121 ++-------------- 4 files changed, 157 insertions(+), 110 deletions(-) create mode 100644 packages/docs/src/pages/docs/types/ios/renewal-info-ios.tsx diff --git a/packages/docs/src/pages/docs/index.tsx b/packages/docs/src/pages/docs/index.tsx index 52706ba4..761852eb 100644 --- a/packages/docs/src/pages/docs/index.tsx +++ b/packages/docs/src/pages/docs/index.tsx @@ -25,6 +25,7 @@ import TypesSubscriptionPeriodIOS from './types/ios/subscription-period-ios'; import TypesPaymentModeIOS from './types/ios/payment-mode-ios'; import TypesSubscriptionStatusIOS from './types/ios/subscription-status-ios'; import TypesAppTransactionIOS from './types/ios/app-transaction-ios'; +import TypesRenewalInfoIOS from './types/ios/renewal-info-ios'; import TypesOneTimePurchaseOfferDetailAndroid from './types/android/one-time-purchase-offer-detail-android'; import TypesSubscriptionOfferAndroid from './types/android/subscription-offer-android'; import TypesPricingPhaseAndroid from './types/android/pricing-phase-android'; @@ -264,6 +265,10 @@ function Docs() { to: '/docs/types/ios/app-transaction-ios', label: 'AppTransactionIOS', }, + { + to: '/docs/types/ios/renewal-info-ios', + label: 'RenewalInfoIOS', + }, ], }, { @@ -768,6 +773,10 @@ function Docs() { path="types/ios/app-transaction-ios" element={} /> + } + /> } diff --git a/packages/docs/src/pages/docs/types/active-subscription.tsx b/packages/docs/src/pages/docs/types/active-subscription.tsx index ac07c0bd..55ce4360 100644 --- a/packages/docs/src/pages/docs/types/active-subscription.tsx +++ b/packages/docs/src/pages/docs/types/active-subscription.tsx @@ -176,7 +176,7 @@ function ActiveSubscription() { renewalInfoIOS - + RenewalInfoIOS? 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 00000000..abd2ea21 --- /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/purchase.tsx b/packages/docs/src/pages/docs/types/purchase.tsx index 59c82c6c..e70b841e 100644 --- a/packages/docs/src/pages/docs/types/purchase.tsx +++ b/packages/docs/src/pages/docs/types/purchase.tsx @@ -348,8 +348,11 @@ function Purchase() { renewalInfoIOS - Subscription renewal information (see RenewalInfoIOS - below) + Subscription renewal information — see{' '} + + RenewalInfoIOS + + . @@ -367,113 +370,13 @@ function Purchase() { -
              - - 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" -
              - jsonRepresentation - - Raw JWS representation of the StoreKit renewal info — - useful for server-side validation. -
              -
              +

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

              From e14e691f5e457cb39283eef9257edacfd7cacaf7 Mon Sep 17 00:00:00 2001 From: hyochan Date: Sat, 25 Apr 2026 22:22:03 +0900 Subject: [PATCH 12/41] docs: split Events into flat per-listener pages, smooth sidebar collapse, full-bleed sidebar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Events - One page per listener under /docs/events/{purchase-updated-listener, purchase-error-listener, subscription-billing-issue-listener, ios/promoted-product-listener-ios, android/user-choice-billing-listener-android, android/developer-provided-billing-listener-android}, mirroring the flat APIs/Types layout - /docs/events becomes the index/hub with Event System Overview, three listener tables, and Event Listener Management - Events sidebar entry is now a MenuDropdown with iOS Specific / Android Specific subgroups - Every cross-doc link now routes straight to its listener page — home.tsx chips, introduction.tsx, fetch-products / request-purchase Important callouts, lifecycle/subscription, etc. Sidebar - Collapse animation reworked as CSS-only grid-template-rows: 0fr → 1fr, no more JS height measurement (which clipped iOS/Android Specific contents to a single row) - New --sidebar-edge-fill variable + negative left margin extends the sidebar background and active-row highlight to the viewport edge - Nested group headers (iOS Specific / Android Specific) indent one level deeper than the listener rows above them so the hierarchy reads correctly - Drop the "Documentation" h3 (the Docs route already labels the section) and trim the top padding above Getting Started Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/docs/src/components/MenuDropdown.tsx | 56 +- packages/docs/src/lib/searchData.ts | 2 +- .../src/pages/docs/apis/fetch-products.tsx | 4 +- .../src/pages/docs/apis/request-purchase.tsx | 4 +- packages/docs/src/pages/docs/events.tsx | 1228 ++--------------- ...oper-provided-billing-listener-android.tsx | 277 ++++ .../user-choice-billing-listener-android.tsx | 266 ++++ .../ios/promoted-product-listener-ios.tsx | 200 +++ .../docs/events/purchase-error-listener.tsx | 233 ++++ .../docs/events/purchase-updated-listener.tsx | 218 +++ .../subscription-billing-issue-listener.tsx | 83 ++ .../docs/src/pages/docs/getting-started.tsx | 8 +- packages/docs/src/pages/docs/index.tsx | 81 +- .../src/pages/docs/lifecycle/subscription.tsx | 14 +- packages/docs/src/pages/home.tsx | 12 +- packages/docs/src/pages/introduction.tsx | 4 +- packages/docs/src/styles/documentation.css | 45 +- 17 files changed, 1502 insertions(+), 1233 deletions(-) create mode 100644 packages/docs/src/pages/docs/events/android/developer-provided-billing-listener-android.tsx create mode 100644 packages/docs/src/pages/docs/events/android/user-choice-billing-listener-android.tsx create mode 100644 packages/docs/src/pages/docs/events/ios/promoted-product-listener-ios.tsx create mode 100644 packages/docs/src/pages/docs/events/purchase-error-listener.tsx create mode 100644 packages/docs/src/pages/docs/events/purchase-updated-listener.tsx create mode 100644 packages/docs/src/pages/docs/events/subscription-billing-issue-listener.tsx diff --git a/packages/docs/src/components/MenuDropdown.tsx b/packages/docs/src/components/MenuDropdown.tsx index 80b8668b..7c09a893 100644 --- a/packages/docs/src/components/MenuDropdown.tsx +++ b/packages/docs/src/components/MenuDropdown.tsx @@ -1,4 +1,4 @@ -import { useId, useState, useRef, useEffect } from 'react'; +import { useId, useState, useEffect } from 'react'; import { NavLink, useLocation, useNavigate } from 'react-router-dom'; export interface MenuItem { @@ -54,50 +54,6 @@ function Chevron({ isExpanded }: { isExpanded: boolean }) { ); } -function useCollapse(isExpanded: boolean) { - const ref = useRef(null); - const isFirstRun = useRef(true); - - useEffect(() => { - const el = ref.current; - if (!el) return; - - // Skip animation on first mount — set the resting state directly so - // pages don't flicker open/closed when the sidebar renders. - if (isFirstRun.current) { - isFirstRun.current = false; - el.style.maxHeight = isExpanded ? 'none' : '0px'; - return; - } - - if (isExpanded) { - // Expanding: from 0 → scrollHeight, then unlock to 'none' so nested - // submenus can grow freely. - el.style.maxHeight = `${el.scrollHeight}px`; - const handleEnd = (e: TransitionEvent) => { - if (e.propertyName === 'max-height') { - el.style.maxHeight = 'none'; - } - }; - el.addEventListener('transitionend', handleEnd, { once: true }); - return () => el.removeEventListener('transitionend', handleEnd); - } - - // Collapsing: maxHeight is currently 'none' (after a previous expand) - // or some pixel value. Snap to a numeric pixel value, force a reflow - // so the browser registers it as the transition's starting point, - // then animate to 0 in the next frame. - el.style.maxHeight = `${el.scrollHeight}px`; - void el.offsetHeight; - const id = requestAnimationFrame(() => { - el.style.maxHeight = '0px'; - }); - return () => cancelAnimationFrame(id); - }, [isExpanded]); - - return ref; -} - function SubMenu({ group, onItemClick }: SubMenuProps) { const [isExpanded, setIsExpanded] = useState(false); const location = useLocation(); @@ -107,8 +63,6 @@ function SubMenu({ group, onItemClick }: SubMenuProps) { (item) => location.pathname === item.to ); - const contentRef = useCollapse(isExpanded); - useEffect(() => { if (isAnyChildActive) { setIsExpanded(true); @@ -144,9 +98,8 @@ function SubMenu({ group, onItemClick }: SubMenuProps) {
                {group.items.map((item) => ( @@ -189,8 +142,6 @@ export function MenuDropdown({ ); const isGroupActive = isTitleActive || isChildActive; - const contentRef = useCollapse(isExpanded); - useEffect(() => { if (isGroupActive) { setIsExpanded(true); @@ -242,9 +193,8 @@ export function MenuDropdown({
                {items.map((entry) => diff --git a/packages/docs/src/lib/searchData.ts b/packages/docs/src/lib/searchData.ts index 08fd2080..467305e4 100644 --- a/packages/docs/src/lib/searchData.ts +++ b/packages/docs/src/lib/searchData.ts @@ -740,7 +740,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', diff --git a/packages/docs/src/pages/docs/apis/fetch-products.tsx b/packages/docs/src/pages/docs/apis/fetch-products.tsx index cd7b74f2..5387b8f0 100644 --- a/packages/docs/src/pages/docs/apis/fetch-products.tsx +++ b/packages/docs/src/pages/docs/apis/fetch-products.tsx @@ -33,11 +33,11 @@ function FetchProducts() { not rely on their return values for actual purchase results . Instead, listen for events through{' '} - + purchaseUpdatedListener {' '} or{' '} - + purchaseErrorListener . diff --git a/packages/docs/src/pages/docs/apis/request-purchase.tsx b/packages/docs/src/pages/docs/apis/request-purchase.tsx index 3165aaba..d47a13fa 100644 --- a/packages/docs/src/pages/docs/apis/request-purchase.tsx +++ b/packages/docs/src/pages/docs/apis/request-purchase.tsx @@ -32,11 +32,11 @@ function RequestPurchase() { not rely on their return values for actual purchase results . Instead, listen for events through{' '} - + purchaseUpdatedListener {' '} or{' '} - + purchaseErrorListener . diff --git a/packages/docs/src/pages/docs/events.tsx b/packages/docs/src/pages/docs/events.tsx index bc21e4e6..1f3e4d14 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 @@ -84,1173 +93,114 @@ function Events() {

                - - 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}") -}`} - ), - 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+). +
                +
                + +
                + + 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 00000000..7101c5db --- /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: ( + {`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 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 00000000..0a3f0a4a --- /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 00000000..9c3d8e1e --- /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 00000000..a3214e81 --- /dev/null +++ b/packages/docs/src/pages/docs/events/purchase-error-listener.tsx @@ -0,0 +1,233 @@ +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 } 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}") +}`} + ), + 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 00000000..9ac39399 --- /dev/null +++ b/packages/docs/src/pages/docs/events/purchase-updated-listener.tsx @@ -0,0 +1,218 @@ +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 { 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. +
                +
                + ); +} + +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 00000000..420044c0 --- /dev/null +++ b/packages/docs/src/pages/docs/events/subscription-billing-issue-listener.tsx @@ -0,0 +1,83 @@ +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 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. +

                + +

                + 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/getting-started.tsx b/packages/docs/src/pages/docs/getting-started.tsx index 0caadccf..f3e3fe5f 100644 --- a/packages/docs/src/pages/docs/getting-started.tsx +++ b/packages/docs/src/pages/docs/getting-started.tsx @@ -369,10 +369,10 @@ await iap.request_purchase(props)`} , fetchProducts, requestPurchase, …). Cross-platform symbols live at the root; iOS- and Android-only symbols are grouped under{' '} - iOS Specific /{' '} - Android Specific. Open - a function page when you need its exact signature, params, and a - copy-pasteable example. + iOS Specific /{' '} + Android Specific. + Open a function page when you need its exact signature, params, and + a copy-pasteable example.
              • diff --git a/packages/docs/src/pages/docs/index.tsx b/packages/docs/src/pages/docs/index.tsx index 761852eb..f8adf3fc 100644 --- a/packages/docs/src/pages/docs/index.tsx +++ b/packages/docs/src/pages/docs/index.tsx @@ -75,6 +75,12 @@ import APIsIsBillingProgramAvailableAndroid from './apis/android/is-billing-prog import APIsLaunchExternalLinkAndroid from './apis/android/launch-external-link-android'; import APIsCreateBillingProgramReportingDetailsAndroid from './apis/android/create-billing-program-reporting-details-android'; import Events from './events'; +import EventsPurchaseUpdatedListener from './events/purchase-updated-listener'; +import EventsPurchaseErrorListener from './events/purchase-error-listener'; +import EventsSubscriptionBillingIssueListener from './events/subscription-billing-issue-listener'; +import EventsPromotedProductListenerIOS from './events/ios/promoted-product-listener-ios'; +import EventsUserChoiceBillingListenerAndroid from './events/android/user-choice-billing-listener-android'; +import EventsDeveloperProvidedBillingListenerAndroid from './events/android/developer-provided-billing-listener-android'; import Errors from './errors'; import Purchase from './features/purchase'; import SubscriptionFeature from './features/subscription/index'; @@ -155,7 +161,6 @@ function Docs() {
              purchaseUpdatedListener Purchase state changes purchaseErrorListener Error handling promotedProductListenerIOS App Store promoted products userChoiceBillingListenerAndroid User Choice Billing selection developerProvidedBillingListener External billing choice subscriptionBillingIssueListener diff --git a/packages/docs/src/pages/introduction.tsx b/packages/docs/src/pages/introduction.tsx index 381068b8..328debcb 100644 --- a/packages/docs/src/pages/introduction.tsx +++ b/packages/docs/src/pages/introduction.tsx @@ -111,11 +111,11 @@ function Introduction() {
            • Standard event patterns —{' '} - + purchaseUpdatedListener ,{' '} - + purchaseErrorListener
            • diff --git a/packages/docs/src/styles/documentation.css b/packages/docs/src/styles/documentation.css index 8927fb98..2c0a83f7 100644 --- a/packages/docs/src/styles/documentation.css +++ b/packages/docs/src/styles/documentation.css @@ -6,8 +6,16 @@ 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; @@ -19,7 +27,9 @@ 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 @@ -125,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; @@ -160,7 +171,8 @@ .menu-dropdown-header { display: flex; align-items: center; - 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)); border-left: 2px solid transparent; transition: all 0.15s ease; } @@ -222,9 +234,23 @@ color: #666; } +/* Collapse animation via grid-template-rows. The track collapses from + 1fr → 0fr (or vice-versa) and CSS interpolates intrinsic content + height for free — no JS measurement, no clipping when nested + submenus expand inside the parent. */ .menu-dropdown-content { + display: grid; + grid-template-rows: 0fr; + transition: grid-template-rows 0.25s ease-in-out; +} + +.menu-dropdown-content[data-expanded='true'] { + grid-template-rows: 1fr; +} + +.menu-dropdown-content > .menu-dropdown-items { overflow: hidden; - transition: max-height 0.25s ease-in-out; + min-height: 0; } .menu-dropdown-items { @@ -285,8 +311,13 @@ list-style: none; } +/* Nested group headers (e.g. "iOS Specific" inside Events / APIs / Types) + sit one indent level deeper than the listener/function rows above + them so the hierarchy reads as: parent group → listener rows → + sub-group → sub-group rows. The inner row indent stays even deeper + via `.menu-dropdown-items--nested .menu-dropdown-item`. */ .menu-dropdown--nested > .menu-dropdown-header { - padding-left: calc(var(--spacing-lg) + 0.5rem); + padding-left: calc(var(--sidebar-edge-fill, 0px) + var(--spacing-lg) + 1rem); } .menu-dropdown-title.menu-dropdown-title--nested { @@ -302,7 +333,7 @@ } .menu-dropdown-items--nested .menu-dropdown-item { - padding-left: calc(var(--spacing-lg) + 1.5rem); + padding-left: calc(var(--sidebar-edge-fill, 0px) + var(--spacing-lg) + 2rem); } /* Main content area */ From 9f5b10cda9640823fe7a0b387d8f2d8d90c94053 Mon Sep 17 00:00:00 2001 From: hyochan Date: Sat, 25 Apr 2026 22:26:55 +0900 Subject: [PATCH 13/41] =?UTF-8?q?docs(review):=20address=20Gemini=20round?= =?UTF-8?q?=204=20=E2=80=94=20legacy=20anchors,=20naming,=20deprecations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Restore /docs/apis and /docs/types legacy anchor redirects so old bookmarks like /docs/apis#fetch-products land on the new flat /docs/apis/fetch-products route (and same for types/iOS/Android). The redirect maps mirror the routes wired in pages/docs/index.tsx. - types/index uses the canonical name ProductSubscription - fetch-products see-also link uses ProductSubscription - active-subscription willExpireSoon strikethrough matches every other deprecated field - Restore the google-1-3-21 release-notes anchor (1-3-20 was a typo) - features/purchase API Terminology link now points at the Request APIs callout on /docs/apis/fetch-products - apis/index marks getStorefrontIOS as deprecated (line-through + cross-link to the cross-platform getStorefront) - apis/index annotates the external-purchase iOS APIs with iOS 17.4+ / iOS 16+ availability - Restore the Play Billing 8.2.0+ deprecation callout on acknowledgePurchaseAndroid and consumePurchaseAndroid Co-Authored-By: Claude Opus 4.7 (1M context) --- .../android/acknowledge-purchase-android.tsx | 13 +++ .../apis/android/consume-purchase-android.tsx | 13 +++ .../src/pages/docs/apis/fetch-products.tsx | 2 +- packages/docs/src/pages/docs/apis/index.tsx | 110 +++++++++++++++++- .../docs/src/pages/docs/features/purchase.tsx | 5 +- .../pages/docs/types/active-subscription.tsx | 4 +- packages/docs/src/pages/docs/types/index.tsx | 56 ++++++++- .../docs/src/pages/docs/types/purchase.tsx | 2 +- 8 files changed, 193 insertions(+), 12 deletions(-) diff --git a/packages/docs/src/pages/docs/apis/android/acknowledge-purchase-android.tsx b/packages/docs/src/pages/docs/apis/android/acknowledge-purchase-android.tsx index 07aecb97..2245874f 100644 --- a/packages/docs/src/pages/docs/apis/android/acknowledge-purchase-android.tsx +++ b/packages/docs/src/pages/docs/apis/android/acknowledge-purchase-android.tsx @@ -24,6 +24,19 @@ function AcknowledgePurchaseAndroid() { days or the purchase will be refunded.

              +
              +

              + ⚠️ Deprecated in Google Play Billing Library 8.2.0+.{' '} + Direct acknowledge / consume calls are being phased out in favor of + the unified Billing Programs API ( + + finishTransaction + {' '} + handles acknowledgment automatically and is the recommended path for + new code). +

              +
              +

              Signature

              {{ diff --git a/packages/docs/src/pages/docs/apis/android/consume-purchase-android.tsx b/packages/docs/src/pages/docs/apis/android/consume-purchase-android.tsx index 5fcc89f3..4f3fa0a2 100644 --- a/packages/docs/src/pages/docs/apis/android/consume-purchase-android.tsx +++ b/packages/docs/src/pages/docs/apis/android/consume-purchase-android.tsx @@ -24,6 +24,19 @@ function ConsumePurchaseAndroid() { 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

              {{ diff --git a/packages/docs/src/pages/docs/apis/fetch-products.tsx b/packages/docs/src/pages/docs/apis/fetch-products.tsx index 5387b8f0..8c5d0f9b 100644 --- a/packages/docs/src/pages/docs/apis/fetch-products.tsx +++ b/packages/docs/src/pages/docs/apis/fetch-products.tsx @@ -160,7 +160,7 @@ var products = await iap.fetch_products(request)`}

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

    ); diff --git a/packages/docs/src/pages/docs/apis/index.tsx b/packages/docs/src/pages/docs/apis/index.tsx index 7dce7fc3..df215243 100644 --- a/packages/docs/src/pages/docs/apis/index.tsx +++ b/packages/docs/src/pages/docs/apis/index.tsx @@ -1,10 +1,95 @@ -import { Link } from 'react-router-dom'; +import { useEffect } from 'react'; +import { Link, useLocation, useNavigate } from 'react-router-dom'; import AnchorLink from '../../../components/AnchorLink'; import SEO from '../../../components/SEO'; import { useScrollToHash } from '../../../hooks/useScrollToHash'; +// 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', + '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', + 'show-manage-subscriptions-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', + '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', + 'present-external-purchase-link-ios': + '/docs/apis/ios/present-external-purchase-link-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', + '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', + 'create-alternative-billing-token-android': + '/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-with-provider': '/docs/features/validation', + validation: '/docs/features/validation', + refund: '/docs/features/refund', + debugging: '/docs/features/debugging', +}; + function APIsIndex() { useScrollToHash(); + const location = useLocation(); + const navigate = useNavigate(); + + useEffect(() => { + if (!location.hash) return; + const anchor = location.hash.slice(1); + const redirect = LEGACY_ANCHOR_REDIRECTS[anchor]; + if (redirect) { + navigate(redirect, { replace: true }); + } + }, [location.hash, navigate]); return (
    @@ -200,10 +285,18 @@ function APIsIndex() { - getStorefrontIOS + + getStorefrontIOS + - Get the iOS storefront country code. + + Deprecated. Use cross-platform{' '} + + getStorefront + {' '} + instead. + @@ -340,7 +433,10 @@ function APIsIndex() { canPresentExternalPurchaseNoticeIOS - Check eligibility for the external purchase notice sheet. + + Check eligibility for the external purchase notice sheet (iOS + 17.4+). + @@ -348,7 +444,7 @@ function APIsIndex() { presentExternalPurchaseNoticeSheetIOS - Present the external purchase notice sheet. + Present the external purchase notice sheet (iOS 17.4+). @@ -356,7 +452,9 @@ function APIsIndex() { presentExternalPurchaseLinkIOS - Present an external purchase link (StoreKit External). + + Present an external purchase link, StoreKit External (iOS 16+). + diff --git a/packages/docs/src/pages/docs/features/purchase.tsx b/packages/docs/src/pages/docs/features/purchase.tsx index cd657e07..924ef357 100644 --- a/packages/docs/src/pages/docs/features/purchase.tsx +++ b/packages/docs/src/pages/docs/features/purchase.tsx @@ -356,7 +356,10 @@ func _exit_tree() -> void: purchase results — instead, listen for events through{' '} purchaseUpdatedListener or{' '} purchaseErrorListener. See{' '} - API Terminology for details. + + API Terminology + {' '} + for details.

    diff --git a/packages/docs/src/pages/docs/types/active-subscription.tsx b/packages/docs/src/pages/docs/types/active-subscription.tsx index 55ce4360..55657462 100644 --- a/packages/docs/src/pages/docs/types/active-subscription.tsx +++ b/packages/docs/src/pages/docs/types/active-subscription.tsx @@ -63,7 +63,9 @@ function ActiveSubscription() { - willExpireSoon + + willExpireSoon + boolean? diff --git a/packages/docs/src/pages/docs/types/index.tsx b/packages/docs/src/pages/docs/types/index.tsx index b5cb9003..4188e62c 100644 --- a/packages/docs/src/pages/docs/types/index.tsx +++ b/packages/docs/src/pages/docs/types/index.tsx @@ -1,8 +1,49 @@ -import { Link } from 'react-router-dom'; +import { useEffect } from 'react'; +import { Link, useLocation, useNavigate } from 'react-router-dom'; import AnchorLink from '../../../components/AnchorLink'; import SEO from '../../../components/SEO'; import { useScrollToHash } from '../../../hooks/useScrollToHash'; +// Old bookmarks like /docs/types#product redirect to the flat +// per-symbol pages introduced in this PR so existing links keep +// working. Legacy combined-page anchors (request, alternative, +// verification, ios, android, offer) point to the relevant section +// of the new index instead. +const LEGACY_ANCHOR_REDIRECTS: Record = { + product: '/docs/types/product', + 'subscription-product': '/docs/types/subscription-product', + 'product-subscription': '/docs/types/subscription-product', + storefront: '/docs/types/storefront', + purchase: '/docs/types/purchase', + 'active-subscription': '/docs/types/active-subscription', + 'product-request': '/docs/types/product-request', + 'request-purchase-props': '/docs/types/request-purchase-props', + 'discount-offer': '/docs/types/discount-offer', + 'subscription-offer': '/docs/types/subscription-offer', + 'verify-purchase': '/docs/types/verify-purchase', + 'verify-purchase-with-provider-props': + '/docs/types/verify-purchase-with-provider-props', + 'verify-purchase-with-provider-result': + '/docs/types/verify-purchase-with-provider-result', + 'alternative-billing': '/docs/types/alternative-billing-types', + 'billing-programs': '/docs/types/billing-programs', + 'external-purchase-link': '/docs/types/external-purchase-link', + // iOS-specific + 'discount-offer-ios': '/docs/types/ios/discount-offer-ios', + 'discount-ios': '/docs/types/ios/discount-ios', + 'subscription-period-ios': '/docs/types/ios/subscription-period-ios', + 'payment-mode-ios': '/docs/types/ios/payment-mode-ios', + 'subscription-status-ios': '/docs/types/ios/subscription-status-ios', + 'app-transaction-ios': '/docs/types/ios/app-transaction-ios', + 'renewal-info-ios': '/docs/types/ios/renewal-info-ios', + // Android-specific + 'one-time-purchase-offer-detail-android': + '/docs/types/android/one-time-purchase-offer-detail-android', + 'subscription-offer-android': + '/docs/types/android/subscription-offer-android', + 'pricing-phase-android': '/docs/types/android/pricing-phase-android', +}; + interface TypeRow { to: string; name: string; @@ -17,7 +58,7 @@ const COMMON_TYPES: TypeRow[] = [ }, { to: '/docs/types/subscription-product', - name: 'SubscriptionProduct', + name: 'ProductSubscription', description: 'Subscription product with billing periods and offers.', }, { @@ -171,6 +212,17 @@ function TypeTable({ rows }: { rows: TypeRow[] }) { function TypesIndex() { useScrollToHash(); + const location = useLocation(); + const navigate = useNavigate(); + + useEffect(() => { + if (!location.hash) return; + const anchor = location.hash.slice(1); + const redirect = LEGACY_ANCHOR_REDIRECTS[anchor]; + if (redirect) { + navigate(redirect, { replace: true }); + } + }, [location.hash, navigate]); return (
    diff --git a/packages/docs/src/pages/docs/types/purchase.tsx b/packages/docs/src/pages/docs/types/purchase.tsx index e70b841e..913b415f 100644 --- a/packages/docs/src/pages/docs/types/purchase.tsx +++ b/packages/docs/src/pages/docs/types/purchase.tsx @@ -73,7 +73,7 @@ function Purchase() { Note: iOS StoreKit 2 only returns Transaction objects on successful purchases, so iOS purchases always have{' '} Purchased state. See{' '} - + release notes {' '} for details. From 753bfd58cb05d980b807a56840ec34ac7663a8b2 Mon Sep 17 00:00:00 2001 From: hyochan Date: Sat, 25 Apr 2026 22:37:26 +0900 Subject: [PATCH 14/41] =?UTF-8?q?docs(review):=20address=20CodeRabbit=20ro?= =?UTF-8?q?und=205=20=E2=80=94=20request*=20framing,=20signature=20accurac?= =?UTF-8?q?y,=20casing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fetchProducts page reframes the Request APIs callout as a sibling-API note (alert-card--info) so readers don't misread it as applying to fetchProducts itself; the explicit message is "this note is about sibling APIs, not fetchProducts" - fetchProducts Kotlin and KMP signatures now return FetchProductsResult to match the actual implementations and align with Swift/Dart - developerProvidedBillingListenerAndroid renames every TS/Dart reference to the canonical DeveloperProvidedBillingDetailsAndroid - Dart examples in getting-started, request-purchase, request-purchase-props, promoted-product-listener-ios use ProductQueryType.InApp (PascalCase) per the canonical enum in libraries/flutter_inapp_purchase/lib/types.dart - searchData.ts gains a Billing Programs entry so the page is searchable - lifecycle/subscription replaces the camelCase expirationReason values (autoRenewDisabled, billingError, didNotConsentToPriceIncrease) with the canonical SCREAMING_SNAKE_CASE strings from the GraphQL schema Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/docs/src/lib/searchData.ts | 8 ++++ .../src/pages/docs/apis/fetch-products.tsx | 43 +++++++++---------- .../src/pages/docs/apis/request-purchase.tsx | 2 +- ...oper-provided-billing-listener-android.tsx | 8 ++-- .../ios/promoted-product-listener-ios.tsx | 4 +- .../docs/src/pages/docs/getting-started.tsx | 2 +- .../src/pages/docs/lifecycle/subscription.tsx | 7 ++- .../docs/types/request-purchase-props.tsx | 2 +- 8 files changed, 43 insertions(+), 33 deletions(-) diff --git a/packages/docs/src/lib/searchData.ts b/packages/docs/src/lib/searchData.ts index 467305e4..36dbc824 100644 --- a/packages/docs/src/lib/searchData.ts +++ b/packages/docs/src/lib/searchData.ts @@ -649,6 +649,14 @@ export const apiData: ApiItem[] = [ 'iOS external purchase APIs: canPresent, presentNoticeSheet, presentLink (iOS 17.4+)', 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 { diff --git a/packages/docs/src/pages/docs/apis/fetch-products.tsx b/packages/docs/src/pages/docs/apis/fetch-products.tsx index 8c5d0f9b..a3668edd 100644 --- a/packages/docs/src/pages/docs/apis/fetch-products.tsx +++ b/packages/docs/src/pages/docs/apis/fetch-products.tsx @@ -20,31 +20,34 @@ function FetchProducts() {

    Retrieve products or subscriptions from the store by SKU.

    - Request APIs + Note about request* APIs -
    +

    - ⚠️ Important: APIs starting with request{' '} - are event-based operations, not promise-based. + ℹ️{' '} + 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.

    - 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{' '} + 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 {' '} - or{' '} + /{' '} purchaseErrorListener - - . -

    -

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

    -

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

    Signature

    @@ -83,10 +82,10 @@ type FetchProductsResult = {`func fetchProducts(_ request: ProductRequest) async throws -> FetchProductsResult`} ), kotlin: ( - {`suspend fun fetchProducts(request: ProductRequest): List`} + {`suspend fun fetchProducts(request: ProductRequest): FetchProductsResult`} ), kmp: ( - {`suspend fun fetchProducts(request: ProductRequest): List`} + {`suspend fun fetchProducts(request: ProductRequest): FetchProductsResult`} ), dart: ( {`Future fetchProducts({ diff --git a/packages/docs/src/pages/docs/apis/request-purchase.tsx b/packages/docs/src/pages/docs/apis/request-purchase.tsx index d47a13fa..2568a9ed 100644 --- a/packages/docs/src/pages/docs/apis/request-purchase.tsx +++ b/packages/docs/src/pages/docs/apis/request-purchase.tsx @@ -151,7 +151,7 @@ await requestPurchase({ apple: RequestPurchaseIosProps(sku: 'com.app.premium'), google: RequestPurchaseAndroidProps(skus: ['com.app.premium']), ), - type: ProductQueryType.inApp, + type: ProductQueryType.InApp, ), );`} ), 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 index 7101c5db..0306709e 100644 --- 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 @@ -33,7 +33,7 @@ function DeveloperProvidedBillingListenerAndroid() { {{ typescript: ( {`developerProvidedBillingListener( - listener: (details: DeveloperProvidedBillingDetails) => void + listener: (details: DeveloperProvidedBillingDetailsAndroid) => void ): Subscription`} ), swift: ( @@ -52,7 +52,7 @@ fun addDeveloperProvidedBillingListener( )`} ), dart: ( - {`Stream get developerProvidedBillingStream; + {`Stream get developerProvidedBillingStream; // Android only (8.3.0+)`} ), }} @@ -165,7 +165,7 @@ subscription.cancel();`} {{ typescript: ( - {`interface DeveloperProvidedBillingDetails { + {`interface DeveloperProvidedBillingDetailsAndroid { externalTransactionToken: string; }`} ), @@ -183,7 +183,7 @@ subscription.cancel();`} )`} ), dart: ( - {`class DeveloperProvidedBillingDetails { + {`class DeveloperProvidedBillingDetailsAndroid { final String externalTransactionToken; }`} ), diff --git a/packages/docs/src/pages/docs/events/ios/promoted-product-listener-ios.tsx b/packages/docs/src/pages/docs/events/ios/promoted-product-listener-ios.tsx index 9c3d8e1e..30c9063b 100644 --- a/packages/docs/src/pages/docs/events/ios/promoted-product-listener-ios.tsx +++ b/packages/docs/src/pages/docs/events/ios/promoted-product-listener-ios.tsx @@ -132,7 +132,7 @@ final subscription = FlutterInappPurchase.promotedProductIOS.listen((productId) // Fetch product details final products = await FlutterInappPurchase.instance.fetchProducts( - ProductRequest(skus: [productId!], type: ProductQueryType.inApp), + ProductRequest(skus: [productId!], type: ProductQueryType.InApp), ); if (products.isNotEmpty) { @@ -146,7 +146,7 @@ final subscription = FlutterInappPurchase.promotedProductIOS.listen((productId) request: RequestPurchasePropsByPlatforms( apple: RequestPurchaseIosProps(sku: productId!), ), - type: ProductQueryType.inApp, + type: ProductQueryType.InApp, ), ); } diff --git a/packages/docs/src/pages/docs/getting-started.tsx b/packages/docs/src/pages/docs/getting-started.tsx index f3e3fe5f..accc13e7 100644 --- a/packages/docs/src/pages/docs/getting-started.tsx +++ b/packages/docs/src/pages/docs/getting-started.tsx @@ -265,7 +265,7 @@ await iap.requestPurchase( apple: RequestPurchaseIosProps(sku: 'com.app.premium'), google: RequestPurchaseAndroidProps(skus: ['com.app.premium']), ), - type: ProductQueryType.inApp, + type: ProductQueryType.InApp, ), );`} ), diff --git a/packages/docs/src/pages/docs/lifecycle/subscription.tsx b/packages/docs/src/pages/docs/lifecycle/subscription.tsx index 46b6e08c..57796d2c 100644 --- a/packages/docs/src/pages/docs/lifecycle/subscription.tsx +++ b/packages/docs/src/pages/docs/lifecycle/subscription.tsx @@ -781,8 +781,11 @@ function Subscription() {
  • expirationReason: Why the subscription - expired (autoRenewDisabled, billingError, - didNotConsentToPriceIncrease, etc.). + expired — "VOLUNTARY",{' '} + "BILLING_ERROR",{' '} + "DID_NOT_AGREE_TO_PRICE_INCREASE",{' '} + "PRODUCT_NOT_AVAILABLE", or{' '} + "UNKNOWN".
  • gracePeriodExpirationDate: Grace period end diff --git a/packages/docs/src/pages/docs/types/request-purchase-props.tsx b/packages/docs/src/pages/docs/types/request-purchase-props.tsx index bc2fd957..5f3bbf8b 100644 --- a/packages/docs/src/pages/docs/types/request-purchase-props.tsx +++ b/packages/docs/src/pages/docs/types/request-purchase-props.tsx @@ -166,7 +166,7 @@ await FlutterInappPurchase.instance.requestPurchase( apple: RequestPurchaseIosProps(sku: 'premium'), google: RequestPurchaseAndroidProps(skus: ['premium']), ), - type: ProductQueryType.inApp, + type: ProductQueryType.InApp, ), ); From 32672e55f324556f70f3d853b05f11fafee28fd5 Mon Sep 17 00:00:00 2001 From: hyochan Date: Sat, 25 Apr 2026 22:40:08 +0900 Subject: [PATCH 15/41] docs(review): preserve verify-purchase anchor granularity in legacy redirects Per CodeRabbit, the legacy redirects for verify-purchase and verify-purchase-with-provider should land on the matching anchors on the validation feature page so old bookmarks keep their target section, not just the page top. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/docs/src/pages/docs/apis/index.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/docs/src/pages/docs/apis/index.tsx b/packages/docs/src/pages/docs/apis/index.tsx index df215243..6d60e34a 100644 --- a/packages/docs/src/pages/docs/apis/index.tsx +++ b/packages/docs/src/pages/docs/apis/index.tsx @@ -70,8 +70,9 @@ const LEGACY_ANCHOR_REDIRECTS: Record = { '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-with-provider': '/docs/features/validation', + 'verify-purchase': '/docs/features/validation#verify-purchase', + 'verify-purchase-with-provider': + '/docs/features/validation#verify-purchase-with-provider', validation: '/docs/features/validation', refund: '/docs/features/refund', debugging: '/docs/features/debugging', From 4b1ec078c9b8495803620b9e076f945f2f6c37cc Mon Sep 17 00:00:00 2001 From: hyochan Date: Sat, 25 Apr 2026 22:51:16 +0900 Subject: [PATCH 16/41] docs: link bare type and field references throughout the docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per user request — readers should be able to click any non-primitive type / config-field name in prose and land on its reference. Wraps bare TypeName spans in across the user- facing docs (init-connection, types/*, features setup pages, getting-started, etc.). The init-connection signature now links alternativeBillingModeAndroid and enableBillingProgramAndroid to their exact anchor sections; getting-started's prose links Product, Purchase, RequestPurchaseProps, PurchaseError; types pages cross-link ProductIOS / ProductAndroid / PurchaseIOS / PurchaseAndroid anchors; billing-programs and alternative-billing-types now link every BillingProgramAndroid / enableBillingProgramAndroid mention; setup pages link ErrorCode and PurchaseError. Excludes /docs/updates/releases (release-notes archive) and code samples inside literals so wire formats stay verbatim. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../enable-billing-program-android.tsx | 14 +++++++-- .../src/pages/docs/apis/init-connection.tsx | 10 +++++-- .../docs/src/pages/docs/getting-started.tsx | 30 ++++++++++++++----- packages/docs/src/pages/docs/setup/expo.tsx | 6 +++- packages/docs/src/pages/docs/setup/index.tsx | 5 +++- .../src/pages/docs/setup/react-native.tsx | 5 +++- .../docs/types/alternative-billing-types.tsx | 21 +++++++++---- .../src/pages/docs/types/billing-programs.tsx | 7 +++-- .../docs/src/pages/docs/types/product.tsx | 11 +++++-- .../docs/src/pages/docs/types/purchase.tsx | 11 +++++-- .../docs/types/request-purchase-props.tsx | 5 +++- 11 files changed, 98 insertions(+), 27 deletions(-) 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 index a3773b84..7ef94c87 100644 --- 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 @@ -1,3 +1,4 @@ +import { Link } from 'react-router-dom'; import CodeBlock from '../../../../components/CodeBlock'; import LanguageTabs from '../../../../components/LanguageTabs'; import SEO from '../../../../components/SEO'; @@ -20,9 +21,16 @@ function 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. + as the{' '} + + enableBillingProgramAndroid + {' '} + field of{' '} + + InitConnectionConfig + {' '} + when calling initConnection() — there is no separate + top-level call.

    Signature

    diff --git a/packages/docs/src/pages/docs/apis/init-connection.tsx b/packages/docs/src/pages/docs/apis/init-connection.tsx index b4fe9a6d..3ca04cf0 100644 --- a/packages/docs/src/pages/docs/apis/init-connection.tsx +++ b/packages/docs/src/pages/docs/apis/init-connection.tsx @@ -111,8 +111,14 @@ var success = await iap.init_connection(config)`}
    InitConnectionConfig {' '} for the full list of supported config fields ( - alternativeBillingModeAndroid [deprecated],{' '} - enableBillingProgramAndroid). + + alternativeBillingModeAndroid + {' '} + [deprecated],{' '} + + enableBillingProgramAndroid + + ).

    ); diff --git a/packages/docs/src/pages/docs/getting-started.tsx b/packages/docs/src/pages/docs/getting-started.tsx index accc13e7..92e5ab92 100644 --- a/packages/docs/src/pages/docs/getting-started.tsx +++ b/packages/docs/src/pages/docs/getting-started.tsx @@ -378,18 +378,31 @@ await iap.request_purchase(props)`} Types {' '} - — flat reference, one page per type (Product,{' '} - Purchase, RequestPurchaseProps, …). Same - iOS / Android grouping as APIs. Field tables auto-link to related - types so you can chase a shape without leaving the docs. + — flat reference, one page per type ( + + Product + + ,{' '} + + Purchase + + ,{' '} + + RequestPurchaseProps + + , …). Same iOS / Android grouping as APIs. Field tables auto-link to + related types so you can chase a shape without leaving the docs.
  • Events &{' '} Errors {' '} - — listener patterns and the unified PurchaseError codes - that every SDK normalizes to. + — listener patterns and the unified{' '} + + PurchaseError + {' '} + codes that every SDK normalizes to.
  • @@ -423,7 +436,10 @@ await iap.request_purchase(props)`}

  • Errors — unified{' '} - PurchaseError codes + + PurchaseError + {' '} + codes
  • diff --git a/packages/docs/src/pages/docs/setup/expo.tsx b/packages/docs/src/pages/docs/setup/expo.tsx index 19387201..f5e3d5f8 100644 --- a/packages/docs/src/pages/docs/setup/expo.tsx +++ b/packages/docs/src/pages/docs/setup/expo.tsx @@ -1,3 +1,4 @@ +import { Link } from 'react-router-dom'; import CodeBlock from '../../../components/CodeBlock'; import SEO from '../../../components/SEO'; @@ -465,7 +466,10 @@ function Store() {

    - Errors are automatically normalized to the ErrorCode{' '} + Errors are automatically normalized to the{' '} + + ErrorCode + {' '} enum. Use the provided helper functions:

    diff --git a/packages/docs/src/pages/docs/setup/index.tsx b/packages/docs/src/pages/docs/setup/index.tsx index c373b6fb..7c2662d3 100644 --- a/packages/docs/src/pages/docs/setup/index.tsx +++ b/packages/docs/src/pages/docs/setup/index.tsx @@ -147,7 +147,10 @@ function SetupIndex() {
  • Error Handling — unified{' '} - PurchaseError codes + + PurchaseError + {' '} + codes
  • diff --git a/packages/docs/src/pages/docs/setup/react-native.tsx b/packages/docs/src/pages/docs/setup/react-native.tsx index 8bb4c98b..c9c0505a 100644 --- a/packages/docs/src/pages/docs/setup/react-native.tsx +++ b/packages/docs/src/pages/docs/setup/react-native.tsx @@ -364,7 +364,10 @@ await endConnection();`}

    - Errors are automatically normalized to the ErrorCode{' '} + Errors are automatically normalized to the{' '} + + ErrorCode + {' '} enum. Use the provided helper functions:

    diff --git a/packages/docs/src/pages/docs/types/alternative-billing-types.tsx b/packages/docs/src/pages/docs/types/alternative-billing-types.tsx index a1636aba..4c7e248e 100644 --- a/packages/docs/src/pages/docs/types/alternative-billing-types.tsx +++ b/packages/docs/src/pages/docs/types/alternative-billing-types.tsx @@ -34,8 +34,11 @@ function AlternativeBillingTypes() {
    Deprecated: Use{' '} - enableBillingProgramAndroid with{' '} - + + enableBillingProgramAndroid + {' '} + with{' '} + BillingProgramAndroid {' '} instead. @@ -113,7 +116,9 @@ function AlternativeBillingTypes() { - enableBillingProgramAndroid + + enableBillingProgramAndroid + (Recommended) Enable a specific billing program @@ -125,13 +130,19 @@ function AlternativeBillingTypes() { - alternativeBillingModeAndroid + + alternativeBillingModeAndroid + (Deprecated) {' '} - Use enableBillingProgramAndroid instead. + Use{' '} + + enableBillingProgramAndroid + {' '} + instead. diff --git a/packages/docs/src/pages/docs/types/billing-programs.tsx b/packages/docs/src/pages/docs/types/billing-programs.tsx index c247e36d..c420065e 100644 --- a/packages/docs/src/pages/docs/types/billing-programs.tsx +++ b/packages/docs/src/pages/docs/types/billing-programs.tsx @@ -32,8 +32,11 @@ function BillingPrograms() {

    Enum for different billing program types. Use with{' '} - enableBillingProgramAndroid in{' '} - + + enableBillingProgramAndroid + {' '} + in{' '} + InitConnectionConfig : diff --git a/packages/docs/src/pages/docs/types/product.tsx b/packages/docs/src/pages/docs/types/product.tsx index 1f1332b4..8c96ce1f 100644 --- a/packages/docs/src/pages/docs/types/product.tsx +++ b/packages/docs/src/pages/docs/types/product.tsx @@ -22,8 +22,15 @@ function 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.

    diff --git a/packages/docs/src/pages/docs/types/purchase.tsx b/packages/docs/src/pages/docs/types/purchase.tsx index 913b415f..a2ec1651 100644 --- a/packages/docs/src/pages/docs/types/purchase.tsx +++ b/packages/docs/src/pages/docs/types/purchase.tsx @@ -22,8 +22,15 @@ function 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.

    diff --git a/packages/docs/src/pages/docs/types/request-purchase-props.tsx b/packages/docs/src/pages/docs/types/request-purchase-props.tsx index 5f3bbf8b..8eb95c46 100644 --- a/packages/docs/src/pages/docs/types/request-purchase-props.tsx +++ b/packages/docs/src/pages/docs/types/request-purchase-props.tsx @@ -467,7 +467,10 @@ await iap.request_purchase(subs_props)`} RequestSubscriptionIosProps

    - iOS subscriptions extend RequestPurchaseIosProps{' '} + iOS subscriptions extend{' '} + + RequestPurchaseIosProps + {' '} with these additional subscription-only fields:

    From 05120bde852f184dc3962eb89fec8d94ee8a1f2d Mon Sep 17 00:00:00 2001 From: hyochan Date: Sat, 25 Apr 2026 22:54:24 +0900 Subject: [PATCH 17/41] docs(review): preserve hash on legacy redirects + complete legacy anchor maps Round 6 from Gemini: - pages/docs/index.tsx introduces NavigatePreservingHash so a legacy /docs/types/request#request-purchase-props redirect lands on /docs/types with the hash intact, allowing apis/index and types/index to translate it into a flat per-symbol page - apis/index.tsx LEGACY_ANCHOR_REDIRECTS now covers every section-level anchor that used to live on the old combined APIs page: platform-specific-apis, ios-apis, android-apis, terminology, request-apis, transaction-vs-purchase, naming-convention, plus the legacy debugging-logging slug - apis/index gains a Validation section that surfaces verifyPurchase and verifyPurchaseWithProvider so the index actually lists every public function (with deep links into the validation feature page) - types/index.tsx LEGACY_ANCHOR_REDIRECTS now maps all the old per-section anchors (product-common, purchase-state, active-subscription-common, payment-mode, app-transaction, etc.) into the flat per-symbol pages, plus the top-level combined-page anchors (common, platform-specific, ios, android, offer, request, alternative, verification) Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/docs/src/pages/docs/apis/index.tsx | 52 ++++++++++++++++++++ packages/docs/src/pages/docs/index.tsx | 42 +++++++++++----- packages/docs/src/pages/docs/types/index.tsx | 34 +++++++++++++ 3 files changed, 115 insertions(+), 13 deletions(-) diff --git a/packages/docs/src/pages/docs/apis/index.tsx b/packages/docs/src/pages/docs/apis/index.tsx index 6d60e34a..bfaefc6e 100644 --- a/packages/docs/src/pages/docs/apis/index.tsx +++ b/packages/docs/src/pages/docs/apis/index.tsx @@ -76,6 +76,15 @@ const LEGACY_ANCHOR_REDIRECTS: Record = { 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-purchase#request-apis', + 'transaction-vs-purchase': '/docs/apis#transaction-vs-purchase', + 'naming-convention': '/docs/apis#naming-convention', }; function APIsIndex() { @@ -263,6 +272,49 @@ function APIsIndex() {
    +
    + + Validation + +

    + 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. +
    +
    +
    iOS Functions diff --git a/packages/docs/src/pages/docs/index.tsx b/packages/docs/src/pages/docs/index.tsx index f8adf3fc..0e35efea 100644 --- a/packages/docs/src/pages/docs/index.tsx +++ b/packages/docs/src/pages/docs/index.tsx @@ -1,5 +1,11 @@ import { useState, useEffect } from 'react'; -import { Route, Routes, Navigate, NavLink } from 'react-router-dom'; +import { + Route, + Routes, + Navigate, + NavLink, + useLocation, +} from 'react-router-dom'; import { MenuDropdown } from '../../components/MenuDropdown'; import GettingStarted from './getting-started'; import Ecosystem from './ecosystem'; @@ -116,6 +122,16 @@ import FoundationRoadmapBudget from './foundation/roadmap-budget'; import FoundationFoundingSupporters from './foundation/founding-supporters'; import NotFound from '../404'; +/* Preserve the URL hash when redirecting away from a deprecated path so + downstream pages (apis/index, types/index) can still translate the + anchor into a flat per-symbol page. Without this, a link like + /docs/types/request#request-purchase-props would land on /docs/types + minus the hash, defeating LEGACY_ANCHOR_REDIRECTS. */ +function NavigatePreservingHash({ to }: { to: string }) { + const { hash } = useLocation(); + return ; +} + function Docs() { const [isSidebarOpen, setIsSidebarOpen] = useState(false); const [isScrolled, setIsScrolled] = useState(false); @@ -833,27 +849,27 @@ function Docs() { /> } + element={} /> } + element={} /> } + element={} /> } + element={} /> } + element={} /> } + element={} /> } /> } /> @@ -1011,27 +1027,27 @@ function Docs() { /> } + element={} /> } + element={} /> } + element={} /> } + element={} /> } + element={} /> } + element={} /> = { 'subscription-offer-android': '/docs/types/android/subscription-offer-android', 'pricing-phase-android': '/docs/types/android/pricing-phase-android', + // Legacy section anchors that pointed at the old combined type pages + 'product-common': '/docs/types/product#product-common', + 'product-ios': '/docs/types/product#product-ios', + 'product-android': '/docs/types/product#product-android', + 'subscription-product-common': + '/docs/types/subscription-product#subscription-product-common', + 'subscription-product-ios': + '/docs/types/subscription-product#subscription-product-ios', + 'subscription-product-android': + '/docs/types/subscription-product#subscription-product-android', + 'purchase-state': '/docs/types/purchase#purchase-state', + 'purchase-common': '/docs/types/purchase#purchase-common', + 'purchase-ios': '/docs/types/purchase#purchase-ios', + 'purchase-android': '/docs/types/purchase#purchase-android', + 'active-subscription-common': + '/docs/types/active-subscription#active-subscription-common', + 'active-subscription-ios': + '/docs/types/active-subscription#active-subscription-ios', + 'active-subscription-android': + '/docs/types/active-subscription#active-subscription-android', + 'app-transaction': '/docs/types/ios/app-transaction-ios', + 'payment-mode': '/docs/types/ios/payment-mode-ios', + 'subscription-period': '/docs/types/ios/subscription-period-ios', + 'subscription-status': '/docs/types/ios/subscription-status-ios', + 'discount-ios-deprecated': '/docs/types/ios/discount-ios', + // Top-level anchors on the old combined types page + common: '/docs/types', + 'platform-specific': '/docs/types', + ios: '/docs/types', + android: '/docs/types', + offer: '/docs/types', + request: '/docs/types', + alternative: '/docs/types', + verification: '/docs/types', }; interface TypeRow { From a434a74c2765d033aa9a71e5dca3b1eaa5188ff9 Mon Sep 17 00:00:00 2001 From: hyochan Date: Sat, 25 Apr 2026 23:03:13 +0900 Subject: [PATCH 18/41] docs(review): align finishTransaction TS shape, deduplicate AppTransaction, a11y on collapsed dropdowns Round 7 from Copilot + Gemini follow-up: - finish-transaction TS signature now declares `MutationFinishTransactionArgs` (the actual options-object shape expo-iap / react-native-iap export) and the example uses `{ purchase, isConsumable }`. Other docs callers (introduction, getting-started, get-available-purchases, restore-purchases) updated to the same shape; Swift/Kotlin/Dart positional forms stay since those languages keep positional args. - get-app-transaction-ios stops embedding a partial AppTransaction struct; signature now references AppTransactionIOS with a `See` link to the canonical type page. Field list mentions deviceVerification, deviceVerificationNonce, signedDate, appId, appVersionId, preorderDate, appTransactionId, originalPlatform. - MenuDropdown collapse content now sets aria-hidden + visibility:hidden + pointer-events:none (delayed on close so the closing animation plays, instant on open). Inner links drop out of the tab order and screen-reader tree when the section is collapsed. - SubMenu React key includes the first child route to guard against label collisions across siblings. - apis/index legacy redirect for `request-apis` now points at the fetch-products anchor (the actual location). - types/index legacy section anchors now resolve to the new combined index sections (#common, #ios-types, #android-types, #alternative-billing, #validation). Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/docs/src/components/MenuDropdown.tsx | 4 ++- .../pages/docs/apis/finish-transaction.tsx | 9 ++++-- .../docs/apis/get-available-purchases.tsx | 2 +- packages/docs/src/pages/docs/apis/index.tsx | 2 +- .../docs/apis/ios/get-app-transaction-ios.tsx | 28 +++++++++++-------- .../src/pages/docs/apis/restore-purchases.tsx | 2 +- .../docs/src/pages/docs/getting-started.tsx | 2 +- packages/docs/src/pages/docs/types/index.tsx | 19 +++++++------ packages/docs/src/pages/introduction.tsx | 2 +- packages/docs/src/styles/documentation.css | 15 ++++++++-- 10 files changed, 54 insertions(+), 31 deletions(-) diff --git a/packages/docs/src/components/MenuDropdown.tsx b/packages/docs/src/components/MenuDropdown.tsx index 7c09a893..91518464 100644 --- a/packages/docs/src/components/MenuDropdown.tsx +++ b/packages/docs/src/components/MenuDropdown.tsx @@ -100,6 +100,7 @@ function SubMenu({ group, onItemClick }: SubMenuProps) { id={submenuContentId} className="menu-dropdown-content" data-expanded={isExpanded} + aria-hidden={!isExpanded} >
      {group.items.map((item) => ( @@ -195,12 +196,13 @@ export function MenuDropdown({ id={contentId} className="menu-dropdown-content" data-expanded={isExpanded} + aria-hidden={!isExpanded} >
        {items.map((entry) => isGroup(entry) ? ( diff --git a/packages/docs/src/pages/docs/apis/finish-transaction.tsx b/packages/docs/src/pages/docs/apis/finish-transaction.tsx index c290ebfc..9ceb26ab 100644 --- a/packages/docs/src/pages/docs/apis/finish-transaction.tsx +++ b/packages/docs/src/pages/docs/apis/finish-transaction.tsx @@ -24,7 +24,12 @@ function FinishTransaction() { {{ typescript: ( - {`finishTransaction(purchase: Purchase, isConsumable?: boolean): Promise`} + {`finishTransaction(args: MutationFinishTransactionArgs): Promise + +interface MutationFinishTransactionArgs { + purchase: Purchase; + isConsumable?: boolean | null; +}`} ), swift: ( {`func finishTransaction(_ purchase: Purchase) async throws`} @@ -57,7 +62,7 @@ purchaseUpdatedListener(async (purchase) => { await grantProduct(purchase.productId); const isConsumable = purchase.productId.includes('coins'); - await finishTransaction(purchase, isConsumable); + await finishTransaction({ purchase, isConsumable }); });`} ), swift: ( diff --git a/packages/docs/src/pages/docs/apis/get-available-purchases.tsx b/packages/docs/src/pages/docs/apis/get-available-purchases.tsx index 8e011cd0..13e4c35d 100644 --- a/packages/docs/src/pages/docs/apis/get-available-purchases.tsx +++ b/packages/docs/src/pages/docs/apis/get-available-purchases.tsx @@ -61,7 +61,7 @@ const purchases = await getAvailablePurchases(); for (const purchase of purchases) { const verified = await verifyOnServer(purchase); if (verified) { - await finishTransaction(purchase, false); + await finishTransaction({ purchase, isConsumable: false }); } }`} ), diff --git a/packages/docs/src/pages/docs/apis/index.tsx b/packages/docs/src/pages/docs/apis/index.tsx index bfaefc6e..6de5f390 100644 --- a/packages/docs/src/pages/docs/apis/index.tsx +++ b/packages/docs/src/pages/docs/apis/index.tsx @@ -82,7 +82,7 @@ const LEGACY_ANCHOR_REDIRECTS: Record = { 'ios-apis': '/docs/apis#ios-functions', 'android-apis': '/docs/apis#android-functions', terminology: '/docs/apis#terminology', - 'request-apis': '/docs/apis/request-purchase#request-apis', + 'request-apis': '/docs/apis/fetch-products#request-apis', 'transaction-vs-purchase': '/docs/apis#transaction-vs-purchase', 'naming-convention': '/docs/apis#naming-convention', }; 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 index a1a00645..91d8c97b 100644 --- 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 @@ -1,3 +1,4 @@ +import { Link } from 'react-router-dom'; import CodeBlock from '../../../../components/CodeBlock'; import LanguageTabs from '../../../../components/LanguageTabs'; import SEO from '../../../../components/SEO'; @@ -24,21 +25,24 @@ function GetAppTransactionIOS() { {{ swift: ( - {`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? -}`} + {`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). +

    ); } diff --git a/packages/docs/src/pages/docs/apis/restore-purchases.tsx b/packages/docs/src/pages/docs/apis/restore-purchases.tsx index da031389..ff81184c 100644 --- a/packages/docs/src/pages/docs/apis/restore-purchases.tsx +++ b/packages/docs/src/pages/docs/apis/restore-purchases.tsx @@ -69,7 +69,7 @@ const handleRestore = async () => { if (!result.isValid) continue; await grantProduct(purchase.productId); - await finishTransaction(purchase, false); + await finishTransaction({ purchase, isConsumable: false }); } };`}
    ), diff --git a/packages/docs/src/pages/docs/getting-started.tsx b/packages/docs/src/pages/docs/getting-started.tsx index 92e5ab92..81ad8fe7 100644 --- a/packages/docs/src/pages/docs/getting-started.tsx +++ b/packages/docs/src/pages/docs/getting-started.tsx @@ -125,7 +125,7 @@ purchaseUpdatedListener(async (purchase) => { if (!isValid) return; await grantEntitlement(purchase.productId); - await finishTransaction(purchase, /* isConsumable */ false); + await finishTransaction({ purchase, isConsumable: false }); }); // 4. Initiate a purchase. diff --git a/packages/docs/src/pages/docs/types/index.tsx b/packages/docs/src/pages/docs/types/index.tsx index c028e970..9426736e 100644 --- a/packages/docs/src/pages/docs/types/index.tsx +++ b/packages/docs/src/pages/docs/types/index.tsx @@ -67,15 +67,16 @@ const LEGACY_ANCHOR_REDIRECTS: Record = { 'subscription-period': '/docs/types/ios/subscription-period-ios', 'subscription-status': '/docs/types/ios/subscription-status-ios', 'discount-ios-deprecated': '/docs/types/ios/discount-ios', - // Top-level anchors on the old combined types page - common: '/docs/types', - 'platform-specific': '/docs/types', - ios: '/docs/types', - android: '/docs/types', - offer: '/docs/types', - request: '/docs/types', - alternative: '/docs/types', - verification: '/docs/types', + // Top-level anchors on the old combined types page — map to the + // matching section on the new types index when one exists. + common: '/docs/types#common', + 'platform-specific': '/docs/types#ios-types', + ios: '/docs/types#ios-types', + android: '/docs/types#android-types', + alternative: '/docs/types#alternative-billing', + verification: '/docs/types#validation', + offer: '/docs/types#common', + request: '/docs/types#common', }; interface TypeRow { diff --git a/packages/docs/src/pages/introduction.tsx b/packages/docs/src/pages/introduction.tsx index 328debcb..f968f2cb 100644 --- a/packages/docs/src/pages/introduction.tsx +++ b/packages/docs/src/pages/introduction.tsx @@ -341,7 +341,7 @@ 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 diff --git a/packages/docs/src/styles/documentation.css b/packages/docs/src/styles/documentation.css index 2c0a83f7..060e9a20 100644 --- a/packages/docs/src/styles/documentation.css +++ b/packages/docs/src/styles/documentation.css @@ -237,15 +237,26 @@ /* Collapse animation via grid-template-rows. The track collapses from 1fr → 0fr (or vice-versa) and CSS interpolates intrinsic content height for free — no JS measurement, no clipping when nested - submenus expand inside the parent. */ + submenus expand inside the parent. The visibility step is delayed + on collapse so the closing animation plays, then the inner links + drop out of the tab order / accessibility tree. */ .menu-dropdown-content { display: grid; grid-template-rows: 0fr; - transition: grid-template-rows 0.25s ease-in-out; + visibility: hidden; + pointer-events: none; + transition: + grid-template-rows 0.25s ease-in-out, + visibility 0s 0.25s; } .menu-dropdown-content[data-expanded='true'] { grid-template-rows: 1fr; + visibility: visible; + pointer-events: auto; + transition: + grid-template-rows 0.25s ease-in-out, + visibility 0s 0s; } .menu-dropdown-content > .menu-dropdown-items { From 64d4d99e4a8ea96a9734560bce12f8254eeb6194 Mon Sep 17 00:00:00 2001 From: hyochan Date: Sat, 25 Apr 2026 23:17:16 +0900 Subject: [PATCH 19/41] docs: weave useIAP, react-native-iap import comment, Flutter/KMP DSL into examples + link listener refs TS examples on every per-symbol API page (and features/purchase) now show both the direct expo-iap call style and the equivalent useIAP() hook variant, with a top comment noting that react-native-iap exposes the exact same API. Connection lifecycle pages (initConnection / endConnection) flag that useIAP auto-handles connect/disconnect, so the manual call is the explicit-control alternative. Dart and KMP examples in request-purchase, finish-transaction, and the Getting Started walkthrough now also document the language-idiomatic patterns: Flutter's `iap.requestPurchaseWithBuilder(build: (builder) {})` DSL and KMP's `kmpIAP.requestPurchase { ios { } android { } }` DSL, plus the `purchase.toPurchaseInput()` flow that feeds finishTransaction. Bare purchaseUpdatedListener / purchaseErrorListener mentions throughout the docs are now wrapped in tags so readers can click straight into the per-listener page. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../enable-billing-program-android.tsx | 21 +- .../docs/apis/deep-link-to-subscriptions.tsx | 29 ++- .../src/pages/docs/apis/end-connection.tsx | 20 +- .../src/pages/docs/apis/fetch-products.tsx | 31 ++- .../pages/docs/apis/finish-transaction.tsx | 44 +++- .../docs/apis/get-active-subscriptions.tsx | 26 +- .../docs/apis/get-available-purchases.tsx | 32 ++- .../src/pages/docs/apis/get-storefront.tsx | 25 +- .../docs/apis/has-active-subscriptions.tsx | 21 +- packages/docs/src/pages/docs/apis/index.tsx | 10 +- .../src/pages/docs/apis/init-connection.tsx | 22 +- .../src/pages/docs/apis/request-purchase.tsx | 89 ++++++- .../src/pages/docs/apis/restore-purchases.tsx | 44 +++- .../docs/features/offer-code-redemption.tsx | 8 +- .../docs/src/pages/docs/features/purchase.tsx | 241 +++++++++++++++++- .../docs/src/pages/docs/getting-started.tsx | 31 +++ .../docs/src/pages/docs/lifecycle/index.tsx | 10 +- .../docs/types/external-purchase-link.tsx | 8 +- packages/docs/src/pages/home.tsx | 24 +- 19 files changed, 682 insertions(+), 54 deletions(-) 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 index 7ef94c87..39452d6c 100644 --- 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 @@ -37,10 +37,27 @@ function EnableBillingProgramAndroid() { {{ typescript: ( - {`await initConnection({ + {`// 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( 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 index fdcfd440..8f96820c 100644 --- a/packages/docs/src/pages/docs/apis/deep-link-to-subscriptions.tsx +++ b/packages/docs/src/pages/docs/apis/deep-link-to-subscriptions.tsx @@ -54,12 +54,37 @@ interface DeepLinkOptions { {{ typescript: ( - {`import { deepLinkToSubscriptions } from 'expo-iap'; + {`// 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 ( +
    diff --git a/packages/docs/src/pages/docs/apis/fetch-products.tsx b/packages/docs/src/pages/docs/apis/fetch-products.tsx index d4808340..4245acaf 100644 --- a/packages/docs/src/pages/docs/apis/fetch-products.tsx +++ b/packages/docs/src/pages/docs/apis/fetch-products.tsx @@ -94,7 +94,10 @@ type FetchProductsResult = });`} ), gdscript: ( - {`func fetch_products(request: ProductRequest) -> Array[Product]`} + {`# 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`} ), }} diff --git a/packages/docs/src/pages/docs/apis/request-purchase.tsx b/packages/docs/src/pages/docs/apis/request-purchase.tsx index 53faa4ed..ade42a18 100644 --- a/packages/docs/src/pages/docs/apis/request-purchase.tsx +++ b/packages/docs/src/pages/docs/apis/request-purchase.tsx @@ -73,10 +73,10 @@ type RequestPurchaseProps = {`func requestPurchase(_ params: RequestPurchaseProps) async throws -> RequestPurchaseResult?`} ), kotlin: ( - {`suspend fun requestPurchase(props: RequestPurchaseProps): List`} + {`suspend fun requestPurchase(props: RequestPurchaseProps): Purchase?`} ), kmp: ( - {`suspend fun requestPurchase(props: RequestPurchaseProps): List`} + {`suspend fun requestPurchase(props: RequestPurchaseProps): Purchase?`} ), dart: ( {`Future requestPurchase(RequestPurchaseProps props);`} From 527bedd98b5b4b3279f91b07e508a3016ad85d31 Mon Sep 17 00:00:00 2001 From: hyochan Date: Sat, 25 Apr 2026 23:50:07 +0900 Subject: [PATCH 24/41] docs(review): finishTransaction Swift, Horizon caveat, hash-preserving redirects, listener consistency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round 10 from Copilot/Gemini/CodeRabbit: - finish-transaction Swift signature now exposes the isConsumable flag (matches the example below). - events.tsx subscriptionBillingIssueListener row now flags that Horizon does not emit it. - types/active-subscription Android fields autoRenewingAndroid, basePlanIdAndroid, purchaseTokenAndroid are now typed nullable (boolean? / string?) per the schema/Swift/Kotlin sources. - types/billing-programs prose link for enableBillingProgramAndroid points at the API/config docs page (not the same-page enum anchor). Type-cell wraps for BillingProgramAndroid keep their in-page anchor. - pages/docs/index validation/debugging redirect routes now use NavigatePreservingHash so old deep links like /docs/apis/validation #verify-purchase land on the matching anchor instead of the page top. - Getting Started Swift / Kotlin / KMP examples now use the canonical purchaseUpdates stream/flow names (matching the listener reference page) — was onPurchaseSuccess / purchaseFlow / purchaseUpdatedFlow. - subscription-billing-issue-listener page gains the Example section (TS dual-style + useIAP variant, Swift, Kotlin, KMP, Dart, GDScript) matching every other listener page in the PR. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../pages/docs/apis/finish-transaction.tsx | 2 +- packages/docs/src/pages/docs/events.tsx | 2 +- .../subscription-billing-issue-listener.tsx | 89 +++++++++++++++++++ .../docs/src/pages/docs/getting-started.tsx | 8 +- packages/docs/src/pages/docs/index.tsx | 4 +- .../pages/docs/types/active-subscription.tsx | 6 +- .../src/pages/docs/types/billing-programs.tsx | 2 +- 7 files changed, 101 insertions(+), 12 deletions(-) diff --git a/packages/docs/src/pages/docs/apis/finish-transaction.tsx b/packages/docs/src/pages/docs/apis/finish-transaction.tsx index 44713375..fe1cb88e 100644 --- a/packages/docs/src/pages/docs/apis/finish-transaction.tsx +++ b/packages/docs/src/pages/docs/apis/finish-transaction.tsx @@ -32,7 +32,7 @@ interface MutationFinishTransactionArgs { }`} ), swift: ( - {`func finishTransaction(_ purchase: Purchase) async throws`} + {`func finishTransaction(_ purchase: Purchase, isConsumable: Bool = false) async throws`} ), kotlin: ( {`suspend fun finishTransaction(purchase: Purchase, isConsumable: Boolean = false)`} diff --git a/packages/docs/src/pages/docs/events.tsx b/packages/docs/src/pages/docs/events.tsx index c99f264b..6d747c86 100644 --- a/packages/docs/src/pages/docs/events.tsx +++ b/packages/docs/src/pages/docs/events.tsx @@ -137,7 +137,7 @@ function Events() { Fires when an active subscription enters a billing issue state - (iOS 18+ / Play Billing 8.1+). + (iOS 18+ / Play Billing 8.1+; not emitted on Horizon). 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 index 420044c0..0e896b36 100644 --- a/packages/docs/src/pages/docs/events/subscription-billing-issue-listener.tsx +++ b/packages/docs/src/pages/docs/events/subscription-billing-issue-listener.tsx @@ -69,6 +69,95 @@ val subscriptionBillingIssueListener: Flow`} 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 + +// Play Billing Library 8.1+ +openIapStore.addSubscriptionBillingIssueListener { purchase -> + println("Billing issue on \${purchase.productId}") + showBillingIssueBanner(purchase) +}`} + ), + kmp: ( + {`import io.github.hyochan.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{' '} diff --git a/packages/docs/src/pages/docs/getting-started.tsx b/packages/docs/src/pages/docs/getting-started.tsx index da2679ee..386b2671 100644 --- a/packages/docs/src/pages/docs/getting-started.tsx +++ b/packages/docs/src/pages/docs/getting-started.tsx @@ -151,8 +151,8 @@ let products = try await store.fetchProducts( ) // 3. Listen for purchase results — requestPurchase is event-based. -store.onPurchaseSuccess = { purchase in - Task { +Task { + for await purchase in store.purchaseUpdates { // Verify on your backend, grant entitlement, then finish. try await store.finishTransaction(purchase, isConsumable: false) } @@ -187,7 +187,7 @@ val products = store.fetchProducts( // 3. Listen for purchase results. scope.launch { - store.purchaseFlow.collect { purchase -> + store.purchaseUpdates.collect { purchase -> // Verify on your backend, grant entitlement, then finish. store.finishTransaction(purchase, isConsumable = false) } @@ -220,7 +220,7 @@ val products = kmpIAP.fetchProducts( // 3. Listen for purchase results. scope.launch { - kmpIAP.purchaseUpdatedFlow.collect { purchase -> + kmpIAP.purchaseUpdates.collect { purchase -> // Verify on your backend, grant entitlement, then finish. kmpIAP.finishTransaction(purchase, isConsumable = false) } diff --git a/packages/docs/src/pages/docs/index.tsx b/packages/docs/src/pages/docs/index.tsx index 24563473..af1b86dc 100644 --- a/packages/docs/src/pages/docs/index.tsx +++ b/packages/docs/src/pages/docs/index.tsx @@ -1083,11 +1083,11 @@ function Docs() { /> } + element={} /> } + element={} /> } /> autoRenewingAndroid - boolean + boolean? Whether subscription will auto-renew @@ -216,7 +216,7 @@ function ActiveSubscription() { basePlanIdAndroid - string + string? Base plan identifier. ⚠️ May be @@ -232,7 +232,7 @@ function ActiveSubscription() { purchaseTokenAndroid - string + string? Purchase token for upgrade/downgrade operations diff --git a/packages/docs/src/pages/docs/types/billing-programs.tsx b/packages/docs/src/pages/docs/types/billing-programs.tsx index c420065e..a55ea873 100644 --- a/packages/docs/src/pages/docs/types/billing-programs.tsx +++ b/packages/docs/src/pages/docs/types/billing-programs.tsx @@ -32,7 +32,7 @@ function BillingPrograms() {

    Enum for different billing program types. Use with{' '} - + enableBillingProgramAndroid {' '} in{' '} From ab852758db5c3b7714022e0483713e07ed99270d Mon Sep 17 00:00:00 2001 From: hyochan Date: Sat, 25 Apr 2026 23:56:54 +0900 Subject: [PATCH 25/41] docs(review): collapse SubMenu header into single disclosure button (a11y) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per Gemini, the nested SubMenu rendered two adjacent buttons that did the same thing — title and chevron both toggled the same submenu. That created a redundant tab stop for keyboard users. Combine them into a single - - + +

    (top-level + MenuDropdown — wraps a separate nav button + toggle button) or a +
    ); } diff --git a/packages/docs/src/pages/docs/features/discount.tsx b/packages/docs/src/pages/docs/features/discount.tsx index bff69f84..8a6b8c21 100644 --- a/packages/docs/src/pages/docs/features/discount.tsx +++ b/packages/docs/src/pages/docs/features/discount.tsx @@ -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 8133f072..d3cba870 100644 --- a/packages/docs/src/pages/docs/features/external-purchase.tsx +++ b/packages/docs/src/pages/docs/features/external-purchase.tsx @@ -2824,6 +2824,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 24cab93b..9bbfafb9 100644 --- a/packages/docs/src/pages/docs/features/offer-code-redemption.tsx +++ b/packages/docs/src/pages/docs/features/offer-code-redemption.tsx @@ -583,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 0fd11ccd..78e461e4 100644 --- a/packages/docs/src/pages/docs/features/purchase.tsx +++ b/packages/docs/src/pages/docs/features/purchase.tsx @@ -2029,6 +2029,64 @@ func _ready() -> void: }} + +
    + + Native References + + +
    ); } diff --git a/packages/docs/src/pages/docs/features/refund.tsx b/packages/docs/src/pages/docs/features/refund.tsx index c50248db..90001f32 100644 --- a/packages/docs/src/pages/docs/features/refund.tsx +++ b/packages/docs/src/pages/docs/features/refund.tsx @@ -466,6 +466,58 @@ app.post('/webhooks/google', async (req, res) => { + +
    + + Native References + + +
    ); } diff --git a/packages/docs/src/pages/docs/features/subscription-billing-issue.tsx b/packages/docs/src/pages/docs/features/subscription-billing-issue.tsx index f810c994..142fe399 100644 --- a/packages/docs/src/pages/docs/features/subscription-billing-issue.tsx +++ b/packages/docs/src/pages/docs/features/subscription-billing-issue.tsx @@ -226,6 +226,54 @@ kmpIapInstance.subscriptionBillingIssueListener .inBillingRetryPeriod or .inGracePeriod.

    + +
    + + Native References + + +
    ); } diff --git a/packages/docs/src/pages/docs/features/subscription/index.tsx b/packages/docs/src/pages/docs/features/subscription/index.tsx index 5aea88e6..3ff4f906 100644 --- a/packages/docs/src/pages/docs/features/subscription/index.tsx +++ b/packages/docs/src/pages/docs/features/subscription/index.tsx @@ -3339,6 +3339,54 @@ func manage_subscriptions() -> void: + +
    + + Native References + + +
    ); } diff --git a/packages/docs/src/pages/docs/features/subscription/upgrade-downgrade.tsx b/packages/docs/src/pages/docs/features/subscription/upgrade-downgrade.tsx index 05eb7491..73ee0fa2 100644 --- a/packages/docs/src/pages/docs/features/subscription/upgrade-downgrade.tsx +++ b/packages/docs/src/pages/docs/features/subscription/upgrade-downgrade.tsx @@ -1100,6 +1100,34 @@ func format_date(timestamp: int) -> String:
      +
    • + + Apple · App Store Subscriptions + +
      + + Subscription groups, upgrade/downgrade hierarchy, and tier + order + +
    • +
    • + + Apple · Product.SubscriptionInfo.RenewalInfo + +
      + + autoRenewPreference is the source of truth + for the next-renewed product ID + +
    • void:
        +
      • + + Google Play Billing: Upgrade or downgrade a subscription + +
        + + Replacement modes, proration, and base-plan switching + +
      • + +
        + + Native References + + +
        ); } diff --git a/packages/docs/src/pages/docs/types/active-subscription.tsx b/packages/docs/src/pages/docs/types/active-subscription.tsx index 5871e307..ded7be2c 100644 --- a/packages/docs/src/pages/docs/types/active-subscription.tsx +++ b/packages/docs/src/pages/docs/types/active-subscription.tsx @@ -29,6 +29,24 @@ function ActiveSubscription() { . Provides a unified view of subscription status across platforms.

        +

        + Native references:{' '} + + Apple · Product.SubscriptionInfo.Status + + {' · '} + + Google · Sell subscriptions + +

        Common Fields diff --git a/packages/docs/src/pages/docs/types/alternative-billing-types.tsx b/packages/docs/src/pages/docs/types/alternative-billing-types.tsx index 4c7e248e..b91fe497 100644 --- a/packages/docs/src/pages/docs/types/alternative-billing-types.tsx +++ b/packages/docs/src/pages/docs/types/alternative-billing-types.tsx @@ -25,6 +25,24 @@ function AlternativeBillingTypes() { Types for configuring alternative billing systems, primarily used for Android.

        +

        + Native references:{' '} + + Google · Alternative billing + + {' · '} + + Google · User Choice Billing + +

        AlternativeBillingModeAndroid{' '} diff --git a/packages/docs/src/pages/docs/types/android/one-time-purchase-offer-detail-android.tsx b/packages/docs/src/pages/docs/types/android/one-time-purchase-offer-detail-android.tsx index b756d5eb..65040eb5 100644 --- a/packages/docs/src/pages/docs/types/android/one-time-purchase-offer-detail-android.tsx +++ b/packages/docs/src/pages/docs/types/android/one-time-purchase-offer-detail-android.tsx @@ -39,6 +39,16 @@ function OneTimePurchaseOfferDetailAndroid() { . For implementation examples, see the{' '} Discounts feature guide.

        +

        + Native reference:{' '} + + Google · ProductDetails.OneTimePurchaseOfferDetails + +

        diff --git a/packages/docs/src/pages/docs/types/android/subscription-offer-android.tsx b/packages/docs/src/pages/docs/types/android/subscription-offer-android.tsx index f5ca6178..25b9d156 100644 --- a/packages/docs/src/pages/docs/types/android/subscription-offer-android.tsx +++ b/packages/docs/src/pages/docs/types/android/subscription-offer-android.tsx @@ -28,6 +28,16 @@ function SubscriptionOfferAndroid() { (cross-platform) instead.

        Offer details for subscription purchases:

        +

        + Native reference:{' '} + + Google · ProductDetails.SubscriptionOfferDetails + +

        diff --git a/packages/docs/src/pages/docs/types/billing-programs.tsx b/packages/docs/src/pages/docs/types/billing-programs.tsx index a55ea873..5c107ca4 100644 --- a/packages/docs/src/pages/docs/types/billing-programs.tsx +++ b/packages/docs/src/pages/docs/types/billing-programs.tsx @@ -26,6 +26,24 @@ function BillingPrograms() { API, which provides a more structured approach to external offers and content links. Version 8.3.0 adds External Payments for Japan.

        +

        + Native references:{' '} + + Google · Play Billing 8.2.0 release notes + + {' · '} + + 8.3.0 release notes + +

        BillingProgramAndroid diff --git a/packages/docs/src/pages/docs/types/discount-offer.tsx b/packages/docs/src/pages/docs/types/discount-offer.tsx index d73317f3..6369cf12 100644 --- a/packages/docs/src/pages/docs/types/discount-offer.tsx +++ b/packages/docs/src/pages/docs/types/discount-offer.tsx @@ -26,6 +26,24 @@ function DiscountOffer() { supported on Android (Google Play Billing Library 7.0+). iOS does not support one-time purchase discounts.

        +

        + Native references:{' '} + + Google · Discounted offers + + {' · '} + + ProductDetails.OneTimePurchaseOfferDetails + +

        Common Fields diff --git a/packages/docs/src/pages/docs/types/external-purchase-link.tsx b/packages/docs/src/pages/docs/types/external-purchase-link.tsx index 431deb9e..317f3872 100644 --- a/packages/docs/src/pages/docs/types/external-purchase-link.tsx +++ b/packages/docs/src/pages/docs/types/external-purchase-link.tsx @@ -26,6 +26,24 @@ function ExternalPurchaseLink() { payment using Apple's StoreKit ExternalPurchase API. Available from iOS 17.4+ (notice sheet) and iOS 18.2+ (custom links).

        +

        + Native references:{' '} + + Apple · StoreKit ExternalPurchase + + {' · '} + + External purchase entitlement + +

        diff --git a/packages/docs/src/pages/docs/types/ios/app-transaction-ios.tsx b/packages/docs/src/pages/docs/types/ios/app-transaction-ios.tsx index 5fbbaecc..1fe29e47 100644 --- a/packages/docs/src/pages/docs/types/ios/app-transaction-ios.tsx +++ b/packages/docs/src/pages/docs/types/ios/app-transaction-ios.tsx @@ -28,6 +28,16 @@ function AppTransactionIos() { . Contains metadata about the app's purchase and installation.

        +

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

        Fields diff --git a/packages/docs/src/pages/docs/types/ios/discount-ios.tsx b/packages/docs/src/pages/docs/types/ios/discount-ios.tsx index 1aeef0e4..93cfcb37 100644 --- a/packages/docs/src/pages/docs/types/ios/discount-ios.tsx +++ b/packages/docs/src/pages/docs/types/ios/discount-ios.tsx @@ -25,6 +25,16 @@ function DiscountIos() { instead.

        Discount info returned as part of product details:

        +

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

        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 index e44f73c9..8df79822 100644 --- a/packages/docs/src/pages/docs/types/ios/discount-offer-ios.tsx +++ b/packages/docs/src/pages/docs/types/ios/discount-offer-ios.tsx @@ -28,6 +28,16 @@ function DiscountOfferIos() { Used when requesting a purchase with a promotional offer. Generate signature server-side.

        +

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

        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 index 4a3b8169..e9b9f971 100644 --- a/packages/docs/src/pages/docs/types/ios/payment-mode-ios.tsx +++ b/packages/docs/src/pages/docs/types/ios/payment-mode-ios.tsx @@ -19,6 +19,16 @@ function PaymentModeIos() { PaymentMode

        Payment mode for offers:

        +

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

        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 index cffa441f..5997494d 100644 --- a/packages/docs/src/pages/docs/types/ios/subscription-period-ios.tsx +++ b/packages/docs/src/pages/docs/types/ios/subscription-period-ios.tsx @@ -19,6 +19,16 @@ function SubscriptionPeriodIos() { SubscriptionPeriodIOS

        Subscription period units:

        +

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

        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 index 6862e349..f17880d2 100644 --- a/packages/docs/src/pages/docs/types/ios/subscription-status-ios.tsx +++ b/packages/docs/src/pages/docs/types/ios/subscription-status-ios.tsx @@ -23,6 +23,16 @@ function SubscriptionStatusIos() { subscriptionStatusIOS(sku) to get detailed subscription state.

        +

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

        diff --git a/packages/docs/src/pages/docs/types/product-request.tsx b/packages/docs/src/pages/docs/types/product-request.tsx index 06e01dcf..cf393688 100644 --- a/packages/docs/src/pages/docs/types/product-request.tsx +++ b/packages/docs/src/pages/docs/types/product-request.tsx @@ -28,6 +28,24 @@ function ProductRequest() { .

        +

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

        Fields diff --git a/packages/docs/src/pages/docs/types/product.tsx b/packages/docs/src/pages/docs/types/product.tsx index 8c96ce1f..8e007389 100644 --- a/packages/docs/src/pages/docs/types/product.tsx +++ b/packages/docs/src/pages/docs/types/product.tsx @@ -32,6 +32,24 @@ function Product() { , discriminated by the platform field.

        +

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

        Common Fields diff --git a/packages/docs/src/pages/docs/types/purchase.tsx b/packages/docs/src/pages/docs/types/purchase.tsx index a2ec1651..bab1f753 100644 --- a/packages/docs/src/pages/docs/types/purchase.tsx +++ b/packages/docs/src/pages/docs/types/purchase.tsx @@ -32,6 +32,24 @@ function Purchase() { , discriminated by the platform field.

        +

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

        PurchaseState diff --git a/packages/docs/src/pages/docs/types/request-purchase-props.tsx b/packages/docs/src/pages/docs/types/request-purchase-props.tsx index 8eb95c46..28eb347d 100644 --- a/packages/docs/src/pages/docs/types/request-purchase-props.tsx +++ b/packages/docs/src/pages/docs/types/request-purchase-props.tsx @@ -29,6 +29,24 @@ function RequestPurchaseProps() { .

        +

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

        RequestPurchaseProps diff --git a/packages/docs/src/pages/docs/types/storefront.tsx b/packages/docs/src/pages/docs/types/storefront.tsx index 047668d2..2b4c2332 100644 --- a/packages/docs/src/pages/docs/types/storefront.tsx +++ b/packages/docs/src/pages/docs/types/storefront.tsx @@ -30,6 +30,24 @@ function Storefront() { .

        +

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

        Return shape

        diff --git a/packages/docs/src/pages/docs/types/subscription-offer.tsx b/packages/docs/src/pages/docs/types/subscription-offer.tsx index 543c38e5..7aa690e1 100644 --- a/packages/docs/src/pages/docs/types/subscription-offer.tsx +++ b/packages/docs/src/pages/docs/types/subscription-offer.tsx @@ -26,6 +26,24 @@ function SubscriptionOffer() { both iOS (introductory and promotional offers) and Android (offer tokens with pricing phases).

        +

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

        Common Fields diff --git a/packages/docs/src/pages/docs/types/subscription-product.tsx b/packages/docs/src/pages/docs/types/subscription-product.tsx index 4a10ead8..a52bc6d5 100644 --- a/packages/docs/src/pages/docs/types/subscription-product.tsx +++ b/packages/docs/src/pages/docs/types/subscription-product.tsx @@ -25,6 +25,24 @@ function SubscriptionProduct() { 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 diff --git a/packages/docs/src/pages/docs/types/verify-purchase.tsx b/packages/docs/src/pages/docs/types/verify-purchase.tsx index aa76db22..36d4b3a1 100644 --- a/packages/docs/src/pages/docs/types/verify-purchase.tsx +++ b/packages/docs/src/pages/docs/types/verify-purchase.tsx @@ -23,6 +23,32 @@ function VerifyPurchase() { 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 From c32e95ee3b1948ba43e3fbf74b437b96ce0d80db Mon Sep 17 00:00:00 2001 From: hyochan Date: Sun, 26 Apr 2026 00:50:22 +0900 Subject: [PATCH 28/41] docs: fix table layout regressions on external-purchase and billing-issue pages - Revert tables from display:block (which caused tall empty rows) back to display:table; width:100% with the shrink-to-fit pattern on non-last columns and word-wrap on the last column - Platform Support: split iOS row into two lines for Notice Sheet vs New APIs versions - Platform behavior: collapse 4 columns to 3, fold Minimum Version into the Signal Source cell as a sub-line so the long Delivery text no longer fights for width; macOS / tvOS / watchOS / visionOS now stack one per line Co-Authored-By: Claude Opus 4.7 (1M context) --- .../pages/docs/features/external-purchase.tsx | 6 +++- .../features/subscription-billing-issue.tsx | 28 +++++++++++++------ packages/docs/src/styles/documentation.css | 27 +++++++++++------- 3 files changed, 42 insertions(+), 19 deletions(-) diff --git a/packages/docs/src/pages/docs/features/external-purchase.tsx b/packages/docs/src/pages/docs/features/external-purchase.tsx index d3cba870..48cbc92d 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() {
        - + diff --git a/packages/docs/src/pages/docs/features/subscription-billing-issue.tsx b/packages/docs/src/pages/docs/features/subscription-billing-issue.tsx index 142fe399..0ccfdeeb 100644 --- a/packages/docs/src/pages/docs/features/subscription-billing-issue.tsx +++ b/packages/docs/src/pages/docs/features/subscription-billing-issue.tsx @@ -34,7 +34,6 @@ function SubscriptionBillingIssue() { - @@ -42,42 +41,55 @@ function SubscriptionBillingIssue() { - - - - + - - + -
        iOS External Purchase URLiOS 17.4+ (Notice Sheet), iOS 18.2+ (New APIs) + iOS 17.4+ (Notice Sheet) +
        + iOS 18.2+ (New APIs) +
        StoreKit 2
        Platform Signal Source DeliveryMinimum Version
        iOS / iPadOS StoreKit.Message.Reason.billingIssue +
        + iOS 18.0+
        Push, while app is activeiOS 18.0+
        Mac Catalyst StoreKit.Message.Reason.billingIssue +
        + Mac Catalyst 18.0+
        Push, while app is activeMac Catalyst 18.0+
        Android (Play) Purchase.isSuspended +
        + Play Billing Library 8.1+
        Poll via getAvailablePurchases or on{' '} onPurchasesUpdated Play Billing Library 8.1+
        Android (Meta Horizon)Not available + Not available +
        + Billing 7.0 compat SDK +
        Never fires (silent no-op)N/A — Billing 7.0 compat SDK
        macOS / tvOS / watchOS / visionOS + macOS +
        + tvOS +
        + watchOS +
        + visionOS +
        StoreKit.Message not available Never firesN/A
        diff --git a/packages/docs/src/styles/documentation.css b/packages/docs/src/styles/documentation.css index 9fd1acc8..cb167f13 100644 --- a/packages/docs/src/styles/documentation.css +++ b/packages/docs/src/styles/documentation.css @@ -559,19 +559,16 @@ opacity: 1; } -/* Tables — block layout so they scroll horizontally only when content - exceeds the container. `display: block` plus `overflow-x: auto` keeps - the scrollbar hidden until a row actually overflows. */ +/* Tables — full-width table layout. Non-last columns shrink to fit their + nowrap content; the last column absorbs remaining width and wraps. */ .error-table, .doc-page table { - display: block; - max-width: 100%; - overflow-x: auto; - -webkit-overflow-scrolling: touch; + width: 100%; border-collapse: collapse; margin: 1.5rem 0; background: var(--bg-primary); border-radius: 0.5rem; + overflow: hidden; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); } @@ -599,23 +596,28 @@ border-bottom: 1px solid var(--border-color); } -/* Non-last columns (Name, Type, etc.) keep identifiers on one line; if the - total exceeds the container width the whole table scrolls horizontally. */ +/* Non-last columns (Name, Type, etc.) shrink to fit their content and + keep identifiers on one line. */ .error-table th:not(:last-child), .error-table td:not(:last-child), .doc-page table th:not(:last-child), .doc-page table td:not(:last-child) { + width: 1%; white-space: nowrap; vertical-align: top; } -/* Last column (Summary/Description) wraps naturally. */ +/* Last column (Summary/Description) absorbs remaining width and wraps at + word boundaries only — never breaks single characters per line. */ .error-table th:last-child, .error-table td:last-child, .doc-page table th:last-child, .doc-page table td:last-child { + width: auto; white-space: normal; vertical-align: top; + overflow-wrap: anywhere; + word-break: normal; } .error-table tbody tr:last-child td, @@ -939,6 +941,11 @@ top: 0 !important; left: 0; bottom: 0; + /* Reset desktop centering offset — at viewports below --max-width, + calc((--max-width - 100vw) / 2) is positive and would push the + fixed drawer hundreds of pixels to the right of the viewport. */ + margin-left: 0; + --sidebar-edge-fill: 0px; width: 280px; max-width: 85vw; height: 100vh !important; From 3922e3de01a6f4ed821bf63f04ea03188d67a0c7 Mon Sep 17 00:00:00 2001 From: hyochan Date: Sun, 26 Apr 2026 00:51:12 +0900 Subject: [PATCH 29/41] =?UTF-8?q?docs(review):=20fix=20Onside=20=E2=86=92?= =?UTF-8?q?=20Onsite=20typo=20(Copilot/Gemini)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/docs/src/pages/docs/features/external-purchase.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/docs/src/pages/docs/features/external-purchase.tsx b/packages/docs/src/pages/docs/features/external-purchase.tsx index 48cbc92d..23644b67 100644 --- a/packages/docs/src/pages/docs/features/external-purchase.tsx +++ b/packages/docs/src/pages/docs/features/external-purchase.tsx @@ -2810,7 +2810,7 @@ func _ready_user_choice() -> void: Alternative Marketplace {' '} - - Onside & alternative billing flows + - Onsite & alternative billing flows
      • From 10af2cd803f41509dff06603c24708b4d9dd3eca Mon Sep 17 00:00:00 2001 From: hyochan Date: Sun, 26 Apr 2026 00:52:00 +0900 Subject: [PATCH 30/41] docs: revert false-positive Onside typo fix Onside is the proper-noun name of the alternative marketplace (see /docs/features/alternative-marketplace/onside). The Copilot/ Gemini "typo" suggestions were false positives. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/docs/src/pages/docs/features/external-purchase.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/docs/src/pages/docs/features/external-purchase.tsx b/packages/docs/src/pages/docs/features/external-purchase.tsx index 23644b67..48cbc92d 100644 --- a/packages/docs/src/pages/docs/features/external-purchase.tsx +++ b/packages/docs/src/pages/docs/features/external-purchase.tsx @@ -2810,7 +2810,7 @@ func _ready_user_choice() -> void: Alternative Marketplace {' '} - - Onsite & alternative billing flows + - Onside & alternative billing flows
      • From b97d672cbff734fcfc98d662d124a8b195e64fee Mon Sep 17 00:00:00 2001 From: hyochan Date: Sun, 26 Apr 2026 00:52:53 +0900 Subject: [PATCH 31/41] docs(review): close /review-pr findings (search index, nested links, deprecation marks) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - home.tsx: drop nested inside spec-item Standard Events cards (browsers silently flatten nested anchors → non-deterministic click target) - searchData.ts: retarget 5 grouped entries that pointed at NavigatePreservingHash redirects (`/docs/types/{request,verification,ios,android,alternative}`); land on the new flat per-symbol pages or `#ios-types`/`#android-types` anchors - searchData.ts: anchor `pricing-phases-android` to its section on the pricing-phase page so the container type is reachable from search - searchData.ts: backfill 14 missing per-symbol entries — `validateReceipt`, iOS `getAllTransactionsIOS`/`getStorefrontIOS`/`validateReceiptIOS` and the five external-purchase APIs, plus the four Android Billing-Programs APIs - types/product.tsx + types/subscription-product.tsx: mark `subscriptionInfoIOS` and `discountsIOS` deprecated with strikethrough + bold "Deprecated." hint — matches schema (`type-ios.graphql:103,144`) and existing pattern - events/purchase-updated-listener.tsx, events/.../developer-provided-billing-listener-android.tsx: fix listener name + finishTransaction object-arg signature in TS sample - types/android/subscription-offer-android.tsx: anchor link to PricingPhasesAndroid section Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/docs/src/lib/searchData.ts | 148 +++++++++++++++++- ...oper-provided-billing-listener-android.tsx | 6 +- .../docs/events/purchase-updated-listener.tsx | 6 +- .../android/subscription-offer-android.tsx | 2 +- .../docs/src/pages/docs/types/product.tsx | 10 +- .../pages/docs/types/subscription-product.tsx | 8 +- packages/docs/src/pages/home.tsx | 24 +-- 7 files changed, 169 insertions(+), 35 deletions(-) diff --git a/packages/docs/src/lib/searchData.ts b/packages/docs/src/lib/searchData.ts index 36dbc824..7366a768 100644 --- a/packages/docs/src/lib/searchData.ts +++ b/packages/docs/src/lib/searchData.ts @@ -136,6 +136,16 @@ export const apiData: ApiItem[] = [ returns: 'VerifyPurchaseWithProviderResult!', 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 { @@ -291,6 +301,92 @@ export const apiData: ApiItem[] = [ returns: '', 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 { @@ -341,6 +437,46 @@ export const apiData: ApiItem[] = [ returns: 'String', 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 (moved to Features) { @@ -518,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', @@ -526,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', @@ -534,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', @@ -549,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) @@ -622,7 +758,7 @@ export const apiData: ApiItem[] = [ title: 'PricingPhasesAndroid', category: 'Types (Android)', description: 'Android pricing phases container: pricingPhaseList array', - path: '/docs/types/android/pricing-phase-android', + path: '/docs/types/android/pricing-phase-android#pricing-phases-android', }, // Alternative Billing Types (from types/alternative.tsx) 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 index 0306709e..2d3febff 100644 --- 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 @@ -32,7 +32,7 @@ function DeveloperProvidedBillingListenerAndroid() { {{ typescript: ( - {`developerProvidedBillingListener( + {`developerProvidedBillingListenerAndroid( listener: (details: DeveloperProvidedBillingDetailsAndroid) => void ): Subscription`} ), @@ -66,9 +66,9 @@ fun addDeveloperProvidedBillingListener( {{ typescript: ( - {`import { developerProvidedBillingListener } from 'expo-iap'; + {`import { developerProvidedBillingListenerAndroid } from 'expo-iap'; -const subscription = developerProvidedBillingListener(async (details) => { +const subscription = developerProvidedBillingListenerAndroid(async (details) => { console.log('User selected developer billing'); console.log('Token:', details.externalTransactionToken); diff --git a/packages/docs/src/pages/docs/events/purchase-updated-listener.tsx b/packages/docs/src/pages/docs/events/purchase-updated-listener.tsx index 9ac39399..2ee9dda0 100644 --- a/packages/docs/src/pages/docs/events/purchase-updated-listener.tsx +++ b/packages/docs/src/pages/docs/events/purchase-updated-listener.tsx @@ -57,7 +57,9 @@ val purchaseUpdates: Flow`} {{ typescript: ( - {`import { purchaseUpdatedListener } from 'expo-iap'; + {`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); @@ -70,7 +72,7 @@ const subscription = purchaseUpdatedListener(async (purchase) => { await deliverProduct(purchase.productId); // Finish the transaction - await finishTransaction(purchase, { isConsumable: false }); + await finishTransaction({ purchase, isConsumable: false }); } }); diff --git a/packages/docs/src/pages/docs/types/android/subscription-offer-android.tsx b/packages/docs/src/pages/docs/types/android/subscription-offer-android.tsx index 25b9d156..42426797 100644 --- a/packages/docs/src/pages/docs/types/android/subscription-offer-android.tsx +++ b/packages/docs/src/pages/docs/types/android/subscription-offer-android.tsx @@ -89,7 +89,7 @@ function SubscriptionOfferAndroid() { pricingPhases - + PricingPhasesAndroid diff --git a/packages/docs/src/pages/docs/types/product.tsx b/packages/docs/src/pages/docs/types/product.tsx index 8e007389..24f35590 100644 --- a/packages/docs/src/pages/docs/types/product.tsx +++ b/packages/docs/src/pages/docs/types/product.tsx @@ -190,11 +190,15 @@ function Product() { - 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 diff --git a/packages/docs/src/pages/docs/types/subscription-product.tsx b/packages/docs/src/pages/docs/types/subscription-product.tsx index a52bc6d5..4946a356 100644 --- a/packages/docs/src/pages/docs/types/subscription-product.tsx +++ b/packages/docs/src/pages/docs/types/subscription-product.tsx @@ -135,10 +135,14 @@ function SubscriptionProduct() { - discountsIOS + + discountsIOS + - Array of available discounts. Each contains:{' '} + Deprecated. Use{' '} + subscriptionOffers instead. Array of + available discounts. Each contains:{' '} identifier, type,{' '} numberOfPeriods, price,{' '} localizedPrice, paymentMode,{' '} diff --git a/packages/docs/src/pages/home.tsx b/packages/docs/src/pages/home.tsx index 80ea03bf..6ae484c0 100644 --- a/packages/docs/src/pages/home.tsx +++ b/packages/docs/src/pages/home.tsx @@ -490,54 +490,42 @@ function Home() { to="/docs/events/purchase-updated-listener" className="spec-item" > - - purchaseUpdatedListener - + purchaseUpdatedListener Purchase state changes - - purchaseErrorListener - + purchaseErrorListener Error handling - - promotedProductListenerIOS - + promotedProductListenerIOS App Store promoted products - - userChoiceBillingListenerAndroid - + userChoiceBillingListenerAndroid User Choice Billing selection - - developerProvidedBillingListener - + developerProvidedBillingListener External billing choice - - subscriptionBillingIssueListener - + subscriptionBillingIssueListener Suspended / retry subscriptions From 5f7d9d34c1d30f47f1964a8bc585b0e9eeb2cdb4 Mon Sep 17 00:00:00 2001 From: hyochan Date: Sun, 26 Apr 2026 01:01:05 +0900 Subject: [PATCH 32/41] docs(review): close Round 12 findings (Copilot/Gemini) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MenuDropdown: title is now a NavLink so right-click / Cmd-click / middle-click work as expected; collapsed dropdown content gets `inert` so its links are removed from the keyboard tab order (aria-hidden alone left them tabbable). SubMenu key drops the unstable items[0]?.to and uses the entry index instead. - apis/index redirect effect: tightened the same-page guard so the redirect short-circuits whenever target pathname + hash match the current URL, not only when the hash already has a leading "#". - validate-receipt: link label corrected from VerifyPurchaseResult to VerifyPurchaseWithProviderResult to match the linked type. - purchase-error-listener TS example: imports `restorePurchases` and notes that showRetryDialog / showErrorMessage are user-defined. - active-subscription currentPlanId warning: added the underlying cause — Google Play Billing's Purchase object does not expose `basePlanId` directly so it has to be inferred. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/docs/src/components/MenuDropdown.tsx | 34 +++++++++++++++---- packages/docs/src/pages/docs/apis/index.tsx | 16 +++++---- .../src/pages/docs/apis/validate-receipt.tsx | 2 +- .../docs/events/purchase-error-listener.tsx | 7 +++- .../pages/docs/types/active-subscription.tsx | 4 ++- 5 files changed, 47 insertions(+), 16 deletions(-) diff --git a/packages/docs/src/components/MenuDropdown.tsx b/packages/docs/src/components/MenuDropdown.tsx index 2c655d8f..bc204966 100644 --- a/packages/docs/src/components/MenuDropdown.tsx +++ b/packages/docs/src/components/MenuDropdown.tsx @@ -96,6 +96,7 @@ function SubMenu({ group, onItemClick }: SubMenuProps) { className="menu-dropdown-content" data-expanded={isExpanded} aria-hidden={!isExpanded} + {...(!isExpanded ? { inert: '' } : {})} >
          {group.items.map((item) => ( @@ -161,9 +162,26 @@ export function MenuDropdown({
          - +
          + {/* `inert` removes collapsed descendants from the tab order and the + accessibility tree. `aria-hidden` alone wouldn't — keyboard users + would still tab into hidden links. */}
            - {items.map((entry) => + {items.map((entry, index) => isGroup(entry) ? ( diff --git a/packages/docs/src/pages/docs/apis/index.tsx b/packages/docs/src/pages/docs/apis/index.tsx index f1cab740..5205f95e 100644 --- a/packages/docs/src/pages/docs/apis/index.tsx +++ b/packages/docs/src/pages/docs/apis/index.tsx @@ -104,14 +104,16 @@ function APIsIndex() { const anchor = location.hash.slice(1); const redirect = LEGACY_ANCHOR_REDIRECTS[anchor]; if (!redirect) return; - // Skip the navigate when the legacy anchor already resolves to this - // exact page + hash — otherwise React Router fires the effect again - // and we infinite-loop on same-page anchors like `terminology`. + // 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('#'); - if ( - redirectPath === location.pathname && - (redirectHash === '' || `#${redirectHash}` === location.hash) - ) { + const currentHash = location.hash.startsWith('#') + ? location.hash.slice(1) + : ''; + if (redirectPath === location.pathname && redirectHash === currentHash) { return; } navigate(redirect, { replace: true }); diff --git a/packages/docs/src/pages/docs/apis/validate-receipt.tsx b/packages/docs/src/pages/docs/apis/validate-receipt.tsx index fbd0149e..7d1895bf 100644 --- a/packages/docs/src/pages/docs/apis/validate-receipt.tsx +++ b/packages/docs/src/pages/docs/apis/validate-receipt.tsx @@ -37,7 +37,7 @@ function ValidateReceipt() { {' '} input and returns the same{' '} - VerifyPurchaseResult + VerifyPurchaseWithProviderResult .

            diff --git a/packages/docs/src/pages/docs/events/purchase-error-listener.tsx b/packages/docs/src/pages/docs/events/purchase-error-listener.tsx index a3214e81..1fbb0dba 100644 --- a/packages/docs/src/pages/docs/events/purchase-error-listener.tsx +++ b/packages/docs/src/pages/docs/events/purchase-error-listener.tsx @@ -51,7 +51,12 @@ val purchaseErrors: Flow`} {{ typescript: ( - {`import { purchaseErrorListener, ErrorCode } from 'expo-iap'; + {`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); diff --git a/packages/docs/src/pages/docs/types/active-subscription.tsx b/packages/docs/src/pages/docs/types/active-subscription.tsx index ded7be2c..2d92dc95 100644 --- a/packages/docs/src/pages/docs/types/active-subscription.tsx +++ b/packages/docs/src/pages/docs/types/active-subscription.tsx @@ -135,7 +135,9 @@ function ActiveSubscription() { 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{' '} + be inaccurate for multi-plan subscriptions because Google Play + Billing's Purchase object does not expose{' '} + basePlanId directly — it has to be inferred. See{' '} limitation From 6bfee7c0b770cdbe15902b1166aff004a1004a54 Mon Sep 17 00:00:00 2001 From: hyochan Date: Sun, 26 Apr 2026 01:03:43 +0900 Subject: [PATCH 33/41] docs(review): use object-arg finishTransaction in purchase.tsx TS samples (Gemini) Both TS finishTransaction calls in features/purchase.tsx were still using the old positional signature. The TS SDK now expects MutationFinishTransactionArgs ({ purchase, isConsumable }), matching the canonical API page. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/docs/src/pages/docs/features/purchase.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/docs/src/pages/docs/features/purchase.tsx b/packages/docs/src/pages/docs/features/purchase.tsx index 78e461e4..d0eb1498 100644 --- a/packages/docs/src/pages/docs/features/purchase.tsx +++ b/packages/docs/src/pages/docs/features/purchase.tsx @@ -1109,7 +1109,7 @@ const handlePurchase = async (purchase: Purchase) => { // - isConsumable: true = consume the purchase (can buy again) // - isConsumable: false = acknowledge only (one-time purchase) const isConsumable = purchase.productId.includes('consumable'); - await finishTransaction(purchase, isConsumable); + await finishTransaction({ purchase, isConsumable }); console.log('Transaction finished successfully'); }; @@ -1319,7 +1319,7 @@ function PurchaseProvider({ children }: { children: React.ReactNode }) { // Step 3: Finish transaction const isConsumable = purchase.productId.includes('coins'); - await finishTransaction(purchase, isConsumable); + await finishTransaction({ purchase, isConsumable }); console.log('Purchase completed successfully!'); } catch (error) { From 12d5d53bc77da77845cde0d47dfc1bfe9aeb993a Mon Sep 17 00:00:00 2001 From: hyochan Date: Sun, 26 Apr 2026 01:13:02 +0900 Subject: [PATCH 34/41] docs: revert MenuDropdown title-as-NavLink + inert (visual regression) The Round 12 a11y changes (NavLink-as-title and `inert` on collapsed content) introduced a visible padding/spacing regression in the sidebar: anchors don't render with the same flex/padding behaviour as the original button, and the inert attribute interacted badly with the grid-row collapse animation. Restore the original button title and the original collapsed-content markup. Keep the SubMenu key change (entry index) since it's behaviour-only. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/docs/src/components/MenuDropdown.tsx | 30 +++---------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/packages/docs/src/components/MenuDropdown.tsx b/packages/docs/src/components/MenuDropdown.tsx index bc204966..9a7d0d0d 100644 --- a/packages/docs/src/components/MenuDropdown.tsx +++ b/packages/docs/src/components/MenuDropdown.tsx @@ -96,7 +96,6 @@ function SubMenu({ group, onItemClick }: SubMenuProps) { className="menu-dropdown-content" data-expanded={isExpanded} aria-hidden={!isExpanded} - {...(!isExpanded ? { inert: '' } : {})} >
              {group.items.map((item) => ( @@ -162,26 +161,9 @@ export function MenuDropdown({
              - { - // Let modifier-clicks (Cmd/Ctrl/Shift/middle) and right-clicks - // fall through to the browser so users can open in a new tab, - // copy the link, etc. — semantics a
              - {/* `inert` removes collapsed descendants from the tab order and the - accessibility tree. `aria-hidden` alone wouldn't — keyboard users - would still tab into hidden links. */}
                {items.map((entry, index) => From bbf238ffbd0198b19ffab08b52be66906a2a773f Mon Sep 17 00:00:00 2001 From: hyochan Date: Sun, 26 Apr 2026 01:20:52 +0900 Subject: [PATCH 35/41] docs(review): close Round 13 findings (Copilot/Gemini/CodeRabbit) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sidebar/MenuDropdown: - Mark the decorative `└` prefix span as `aria-hidden="true"` so screen readers don't announce it. - Switch the SubMenu key from the array index to a sanitized `entry.label` slug so it survives reordering and doesn't depend on position. Validate-receipt API pages: - /docs/apis/validate-receipt: relink the deprecation note to /docs/types/verify-purchase#verify-purchase-result with the correct label (the prior link pointed at the WithProvider page). - /docs/apis/ios/validate-receipt-ios: deep-link to the #verify-purchase anchor and align the Swift signature on ReceiptValidationProps / ReceiptValidationResultIOS to match searchData metadata. - /docs/apis/index: validateReceiptIOS row gains the same strikethrough + bold "Deprecated." treatment as validateReceipt. API examples / signatures: - deep-link-to-subscriptions TS signature: `options?: DeepLinkOptions` (matches the canonical optional/null type and platform impls). - restore-purchases useIAP variant: replaces the undefined `verifyOnServer` with `verifyPurchase`, adds the missing `grantProduct` step before `finishTransaction` so the two flows match. - types/alternative-billing-types end-to-end TS sample: switch to the Android-suffixed APIs (`checkAlternativeBillingAvailabilityAndroid`, `showAlternativeBillingDialogAndroid`, `createAlternativeBillingTokenAndroid`) and use their boolean returns. Linked `enableBillingProgramAndroid` to the `#init-connection-config` anchor (the field, not the enum). - types/billing-programs TS requestPurchase example: wrap the Google payload in `request: { ... }` and add `type: 'in-app'` to satisfy `MutationRequestPurchaseArgs`. Other fixes: - searchData: getAppTransactionIOS returns `AppTransactionIOS` (not `AppTransaction`). - subscription-billing-issue-listener: promote "Listener Setup" and "Example" from `

                ` to `AnchorLink` `h2` so the heading hierarchy doesn't skip a level. KMP example uses `val kmpIAP = KmpIAP()` class pattern to match the rest of the docs. - features/refund webhook sample: actually verify+decode `signedTransactionInfo` (it's itself a JWS) before reading `transactionId`. - docs index: add `apis/refund` → `features/refund` legacy redirect alongside `apis/validation` and `apis/debugging`. - types/discount-offer: broaden the description to cover the unified subscription + one-time enum and remove the bogus `WinBack` reference (WinBack lives on `SubscriptionOfferTypeIOS`). - acknowledge-purchase-android: add TS / KMP / Dart / GDScript signatures alongside the Kotlin one. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/docs/src/components/MenuDropdown.tsx | 15 +++++++++++---- packages/docs/src/lib/searchData.ts | 2 +- .../android/acknowledge-purchase-android.tsx | 12 ++++++++++++ .../docs/apis/deep-link-to-subscriptions.tsx | 2 +- packages/docs/src/pages/docs/apis/index.tsx | 13 +++++++++++-- .../docs/apis/ios/validate-receipt-ios.tsx | 7 +++++-- .../src/pages/docs/apis/restore-purchases.tsx | 8 ++++++-- .../src/pages/docs/apis/validate-receipt.tsx | 4 ++-- .../subscription-billing-issue-listener.tsx | 13 ++++++++++--- .../docs/src/pages/docs/features/refund.tsx | 10 +++++++--- packages/docs/src/pages/docs/index.tsx | 4 ++++ .../docs/types/alternative-billing-types.tsx | 18 +++++++++--------- .../src/pages/docs/types/billing-programs.tsx | 15 +++++++++------ .../src/pages/docs/types/discount-offer.tsx | 15 ++++++++++----- 14 files changed, 98 insertions(+), 40 deletions(-) diff --git a/packages/docs/src/components/MenuDropdown.tsx b/packages/docs/src/components/MenuDropdown.tsx index 9a7d0d0d..2cf9658f 100644 --- a/packages/docs/src/components/MenuDropdown.tsx +++ b/packages/docs/src/components/MenuDropdown.tsx @@ -107,7 +107,9 @@ function SubMenu({ group, onItemClick }: SubMenuProps) { } onClick={onItemClick} > - + {item.label} @@ -192,10 +194,10 @@ export function MenuDropdown({ aria-hidden={!isExpanded} >
                  - {items.map((entry, index) => + {items.map((entry) => isGroup(entry) ? ( @@ -208,7 +210,12 @@ export function MenuDropdown({ } onClick={onItemClick} > - + {entry.label} diff --git a/packages/docs/src/lib/searchData.ts b/packages/docs/src/lib/searchData.ts index 7366a768..1ec9bdc0 100644 --- a/packages/docs/src/lib/searchData.ts +++ b/packages/docs/src/lib/searchData.ts @@ -289,7 +289,7 @@ export const apiData: ApiItem[] = [ category: 'iOS Specific', description: 'Fetch the current app transaction (iOS 16+)', parameters: '', - returns: 'AppTransaction', + returns: 'AppTransactionIOS', path: '/docs/apis/ios/get-app-transaction-ios', }, { diff --git a/packages/docs/src/pages/docs/apis/android/acknowledge-purchase-android.tsx b/packages/docs/src/pages/docs/apis/android/acknowledge-purchase-android.tsx index 2245874f..a20454e8 100644 --- a/packages/docs/src/pages/docs/apis/android/acknowledge-purchase-android.tsx +++ b/packages/docs/src/pages/docs/apis/android/acknowledge-purchase-android.tsx @@ -40,9 +40,21 @@ function AcknowledgePurchaseAndroid() {

                  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`} + ), }} 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 index 8f96820c..7843121a 100644 --- a/packages/docs/src/pages/docs/apis/deep-link-to-subscriptions.tsx +++ b/packages/docs/src/pages/docs/apis/deep-link-to-subscriptions.tsx @@ -25,7 +25,7 @@ function DeepLinkToSubscriptions() { {{ typescript: ( - {`deepLinkToSubscriptions(options: DeepLinkOptions): Promise + {`deepLinkToSubscriptions(options?: DeepLinkOptions): Promise interface DeepLinkOptions { skuAndroid?: string; diff --git a/packages/docs/src/pages/docs/apis/index.tsx b/packages/docs/src/pages/docs/apis/index.tsx index 5205f95e..1646c723 100644 --- a/packages/docs/src/pages/docs/apis/index.tsx +++ b/packages/docs/src/pages/docs/apis/index.tsx @@ -590,10 +590,19 @@ function APIsIndex() { - validateReceiptIOS + + validateReceiptIOS + - Validate a receipt against the App Store (legacy path). + + Deprecated. Legacy App Store receipt + validation. Use{' '} + + verifyPurchase + {' '} + instead. + 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 index 8f7014d7..9072429f 100644 --- a/packages/docs/src/pages/docs/apis/ios/validate-receipt-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/validate-receipt-ios.tsx @@ -24,7 +24,10 @@ function ValidateReceiptIOS() {

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

                  @@ -34,7 +37,7 @@ function ValidateReceiptIOS() { {{ swift: ( {`@available(*, deprecated, message: "Use verifyPurchase()") -func validateReceiptIOS(options: PurchaseVerificationProps) async throws -> PurchaseVerificationResult`} +func validateReceiptIOS(options: ReceiptValidationProps) async throws -> ReceiptValidationResultIOS`}
                  ), }}
                  diff --git a/packages/docs/src/pages/docs/apis/restore-purchases.tsx b/packages/docs/src/pages/docs/apis/restore-purchases.tsx index fdef2906..92c7d944 100644 --- a/packages/docs/src/pages/docs/apis/restore-purchases.tsx +++ b/packages/docs/src/pages/docs/apis/restore-purchases.tsx @@ -103,8 +103,12 @@ function RestoreButton() { useEffect(() => { (async () => { for (const purchase of availablePurchases) { - const verified = await verifyOnServer(purchase); - if (!verified) continue; + 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 }); } })(); diff --git a/packages/docs/src/pages/docs/apis/validate-receipt.tsx b/packages/docs/src/pages/docs/apis/validate-receipt.tsx index 7d1895bf..e34962e8 100644 --- a/packages/docs/src/pages/docs/apis/validate-receipt.tsx +++ b/packages/docs/src/pages/docs/apis/validate-receipt.tsx @@ -36,8 +36,8 @@ function ValidateReceipt() { VerifyPurchaseProps {' '} input and returns the same{' '} - - VerifyPurchaseWithProviderResult + + VerifyPurchaseResult .

                  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 index 0e896b36..477a36ac 100644 --- a/packages/docs/src/pages/docs/events/subscription-billing-issue-listener.tsx +++ b/packages/docs/src/pages/docs/events/subscription-billing-issue-listener.tsx @@ -1,4 +1,5 @@ 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'; @@ -26,7 +27,9 @@ function SubscriptionBillingIssueListener() { (tvOS, watchOS, visionOS, macOS, Meta Horizon).

                  -

                  Listener Setup

                  + + Listener Setup + {{ typescript: ( @@ -69,7 +72,9 @@ val subscriptionBillingIssueListener: Flow`} session; iOS fires per Message delivery.

                  -

                  Example

                  + + Example + {{ typescript: ( @@ -122,7 +127,9 @@ openIapStore.addSubscriptionBillingIssueListener { purchase -> }`} ), kmp: ( - {`import io.github.hyochan.kmpiap.kmpIAP + {`import io.github.hyochan.kmpiap.KmpIAP + +val kmpIAP = KmpIAP() // Play Billing 8.1+ on Android, iOS 18+ on Apple targets lifecycleScope.launch { diff --git a/packages/docs/src/pages/docs/features/refund.tsx b/packages/docs/src/pages/docs/features/refund.tsx index 90001f32..3a08cc8c 100644 --- a/packages/docs/src/pages/docs/features/refund.tsx +++ b/packages/docs/src/pages/docs/features/refund.tsx @@ -258,14 +258,18 @@ match status: - {`// Server webhook handler (Node.js) + {`// Server webhook handler (Node.js). +// In App Store Server Notifications V2, signedTransactionInfo is itself a +// signed JWS string — verify and decode it to read its fields. app.post('/webhooks/apple', async (req, res) => { const { signedPayload } = req.body; const decoded = await verifyAndDecodeJWS(signedPayload); if (decoded.notificationType === 'REFUND' || decoded.notificationType === 'REVOKE') { - const transactionId = decoded.data.signedTransactionInfo.transactionId; - await revokeEntitlement(transactionId); + const transactionInfo = await verifyAndDecodeJWS( + decoded.data.signedTransactionInfo, + ); + await revokeEntitlement(transactionInfo.transactionId); } res.sendStatus(200); diff --git a/packages/docs/src/pages/docs/index.tsx b/packages/docs/src/pages/docs/index.tsx index af1b86dc..b68c0515 100644 --- a/packages/docs/src/pages/docs/index.tsx +++ b/packages/docs/src/pages/docs/index.tsx @@ -1089,6 +1089,10 @@ function Docs() { path="apis/debugging" element={} /> + } + /> } />
                  Deprecated: Use{' '} - + enableBillingProgramAndroid {' '} with{' '} @@ -483,9 +483,9 @@ await iap.request_purchase(props) {`import { initConnection, fetchProducts, - checkAlternativeBillingAvailability, - showAlternativeBillingDialog, - createAlternativeBillingToken, + checkAlternativeBillingAvailabilityAndroid, + showAlternativeBillingDialogAndroid, + createAlternativeBillingTokenAndroid, } from 'expo-iap'; // Step 1: Initialize with external offer (recommended) @@ -494,8 +494,8 @@ await initConnection({ }); // Step 2: Check if alternative billing is available -const availability = await checkAlternativeBillingAvailability(); -if (!availability.isAvailable) { +const isAvailable = await checkAlternativeBillingAvailabilityAndroid(); +if (!isAvailable) { console.log('Alternative billing not available in this region'); // Fall back to standard Google Play billing return; @@ -508,14 +508,14 @@ const products = await fetchProducts({ }); // Step 4: Show required Google Play disclosure dialog -const dialogResult = await showAlternativeBillingDialog(); -if (dialogResult.responseCode !== 0) { +const accepted = await showAlternativeBillingDialogAndroid(); +if (!accepted) { console.log('User did not accept alternative billing'); return; } // Step 5: Create token for this transaction -const token = await createAlternativeBillingToken(products[0].id); +const token = await createAlternativeBillingTokenAndroid(products[0].id); // Step 6: Process purchase with your backend const paymentResult = await yourBackend.processAlternativePurchase({ diff --git a/packages/docs/src/pages/docs/types/billing-programs.tsx b/packages/docs/src/pages/docs/types/billing-programs.tsx index 5c107ca4..de6b5f19 100644 --- a/packages/docs/src/pages/docs/types/billing-programs.tsx +++ b/packages/docs/src/pages/docs/types/billing-programs.tsx @@ -476,14 +476,17 @@ const result = await isBillingProgramAvailableAndroid('external-payments'); if (result.isAvailable) { // Purchase with developer billing option await requestPurchase({ - google: { - skus: ['product_id'], - developerBillingOption: { - billingProgram: 'external-payments', - linkUri: 'https://your-site.com/checkout', - launchMode: 'launch-in-external-browser-or-app', + request: { + google: { + skus: ['product_id'], + developerBillingOption: { + billingProgram: 'external-payments', + linkUri: 'https://your-site.com/checkout', + launchMode: 'launch-in-external-browser-or-app', + }, }, }, + type: 'in-app', }); }`} ), diff --git a/packages/docs/src/pages/docs/types/discount-offer.tsx b/packages/docs/src/pages/docs/types/discount-offer.tsx index 6369cf12..b7950091 100644 --- a/packages/docs/src/pages/docs/types/discount-offer.tsx +++ b/packages/docs/src/pages/docs/types/discount-offer.tsx @@ -22,9 +22,14 @@ function DiscountOffer() { 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. + Unified discount-offer type covering both subscription discounts ( + Introductory, Promotional) and one-time + product offers (OneTime, Android only on Google Play + Billing Library 7.0+). For iOS-specific WinBack offers see{' '} + + SubscriptionOfferTypeIOS + + ; iOS does not support one-time product discounts.

                  Native references:{' '} @@ -103,8 +108,8 @@ function DiscountOffer() { Type of offer: Introductory,{' '} - Promotional, WinBack (iOS 18+), or{' '} - OneTime + Promotional, or OneTime (Android-only + Play Billing 7.0+ feature). From 801e983e21eae7ea8832625d919c60f5f9555d7b Mon Sep 17 00:00:00 2001 From: hyochan Date: Sun, 26 Apr 2026 01:35:05 +0900 Subject: [PATCH 36/41] docs(review): close Round 14 findings (Copilot/Gemini) MenuDropdown: - Drop the JS `isHovered` state + onMouseEnter/Leave handlers; the hover color is already covered by the existing `.menu-dropdown-title:hover` CSS rule, so the JS state was just noise (and Copilot kept flagging it as unused). - Pass parent expanded state down to SubMenu and apply `tabIndex={-1}` to every NavLink inside a collapsed dropdown / submenu, so keyboard users cannot tab into hidden links even if a browser disregards `visibility: hidden` for focus order. apis/index + types/index redirect effects: - Strip a trailing slash before comparing `redirectPath` and `location.pathname` so `/foo` and `/foo/` no longer trigger an unnecessary redirect. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/docs/src/components/MenuDropdown.tsx | 17 +++++++++-------- packages/docs/src/pages/docs/apis/index.tsx | 8 +++++++- packages/docs/src/pages/docs/types/index.tsx | 10 ++++++++-- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/packages/docs/src/components/MenuDropdown.tsx b/packages/docs/src/components/MenuDropdown.tsx index 2cf9658f..adbbbd04 100644 --- a/packages/docs/src/components/MenuDropdown.tsx +++ b/packages/docs/src/components/MenuDropdown.tsx @@ -27,6 +27,7 @@ interface MenuDropdownProps { interface SubMenuProps { group: MenuGroup; onItemClick?: () => void; + parentExpanded: boolean; } function Chevron({ isExpanded }: { isExpanded: boolean }) { @@ -54,10 +55,14 @@ function Chevron({ isExpanded }: { isExpanded: boolean }) { ); } -function SubMenu({ group, onItemClick }: SubMenuProps) { +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 @@ -102,6 +107,7 @@ function SubMenu({ group, onItemClick }: SubMenuProps) {

                • `menu-dropdown-item ${isActive ? 'active' : ''}` } @@ -127,7 +133,6 @@ export function MenuDropdown({ onItemClick, }: MenuDropdownProps) { const [isExpanded, setIsExpanded] = useState(false); - const [isHovered, setIsHovered] = useState(false); const location = useLocation(); const navigate = useNavigate(); const contentId = useId(); @@ -166,13 +171,7 @@ export function MenuDropdown({ @@ -200,11 +199,13 @@ export function MenuDropdown({ key={`${titleTo}::group::${entry.label.replace(/\s+/g, '-').toLowerCase()}`} group={entry} onItemClick={onItemClick} + parentExpanded={isExpanded} /> ) : (
                • `menu-dropdown-item ${isActive ? 'active' : ''}` } diff --git a/packages/docs/src/pages/docs/apis/index.tsx b/packages/docs/src/pages/docs/apis/index.tsx index 1646c723..fc4fc54d 100644 --- a/packages/docs/src/pages/docs/apis/index.tsx +++ b/packages/docs/src/pages/docs/apis/index.tsx @@ -113,7 +113,13 @@ function APIsIndex() { const currentHash = location.hash.startsWith('#') ? location.hash.slice(1) : ''; - if (redirectPath === location.pathname && redirectHash === currentHash) { + // 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; } navigate(redirect, { replace: true }); diff --git a/packages/docs/src/pages/docs/types/index.tsx b/packages/docs/src/pages/docs/types/index.tsx index 98e6ad24..4692f1c7 100644 --- a/packages/docs/src/pages/docs/types/index.tsx +++ b/packages/docs/src/pages/docs/types/index.tsx @@ -265,9 +265,15 @@ function TypesIndex() { // exact page + hash — prevents an infinite redirect loop on // same-page section anchors like `common`. 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 ( - redirectPath === location.pathname && - (redirectHash === '' || `#${redirectHash}` === location.hash) + stripSlash(redirectPath) === stripSlash(location.pathname) && + redirectHash === currentHash ) { return; } From 3df44b331f37beb86827b425f65d78486a6f21e6 Mon Sep 17 00:00:00 2001 From: hyochan Date: Sun, 26 Apr 2026 01:42:16 +0900 Subject: [PATCH 37/41] docs(review): close Round 15 findings (Copilot/Gemini) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - apis/validate-receipt: switch from `useEffect` + `navigate` to `` so the deprecation redirect happens declaratively during render — no flash of intermediate content, no effect-driven re-render. - types/android/subscription-offer-android: surface the "(Deprecated)" hint in the SEO title + description so search engines stop promoting the deprecated symbol. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/pages/docs/apis/validate-receipt.tsx | 62 +++---------------- .../android/subscription-offer-android.tsx | 6 +- 2 files changed, 13 insertions(+), 55 deletions(-) diff --git a/packages/docs/src/pages/docs/apis/validate-receipt.tsx b/packages/docs/src/pages/docs/apis/validate-receipt.tsx index e34962e8..0bb78976 100644 --- a/packages/docs/src/pages/docs/apis/validate-receipt.tsx +++ b/packages/docs/src/pages/docs/apis/validate-receipt.tsx @@ -1,56 +1,14 @@ -import { useEffect } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; -import SEO from '../../../components/SEO'; - +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() { - const navigate = useNavigate(); - - // 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. - useEffect(() => { - navigate('/docs/features/validation#verify-purchase', { replace: true }); - }, [navigate]); - - return ( -
                  - -

                  - validateReceipt -

                  -
                  -

                  - ⚠️ Deprecated. The cross-platform{' '} - validateReceipt mutation has been replaced by{' '} - - verifyPurchase - - . Use that instead — it accepts the same{' '} - - VerifyPurchaseProps - {' '} - input and returns the same{' '} - - VerifyPurchaseResult - - . -

                  -

                  - Redirecting to{' '} - - /docs/features/validation - - … -

                  -
                  -
                  - ); + return ; } export default ValidateReceipt; diff --git a/packages/docs/src/pages/docs/types/android/subscription-offer-android.tsx b/packages/docs/src/pages/docs/types/android/subscription-offer-android.tsx index 42426797..777a97be 100644 --- a/packages/docs/src/pages/docs/types/android/subscription-offer-android.tsx +++ b/packages/docs/src/pages/docs/types/android/subscription-offer-android.tsx @@ -9,10 +9,10 @@ function SubscriptionOfferAndroid() { return (

                  ProductSubscriptionAndroidOfferDetails

                  From 43c7258732ab2924db930d6f42f556ba0984b715 Mon Sep 17 00:00:00 2001 From: hyochan Date: Sun, 26 Apr 2026 01:54:14 +0900 Subject: [PATCH 38/41] docs(review): close Round 16 findings (Copilot/Gemini) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit apis/index Terminology section: - Remove the duplicate Request APIs explanation now that the canonical copy lives on /docs/apis/fetch-products#request-apis (and the legacy hash redirect already routes there). - Drop the no-op /docs/apis#transaction-vs-purchase entry from LEGACY_ANCHOR_REDIRECTS — same source and target, would never fire meaningfully. MenuDropdown: - Apply `tabIndex={parentExpanded ? 0 : -1}` to the SubMenu toggle button so the entire collapsed-submenu UI (including the toggle button) is removed from the keyboard tab order, not just its child NavLinks. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/docs/src/components/MenuDropdown.tsx | 1 + packages/docs/src/pages/docs/apis/index.tsx | 47 ------------------- 2 files changed, 1 insertion(+), 47 deletions(-) diff --git a/packages/docs/src/components/MenuDropdown.tsx b/packages/docs/src/components/MenuDropdown.tsx index adbbbd04..9337c139 100644 --- a/packages/docs/src/components/MenuDropdown.tsx +++ b/packages/docs/src/components/MenuDropdown.tsx @@ -84,6 +84,7 @@ function SubMenu({ group, onItemClick, parentExpanded }: SubMenuProps) {
                • Error Handling — unified{' '} - + PurchaseError {' '} - codes + shape and error codes