{"id":33283177,"url":"https://github.com/cap-go/capacitor-native-purchases","last_synced_at":"2026-04-02T10:30:46.186Z","repository":{"id":187752250,"uuid":"677494519","full_name":"Cap-go/capacitor-native-purchases","owner":"Cap-go","description":"Capacitor plugin to manage IAP on capacitor with latest libs Android IOS","archived":false,"fork":false,"pushed_at":"2026-02-28T22:15:20.000Z","size":7265,"stargazers_count":37,"open_issues_count":5,"forks_count":9,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-03-01T01:36:39.374Z","etag":null,"topics":["capacitor","capacitor-plugin","ionic"],"latest_commit_sha":null,"homepage":"https://capgo.app","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Cap-go.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null},"funding":{"github":"Cap-go","patreon":null,"open_collective":"capgo","ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"otechie":null,"custom":null}},"created_at":"2023-08-11T18:04:16.000Z","updated_at":"2026-02-23T06:45:27.000Z","dependencies_parsed_at":"2023-08-12T02:56:53.432Z","dependency_job_id":"b147580b-b910-46ba-bf3e-1e6c31f75435","html_url":"https://github.com/Cap-go/capacitor-native-purchases","commit_stats":{"total_commits":100,"total_committers":3,"mean_commits":"33.333333333333336","dds":0.51,"last_synced_commit":"e09d965f54afce3d2a2667ba113e7af90116c687"},"previous_names":["cap-go/native-purchases","cap-go/capacitor-native-purchases"],"tags_count":245,"template":false,"template_full_name":null,"purl":"pkg:github/Cap-go/capacitor-native-purchases","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Cap-go%2Fcapacitor-native-purchases","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Cap-go%2Fcapacitor-native-purchases/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Cap-go%2Fcapacitor-native-purchases/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Cap-go%2Fcapacitor-native-purchases/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Cap-go","download_url":"https://codeload.github.com/Cap-go/capacitor-native-purchases/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Cap-go%2Fcapacitor-native-purchases/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30226764,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-07T19:01:10.287Z","status":"ssl_error","status_checked_at":"2026-03-07T18:59:58.103Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["capacitor","capacitor-plugin","ionic"],"created_at":"2025-11-17T14:04:05.178Z","updated_at":"2026-04-02T10:30:46.172Z","avatar_url":"https://github.com/Cap-go.png","language":"Java","funding_links":["https://github.com/sponsors/Cap-go","https://opencollective.com/capgo"],"categories":[],"sub_categories":[],"readme":"# native-purchases\n \u003ca href=\"https://capgo.app/\"\u003e\u003cimg src='https://raw.githubusercontent.com/Cap-go/capgo/main/assets/capgo_banner.png' alt='Capgo - Instant updates for capacitor'/\u003e\u003c/a\u003e\n\n\u003cdiv align=\"center\"\u003e\n  \u003ch2\u003e\u003ca href=\"https://capgo.app/?ref=plugin_native_purchases\"\u003e ➡️ Get Instant updates for your App with Capgo\u003c/a\u003e\u003c/h2\u003e\n  \u003ch2\u003e\u003ca href=\"https://capgo.app/consulting/?ref=plugin_native_purchases\"\u003e Missing a feature? We’ll build the plugin for you 💪\u003c/a\u003e\u003c/h2\u003e\n\u003c/div\u003e\n\n## In-app Purchases Made Easy\n\nThis plugin allows you to implement in-app purchases and subscriptions in your Capacitor app using native APIs.\n\n## Why Native Purchases?\n\nThe only **free**, **battle-tested** in-app purchase plugin for Capacitor with full feature parity:\n\n- **StoreKit 2 (iOS)** - Uses Apple's latest purchase APIs for iOS 15+\n- **Google Play Billing 7.x (Android)** - Implements the newest billing library\n- **Complete feature set** - In-app products AND subscriptions with base plans\n- **Same JavaScript API** - Compatible interface with paid alternatives\n- **Comprehensive validation** - Built-in receipt/token validation examples\n- **Modern package management** - Supports both Swift Package Manager (SPM) and CocoaPods (SPM-ready for Capacitor 8)\n- **Production-ready** - Extensive documentation, testing guides, refund handling\n\nPerfect for apps monetizing through one-time purchases or recurring subscriptions.\n\n## Documentation\n\nThe most complete doc is available here: https://capgo.app/docs/plugins/native-purchases/\n\n## Compatibility\n\n| Plugin version | Capacitor compatibility | Maintained |\n| -------------- | ----------------------- | ---------- |\n| v8.\\*.\\*       | v8.\\*.\\*                | ✅          |\n| v7.\\*.\\*       | v7.\\*.\\*                | On demand   |\n| v6.\\*.\\*       | v6.\\*.\\*                | ❌          |\n| v5.\\*.\\*       | v5.\\*.\\*                | ❌          |\n\n\u003e **Note:** The major version of this plugin follows the major version of Capacitor. Use the version that matches your Capacitor installation (e.g., plugin v8 for Capacitor 8). Only the latest major version is actively maintained.\n\n## Install\n\n```bash\nnpm install @capgo/native-purchases\nnpx cap sync\n```\n\n## 📚 Testing Guides\n\nComplete visual testing guides for both platforms:\n\n| Platform | Guide | Content |\n|----------|-------|---------|\n| 🍎 **iOS** | **[iOS Testing Guide](./docs/iOS_TESTING_GUIDE.md)** | StoreKit Local Testing, Sandbox Testing, Developer Mode setup |\n| 🤖 **Android** | **[Android Testing Guide](./docs/ANDROID_TESTING_GUIDE.md)** | Internal Testing, License Testing, Internal App Sharing |\n\n\u003e 💡 **Quick Start**: Choose **StoreKit Local Testing** for iOS or **Internal Testing** for Android for the fastest development experience.\n\n## Android\n\nAdd this to manifest\n\n```xml\n\u003cuses-permission android:name=\"com.android.vending.BILLING\" /\u003e\n```\n\n### Testing with Google Play Console\n\n\u003e 📖 **[Complete Android Testing Guide](./docs/ANDROID_TESTING_GUIDE.md)** - Comprehensive guide covering Internal Testing, License Testing, and Internal App Sharing methods with step-by-step instructions, troubleshooting, and best practices.\n\nFor testing in-app purchases on Android:\n\n1. Upload your app to Google Play Console (internal testing track is sufficient)\n2. Create test accounts in Google Play Console:\n   - Go to Google Play Console\n   - Navigate to \"Setup\" \u003e \"License testing\"\n   - Add Gmail accounts to \"License testers\" list\n3. Install the app from Google Play Store on a device signed in with a test account\n4. Test purchases will be free and won't charge real money\n\n## iOS\n\nAdd the \"In-App Purchase\" capability to your Xcode project:\n\n1. Open your project in Xcode\n2. Select your app target\n3. Go to \"Signing \u0026 Capabilities\" tab\n4. Click the \"+\" button to add a capability\n5. Search for and add \"In-App Purchase\"\n\n\u003e ⚠️ **App Store Requirement**: You MUST display product names and prices using data from the plugin (`product.title`, `product.priceString`). Hardcoded values will cause App Store rejection.\n\n\u003e 📖 **[Complete iOS Testing Guide](./docs/iOS_TESTING_GUIDE.md)** - Comprehensive guide covering both Sandbox and StoreKit local testing methods with step-by-step instructions, troubleshooting, and best practices.\n\n### Testing with Sandbox\n\nFor testing in-app purchases on iOS:\n\n1. Create a sandbox test user in App Store Connect:\n   - Go to App Store Connect\n   - Navigate to \"Users and Access\" \u003e \"Sandbox Testers\"\n   - Create a new sandbox tester account\n2. On your iOS device, sign out of your regular Apple ID in Settings \u003e App Store\n3. Install and run your app\n4. When prompted for Apple ID during purchase testing, use your sandbox account credentials\n\n## Usage\n\nImport the plugin in your TypeScript file:\n\n```typescript\nimport { NativePurchases, PURCHASE_TYPE } from '@capgo/native-purchases';\n```\n\n### ⚠️ Important: In-App vs Subscription Purchases\n\nThere are two types of purchases with different requirements:\n\n| Purchase Type | productType | planIdentifier | Use Case |\n|---------------|-------------|----------------|----------|\n| **In-App Purchase** | `PURCHASE_TYPE.INAPP` | ❌ Not needed | One-time purchases (premium features, remove ads, etc.) |\n| **Subscription** | `PURCHASE_TYPE.SUBS` | ✅ **REQUIRED (Android only)** | Recurring purchases (monthly/yearly subscriptions) |\n\n**Key Rules:**\n- ✅ **In-App Products**: Use `productType: PURCHASE_TYPE.INAPP`, no `planIdentifier` needed on any platform\n- ✅ **Subscriptions on Android**: Must use `productType: PURCHASE_TYPE.SUBS` AND `planIdentifier: \"your-plan-id\"` (the Base Plan ID from Google Play Console)\n- ✅ **Subscriptions on iOS**: Use `productType: PURCHASE_TYPE.SUBS`, `planIdentifier` is optional and ignored\n- ❌ **Missing planIdentifier** for Android subscriptions will cause purchase failures\n\n**About planIdentifier (Android-specific):**\nThe `planIdentifier` parameter is **only required for Android subscriptions**. It should be set to the Base Plan ID that you configure in the Google Play Console when creating your subscription product. For example, if you create a monthly subscription with base plan ID \"monthly-plan\" in Google Play Console, you would use `planIdentifier: \"monthly-plan\"` when purchasing that subscription.\n\niOS does not use this parameter - subscriptions on iOS only require the product identifier.\n\n### Complete Example: Get Product Info and Purchase\n\nHere's a complete example showing how to get product information and make purchases for both in-app products and subscriptions:\n\n```typescript\nimport { NativePurchases, PURCHASE_TYPE } from '@capgo/native-purchases';\n\nclass PurchaseManager {\n  // In-app product (one-time purchase)\n  private premiumProductId = 'com.yourapp.premium_features';\n  \n  // Subscription products (require planIdentifier on Android)\n  private monthlySubId = 'com.yourapp.premium.monthly';\n  private monthlyPlanId = 'monthly-plan';  // Base plan ID from Google Play Console (Android only)\n\n  private yearlySubId = 'com.yourapp.premium.yearly';\n  private yearlyPlanId = 'yearly-plan';    // Base plan ID from Google Play Console (Android only)\n\n  async initializeStore() {\n    try {\n      // 1. Check if billing is supported\n      const { isBillingSupported } = await NativePurchases.isBillingSupported();\n      if (!isBillingSupported) {\n        throw new Error('Billing not supported on this device');\n      }\n\n      // 2. Get product information (REQUIRED by Apple - no hardcoded prices!)\n      await this.loadProducts();\n      \n    } catch (error) {\n      console.error('Store initialization failed:', error);\n    }\n  }\n\n  async loadProducts() {\n    try {\n      // Load in-app products\n      const { product: premiumProduct } = await NativePurchases.getProduct({\n        productIdentifier: this.premiumProductId,\n        productType: PURCHASE_TYPE.INAPP\n      });\n      \n      // Load subscription products  \n      const { products: subscriptions } = await NativePurchases.getProducts({\n        productIdentifiers: [this.monthlySubId, this.yearlySubId],\n        productType: PURCHASE_TYPE.SUBS\n      });\n      // Android note: subscriptions can include multiple entries per product (one per offer/base plan).\n      // Use `identifier` (base plan), `offerToken`, and optional `offerId` to pick a specific offer.\n      \n      console.log('Products loaded:', {\n        premium: premiumProduct,\n        subscriptions: subscriptions\n      });\n      \n      // Display products with dynamic info from store\n      this.displayProducts(premiumProduct, subscriptions);\n      \n    } catch (error) {\n      console.error('Failed to load products:', error);\n      throw error;\n    }\n  }\n\n  displayProducts(premiumProduct: any, subscriptions: any[]) {\n    // ✅ CORRECT: Use dynamic product info (required by Apple)\n    \n    // Display one-time purchase\n    document.getElementById('premium-title')!.textContent = premiumProduct.title;\n    document.getElementById('premium-price')!.textContent = premiumProduct.priceString;\n    \n    // Display subscriptions\n    subscriptions.forEach(sub =\u003e {\n      const element = document.getElementById(`sub-${sub.identifier}`);\n      if (element) {\n        element.textContent = `${sub.title} - ${sub.priceString}`;\n      }\n    });\n    \n    // ❌ WRONG: Never hardcode prices - Apple will reject your app\n    // document.getElementById('premium-price')!.textContent = '$9.99';\n  }\n\n  // Purchase one-time product (no planIdentifier needed)\n  async purchaseInAppProduct() {\n    try {\n      console.log('Starting in-app purchase...');\n      \n      const result = await NativePurchases.purchaseProduct({\n        productIdentifier: this.premiumProductId,\n        productType: PURCHASE_TYPE.INAPP,\n        quantity: 1\n      });\n      \n      console.log('In-app purchase successful!', result.transactionId);\n      await this.handleSuccessfulPurchase(result.transactionId, 'premium');\n      \n    } catch (error) {\n      console.error('In-app purchase failed:', error);\n      this.handlePurchaseError(error);\n    }\n  }\n\n  // Purchase subscription (planIdentifier REQUIRED for Android)\n  async purchaseMonthlySubscription() {\n    try {\n      console.log('Starting subscription purchase...');\n\n      const result = await NativePurchases.purchaseProduct({\n        productIdentifier: this.monthlySubId,\n        planIdentifier: this.monthlyPlanId,    // REQUIRED for Android subscriptions, ignored on iOS\n        productType: PURCHASE_TYPE.SUBS,       // REQUIRED for subscriptions\n        quantity: 1\n      });\n\n      console.log('Subscription purchase successful!', result.transactionId);\n      await this.handleSuccessfulPurchase(result.transactionId, 'monthly');\n\n    } catch (error) {\n      console.error('Subscription purchase failed:', error);\n      this.handlePurchaseError(error);\n    }\n  }\n\n  // Purchase yearly subscription (planIdentifier REQUIRED for Android)\n  async purchaseYearlySubscription() {\n    try {\n      console.log('Starting yearly subscription purchase...');\n\n      const result = await NativePurchases.purchaseProduct({\n        productIdentifier: this.yearlySubId,\n        planIdentifier: this.yearlyPlanId,     // REQUIRED for Android subscriptions, ignored on iOS\n        productType: PURCHASE_TYPE.SUBS,       // REQUIRED for subscriptions\n        quantity: 1\n      });\n\n      console.log('Yearly subscription successful!', result.transactionId);\n      await this.handleSuccessfulPurchase(result.transactionId, 'yearly');\n\n    } catch (error) {\n      console.error('Yearly subscription failed:', error);\n      this.handlePurchaseError(error);\n    }\n  }\n\n  async handleSuccessfulPurchase(transactionId: string, purchaseType: string) {\n    // 1. Grant access to premium features\n    localStorage.setItem('premium_active', 'true');\n    localStorage.setItem('purchase_type', purchaseType);\n    \n    // 2. Update UI\n    const statusText = purchaseType === 'premium' ? 'Premium Unlocked' : `${purchaseType} Subscription Active`;\n    document.getElementById('subscription-status')!.textContent = statusText;\n    \n    // 3. Optional: Verify purchase on your server\n    await this.verifyPurchaseOnServer(transactionId);\n  }\n\n  handlePurchaseError(error: any) {\n    // Handle different error scenarios\n    if (error.message.includes('User cancelled')) {\n      console.log('User cancelled the purchase');\n    } else if (error.message.includes('Network')) {\n      alert('Network error. Please check your connection and try again.');\n    } else {\n      alert('Purchase failed. Please try again.');\n    }\n  }\n\n  async verifyPurchaseOnServer(transactionId: string) {\n    try {\n      // Send transaction to your server for verification\n      const response = await fetch('/api/verify-purchase', {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify({ transactionId })\n      });\n      \n      const result = await response.json();\n      console.log('Server verification:', result);\n    } catch (error) {\n      console.error('Server verification failed:', error);\n    }\n  }\n\n  async restorePurchases() {\n    try {\n      await NativePurchases.restorePurchases();\n      console.log('Purchases restored successfully');\n\n      // Check if user has active premium after restore\n      const product = await this.getProductInfo();\n      // Update UI based on restored purchases\n\n    } catch (error) {\n      console.error('Failed to restore purchases:', error);\n    }\n  }\n\n  async openSubscriptionManagement() {\n    try {\n      await NativePurchases.manageSubscriptions();\n      console.log('Opened subscription management page');\n    } catch (error) {\n      console.error('Failed to open subscription management:', error);\n    }\n  }\n}\n\n// Usage in your app\nconst purchaseManager = new PurchaseManager();\n\n// Initialize when app starts\npurchaseManager.initializeStore();\n\n// Attach to UI buttons\ndocument.getElementById('buy-premium-button')?.addEventListener('click', () =\u003e {\n  purchaseManager.purchaseInAppProduct();\n});\n\ndocument.getElementById('buy-monthly-button')?.addEventListener('click', () =\u003e {\n  purchaseManager.purchaseMonthlySubscription();\n});\n\ndocument.getElementById('buy-yearly-button')?.addEventListener('click', () =\u003e {\n  purchaseManager.purchaseYearlySubscription();\n});\n\ndocument.getElementById('restore-button')?.addEventListener('click', () =\u003e {\n  purchaseManager.restorePurchases();\n});\n\ndocument.getElementById('manage-subscriptions-button')?.addEventListener('click', () =\u003e {\n  purchaseManager.openSubscriptionManagement();\n});\n```\n\n### Quick Examples\n\n#### Get Multiple Products\n\n```typescript\nimport { NativePurchases, PURCHASE_TYPE } from '@capgo/native-purchases';\n\n// Get in-app products (one-time purchases)\nconst getInAppProducts = async () =\u003e {\n  try {\n    const { products } = await NativePurchases.getProducts({\n      productIdentifiers: [\n        'com.yourapp.premium_features',\n        'com.yourapp.remove_ads',\n        'com.yourapp.extra_content'\n      ],\n      productType: PURCHASE_TYPE.INAPP\n    });\n    \n    products.forEach(product =\u003e {\n      console.log(`${product.title}: ${product.priceString}`);\n    });\n    \n    return products;\n  } catch (error) {\n    console.error('Error getting in-app products:', error);\n  }\n};\n\n// Get subscription products\nconst getSubscriptions = async () =\u003e {\n  try {\n    const { products } = await NativePurchases.getProducts({\n      productIdentifiers: [\n        'com.yourapp.premium.monthly',\n        'com.yourapp.premium.yearly'\n      ],\n      productType: PURCHASE_TYPE.SUBS\n    });\n    \n    products.forEach(product =\u003e {\n      console.log(`${product.title}: ${product.priceString}`);\n    });\n    \n    return products;\n  } catch (error) {\n    console.error('Error getting subscriptions:', error);\n  }\n};\n```\n\n#### Simple Purchase Flow\n\n```typescript\nimport { NativePurchases, PURCHASE_TYPE } from '@capgo/native-purchases';\n\n// Simple one-time purchase (in-app product)\nconst buyInAppProduct = async () =\u003e {\n  try {\n    // Check billing support\n    const { isBillingSupported } = await NativePurchases.isBillingSupported();\n    if (!isBillingSupported) {\n      alert('Purchases not supported on this device');\n      return;\n    }\n\n    // Get product (for price display)\n    const { product } = await NativePurchases.getProduct({\n      productIdentifier: 'com.yourapp.premium_features',\n      productType: PURCHASE_TYPE.INAPP\n    });\n\n    // Confirm with user (showing real price from store)\n    const confirmed = confirm(`Purchase ${product.title} for ${product.priceString}?`);\n    if (!confirmed) return;\n\n    // Make purchase (no planIdentifier needed for in-app)\n    const result = await NativePurchases.purchaseProduct({\n      productIdentifier: 'com.yourapp.premium_features',\n      productType: PURCHASE_TYPE.INAPP,\n      quantity: 1,\n      appAccountToken: uuidToken // Optional: User identifier in UUID format\n                                 // iOS: Must be valid UUID (required by StoreKit 2)\n                                 // Android: UUID works, or any obfuscated string (max 64 chars)\n                                 // RECOMMENDED: Use UUID v5 for cross-platform compatibility\n                                 // Example: uuidv5(userId, APP_NAMESPACE)\n    });\n\n    alert('Purchase successful! Transaction ID: ' + result.transactionId);\n    \n    // Access the full receipt data for backend validation\n    if (result.receipt) {\n      // iOS: Base64-encoded StoreKit receipt - send this to your backend\n      console.log('iOS Receipt (base64):', result.receipt);\n      await validateReceipt(result.receipt);\n    }\n    \n    if (result.jwsRepresentation) {\n      // iOS: StoreKit 2 JWS representation - alternative to receipt\n      console.log('iOS JWS:', result.jwsRepresentation);\n    }\n    \n    if (result.purchaseToken) {\n      // Android: Purchase token - send this to your backend\n      console.log('Android Purchase Token:', result.purchaseToken);\n      await validatePurchaseToken(result.purchaseToken, result.productIdentifier);\n    }\n    \n  } catch (error) {\n    alert('Purchase failed: ' + error.message);\n  }\n};\n\n// Simple subscription purchase (requires planIdentifier)\nconst buySubscription = async () =\u003e {\n  try {\n    // Check billing support\n    const { isBillingSupported } = await NativePurchases.isBillingSupported();\n    if (!isBillingSupported) {\n      alert('Purchases not supported on this device');\n      return;\n    }\n\n    // Get subscription product (for price display)\n    const { product } = await NativePurchases.getProduct({\n      productIdentifier: 'com.yourapp.premium.monthly',\n      productType: PURCHASE_TYPE.SUBS\n    });\n\n    // Confirm with user (showing real price from store)\n    const confirmed = confirm(`Subscribe to ${product.title} for ${product.priceString}?`);\n    if (!confirmed) return;\n\n    // Make subscription purchase (planIdentifier REQUIRED for Android, ignored on iOS)\n    const result = await NativePurchases.purchaseProduct({\n      productIdentifier: 'com.yourapp.premium.monthly',\n      planIdentifier: 'monthly-plan',           // REQUIRED for Android subscriptions, ignored on iOS\n      productType: PURCHASE_TYPE.SUBS,          // REQUIRED for subscriptions\n      quantity: 1,\n      appAccountToken: uuidToken                // Optional: User identifier in UUID format\n                                                // iOS: Must be valid UUID (required by StoreKit 2)\n                                                // Android: UUID works, or any obfuscated string (max 64 chars)\n                                                // RECOMMENDED: Use UUID v5 for cross-platform compatibility\n                                                // Example: uuidv5(userId, APP_NAMESPACE)\n    });\n\n    alert('Subscription successful! Transaction ID: ' + result.transactionId);\n    \n    // Access the full receipt data for backend validation\n    if (result.receipt) {\n      // iOS: Base64-encoded StoreKit receipt - send this to your backend\n      console.log('iOS Receipt (base64):', result.receipt);\n      await validateReceipt(result.receipt);\n    }\n    \n    if (result.jwsRepresentation) {\n      // iOS: StoreKit 2 JWS representation - alternative to receipt\n      console.log('iOS JWS:', result.jwsRepresentation);\n    }\n    \n    if (result.purchaseToken) {\n      // Android: Purchase token - send this to your backend\n      console.log('Android Purchase Token:', result.purchaseToken);\n      await validatePurchaseToken(result.purchaseToken, result.productIdentifier);\n    }\n    \n  } catch (error) {\n    alert('Subscription failed: ' + error.message);\n  }\n};\n```\n\n### Check if billing is supported\n\nBefore attempting to make purchases, check if billing is supported on the device:\nWe only support Storekit 2 on iOS (iOS 15+) and google play on Android\n\n```typescript\nconst checkBillingSupport = async () =\u003e {\n  try {\n    const { isBillingSupported } = await NativePurchases.isBillingSupported();\n    if (isBillingSupported) {\n      console.log('Billing is supported on this device');\n    } else {\n      console.log('Billing is not supported on this device');\n    }\n  } catch (error) {\n    console.error('Error checking billing support:', error);\n  }\n};\n```\n\n### Manage Subscriptions\n\nAllow users to manage their subscriptions directly from your app. This opens the platform's native subscription management page:\n\n```typescript\nimport { NativePurchases } from '@capgo/native-purchases';\n\nconst openSubscriptionSettings = async () =\u003e {\n  try {\n    await NativePurchases.manageSubscriptions();\n    // On iOS: Opens the App Store subscription management page\n    // On Android: Opens the Google Play subscription management page\n  } catch (error) {\n    console.error('Error opening subscription management:', error);\n  }\n};\n```\n\nThis is particularly useful for:\n- Allowing users to cancel or modify their subscriptions\n- Viewing subscription renewal dates\n- Changing subscription plans\n- Managing billing information\n\n### Using appAccountToken for Fraud Detection and User Linking\n\nThe `appAccountToken` parameter is an optional but highly recommended security feature that helps both you and the platform stores detect fraud and link purchases to specific users in your app.\n\n#### What is appAccountToken?\n\nAn identifier (max 64 characters) that uniquely associates transactions with user accounts in your app. It serves two main purposes:\n\n1. **Fraud Detection**: Google Play and Apple use this to detect irregular activity, such as many devices making purchases on the same account within a brief timeframe\n2. **User Linking**: Links purchases to specific in-game characters, avatars, or in-app profiles that initiated the purchase\n\n#### Platform-Specific Requirements\n\n**IMPORTANT: iOS and Android have different format requirements:**\n\n| Platform | Format Requirement | Maps To |\n|----------|-------------------|---------|\n| **iOS** | **Must be a valid UUID** (e.g., `\"550e8400-e29b-41d4-a716-446655440000\"`) | Apple StoreKit 2's `appAccountToken` parameter |\n| **Android** | Any obfuscated string (max 64 chars) | Google Play's `ObfuscatedAccountId` |\n\n**iOS Specific:**\n- Apple's StoreKit 2 requires the `appAccountToken` to be in UUID format\n- The plugin validates and converts the string to UUID before passing to StoreKit\n- If the format is invalid, the token will be ignored\n\n**Android Specific:**\n- Google recommends using encryption or one-way hash\n- Storing PII in cleartext will result in purchases being blocked by Google Play\n\n#### Critical Security Requirements\n\n**DO NOT use Personally Identifiable Information (PII) in cleartext:**\n- ❌ WRONG: `appAccountToken: 'user@example.com'`\n- ❌ WRONG: `appAccountToken: 'john.doe'`\n- ✅ CORRECT (iOS \u0026 Android): `appAccountToken: uuidv5(userId, NAMESPACE)`\n- ✅ CORRECT (Android only): `appAccountToken: hash(userId).substring(0, 64)`\n\n**For cross-platform compatibility, using UUID format is recommended for both platforms.**\n\n#### Implementation Example\n\n```typescript\n// RECOMMENDED: Use UUID v5 for cross-platform compatibility (works on both iOS and Android)\nimport { v5 as uuidv5 } from 'uuid'; // npm install uuid\n\n// Generate a deterministic UUID from user ID\nfunction generateAppAccountToken(userId: string): string {\n  // Use a consistent namespace UUID for your app (generate once and keep constant)\n  const APP_NAMESPACE = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';\n\n  // Generate deterministic UUID - same userId always produces same UUID\n  const uuid = uuidv5(userId, APP_NAMESPACE);\n\n  return uuid; // e.g., \"550e8400-e29b-41d4-a716-446655440000\"\n}\n\n// ALTERNATIVE: For Android-only apps (SHA-256 hash)\nfunction generateAppAccountTokenAndroidOnly(userId: string): string {\n  // This works on Android but will be ignored on iOS (not UUID format)\n  const hash = crypto.createHash('sha256')\n    .update(userId)\n    .digest('hex')\n    .substring(0, 64); // Ensure max 64 chars\n\n  return hash;\n}\n\n// ALTERNATIVE: HMAC with secret key for Android-only apps\nfunction generateSecureAppAccountTokenAndroidOnly(userId: string, secretKey: string): string {\n  // This works on Android but will be ignored on iOS (not UUID format)\n  const hmac = crypto.createHmac('sha256', secretKey)\n    .update(userId)\n    .digest('hex')\n    .substring(0, 64);\n\n  return hmac;\n}\n\n// Use in your purchase flow (cross-platform)\nconst userId = 'user-12345'; // Your internal user ID\nconst appAccountToken = generateAppAccountToken(userId);\n\nawait NativePurchases.purchaseProduct({\n  productIdentifier: 'com.yourapp.premium',\n  productType: PURCHASE_TYPE.INAPP,\n  appAccountToken: appAccountToken // UUID format works on both iOS and Android\n});\n\n// Later, retrieve purchases for this user\nconst { purchases } = await NativePurchases.getPurchases({\n  appAccountToken: appAccountToken\n});\n```\n\n**Why UUID v5 is Recommended:**\n- ✅ Works on both iOS (required) and Android (accepted)\n- ✅ Deterministic: Same user ID always produces the same UUID\n- ✅ Secure: No PII exposure\n- ✅ Standard format: Widely supported\n- ✅ Reversible mapping: You can store the mapping in your backend\n\n#### Best Practices\n\n1. **Use UUID v5 for cross-platform apps** - Works on both iOS (required) and Android (accepted)\n2. **Keep your namespace UUID constant** - Generate once and hardcode it in your app\n3. **Store the mapping** - Keep a record of userId → appAccountToken in your backend for reverse lookup\n4. **Use during purchase** - Include it when calling `purchaseProduct()`\n5. **Use for queries** - Use it when calling `getPurchases()` to filter by user\n6. **Deterministic generation** - Same user should always get the same token\n7. **Max 64 characters** - UUID format is 36 characters, well within the limit\n\n#### Benefits\n\n- **Fraud Prevention**: Platforms can detect suspicious patterns\n- **Multi-device Support**: Link purchases across devices for the same user\n- **User Management**: Query purchases for specific users\n- **Analytics**: Better insights into user purchasing behavior\n\n## Understanding Transaction Properties\n\nWhen you inspect purchases using `getPurchases()` or `restorePurchases()`, you receive an array of `Transaction` objects. Understanding which properties are available and reliable for different scenarios is crucial for proper implementation.\n\n### Transaction Properties by Platform \u0026 Product Type\n\nHere's a comprehensive breakdown of which properties you can expect and rely on:\n\n| Property | iOS IAP | iOS Subscription | Android IAP | Android Subscription | Notes |\n|----------|---------|------------------|-------------|---------------------|-------|\n| `transactionId` | ✅ Always | ✅ Always | ✅ Always | ✅ Always | Primary identifier for the transaction |\n| `receipt` | ✅ Always | ✅ Always | ❌ Never | ❌ Never | iOS only - base64 receipt for validation |\n| `productIdentifier` | ✅ Always | ✅ Always | ✅ Always | ✅ Always | Product ID purchased |\n| `purchaseDate` | ✅ Always | ✅ Always | ✅ Always | ✅ Always | ISO 8601 format |\n| `productType` | ✅ Always | ✅ Always | ✅ Always | ✅ Always | \"inapp\" or \"subs\" |\n| `ownershipType` | ✅ Always | ✅ Always | ❌ Never | ❌ Never | **iOS only** - \"purchased\" or \"familyShared\" (iOS 15.0+, StoreKit 2) |\n| `environment` | ✅ iOS 16+ | ✅ iOS 16+ | ❌ Never | ❌ Never | **iOS only** - \"Sandbox\", \"Production\", or \"Xcode\" (iOS 16.0+ only, not available on iOS 15) |\n| `quantity` | ✅ Always | ✅ Always | ✅ Always 1 | ✅ Always 1 | iOS supports multiple, Android always 1 |\n| `appAccountToken` | ✅ If provided | ✅ If provided | ✅ If provided | ✅ If provided | Set if passed during purchase |\n| `isActive` | ❌ Not set | ✅ Always | ❌ Not set | ❌ Not set | **iOS subscriptions ONLY** - calculated as expiration \u003e now |\n| `willCancel` | ❌ Not set | ✅ Always | ✅ Always null | ✅ Always null | iOS: subscription renewal status; Android: always null |\n| `originalPurchaseDate` | ❌ Not set | ✅ Always | ❌ Not set | ❌ Not set | **iOS subscriptions ONLY** |\n| `expirationDate` | ❌ Not set | ✅ Always | ❌ Not set | ❌ Not set | **iOS subscriptions ONLY** |\n| `purchaseState` | ❌ Not set | ❌ Not set | ✅ Always | ✅ Always | **Android ONLY** - \"PURCHASED\", \"PENDING\", \"0\" (numeric) |\n| `orderId` | ❌ Not set | ❌ Not set | ✅ Always | ✅ Always | **Android ONLY** |\n| `purchaseToken` | ❌ Not set | ❌ Not set | ✅ Always | ✅ Always | **Android ONLY** - for validation |\n| `isAcknowledged` | ❌ Not set | ❌ Not set | ✅ Always | ✅ Always | **Android ONLY** |\n\n### Validating Purchases: Platform-Specific Best Practices\n\n#### iOS In-App Purchases (One-Time)\n\n```typescript\nconst { purchases } = await NativePurchases.getPurchases({\n  productType: PURCHASE_TYPE.INAPP\n});\n\n// Example response for iOS IAP:\n// {\n//   \"transactionId\": \"2000001043762129\",\n//   \"receipt\": \"base64EncodedReceiptData...\",\n//   \"productIdentifier\": \"com.yourapp.premium\",\n//   \"purchaseDate\": \"2025-10-28T06:03:19Z\",\n//   \"productType\": \"inapp\"\n// }\n\npurchases.forEach(purchase =\u003e {\n  // For iOS IAP, the mere presence in the list generally indicates a valid purchase\n  // However, for security, you should validate the receipt on your server\n\n  if (purchase.productIdentifier === 'com.yourapp.premium') {\n    // Option 1: Basic client-side check (not recommended for production)\n    if (purchase.receipt \u0026\u0026 purchase.transactionId) {\n      grantPremiumAccess();\n    }\n\n    // Option 2: Server-side validation (RECOMMENDED)\n    validateReceiptOnServer(purchase.receipt).then(isValid =\u003e {\n      if (isValid) {\n        grantPremiumAccess();\n      }\n    });\n  }\n});\n```\n\n**Key Points for iOS IAP:**\n- ✅ If a purchase appears in `getPurchases()`, it's generally valid\n- ❌ `isActive` is **NOT set** for one-time IAP purchases (only for subscriptions)\n- ❌ `expirationDate` and `originalPurchaseDate` are **NOT set** for IAP\n- 🔒 **Always validate the receipt on your server for production apps**\n- ⚠️ Refunded purchases may still appear but will fail server validation\n\n#### Android In-App Purchases (One-Time)\n\n```typescript\nconst { purchases } = await NativePurchases.getPurchases({\n  productType: PURCHASE_TYPE.INAPP\n});\n\n// Example response for Android IAP:\n// {\n//   \"transactionId\": \"GPA.1234-5678-9012-34567\",\n//   \"productIdentifier\": \"com.yourapp.premium\",\n//   \"purchaseDate\": \"2025-10-28T06:03:19Z\",\n//   \"purchaseState\": \"PURCHASED\",\n//   \"orderId\": \"GPA.1234-5678-9012-34567\",\n//   \"purchaseToken\": \"long-token-string...\",\n//   \"isAcknowledged\": true,\n//   \"productType\": \"inapp\"\n// }\n\npurchases.forEach(purchase =\u003e {\n  // For Android IAP, ALWAYS check purchaseState\n  const isValidPurchase =\n    purchase.purchaseState === 'PURCHASED' \u0026\u0026\n    purchase.isAcknowledged === true;\n\n  if (purchase.productIdentifier === 'com.yourapp.premium' \u0026\u0026 isValidPurchase) {\n    grantPremiumAccess();\n\n    // For extra security, validate on server (RECOMMENDED)\n    validatePurchaseTokenOnServer(purchase.purchaseToken);\n  }\n});\n```\n\n**Key Points for Android IAP:**\n- ✅ **ALWAYS check** `purchaseState === \"PURCHASED\"` or `purchaseState === \"1\"` - this is critical\n- ✅ Check `isAcknowledged === true` (this plugin auto-acknowledges)\n- ❌ `isActive` is **NOT set** on Android (for either IAP or subscriptions)\n- ❌ `expirationDate` and `originalPurchaseDate` are **NOT set** on Android\n- 🔒 For production, validate `purchaseToken` on your server with Google Play API\n\n#### iOS Subscriptions\n\n```typescript\nconst { purchases } = await NativePurchases.getPurchases({\n  productType: PURCHASE_TYPE.SUBS\n});\n\n// Example response for active iOS subscription:\n// {\n//   \"transactionId\": \"2000001043762130\",\n//   \"receipt\": \"base64EncodedReceiptData...\",\n//   \"productIdentifier\": \"com.yourapp.premium.monthly\",\n//   \"purchaseDate\": \"2025-10-28T06:03:19Z\",\n//   \"originalPurchaseDate\": \"2025-09-28T06:03:19Z\",\n//   \"expirationDate\": \"2025-11-28T06:03:19Z\",\n//   \"isActive\": true,\n//   \"willCancel\": false,\n//   \"productType\": \"subs\",\n//   \"isTrialPeriod\": false,\n//   \"isInIntroPricePeriod\": false\n// }\n\npurchases.forEach(purchase =\u003e {\n  // Check if subscription is currently active\n  const isSubscriptionActive = purchase.isActive === true;\n\n  // Alternative: Check expiration date\n  const expirationDate = new Date(purchase.expirationDate);\n  const isActiveByDate = expirationDate \u003e new Date();\n\n  // Check if user has cancelled (still active until expiration)\n  const willAutoRenew = purchase.willCancel === false;\n\n  if (isSubscriptionActive) {\n    grantSubscriptionAccess();\n\n    if (willAutoRenew) {\n      console.log('Subscription will renew on', purchase.expirationDate);\n    } else {\n      console.log('Subscription cancelled, expires on', purchase.expirationDate);\n    }\n  }\n});\n```\n\n**Key Points for iOS Subscriptions:**\n- ✅ `isActive` is reliable for subscriptions\n- ✅ `expirationDate` can be used to check validity\n- ✅ `willCancel` tells you if subscription will auto-renew\n- ⚠️ Even cancelled subscriptions show `isActive: true` until expiration\n- 🔒 Validate receipt on server to detect refunds/revocations\n\n#### Android Subscriptions\n\n```typescript\nconst { purchases } = await NativePurchases.getPurchases({\n  productType: PURCHASE_TYPE.SUBS\n});\n\n// Example response for active Android subscription:\n// {\n//   \"transactionId\": \"GPA.1234-5678-9012-34568\",\n//   \"productIdentifier\": \"com.yourapp.premium.monthly\",\n//   \"purchaseDate\": \"2025-10-28T06:03:19Z\",\n//   \"originalPurchaseDate\": \"2025-09-28T06:03:19Z\",\n//   \"expirationDate\": \"2025-11-28T06:03:19Z\",\n//   \"isActive\": true,\n//   \"purchaseState\": \"PURCHASED\",\n//   \"orderId\": \"GPA.1234-5678-9012-34568\",\n//   \"purchaseToken\": \"long-token-string...\",\n//   \"isAcknowledged\": true,\n//   \"productType\": \"subs\",\n//   \"isTrialPeriod\": false\n// }\n\npurchases.forEach(purchase =\u003e {\n  // Check if subscription is active using multiple signals\n  const isActiveSubscription =\n    purchase.purchaseState === 'PURCHASED' \u0026\u0026\n    purchase.isActive === true \u0026\u0026\n    purchase.isAcknowledged === true;\n\n  // Alternative: Check expiration date\n  const expirationDate = new Date(purchase.expirationDate);\n  const isActiveByDate = expirationDate \u003e new Date();\n\n  if (isActiveSubscription || isActiveByDate) {\n    grantSubscriptionAccess();\n  }\n});\n```\n\n**Key Points for Android Subscriptions:**\n- ✅ Check `purchaseState === \"PURCHASED\"` or `purchaseState === \"1\"`\n- ❌ `isActive` is **NOT set** on Android (even for subscriptions)\n- ❌ `expirationDate` is **NOT set** on Android - must query Google Play API\n- ❌ `originalPurchaseDate` is **NOT set** on Android\n- ✅ `willCancel` is ALWAYS set to `null` on Android\n- 🔒 For subscription status and expiration, query Google Play Developer API on your server\n\n### Handling Refunds and Cancellations\n\nUnderstanding how refunds and cancellations affect your transaction data is critical for proper access control.\n\n#### iOS Refund Behavior\n\n**What happens when a user requests a refund:**\n\n1. **For In-App Purchases (IAP):**\n   - The transaction **may still appear** in `getPurchases()` and `restorePurchases()`\n   - `isActive` is **NOT set** for IAP purchases (only for subscriptions)\n   - The receipt will **NOT disappear** from the device\n   - ✅ **SOLUTION:** Validate the receipt with Apple's servers - refunded transactions will be marked as invalid\n\n2. **For Subscriptions:**\n   - The transaction **will still appear** in purchase history\n   - `isActive` **will be set to `false`** (subscriptions only set this field)\n   - `expirationDate` will be set to the refund date (in the past)\n   - ✅ **SOLUTION:** Check `isActive === false` OR `expirationDate \u003c now` OR validate receipt on server\n\n**Example: Detecting iOS refunds**\n\n```typescript\nasync function checkIOSPurchaseValidity(purchase: Transaction) {\n  // Client-side check (not foolproof)\n  if (purchase.isActive === false) {\n    console.log('Purchase appears to be refunded or expired');\n    return false;\n  }\n\n  // Server-side validation (RECOMMENDED)\n  const validationResult = await fetch('https://your-server.com/validate-receipt', {\n    method: 'POST',\n    body: JSON.stringify({\n      receipt: purchase.receipt,\n      productId: purchase.productIdentifier\n    })\n  }).then(r =\u003e r.json());\n\n  if (!validationResult.isValid || validationResult.isRefunded) {\n    console.log('Receipt validation failed - purchase refunded or invalid');\n    return false;\n  }\n\n  return true;\n}\n```\n\n**Sandbox vs Production Behavior:**\n- ✅ Refund behavior is **consistent** between sandbox and production\n- ⚠️ Sandbox refunds are processed instantly, production may take hours/days\n- ✅ Receipt validation works the same in both environments\n\n#### Android Refund Behavior\n\n**What happens when a user requests a refund:**\n\n1. **For In-App Purchases (IAP):**\n   - The transaction **typically disappears entirely** from `getPurchases()`\n   - Google Play removes refunded purchases from the purchase history\n   - No receipt or transaction will be returned\n   - ✅ **SOLUTION:** If a previously-seen purchase is no longer in the list, it was likely refunded\n\n2. **For Subscriptions:**\n   - The transaction **may disappear** OR\n   - `isActive` will be set to `false`\n   - `purchaseState` may be undefined or the transaction won't be returned at all\n   - ✅ **SOLUTION:** Track purchases on your server and listen for Google Play real-time developer notifications\n\n**Example: Detecting Android refunds**\n\n```typescript\n// Store previously seen purchases in local storage or your database\nconst previousPurchases = getPreviouslyStoredPurchases();\n\nconst { purchases } = await NativePurchases.getPurchases({\n  productType: PURCHASE_TYPE.INAPP\n});\n\n// Check for missing purchases (likely refunded)\npreviousPurchases.forEach(oldPurchase =\u003e {\n  const stillExists = purchases.find(\n    p =\u003e p.transactionId === oldPurchase.transactionId\n  );\n\n  if (!stillExists) {\n    console.log(`Purchase ${oldPurchase.productIdentifier} no longer exists - likely refunded`);\n    revokePremiumAccess(oldPurchase.productIdentifier);\n  }\n});\n\n// Check current purchases for validity\npurchases.forEach(purchase =\u003e {\n  const isValid =\n    purchase.purchaseState === 'PURCHASED' \u0026\u0026\n    purchase.isAcknowledged === true;\n\n  if (!isValid) {\n    console.log('Invalid purchase state detected');\n    // Don't grant access\n  }\n});\n\n// Store current purchases for next comparison\nstorePurchases(purchases);\n```\n\n**Sandbox vs Production Behavior:**\n- ⚠️ Sandbox test accounts can make unlimited \"purchases\" without payment\n- ⚠️ Sandbox refunds are **instant** - purchase disappears immediately\n- ⚠️ Production refunds may take **several hours** before purchase disappears\n- ✅ Testing refunds in production requires real money and real refund requests\n\n**IMPORTANT: Without Server-Side Validation:**\n\nIf you're **not using a backend validator** (not recommended for production), here's what to expect:\n\n| Scenario | iOS Behavior | Android Behavior |\n|----------|-------------|------------------|\n| User requests IAP refund | Transaction may still appear in `restorePurchases()`, check `isActive` | Transaction disappears from `getPurchases()` |\n| User cancels subscription | `willCancel: true`, still active until expiration | Transaction remains, check `isActive` and `expirationDate` |\n| Subscription expires naturally | `isActive: false`, `expirationDate` in past | Transaction disappears OR `isActive: false` |\n| User refunds subscription | Transaction remains with `isActive: false` | Transaction may disappear |\n\n**RECOMMENDATION: Always implement server-side validation**\n- Listen to Apple's App Store Server Notifications (iOS)\n- Listen to Google Play Real-Time Developer Notifications (Android)\n- Validate receipts/tokens on your server before granting access\n- See the [Backend Validation](#backend-validation) section for implementation\n\n### Sandbox vs Production Differences\n\n#### iOS: Sandbox vs Production\n\n**Similarities:**\n- ✅ Transaction structure is identical\n- ✅ All properties return the same data format\n- ✅ `receipt` validation works (use sandbox Apple endpoint)\n- ✅ Refund behavior is consistent\n\n**Differences:**\n\n| Aspect | Sandbox | Production |\n|--------|---------|-----------|\n| Payment processing | Instant, no real money | Real payment, takes seconds |\n| Receipt validation endpoint | `https://sandbox.itunes.apple.com/verifyReceipt` | `https://buy.itunes.apple.com/verifyReceipt` |\n| Subscription duration | Compressed (1 week = 5 minutes) | Real duration (1 week = 7 days) |\n| Refund processing | Instant (via StoreKit Testing) | Takes hours/days, must contact Apple |\n| Test user requirements | Sandbox Apple ID required | Real Apple ID |\n| Transaction IDs | Real format, unique per test | Real format, unique per purchase |\n| `receipt` data | Valid test receipt | Valid production receipt |\n\n**Testing Refunds in Sandbox:**\n1. Use StoreKit Configuration file (local testing) for instant refunds\n2. Or sandbox testing with sandbox Apple ID\n3. Refunds are instant and can be tested repeatedly\n4. Receipt validation will show refunded status immediately\n\n#### Android: Sandbox vs Production\n\n**Similarities:**\n- ✅ Transaction structure is identical\n- ✅ All properties return the same data format\n- ✅ Purchase state values are the same\n\n**Differences:**\n\n| Aspect | License Testing (Sandbox) | Production |\n|--------|--------------------------|-----------|\n| Payment processing | No payment required | Real payment required |\n| Purchase token validation | Works with Google Play API | Works with Google Play API |\n| Transaction IDs | Test format: `GPA.1234-...` | Real format: `GPA.1234-...` |\n| Refund processing | Instant (test account only) | Takes hours, appears as purchase disappearing |\n| Test user requirements | Gmail added to license testers | Real Google account |\n| `purchaseState` values | Same as production | Same as sandbox |\n| Refund detection | Purchase disappears immediately | Purchase disappears after hours/days |\n\n**Testing Refunds in Android Sandbox:**\n1. **License testers** can make unlimited purchases without payment\n2. Refunds are **instant** - purchase disappears from `getPurchases()` immediately\n3. Use **Internal Testing** track for most realistic testing\n4. Real refunds in production require real purchases and real refund requests via Google Play\n\n**Key Difference:**\n- In **sandbox/test**, refunded purchases disappear instantly\n- In **production**, refunded purchases may remain visible for hours before disappearing\n- Always implement server-side validation with Google Play Developer API to catch refunds reliably\n\n### Recommended Access Control Logic\n\nBased on the above, here's the recommended approach for each platform and product type:\n\n```typescript\nimport { NativePurchases, PURCHASE_TYPE, Transaction } from '@capgo/native-purchases';\nimport { Capacitor } from '@capacitor/core';\n\nasync function checkUserAccess(productId: string, productType: PURCHASE_TYPE): Promise\u003cboolean\u003e {\n  try {\n    const { purchases } = await NativePurchases.getPurchases({ productType });\n    const purchase = purchases.find(p =\u003e p.productIdentifier === productId);\n\n    if (!purchase) {\n      return false; // No purchase found\n    }\n\n    const platform = Capacitor.getPlatform();\n\n    if (platform === 'ios') {\n      // iOS Logic\n      if (productType === PURCHASE_TYPE.INAPP) {\n        // For IAP: presence in list + receipt validation\n        // Note: isActive is NOT set for iOS IAP\n        if (!purchase.receipt) return false;\n\n        // IMPORTANT: Validate receipt on server for production\n        const isValid = await validateReceiptOnServer(purchase.receipt);\n        return isValid;\n\n      } else {\n        // For subscriptions: check isActive and expiration\n        // iOS subscriptions ALWAYS have isActive and expirationDate\n        if (purchase.isActive === false) return false;\n        if (purchase.expirationDate) {\n          const expiration = new Date(purchase.expirationDate);\n          if (expiration \u003c new Date()) return false;\n        }\n        return true;\n      }\n\n    } else if (platform === 'android') {\n      // Android Logic\n      if (productType === PURCHASE_TYPE.INAPP) {\n        // For IAP: check purchase state and acknowledgment\n        // Note: Android does NOT set isActive, expirationDate, or originalPurchaseDate\n        const isValid =\n          (purchase.purchaseState === 'PURCHASED' || purchase.purchaseState === '1') \u0026\u0026\n          purchase.isAcknowledged === true;\n\n        if (!isValid) return false;\n\n        // IMPORTANT: Validate purchaseToken on server for production\n        await validatePurchaseTokenOnServer(purchase.purchaseToken);\n        return true;\n\n      } else {\n        // For subscriptions: check purchase state only\n        // Android does NOT set isActive, expirationDate, or originalPurchaseDate\n        // You MUST use Google Play Developer API on your server to get subscription details\n        const isValidState =\n          (purchase.purchaseState === 'PURCHASED' || purchase.purchaseState === '1') \u0026\u0026\n          purchase.isAcknowledged === true;\n\n        if (!isValidState) return false;\n\n        // CRITICAL: Validate subscription status on server with Google Play API\n        // The Purchase object doesn't include expiration dates\n        const serverStatus = await validateAndGetSubscriptionStatus(purchase.purchaseToken);\n        return serverStatus.isActive \u0026\u0026 serverStatus.expirationDate \u003e new Date();\n      }\n    }\n\n    return false;\n  } catch (error) {\n    console.error('Error checking user access:', error);\n    return false;\n  }\n}\n\n// Example usage\nasync function grantAccessBasedOnPurchase() {\n  // Check for premium IAP\n  const hasPremium = await checkUserAccess(\n    'com.yourapp.premium',\n    PURCHASE_TYPE.INAPP\n  );\n\n  // Check for active subscription\n  const hasSubscription = await checkUserAccess(\n    'com.yourapp.premium.monthly',\n    PURCHASE_TYPE.SUBS\n  );\n\n  if (hasPremium || hasSubscription) {\n    unlockPremiumFeatures();\n  }\n}\n```\n\n**Critical Takeaways:**\n1. ✅ For **iOS IAP**: `isActive` is NOT set - validate receipt on server\n2. ✅ For **iOS Subscriptions**: `isActive` and `expirationDate` ARE set - use them!\n3. ✅ For **Android IAP**: Check `purchaseState === \"PURCHASED\"` (or \"1\")\n4. ✅ For **Android Subscriptions**: `isActive` and `expirationDate` are NOT set - must use Google Play API on server\n5. ✅ For **Refunds**: iOS purchases may linger (validate server-side), Android purchases disappear\n6. 🔒 **Always implement server-side validation for production apps**\n\n### API Reference\n\n#### Core Methods\n\n```typescript\n// Check if in-app purchases are supported\nawait NativePurchases.isBillingSupported();\n\n// Get single product information\nawait NativePurchases.getProduct({ productIdentifier: 'product_id' });\n\n// Get multiple products\nawait NativePurchases.getProducts({ productIdentifiers: ['id1', 'id2'] });\n\n// Purchase a product\nawait NativePurchases.purchaseProduct({\n  productIdentifier: 'product_id',\n  quantity: 1\n});\n\n// Restore previous purchases\nawait NativePurchases.restorePurchases();\n\n// Open subscription management page\nawait NativePurchases.manageSubscriptions();\n\n// Get plugin version\nawait NativePurchases.getPluginVersion();\n```\n\n### Important Notes\n\n- **Apple Requirement**: Always display product names and prices from StoreKit data, never hardcode them\n- **Error Handling**: Implement proper error handling for network issues and user cancellations  \n- **Server Verification**: Always verify purchases on your server for security\n- **Testing**: Use the comprehensive testing guides for both iOS and Android platforms\n\n## Backend Validation\n\n### ✅ Full Receipt Data Access\n\n**This plugin provides complete access to verified receipt data for server-side validation.** You get all the information needed to validate purchases with Apple and Google servers.\n\n**For iOS:**\n- ✅ `transaction.receipt` - Complete base64-encoded StoreKit receipt (for Apple's receipt verification API)\n- ✅ `transaction.jwsRepresentation` - StoreKit 2 JSON Web Signature (for App Store Server API v2)\n\n**For Android:**\n- ✅ `transaction.purchaseToken` - Google Play purchase token (for Google Play Developer API)\n- ✅ `transaction.orderId` - Google Play order identifier\n\nThese fields contain the **full verified receipt payload** that you can send directly to your backend for validation with Apple's and Google's servers.\n\n#### Migrating from cordova-plugin-purchase?\n\nIf you're coming from cordova-plugin-purchase, here's the mapping:\n\n| cordova-plugin-purchase | @capgo/native-purchases | Platform | Notes |\n|-------------------------|-------------------------|----------|-------|\n| `transaction.transactionReceipt` | `transaction.receipt` (base64) | iOS | Legacy StoreKit receipt format (same value as Cordova) |\n| — | `transaction.jwsRepresentation` (JWS) | iOS | StoreKit 2 JWS format (iOS 15+, additional field with no Cordova equivalent; Apple's recommended modern format for new implementations) |\n| `transaction.purchaseToken` | `transaction.purchaseToken` | Android | Same field name |\n\n**This plugin already exposes everything you need for backend verification!** The `receipt` and `purchaseToken` fields contain the complete verified receipt data, and `jwsRepresentation` provides an additional StoreKit 2 representation when available.\n\n**Note:** On iOS, `jwsRepresentation` is only available for StoreKit 2 transactions (iOS 15+) and is Apple's recommended modern format. For maximum compatibility, use `receipt` which works on all iOS versions; when available, you can also send `jwsRepresentation` to backends that support App Store Server API v2.\n\n### Why Backend Validation?\n\nIt's crucial to validate receipts on your server to ensure the integrity of purchases. Client-side data can be manipulated, but server-side validation with Apple/Google servers ensures purchases are legitimate.\n\n### Receipt Data Available for Backend Verification\n\nThe `Transaction` object returned by `purchaseProduct()`, `getPurchases()`, and `restorePurchases()` includes all data needed for server-side validation:\n\n**iOS Receipt Data:**\n- **`receipt`** - Base64-encoded StoreKit receipt (legacy format, works with Apple's receipt verification API)\n- **`jwsRepresentation`** - JSON Web Signature for StoreKit 2 (recommended for new implementations, works with App Store Server API)\n- **`transactionId`** - Unique transaction identifier\n\n**Android Receipt Data:**\n- **`purchaseToken`** - Google Play purchase token (required for server-side validation)\n- **`orderId`** - Google Play order identifier\n- **`transactionId`** - Alias for purchaseToken\n\n**All platforms include:**\n- `productIdentifier` - The product that was purchased\n- `purchaseDate` - When the purchase occurred\n- Additional metadata like `appAccountToken`, `quantity`, etc.\n\n### Complete Backend Validation Example\n\n#### Cloudflare Worker Setup\nCreate a new Cloudflare Worker and follow the instructions in folder (`validator`)[/validator/README.md]\n\n#### Client-Side Implementation\n\nHere's how to access the receipt data and send it to your backend for validation:\n\n```typescript\nimport { Capacitor } from '@capacitor/core';\nimport { NativePurchases, PURCHASE_TYPE, Product, Transaction } from '@capgo/native-purchases';\nimport axios from 'axios'; // Make sure to install axios: npm install axios\n\nclass Store {\n  // ... (previous code remains the same)\n\n  // Purchase in-app product\n  async purchaseProduct(productId: string) {\n    try {\n      const transaction = await NativePurchases.purchaseProduct({\n        productIdentifier: productId,\n        productType: PURCHASE_TYPE.INAPP\n      });\n      console.log('In-app purchase successful:', transaction);\n      \n      // Immediately grant access to the purchased content\n      await this.grantAccess(productId);\n      \n      // Initiate server-side validation asynchronously\n      this.validatePurchaseOnServer(transaction).catch(console.error);\n      \n      return transaction;\n    } catch (error) {\n      console.error('Purchase failed:', error);\n      throw error;\n    }\n  }\n\n  // Purchase subscription (requires planIdentifier)\n  async purchaseSubscription(productId: string, planId: string) {\n    try {\n      const transaction = await NativePurchases.purchaseProduct({\n        productIdentifier: productId,\n        planIdentifier: planId,              // REQUIRED for subscriptions\n        productType: PURCHASE_TYPE.SUBS      // REQUIRED for subscriptions\n      });\n      console.log('Subscription purchase successful:', transaction);\n      \n      // Immediately grant access to the subscription content\n      await this.grantAccess(productId);\n      \n      // Initiate server-side validation asynchronously\n      this.validatePurchaseOnServer(transaction).catch(console.error);\n      \n      return transaction;\n    } catch (error) {\n      console.error('Subscription purchase failed:', error);\n      throw error;\n    }\n  }\n\n  private async grantAccess(productId: string) {\n    // Implement logic to grant immediate access to the purchased content\n    console.log(`Granting access to ${productId}`);\n    // Update local app state, unlock features, etc.\n  }\n\n  private async validatePurchaseOnServer(transaction: Transaction) {\n    const serverUrl = 'https://your-server-url.com/validate-purchase';\n    const platform = Capacitor.getPlatform();\n    \n    try {\n      // Prepare receipt data based on platform\n      const receiptData = platform === 'ios' \n        ? {\n            // iOS: Send the full receipt (base64 encoded) or JWS representation\n            receipt: transaction.receipt,                    // StoreKit receipt (base64)\n            jwsRepresentation: transaction.jwsRepresentation, // StoreKit 2 JWS (optional, recommended for new apps)\n            transactionId: transaction.transactionId,\n            platform: 'ios'\n          }\n        : {\n            // Android: Send the purchase token and order ID\n            purchaseToken: transaction.purchaseToken,        // Required for Google Play validation\n            orderId: transaction.orderId,                    // Google Play order ID\n            transactionId: transaction.transactionId,\n            platform: 'android'\n          };\n\n      const response = await axios.post(serverUrl, {\n        ...receiptData,\n        productId: transaction.productIdentifier,\n        purchaseDate: transaction.purchaseDate,\n        // Include user ID or other app-specific data\n        userId: 'your-user-id'\n      });\n\n      console.log('Server validation response:', response.data);\n      return response.data;\n    } catch (error) {\n      console.error('Error in server-side validation:', error);\n      // Implement retry logic or notify the user if necessary\n      throw error;\n    }\n  }\n}\n\n// Usage examples\nconst store = new Store();\nawait store.initialize();\n\ntry {\n  // Purchase in-app product (one-time purchase)\n  await store.purchaseProduct('premium_features');\n  console.log('In-app purchase completed successfully');\n  \n  // Purchase subscription (requires planIdentifier)\n  await store.purchaseSubscription('premium_monthly', 'monthly-plan');\n  console.log('Subscription completed successfully');\n} catch (error) {\n  console.error('Purchase failed:', error);\n}\n```\n\nNow, let's look at how the server-side (Node.js) code handles the validation:\n\n```typescript\nimport express from 'express';\nimport axios from 'axios';\n\nconst app = express();\napp.use(express.json());\n\nconst CLOUDFLARE_WORKER_URL = 'https://your-cloudflare-worker-url.workers.dev';\n\napp.post('/validate-purchase', async (req, res) =\u003e {\n  const { platform, receipt, jwsRepresentation, purchaseToken, productId, userId } = req.body;\n\n  try {\n    let validationResponse;\n\n    if (platform === 'ios') {\n      // iOS: Validate using receipt or JWS representation\n      if (!receipt \u0026\u0026 !jwsRepresentation) {\n        return res.status(400).json({ \n          success: false, \n          error: 'Missing receipt data: either receipt or jwsRepresentation is required for iOS' \n        });\n      }\n      \n      // Option 1: Use legacy receipt validation (recommended for compatibility)\n      if (receipt) {\n        validationResponse = await axios.post(`${CLOUDFLARE_WORKER_URL}/apple`, {\n          receipt: receipt,  // Base64-encoded receipt from transaction.receipt\n          password: 'your-app-shared-secret' // App-Specific Shared Secret from App Store Connect (required for auto-renewable subscriptions)\n        });\n      }\n      // Option 2: Use StoreKit 2 App Store Server API (recommended for new implementations)\n      else if (jwsRepresentation) {\n        // Validate JWS token with App Store Server API\n        // Note: JWS verification requires decoding and validating the signature\n        // Implementation depends on your backend setup - see Apple's documentation:\n        // https://developer.apple.com/documentation/appstoreserverapi/jwstransaction\n        validationResponse = await axios.post(`${CLOUDFLARE_WORKER_URL}/apple-jws`, {\n          jws: jwsRepresentation\n        });\n      }\n    } else if (platform === 'android') {\n      // Android: Validate using purchase token with Google Play Developer API\n      if (!purchaseToken) {\n        return res.status(400).json({ \n          success: false, \n          error: 'Missing purchaseToken for Android validation' \n        });\n      }\n      \n      validationResponse = await axios.post(`${CLOUDFLARE_WORKER_URL}/google`, {\n        purchaseToken: purchaseToken,  // From transaction.purchaseToken\n        productId: productId,\n        packageName: 'com.yourapp.package'\n      });\n    } else {\n      return res.status(400).json({ \n        success: false, \n        error: 'Invalid platform' \n      });\n    }\n\n    const validationResult = validationResponse.data;\n\n    // Process the validation result\n    if (validationResult.isValid) {\n      // Update user status in the database\n      await updateUserPurchase(userId, {\n        productId,\n        platform,\n        transactionId: req.body.transactionId,\n        validated: true,\n        validatedAt: new Date(),\n        receiptData: validationResult\n      });\n      \n      console.log(`Purchase validated for user ${userId}, product ${productId}`);\n      \n      res.json({ \n        success: true, \n        validated: true,\n        message: 'Purchase successfully validated'\n      });\n    } else {\n      // Handle invalid purchase\n      console.warn(`Invalid purchase detected for user ${userId}`);\n      \n      // Flag for investigation but don't block the user immediately\n      await flagSuspiciousPurchase(userId, req.body);\n      \n      res.json({ \n        success: true,  // Don't block the user\n        validated: false,\n        message: 'Purchase validation pending review'\n      });\n    }\n\n  } catch (error) {\n    console.error('Error validating purchase:', error);\n    \n    // Log the error for investigation\n    await logValidationError(userId, req.body, error);\n    \n    // Still respond with success to the app\n    // This ensures the app doesn't block the user's access\n    res.json({ \n      success: true,\n      validated: 'pending',\n      message: 'Validation will be retried'\n    });\n  }\n});\n\n// Helper function to update user purchase status\nasync function updateUserPurchase(userId: string, purchaseData: any) {\n  // Implement your database logic here\n  console.log('Updating purchase for user:', userId);\n}\n\n// Helper function to flag suspicious purchases\nasync function flagSuspiciousPurchase(userId: string, purchaseData: any) {\n  // Implement your logic to flag and review suspicious purchases\n  console.log('Flagging suspicious purchase:', userId);\n}\n\n// Helper function to log validation errors\nasync function logValidationError(userId: string, purchaseData: any, error: any) {\n  // Implement your error logging logic\n  console.log('Logging validation error:', userId, error);\n}\n\n// Start the server\napp.listen(3000, () =\u003e console.log('Server running on port 3000'));\n```\n\n### Alternative: Direct Store API Validation\n\nInstead of using a Cloudflare Worker, you can validate directly with Apple and Google:\n\n**iOS - Apple Receipt Verification API:**\n```typescript\n// Production: https://buy.itunes.apple.com/verifyReceipt\n// Sandbox: https://sandbox.itunes.apple.com/verifyReceipt\n\nasync function validateAppleReceipt(receiptData: string) {\n  const response = await axios.post('https://buy.itunes.apple.com/verifyReceipt', {\n    'receipt-data': receiptData,\n    'password': 'your-shared-secret', // App-Specific Shared Secret from App Store Connect (required for auto-renewable subscriptions)\n    'exclude-old-transactions': true\n  });\n  \n  return response.data;\n}\n```\n\n**Android - Google Play Developer API:**\n```typescript\n// Requires Google Play Developer API credentials\n// See: https://developers.google.com/android-publisher/getting_started\n\nimport { google } from 'googleapis';\n\nasync function validateGooglePurchase(packageName: string, productId: string, purchaseToken: string) {\n  const androidPublisher = google.androidpublisher('v3');\n  \n  const auth = new google.auth.GoogleAuth({\n    keyFile: 'path/to/service-account-key.json',\n    scopes: ['https://www.googleapis.com/auth/androidpublisher'],\n  });\n  \n  const authClient = await auth.getClient();\n  \n  const response = await androidPublisher.purchases.products.get({\n    auth: authClient,\n    packageName: packageName,\n    productId: productId,\n    token: purchaseToken\n  });\n  \n  return response.data;\n}\n```\n\nKey points about this approach:\n\n1. The app immediately grants access after a successful purchase, ensuring a smooth user experience.\n2. The app initiates server-side validation asynchronously, not blocking the user's access.\n3. The server handles the actual validation by calling the Cloudflare Worker.\n4. The server always responds with success to the app, even if validation fails or encounters an error.\n5. The server can update the user's status in the database, log results, and handle any discrepancies without affecting the user's immediate experience.\n\nComments on best practices:\n\n```typescript\n// After successful validation:\n// await updateUserStatus(userId, 'paid');\n\n// It's crucial to not block or revoke access immediately if validation fails\n// Instead, flag suspicious transactions for review:\n// if (!validationResult.isValid) {\n//   await flagSuspiciousPurchase(userId, transactionId);\n// }\n\n// Implement a system to periodically re-check flagged purchases\n// This could be a separate process that runs daily/weekly\n\n// Consider implementing a grace period for new purchases\n// This allows for potential delays in server communication or store processing\n// const GRACE_PERIOD_DAYS = 3;\n// if (daysSincePurchase \u003c GRACE_PERIOD_DAYS) {\n//   grantAccess = true;\n// }\n\n// For subscriptions, regularly check their status with the stores\n// This ensures you catch any cancelled or expired subscriptions\n// setInterval(checkSubscriptionStatuses, 24 * 60 * 60 * 1000); // Daily check\n\n// Implement proper error handling and retry logic for network failures\n// This is especially important for the server-to-Cloudflare communication\n\n// Consider caching validation results to reduce load on your server and the stores\n// const cachedValidation = await getCachedValidation(transactionId);\n// if (cachedValidation) return cachedValidation;\n```\n\nThis approach balances immediate user gratification with proper server-side validation, adhering to Apple and Google's guidelines while still maintaining the integrity of your purchase system.\n\n## API\n\n\u003cdocgen-index\u003e\n\n* [`restorePurchases()`](#restorepurchases)\n* [`getAppTransaction()`](#getapptransaction)\n* [`isEntitledToOldBusinessModel(...)`](#isentitledtooldbusinessmodel)\n* [`purchaseProduct(...)`](#purchaseproduct)\n* [`getProducts(...)`](#getproducts)\n* [`getProduct(...)`](#getproduct)\n* [`isBillingSupported()`](#isbillingsupported)\n* [`getPluginVersion()`](#getpluginversion)\n* [`getPurchases(...)`](#getpurchases)\n* [`manageSubscriptions()`](#managesubscriptions)\n* [`acknowledgePurchase(...)`](#acknowledgepurchase)\n* [`consumePurchase(...)`](#consumepurchase)\n* [`addListener('transactionUpdated', ...)`](#addlistenertransactionupdated-)\n* [`addListener('transactionVerificationFailed', ...)`](#addlistenertransactionverificationfailed-)\n* [`removeAllListeners()`](#removealllisteners)\n* [Interfaces](#interfaces)\n* [Enums](#enums)\n\n\u003c/docgen-index\u003e\n\n\u003cdocgen-api\u003e\n\u003c!--Update the source file JSDoc comments and rerun docgen to update the docs below--\u003e\n\n### restorePurchases()\n\n```typescript\nrestorePurchases() =\u003e Promise\u003cvoid\u003e\n```\n\nRestores a user's previous  and links their appUserIDs to any user's also using those .\n\n--------------------\n\n\n### getAppTransaction()\n\n```typescript\ngetAppTransaction() =\u003e Promise\u003c{ appTransaction: AppTransaction; }\u003e\n```\n\nGets the App \u003ca href=\"#transaction\"\u003eTransaction\u003c/a\u003e information, which provides details about when the user\noriginally downloaded or purchased the app.\n\nThis is useful for implementing business model changes where you want to\ngrandfather users who originally downloaded an earlier version of the app.\n\n**Use Case Example:**\nIf your app was originally free but you're adding a subscription, you can use\n`originalAppVersion` to check if users downloaded before the subscription was added\nand give them free access.\n\n**Platform Notes:**\n- **iOS**: Requires iOS 16.0+. Uses StoreKit 2's \u003ca href=\"#apptransaction\"\u003e`AppTransaction\u003c/a\u003e.shared`.\n- **Android**: Uses Google Play's install referrer data when available.\n\n**Returns:** \u003ccode\u003ePromise\u0026lt;{ appTransaction: \u003ca href=\"#apptransaction\"\u003eAppTransaction\u003c/a\u003e; }\u0026gt;\u003c/code\u003e\n\n**Since:** 7.16.0\n\n--------------------\n\n\n### isEntitledToOldBusinessModel(...)\n\n```typescript\nisEntitledToOldBusinessModel(options: { targetVersion?: string; targetBuildNumber?: string; }) =\u003e Promise\u003c{ isOlderVersion: boolean; originalAppVersion: string; }\u003e\n```\n\nCompares the original app version from the App \u003ca href=\"#transaction\"\u003eTransaction\u003c/a\u003e against a target version\nto determine if the user is entitled to features from an earlier business model.\n\nThis is a utility method that performs the version comparison natively, which can be\nmore reliable than JavaScript-based comparison for semantic versioning.\n\n**Use Case:**\nCheck if the user's original download version is older than a specific version\nto determine if they should be grandfathered into free features.\n\n**Platform Differences:**\n- iOS: Uses build number (CFBundleVersion) from \u003ca href=\"#apptransaction\"\u003eAppTransaction\u003c/a\u003e. Requires iOS 16+.\n- Android: Uses version name from PackageInfo (current installed version, not original).\n\n| Param         | Type                                                                 | Description              |\n| ------------- | -------------------------------------------------------------------- | ------------------------ |\n| **`options`** | \u003ccode\u003e{ targetVersion?: string; targetBuildNumber?: string; }\u003c/code\u003e | - The comparison options |\n\n**Returns:** \u003ccode\u003ePromise\u0026lt;{ isOlderVersion: boolean; originalAppVersion: string; }\u0026gt;\u003c/code\u003e\n\n**Since:** 7.16.0\n\n--------------------\n\n\n### purchaseProduct(...)\n\n```typescript\npurchaseProduct(options: { productIdentifier: string; planIdentifier?: string; productType?: PURCHASE_TYPE; quantity?: number; appAccountToken?: string; isConsumable?: boolean; autoAcknowledgePurchases?: boolean; }) =\u003e Promise\u003cTransaction\u003e\n```\n\nStarted purchase process for the given product.\n\n| Param         | Type                                                                                                                                                                                                                                    | Description               |\n| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- |\n| **`options`** | \u003ccode\u003e{ productIdentifier: string; planIdentifier?: string; productType?: \u003ca href=\"#purchase_type\"\u003ePURCHASE_TYPE\u003c/a\u003e; quantity?: number; appAccountToken?: string; isConsumable?: boolean; autoAcknowledgePurchases?: boolean; }\u003c/code\u003e | - The product to purchase |\n\n**Returns:** \u003ccode\u003ePromise\u0026lt;\u003ca href=\"#transaction\"\u003eTransaction\u003c/a\u003e\u0026gt;\u003c/code\u003e\n\n--------------------\n\n\n### getProducts(...)\n\n```typescript\ngetProducts(options: { productIdentifiers: string[]; productType?: PURCHASE_TYPE; }) =\u003e Promise\u003c{ products: Product[]; }\u003e\n```\n\nGets the product info associated with a list of product identifiers.\n\n| Param         | Type                                                                                                     | Description                                                    |\n| ------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| **`options`** | \u003ccode\u003e{ productIdentifiers: string[]; productType?: \u003ca href=\"#purchase_type\"\u003ePURCHASE_TYPE\u003c/a\u003e; }\u003c/code\u003e | - The product identifiers you wish to retrieve information for |\n\n**Returns:** \u003ccode\u003ePromise\u0026lt;{ products: Product[]; }\u0026gt;\u003c/code\u003e\n\n--------------------\n\n\n### getProduct(...)\n\n```typescript\ngetProduct(options: { productIdentifier: string; productType?: PURCHASE_TYPE; }) =\u003e Promise\u003c{ product: Product; }\u003e\n```\n\nGets the product info for a single product identifier.\n\n**⚠️ Warning:** Do not call `getProduct` concurrently using `Promise.all`.\nThe underlying native billing client does not support concurrent product\nqueries, and doing so causes a race condition that may result in errors\nor missing data. To fetch multiple products at once, use `getProducts`\ninstead — it accepts an array of identifiers and is race-condition-free.\n\n| Param         | Type                                                                                                  | Description                                                   |\n| ------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |\n| **`options`** | \u003ccode\u003e{ productIdentifier: string; productType?: \u003ca href=\"#purchase_type\"\u003ePURCHASE_TYPE\u003c/a\u003e; }\u003c/code\u003e | - The product identifier you wish to retrieve information for |\n\n**Returns:** \u003ccode\u003ePromise\u0026lt;{ product: \u003ca href=\"#product\"\u003eProduct\u003c/a\u003e; }\u0026gt;\u003c/code\u003e\n\n--------------------\n\n\n### isBillingSupported()\n\n```typescript\nisBillingSupported() =\u003e Promise\u003c{ isBillingSupported: boolean; }\u003e\n```\n\nCheck if billing is supported for the current device.\n\n**Returns:** \u003ccode\u003ePromise\u0026lt;{ isBillingSupported: boolean; }\u0026gt;\u003c/code\u003e\n\n--------------------\n\n\n### getPluginVersion()\n\n```typescript\ngetPluginVersion() =\u003e Promise\u003c{ version: string; }\u003e\n```\n\nGet the native Capacitor plugin version\n\n**Returns:** \u003ccode\u003ePromise\u0026lt;{ version: string; }\u0026gt;\u003c/code\u003e\n\n--------------------\n\n\n### getPurchases(...)\n\n```typescript\ngetPurchases(options?: { productType?: PURCHASE_TYPE | undefined; appAccountToken?: string | undefined; onlyCurrentEntitlements?: boolean | undefined; } | undefined) =\u003e Promise\u003c{ purchases: Transaction[]; }\u003e\n```\n\nGets all the user's purchases (both in-app purchases and subscriptions).\nThis method queries the platform's purchase history for the current user.\n\n| Param         | Type                                                                                                                                    | Description                                   |\n| ------------- | --------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- |\n| **`options`** | \u003ccode\u003e{ productType?: \u003ca href=\"#purchase_type\"\u003ePURCHASE_TYPE\u003c/a\u003e; appAccountToken?: string; onlyCurrentEntitlements?: boolean; }\u003c/code\u003e | - Optional parameters for filtering purchases |\n\n**Returns:** \u003ccode\u003ePromise\u0026lt;{ purchases: Transaction[]; }\u0026gt;\u003c/code\u003e\n\n**Since:** 7.2.0\n\n--------------------\n\n\n### manageSubscriptions()\n\n```typescript\nmanageSubscriptions() =\u003e Promise\u003cvoid\u003e\n```\n\nOpens the platform's native subscription management page.\nThis allows users to view, modify, or cancel their subscriptions.\n\n- iOS: Opens the App Store subscription management page for the current app\n- Android: Opens the Google Play subscription management page\n\n**Since:** 7.10.0\n\n--------------------\n\n\n### acknowledgePurchase(...)\n\n```typescript\nacknowledgePurchase(options: { purchaseToken: string; }) =\u003e Promise\u003cvoid\u003e\n```\n\nManually acknowledge/finish a purchase transaction.\n\nThis method is only needed when you set `autoAcknowledgePurchases: false` in purchaseProduct().\n\n**Platform Behavior:**\n- **Android**: Acknowledges the purchase with Google Play. Must be called within 3 days or the purchase will be refunded.\n- **iOS**: Finishes the transaction with StoreKit 2. Unfinished transactions remain in the queue and may block future purchases.\n\n**Acknowledgment Options:**\n\n**1. Client-side (this method)**: Call from your app after validation\n```typescript\nawait NativePurchases.acknowledgePurchase({\n  purchaseToken: transaction.purchaseToken  // Android: purchaseToken, iOS: transactionId\n});\n```\n\n**2. Server-side (Android only, recommended for security)**: Use Google Play Developer API v3\n- Endpoint: `POST https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/products/{productId}/tokens/{token}:acknowledge`\n- Requires OAuth 2.0 authentication with appropriate scopes\n- See: https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.products/acknowledge\n- For subscriptions: Use `/purchases/subscriptions/{subscriptionId}/tokens/{token}:acknowledge` instead\n- Note: iOS has no server-side finish API\n\n**When to use manual acknowledgment:**\n- Server-side validation: Verify the purchase with your backend before acknowledging\n- Entitlement delivery: Ensure user receives content/features before acknowledging\n- Multi-step workflows: Complete all steps before final acknowledgment\n- Security: Prevent client-side manipulation by handling acknowledgment server-side (Android only)\n\n| Param         | Type                                    | Description                   |\n| ------------- | --------------------------------------- | ----------------------------- |\n| **`options`** | \u003ccode\u003e{ purchaseToken: string; }\u003c/code\u003e | - The purchase to acknowledge |\n\n**Since:** 7.14.0\n\n--------------------\n\n\n### consumePurchase(...)\n\n```typescript\nconsumePurchase(options: { purchaseToken: string; }) =\u003e Promise\u003cvoid\u003e\n```\n\nConsume an in-app purchase on Android.\n\nConsuming a purchase does two things:\n1. Acknowledges the purchase (so you don't need to call acknowledgePurchase separately)\n2. Removes ownership, allowing the user to buy the same product again\n\nUse this for consumable products like virtual currency, extra lives, or credits.\n\n**Important:** In Google Play Billing Library 8.x, consumed purchases can no longer\nbe queried via getPurchases(). Once consumed, the purchase is gone.\n\nAndroid only — iOS does not have a separate consume concept.\nOn iOS and web, this method rejects with an error.\n\n| Param         | Type                                    | Description               |\n| ------------- | --------------------------------------- | ------------------------- |\n| **`options`** | \u003ccode\u003e{ purchaseToken: string; }\u003c/code\u003e | - The purchase to consume |\n\n**Since:** 8.2.0\n\n--------------------\n\n\n### addListener('transactionUpdated', ...)\n\n```typescript\naddListener(eventName: 'transactionUpdated', listenerFunc: (transaction: Transaction) =\u003e void) =\u003e Promise\u003cPluginListenerHandle\u003e\n```\n\nListen for StoreKit transaction updates delivered by Apple's \u003ca href=\"#transaction\"\u003eTransaction\u003c/a\u003e.updates.\nFires on app launch if there are unfinished transactions, and for any updates afterward.\niOS only.\n\n| Param              | Type                                                                          |\n| ------------------ | ----------------------------------------------------------------------------- |\n| **`eventName`**    | \u003ccode\u003e'transactionUpdated'\u003c/code\u003e                                             |\n| **`listenerFunc`** | \u003ccode\u003e(transaction: \u003ca href=\"#transaction\"\u003eTransaction\u003c/a\u003e) =\u0026gt; void\u003c/code\u003e |\n\n**Returns:** \u003ccode\u003ePromise\u0026lt;\u003ca href=\"#pluginlistenerhandle\"\u003ePluginListenerHandle\u003c/a\u003e\u0026gt;\u003c/code\u003e\n\n--------------------\n\n\n### addListener('transactionVerificationFailed', ...)\n\n```typescript\naddListener(eventName: 'transactionVerificationFailed', listenerFunc: (payload: TransactionVerificationFailedEvent) =\u003e void) =\u003e Promise\u003cPluginListenerHandle\u003e\n```\n\nListen for StoreKit transaction verification failures delivered by Apple's \u003ca href=\"#transaction\"\u003eTransaction\u003c/a\u003e.updates.\nFires when the verification result is unverified.\niOS only.\n\n| Param              | Type                                                                                                                    |\n| ------------------ | ----------------------------------------------------------------------------------------------------------------------- |\n| **`eventName`**    | \u003ccode\u003e'transactionVerificationFailed'\u003c/code\u003e                                                                            |\n| **`listenerFunc`** | \u003ccode\u003e(payload: \u003ca href=\"#transactionverificationfailedevent\"\u003eTransactionVerificationFailedEvent\u003c/a\u003e) =\u0026gt; void\u003c/code\u003e |\n\n**Returns:** \u003ccode\u003ePromise\u0026lt;\u003ca href=\"#pluginlistenerhandle\"\u003ePluginListenerHandle\u003c/a\u003e\u0026gt;\u003c/code\u003e\n\n--------------------\n\n\n### removeAllListeners()\n\n```typescript\nremoveAllListeners() =\u003e Promise\u003cvoid\u003e\n```\n\nRemove all registered listeners\n\n--------------------\n\n\n### Interfaces\n\n\n#### AppTransaction\n\nRepresents the App \u003ca href=\"#transaction\"\u003eTransaction\u003c/a\u003e information from StoreKit 2.\nThis provides details about when the user originally downloaded or purchased the app,\nwhich is useful for determining if users are entitled to features from earlier business models.\n\n| Prop                       | Type                                                      | Description                                                                                                                                                                                                                                                                                                                                                   | Since  |\n| -------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ |\n| **`originalAppVersion`**   | \u003ccode\u003estring\u003c/code\u003e                                       | The app version that the user originally purchased or downloaded. Use this to determine if users who originally downloaded an earlier version should be entitled to features that were previously free or included. For iOS: This is the `CFBundleShortVersionString` (e.g., \"1.0.0\") For Android: This is the `versionName` from Google Play (e.g., \"1.0.0\") | 7.16.0 |\n| **`originalPurchaseDate`** | \u003ccode\u003estring\u003c/code\u003e                                       | The date when the user originally purchased or downloaded the app. ISO 8601 format.                                                                                                                                                                                                                                                                           | 7.16.0 |\n| **`bundleId`**             | \u003ccode\u003estring\u003c/code\u003e                                       | The bundle identifier of the app.                                                                                                                                                                                                                                                                                                                             | 7.16.0 |\n| **`appVersion`**           | \u003ccode\u003estring\u003c/code\u003e                                       | The current app version installed on the device.                                                                                                                                                                                                                                                                                                              | 7.16.0 |\n| **`environment`**          | \u003ccode\u003e'Sandbox' \\| 'Production' \\| 'Xcode' \\| null\u003c/code\u003e | The server environment where the app was originally purchased.                                                                                                                                                                                                                                                                                                | 7.16.0 |\n| **`jwsRepresentation`**    | \u003ccode\u003estring\u003c/code\u003e                                       | The JWS (JSON Web Signature) representation of the app transaction. Can be sent to your backend for server-side verification.                                                                                                                                                                                                                                 | 7.16.0 |\n\n\n#### Transaction\n\n| Prop                       | Type                                                                                                          | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | Default           | Since  |\n| -------------------------- | ------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | ------ |\n| **`transactionId`**        | \u003ccode\u003estring\u003c/code\u003e                                                                                           | Unique identifier for the transaction.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |                   | 1.0.0  |\n| **`receipt`**              | \u003ccode\u003estring\u003c/code\u003e                                                                                           | Receipt data for validation (base64 encoded StoreKit receipt). **This is the full verified receipt payload from Apple StoreKit.** Send this to your backend for server-side validation with Apple's receipt verification API. The receipt remains available even after refund - server validation is required to detect refunded transactions. **For backend validation:** - Use Apple's receipt verification API: https://buy.itunes.apple.com/verifyReceipt (production) - Or sandbox: https://sandbox.itunes.apple.com/verifyReceipt - This contains all transaction data needed for validation **Note:** Apple recommends migrating to App Store Server API v2 with `jwsRepresentation` for new implementations. The legacy receipt verification API continues to work but may be deprecated in the future.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |                   | 1.0.0  |\n| **`jwsRepresentation`**    | \u003ccode\u003estring\u003c/code\u003e                                                                                           | StoreKit 2 JSON Web Signature (JWS) payload describing the verified transaction. **This is the full verified receipt in JWS format (StoreKit 2).** Send this to your backend when using Apple's App Store Server API v2 instead of raw receipts. Only available when the transaction originated from StoreKit 2 APIs (e.g. \u003ca href=\"#transaction\"\u003eTransaction\u003c/a\u003e.updates). **For backend validation:** - Use Apple's App Store Server API v2 to decode and verify the JWS - This is the modern alternative to the legacy receipt format - Contains signed transaction information from Apple                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |                   | 7.13.2 |\n| **`appAccountToken`**      | \u003ccode\u003estring \\| null\u003c/code\u003e                                                                                   | An optional obfuscated identifier that uniquely associates the transaction with a user account in your app. PURPOSE: - Fraud detection: Helps platforms detect irregular activity (e.g., many devices purchasing on the same account) - User linking: Links purchases to in-game characters, avatars, or in-app profiles PLATFORM DIFFERENCES: - iOS: Must be a valid UUID format (e.g., \"550e8400-e29b-41d4-a716-446655440000\") Apple's StoreKit 2 requires UUID format for the appAccountToken parameter - Android: Can be any obfuscated string (max 64 chars), maps to Google Play's ObfuscatedAccountId Google recommends using encryption or one-way hash SECURITY REQUIREMENTS (especially for Android): - DO NOT store Personally Identifiable Information (PII) like emails in cleartext - Use encryption or a one-way hash to generate an obfuscated identifier - Maximum length: 64 characters (both platforms) - Storing PII in cleartext will result in purchases being blocked by Google Play IMPLEMENTATION EXAMPLE: ```typescript // For iOS: Generate a deterministic UUID from user ID import { v5 as uuidv5 } from 'uuid'; const NAMESPACE = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; // Your app's namespace UUID const appAccountToken = uuidv5(userId, NAMESPACE); // For Android: Can also use UUID or any hashed value // The same UUID approach works for both platforms ``` |                   |        |\n| **`productIdentifier`**    | \u003ccode\u003estring\u003c/code\u003e                                                                                           | \u003ca href=\"#product\"\u003eProduct\u003c/a\u003e identifier associated with the transaction.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |                   | 1.0.0  |\n| **`purchaseDate`**         | \u003ccode\u003estring\u003c/code\u003e                                                                                           | Purchase date of the transaction in ISO 8601 format.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |                   | 1.0.0  |\n| **`isUpgraded`**           | \u003ccode\u003eboolean\u003c/code\u003e                                                                                          | Indicates whether this transaction is the result of a subscription upgrade. Useful for understanding when StoreKit generated the transaction because the customer moved from a lower tier to a higher tier plan.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |                   | 7.13.2 |\n| **`originalPurchaseDate`** | \u003ccode\u003estring\u003c/code\u003e                                                                                           | Original purchase date of the transaction in ISO 8601 format. For subscription renewals, this shows the date of the original subscription purchase, while purchaseDate shows the date of the current renewal.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |                   | 1.0.0  |\n| **`expirationDate`**       | \u003ccode\u003estring\u003c/code\u003e                                                                                           | Expiration date of the transaction in ISO 8601 format. Check this date to determine if a subscription is still valid. Compare with current date: if expirationDate \u0026gt; now, subscription is active.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |                   | 1.0.0  |\n| **`isActive`**             | \u003ccode\u003eboolean\u003c/code\u003e                                                                                          | Whether the subscription is still active/valid. For iOS subscriptions, check if isActive === true to verify an active subscription. For expired or refunded iOS subscriptions, this will be false.                                                                                                                                                                                                                               ","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcap-go%2Fcapacitor-native-purchases","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcap-go%2Fcapacitor-native-purchases","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcap-go%2Fcapacitor-native-purchases/lists"}