@capgo/native-purchases 7.9.4 → 7.12.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,8 +2,8 @@
2
2
  <a href="https://capgo.app/"><img src='https://raw.githubusercontent.com/Cap-go/capgo/main/assets/capgo_banner.png' alt='Capgo - Instant updates for capacitor'/></a>
3
3
 
4
4
  <div align="center">
5
- <h2><a href="https://capgo.app/?ref=plugin"> ➡️ Get Instant updates for your App with Capgo</a></h2>
6
- <h2><a href="https://capgo.app/consulting/?ref=plugin"> Missing a feature? We’ll build the plugin for you 💪</a></h2>
5
+ <h2><a href="https://capgo.app/?ref=plugin_native_purchases"> ➡️ Get Instant updates for your App with Capgo</a></h2>
6
+ <h2><a href="https://capgo.app/consulting/?ref=plugin_native_purchases"> Missing a feature? We’ll build the plugin for you 💪</a></h2>
7
7
  </div>
8
8
 
9
9
  ## In-app Purchases Made Easy
@@ -95,12 +95,18 @@ There are two types of purchases with different requirements:
95
95
  | Purchase Type | productType | planIdentifier | Use Case |
96
96
  |---------------|-------------|----------------|----------|
97
97
  | **In-App Purchase** | `PURCHASE_TYPE.INAPP` | ❌ Not needed | One-time purchases (premium features, remove ads, etc.) |
98
- | **Subscription** | `PURCHASE_TYPE.SUBS` | ✅ **REQUIRED** | Recurring purchases (monthly/yearly subscriptions) |
98
+ | **Subscription** | `PURCHASE_TYPE.SUBS` | ✅ **REQUIRED (Android only)** | Recurring purchases (monthly/yearly subscriptions) |
99
99
 
100
100
  **Key Rules:**
101
- - ✅ **In-App Products**: Use `productType: PURCHASE_TYPE.INAPP`, no `planIdentifier` needed
102
- - ✅ **Subscriptions**: Must use `productType: PURCHASE_TYPE.SUBS` AND `planIdentifier: "your-plan-id"`
103
- - **Missing planIdentifier** for subscriptions will cause purchase failures
101
+ - ✅ **In-App Products**: Use `productType: PURCHASE_TYPE.INAPP`, no `planIdentifier` needed on any platform
102
+ - ✅ **Subscriptions on Android**: Must use `productType: PURCHASE_TYPE.SUBS` AND `planIdentifier: "your-plan-id"` (the Base Plan ID from Google Play Console)
103
+ - **Subscriptions on iOS**: Use `productType: PURCHASE_TYPE.SUBS`, `planIdentifier` is optional and ignored
104
+ - ❌ **Missing planIdentifier** for Android subscriptions will cause purchase failures
105
+
106
+ **About planIdentifier (Android-specific):**
107
+ The `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.
108
+
109
+ iOS does not use this parameter - subscriptions on iOS only require the product identifier.
104
110
 
105
111
  ### Complete Example: Get Product Info and Purchase
106
112
 
@@ -113,12 +119,12 @@ class PurchaseManager {
113
119
  // In-app product (one-time purchase)
114
120
  private premiumProductId = 'com.yourapp.premium_features';
115
121
 
116
- // Subscription products (require planIdentifier)
122
+ // Subscription products (require planIdentifier on Android)
117
123
  private monthlySubId = 'com.yourapp.premium.monthly';
118
- private monthlyPlanId = 'monthly-plan'; // Base plan ID from store
119
-
124
+ private monthlyPlanId = 'monthly-plan'; // Base plan ID from Google Play Console (Android only)
125
+
120
126
  private yearlySubId = 'com.yourapp.premium.yearly';
121
- private yearlyPlanId = 'yearly-plan'; // Base plan ID from store
127
+ private yearlyPlanId = 'yearly-plan'; // Base plan ID from Google Play Console (Android only)
122
128
 
123
129
  async initializeStore() {
124
130
  try {
@@ -203,42 +209,42 @@ class PurchaseManager {
203
209
  }
204
210
  }
205
211
 
206
- // Purchase subscription (planIdentifier REQUIRED)
212
+ // Purchase subscription (planIdentifier REQUIRED for Android)
207
213
  async purchaseMonthlySubscription() {
208
214
  try {
209
215
  console.log('Starting subscription purchase...');
210
-
216
+
211
217
  const result = await NativePurchases.purchaseProduct({
212
218
  productIdentifier: this.monthlySubId,
213
- planIdentifier: this.monthlyPlanId, // REQUIRED for subscriptions
219
+ planIdentifier: this.monthlyPlanId, // REQUIRED for Android subscriptions, ignored on iOS
214
220
  productType: PURCHASE_TYPE.SUBS, // REQUIRED for subscriptions
215
221
  quantity: 1
216
222
  });
217
-
223
+
218
224
  console.log('Subscription purchase successful!', result.transactionId);
219
225
  await this.handleSuccessfulPurchase(result.transactionId, 'monthly');
220
-
226
+
221
227
  } catch (error) {
222
228
  console.error('Subscription purchase failed:', error);
223
229
  this.handlePurchaseError(error);
224
230
  }
225
231
  }
226
232
 
227
- // Purchase yearly subscription (planIdentifier REQUIRED)
233
+ // Purchase yearly subscription (planIdentifier REQUIRED for Android)
228
234
  async purchaseYearlySubscription() {
229
235
  try {
230
236
  console.log('Starting yearly subscription purchase...');
231
-
237
+
232
238
  const result = await NativePurchases.purchaseProduct({
233
239
  productIdentifier: this.yearlySubId,
234
- planIdentifier: this.yearlyPlanId, // REQUIRED for subscriptions
235
- productType: PURCHASE_TYPE.SUBS, // REQUIRED for subscriptions
240
+ planIdentifier: this.yearlyPlanId, // REQUIRED for Android subscriptions, ignored on iOS
241
+ productType: PURCHASE_TYPE.SUBS, // REQUIRED for subscriptions
236
242
  quantity: 1
237
243
  });
238
-
244
+
239
245
  console.log('Yearly subscription successful!', result.transactionId);
240
246
  await this.handleSuccessfulPurchase(result.transactionId, 'yearly');
241
-
247
+
242
248
  } catch (error) {
243
249
  console.error('Yearly subscription failed:', error);
244
250
  this.handlePurchaseError(error);
@@ -289,15 +295,24 @@ class PurchaseManager {
289
295
  try {
290
296
  await NativePurchases.restorePurchases();
291
297
  console.log('Purchases restored successfully');
292
-
298
+
293
299
  // Check if user has active premium after restore
294
300
  const product = await this.getProductInfo();
295
301
  // Update UI based on restored purchases
296
-
302
+
297
303
  } catch (error) {
298
304
  console.error('Failed to restore purchases:', error);
299
305
  }
300
306
  }
307
+
308
+ async openSubscriptionManagement() {
309
+ try {
310
+ await NativePurchases.manageSubscriptions();
311
+ console.log('Opened subscription management page');
312
+ } catch (error) {
313
+ console.error('Failed to open subscription management:', error);
314
+ }
315
+ }
301
316
  }
302
317
 
303
318
  // Usage in your app
@@ -322,6 +337,10 @@ document.getElementById('buy-yearly-button')?.addEventListener('click', () => {
322
337
  document.getElementById('restore-button')?.addEventListener('click', () => {
323
338
  purchaseManager.restorePurchases();
324
339
  });
340
+
341
+ document.getElementById('manage-subscriptions-button')?.addEventListener('click', () => {
342
+ purchaseManager.openSubscriptionManagement();
343
+ });
325
344
  ```
326
345
 
327
346
  ### Quick Examples
@@ -405,7 +424,11 @@ const buyInAppProduct = async () => {
405
424
  productIdentifier: 'com.yourapp.premium_features',
406
425
  productType: PURCHASE_TYPE.INAPP,
407
426
  quantity: 1,
408
- appAccountToken: userUUID // Optional: iOS & Android - links purchases to a user (maps to Google Play ObfuscatedAccountId)
427
+ appAccountToken: uuidToken // Optional: User identifier in UUID format
428
+ // iOS: Must be valid UUID (required by StoreKit 2)
429
+ // Android: UUID works, or any obfuscated string (max 64 chars)
430
+ // RECOMMENDED: Use UUID v5 for cross-platform compatibility
431
+ // Example: uuidv5(userId, APP_NAMESPACE)
409
432
  });
410
433
 
411
434
  alert('Purchase successful! Transaction ID: ' + result.transactionId);
@@ -441,13 +464,17 @@ const buySubscription = async () => {
441
464
  const confirmed = confirm(`Subscribe to ${product.title} for ${product.priceString}?`);
442
465
  if (!confirmed) return;
443
466
 
444
- // Make subscription purchase (planIdentifier REQUIRED for Android)
467
+ // Make subscription purchase (planIdentifier REQUIRED for Android, ignored on iOS)
445
468
  const result = await NativePurchases.purchaseProduct({
446
469
  productIdentifier: 'com.yourapp.premium.monthly',
447
- planIdentifier: 'monthly-plan', // REQUIRED for Android subscriptions
470
+ planIdentifier: 'monthly-plan', // REQUIRED for Android subscriptions, ignored on iOS
448
471
  productType: PURCHASE_TYPE.SUBS, // REQUIRED for subscriptions
449
472
  quantity: 1,
450
- appAccountToken: userUUID // Optional: iOS & Android - links purchases to a user (maps to Google Play ObfuscatedAccountId)
473
+ appAccountToken: uuidToken // Optional: User identifier in UUID format
474
+ // iOS: Must be valid UUID (required by StoreKit 2)
475
+ // Android: UUID works, or any obfuscated string (max 64 chars)
476
+ // RECOMMENDED: Use UUID v5 for cross-platform compatibility
477
+ // Example: uuidv5(userId, APP_NAMESPACE)
451
478
  });
452
479
 
453
480
  alert('Subscription successful! Transaction ID: ' + result.transactionId);
@@ -484,6 +511,656 @@ const checkBillingSupport = async () => {
484
511
  };
485
512
  ```
486
513
 
514
+ ### Manage Subscriptions
515
+
516
+ Allow users to manage their subscriptions directly from your app. This opens the platform's native subscription management page:
517
+
518
+ ```typescript
519
+ import { NativePurchases } from '@capgo/native-purchases';
520
+
521
+ const openSubscriptionSettings = async () => {
522
+ try {
523
+ await NativePurchases.manageSubscriptions();
524
+ // On iOS: Opens the App Store subscription management page
525
+ // On Android: Opens the Google Play subscription management page
526
+ } catch (error) {
527
+ console.error('Error opening subscription management:', error);
528
+ }
529
+ };
530
+ ```
531
+
532
+ This is particularly useful for:
533
+ - Allowing users to cancel or modify their subscriptions
534
+ - Viewing subscription renewal dates
535
+ - Changing subscription plans
536
+ - Managing billing information
537
+
538
+ ### Using appAccountToken for Fraud Detection and User Linking
539
+
540
+ The `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.
541
+
542
+ #### What is appAccountToken?
543
+
544
+ An identifier (max 64 characters) that uniquely associates transactions with user accounts in your app. It serves two main purposes:
545
+
546
+ 1. **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
547
+ 2. **User Linking**: Links purchases to specific in-game characters, avatars, or in-app profiles that initiated the purchase
548
+
549
+ #### Platform-Specific Requirements
550
+
551
+ **IMPORTANT: iOS and Android have different format requirements:**
552
+
553
+ | Platform | Format Requirement | Maps To |
554
+ |----------|-------------------|---------|
555
+ | **iOS** | **Must be a valid UUID** (e.g., `"550e8400-e29b-41d4-a716-446655440000"`) | Apple StoreKit 2's `appAccountToken` parameter |
556
+ | **Android** | Any obfuscated string (max 64 chars) | Google Play's `ObfuscatedAccountId` |
557
+
558
+ **iOS Specific:**
559
+ - Apple's StoreKit 2 requires the `appAccountToken` to be in UUID format
560
+ - The plugin validates and converts the string to UUID before passing to StoreKit
561
+ - If the format is invalid, the token will be ignored
562
+
563
+ **Android Specific:**
564
+ - Google recommends using encryption or one-way hash
565
+ - Storing PII in cleartext will result in purchases being blocked by Google Play
566
+
567
+ #### Critical Security Requirements
568
+
569
+ **DO NOT use Personally Identifiable Information (PII) in cleartext:**
570
+ - ❌ WRONG: `appAccountToken: 'user@example.com'`
571
+ - ❌ WRONG: `appAccountToken: 'john.doe'`
572
+ - ✅ CORRECT (iOS & Android): `appAccountToken: uuidv5(userId, NAMESPACE)`
573
+ - ✅ CORRECT (Android only): `appAccountToken: hash(userId).substring(0, 64)`
574
+
575
+ **For cross-platform compatibility, using UUID format is recommended for both platforms.**
576
+
577
+ #### Implementation Example
578
+
579
+ ```typescript
580
+ // RECOMMENDED: Use UUID v5 for cross-platform compatibility (works on both iOS and Android)
581
+ import { v5 as uuidv5 } from 'uuid'; // npm install uuid
582
+
583
+ // Generate a deterministic UUID from user ID
584
+ function generateAppAccountToken(userId: string): string {
585
+ // Use a consistent namespace UUID for your app (generate once and keep constant)
586
+ const APP_NAMESPACE = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
587
+
588
+ // Generate deterministic UUID - same userId always produces same UUID
589
+ const uuid = uuidv5(userId, APP_NAMESPACE);
590
+
591
+ return uuid; // e.g., "550e8400-e29b-41d4-a716-446655440000"
592
+ }
593
+
594
+ // ALTERNATIVE: For Android-only apps (SHA-256 hash)
595
+ function generateAppAccountTokenAndroidOnly(userId: string): string {
596
+ // This works on Android but will be ignored on iOS (not UUID format)
597
+ const hash = crypto.createHash('sha256')
598
+ .update(userId)
599
+ .digest('hex')
600
+ .substring(0, 64); // Ensure max 64 chars
601
+
602
+ return hash;
603
+ }
604
+
605
+ // ALTERNATIVE: HMAC with secret key for Android-only apps
606
+ function generateSecureAppAccountTokenAndroidOnly(userId: string, secretKey: string): string {
607
+ // This works on Android but will be ignored on iOS (not UUID format)
608
+ const hmac = crypto.createHmac('sha256', secretKey)
609
+ .update(userId)
610
+ .digest('hex')
611
+ .substring(0, 64);
612
+
613
+ return hmac;
614
+ }
615
+
616
+ // Use in your purchase flow (cross-platform)
617
+ const userId = 'user-12345'; // Your internal user ID
618
+ const appAccountToken = generateAppAccountToken(userId);
619
+
620
+ await NativePurchases.purchaseProduct({
621
+ productIdentifier: 'com.yourapp.premium',
622
+ productType: PURCHASE_TYPE.INAPP,
623
+ appAccountToken: appAccountToken // UUID format works on both iOS and Android
624
+ });
625
+
626
+ // Later, retrieve purchases for this user
627
+ const { purchases } = await NativePurchases.getPurchases({
628
+ appAccountToken: appAccountToken
629
+ });
630
+ ```
631
+
632
+ **Why UUID v5 is Recommended:**
633
+ - ✅ Works on both iOS (required) and Android (accepted)
634
+ - ✅ Deterministic: Same user ID always produces the same UUID
635
+ - ✅ Secure: No PII exposure
636
+ - ✅ Standard format: Widely supported
637
+ - ✅ Reversible mapping: You can store the mapping in your backend
638
+
639
+ #### Best Practices
640
+
641
+ 1. **Use UUID v5 for cross-platform apps** - Works on both iOS (required) and Android (accepted)
642
+ 2. **Keep your namespace UUID constant** - Generate once and hardcode it in your app
643
+ 3. **Store the mapping** - Keep a record of userId → appAccountToken in your backend for reverse lookup
644
+ 4. **Use during purchase** - Include it when calling `purchaseProduct()`
645
+ 5. **Use for queries** - Use it when calling `getPurchases()` to filter by user
646
+ 6. **Deterministic generation** - Same user should always get the same token
647
+ 7. **Max 64 characters** - UUID format is 36 characters, well within the limit
648
+
649
+ #### Benefits
650
+
651
+ - **Fraud Prevention**: Platforms can detect suspicious patterns
652
+ - **Multi-device Support**: Link purchases across devices for the same user
653
+ - **User Management**: Query purchases for specific users
654
+ - **Analytics**: Better insights into user purchasing behavior
655
+
656
+ ## Understanding Transaction Properties
657
+
658
+ When 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.
659
+
660
+ ### Transaction Properties by Platform & Product Type
661
+
662
+ Here's a comprehensive breakdown of which properties you can expect and rely on:
663
+
664
+ | Property | iOS IAP | iOS Subscription | Android IAP | Android Subscription | Notes |
665
+ |----------|---------|------------------|-------------|---------------------|-------|
666
+ | `transactionId` | ✅ Always | ✅ Always | ✅ Always | ✅ Always | Primary identifier for the transaction |
667
+ | `receipt` | ✅ Always | ✅ Always | ❌ Never | ❌ Never | iOS only - base64 receipt for validation |
668
+ | `productIdentifier` | ✅ Always | ✅ Always | ✅ Always | ✅ Always | Product ID purchased |
669
+ | `purchaseDate` | ✅ Always | ✅ Always | ✅ Always | ✅ Always | ISO 8601 format |
670
+ | `productType` | ✅ Always | ✅ Always | ✅ Always | ✅ Always | "inapp" or "subs" |
671
+ | `quantity` | ✅ Always | ✅ Always | ✅ Always 1 | ✅ Always 1 | iOS supports multiple, Android always 1 |
672
+ | `appAccountToken` | ✅ If provided | ✅ If provided | ✅ If provided | ✅ If provided | Set if passed during purchase |
673
+ | `isActive` | ❌ Not set | ✅ Always | ❌ Not set | ❌ Not set | **iOS subscriptions ONLY** - calculated as expiration > now |
674
+ | `willCancel` | ❌ Not set | ✅ Always | ✅ Always null | ✅ Always null | iOS: subscription renewal status; Android: always null |
675
+ | `originalPurchaseDate` | ❌ Not set | ✅ Always | ❌ Not set | ❌ Not set | **iOS subscriptions ONLY** |
676
+ | `expirationDate` | ❌ Not set | ✅ Always | ❌ Not set | ❌ Not set | **iOS subscriptions ONLY** |
677
+ | `purchaseState` | ❌ Not set | ❌ Not set | ✅ Always | ✅ Always | **Android ONLY** - "PURCHASED", "PENDING", "0" (numeric) |
678
+ | `orderId` | ❌ Not set | ❌ Not set | ✅ Always | ✅ Always | **Android ONLY** |
679
+ | `purchaseToken` | ❌ Not set | ❌ Not set | ✅ Always | ✅ Always | **Android ONLY** - for validation |
680
+ | `isAcknowledged` | ❌ Not set | ❌ Not set | ✅ Always | ✅ Always | **Android ONLY** |
681
+
682
+ ### Validating Purchases: Platform-Specific Best Practices
683
+
684
+ #### iOS In-App Purchases (One-Time)
685
+
686
+ ```typescript
687
+ const { purchases } = await NativePurchases.getPurchases({
688
+ productType: PURCHASE_TYPE.INAPP
689
+ });
690
+
691
+ // Example response for iOS IAP:
692
+ // {
693
+ // "transactionId": "2000001043762129",
694
+ // "receipt": "base64EncodedReceiptData...",
695
+ // "productIdentifier": "com.yourapp.premium",
696
+ // "purchaseDate": "2025-10-28T06:03:19Z",
697
+ // "productType": "inapp"
698
+ // }
699
+
700
+ purchases.forEach(purchase => {
701
+ // For iOS IAP, the mere presence in the list generally indicates a valid purchase
702
+ // However, for security, you should validate the receipt on your server
703
+
704
+ if (purchase.productIdentifier === 'com.yourapp.premium') {
705
+ // Option 1: Basic client-side check (not recommended for production)
706
+ if (purchase.receipt && purchase.transactionId) {
707
+ grantPremiumAccess();
708
+ }
709
+
710
+ // Option 2: Server-side validation (RECOMMENDED)
711
+ validateReceiptOnServer(purchase.receipt).then(isValid => {
712
+ if (isValid) {
713
+ grantPremiumAccess();
714
+ }
715
+ });
716
+ }
717
+ });
718
+ ```
719
+
720
+ **Key Points for iOS IAP:**
721
+ - ✅ If a purchase appears in `getPurchases()`, it's generally valid
722
+ - ❌ `isActive` is **NOT set** for one-time IAP purchases (only for subscriptions)
723
+ - ❌ `expirationDate` and `originalPurchaseDate` are **NOT set** for IAP
724
+ - 🔒 **Always validate the receipt on your server for production apps**
725
+ - ⚠️ Refunded purchases may still appear but will fail server validation
726
+
727
+ #### Android In-App Purchases (One-Time)
728
+
729
+ ```typescript
730
+ const { purchases } = await NativePurchases.getPurchases({
731
+ productType: PURCHASE_TYPE.INAPP
732
+ });
733
+
734
+ // Example response for Android IAP:
735
+ // {
736
+ // "transactionId": "GPA.1234-5678-9012-34567",
737
+ // "productIdentifier": "com.yourapp.premium",
738
+ // "purchaseDate": "2025-10-28T06:03:19Z",
739
+ // "purchaseState": "PURCHASED",
740
+ // "orderId": "GPA.1234-5678-9012-34567",
741
+ // "purchaseToken": "long-token-string...",
742
+ // "isAcknowledged": true,
743
+ // "productType": "inapp"
744
+ // }
745
+
746
+ purchases.forEach(purchase => {
747
+ // For Android IAP, ALWAYS check purchaseState
748
+ const isValidPurchase =
749
+ purchase.purchaseState === 'PURCHASED' &&
750
+ purchase.isAcknowledged === true;
751
+
752
+ if (purchase.productIdentifier === 'com.yourapp.premium' && isValidPurchase) {
753
+ grantPremiumAccess();
754
+
755
+ // For extra security, validate on server (RECOMMENDED)
756
+ validatePurchaseTokenOnServer(purchase.purchaseToken);
757
+ }
758
+ });
759
+ ```
760
+
761
+ **Key Points for Android IAP:**
762
+ - ✅ **ALWAYS check** `purchaseState === "PURCHASED"` or `purchaseState === "1"` - this is critical
763
+ - ✅ Check `isAcknowledged === true` (this plugin auto-acknowledges)
764
+ - ❌ `isActive` is **NOT set** on Android (for either IAP or subscriptions)
765
+ - ❌ `expirationDate` and `originalPurchaseDate` are **NOT set** on Android
766
+ - 🔒 For production, validate `purchaseToken` on your server with Google Play API
767
+
768
+ #### iOS Subscriptions
769
+
770
+ ```typescript
771
+ const { purchases } = await NativePurchases.getPurchases({
772
+ productType: PURCHASE_TYPE.SUBS
773
+ });
774
+
775
+ // Example response for active iOS subscription:
776
+ // {
777
+ // "transactionId": "2000001043762130",
778
+ // "receipt": "base64EncodedReceiptData...",
779
+ // "productIdentifier": "com.yourapp.premium.monthly",
780
+ // "purchaseDate": "2025-10-28T06:03:19Z",
781
+ // "originalPurchaseDate": "2025-09-28T06:03:19Z",
782
+ // "expirationDate": "2025-11-28T06:03:19Z",
783
+ // "isActive": true,
784
+ // "willCancel": false,
785
+ // "productType": "subs",
786
+ // "isTrialPeriod": false,
787
+ // "isInIntroPricePeriod": false
788
+ // }
789
+
790
+ purchases.forEach(purchase => {
791
+ // Check if subscription is currently active
792
+ const isSubscriptionActive = purchase.isActive === true;
793
+
794
+ // Alternative: Check expiration date
795
+ const expirationDate = new Date(purchase.expirationDate);
796
+ const isActiveByDate = expirationDate > new Date();
797
+
798
+ // Check if user has cancelled (still active until expiration)
799
+ const willAutoRenew = purchase.willCancel === false;
800
+
801
+ if (isSubscriptionActive) {
802
+ grantSubscriptionAccess();
803
+
804
+ if (willAutoRenew) {
805
+ console.log('Subscription will renew on', purchase.expirationDate);
806
+ } else {
807
+ console.log('Subscription cancelled, expires on', purchase.expirationDate);
808
+ }
809
+ }
810
+ });
811
+ ```
812
+
813
+ **Key Points for iOS Subscriptions:**
814
+ - ✅ `isActive` is reliable for subscriptions
815
+ - ✅ `expirationDate` can be used to check validity
816
+ - ✅ `willCancel` tells you if subscription will auto-renew
817
+ - ⚠️ Even cancelled subscriptions show `isActive: true` until expiration
818
+ - 🔒 Validate receipt on server to detect refunds/revocations
819
+
820
+ #### Android Subscriptions
821
+
822
+ ```typescript
823
+ const { purchases } = await NativePurchases.getPurchases({
824
+ productType: PURCHASE_TYPE.SUBS
825
+ });
826
+
827
+ // Example response for active Android subscription:
828
+ // {
829
+ // "transactionId": "GPA.1234-5678-9012-34568",
830
+ // "productIdentifier": "com.yourapp.premium.monthly",
831
+ // "purchaseDate": "2025-10-28T06:03:19Z",
832
+ // "originalPurchaseDate": "2025-09-28T06:03:19Z",
833
+ // "expirationDate": "2025-11-28T06:03:19Z",
834
+ // "isActive": true,
835
+ // "purchaseState": "PURCHASED",
836
+ // "orderId": "GPA.1234-5678-9012-34568",
837
+ // "purchaseToken": "long-token-string...",
838
+ // "isAcknowledged": true,
839
+ // "productType": "subs",
840
+ // "isTrialPeriod": false
841
+ // }
842
+
843
+ purchases.forEach(purchase => {
844
+ // Check if subscription is active using multiple signals
845
+ const isActiveSubscription =
846
+ purchase.purchaseState === 'PURCHASED' &&
847
+ purchase.isActive === true &&
848
+ purchase.isAcknowledged === true;
849
+
850
+ // Alternative: Check expiration date
851
+ const expirationDate = new Date(purchase.expirationDate);
852
+ const isActiveByDate = expirationDate > new Date();
853
+
854
+ if (isActiveSubscription || isActiveByDate) {
855
+ grantSubscriptionAccess();
856
+ }
857
+ });
858
+ ```
859
+
860
+ **Key Points for Android Subscriptions:**
861
+ - ✅ Check `purchaseState === "PURCHASED"` or `purchaseState === "1"`
862
+ - ❌ `isActive` is **NOT set** on Android (even for subscriptions)
863
+ - ❌ `expirationDate` is **NOT set** on Android - must query Google Play API
864
+ - ❌ `originalPurchaseDate` is **NOT set** on Android
865
+ - ✅ `willCancel` is ALWAYS set to `null` on Android
866
+ - 🔒 For subscription status and expiration, query Google Play Developer API on your server
867
+
868
+ ### Handling Refunds and Cancellations
869
+
870
+ Understanding how refunds and cancellations affect your transaction data is critical for proper access control.
871
+
872
+ #### iOS Refund Behavior
873
+
874
+ **What happens when a user requests a refund:**
875
+
876
+ 1. **For In-App Purchases (IAP):**
877
+ - The transaction **may still appear** in `getPurchases()` and `restorePurchases()`
878
+ - `isActive` is **NOT set** for IAP purchases (only for subscriptions)
879
+ - The receipt will **NOT disappear** from the device
880
+ - ✅ **SOLUTION:** Validate the receipt with Apple's servers - refunded transactions will be marked as invalid
881
+
882
+ 2. **For Subscriptions:**
883
+ - The transaction **will still appear** in purchase history
884
+ - `isActive` **will be set to `false`** (subscriptions only set this field)
885
+ - `expirationDate` will be set to the refund date (in the past)
886
+ - ✅ **SOLUTION:** Check `isActive === false` OR `expirationDate < now` OR validate receipt on server
887
+
888
+ **Example: Detecting iOS refunds**
889
+
890
+ ```typescript
891
+ async function checkIOSPurchaseValidity(purchase: Transaction) {
892
+ // Client-side check (not foolproof)
893
+ if (purchase.isActive === false) {
894
+ console.log('Purchase appears to be refunded or expired');
895
+ return false;
896
+ }
897
+
898
+ // Server-side validation (RECOMMENDED)
899
+ const validationResult = await fetch('https://your-server.com/validate-receipt', {
900
+ method: 'POST',
901
+ body: JSON.stringify({
902
+ receipt: purchase.receipt,
903
+ productId: purchase.productIdentifier
904
+ })
905
+ }).then(r => r.json());
906
+
907
+ if (!validationResult.isValid || validationResult.isRefunded) {
908
+ console.log('Receipt validation failed - purchase refunded or invalid');
909
+ return false;
910
+ }
911
+
912
+ return true;
913
+ }
914
+ ```
915
+
916
+ **Sandbox vs Production Behavior:**
917
+ - ✅ Refund behavior is **consistent** between sandbox and production
918
+ - ⚠️ Sandbox refunds are processed instantly, production may take hours/days
919
+ - ✅ Receipt validation works the same in both environments
920
+
921
+ #### Android Refund Behavior
922
+
923
+ **What happens when a user requests a refund:**
924
+
925
+ 1. **For In-App Purchases (IAP):**
926
+ - The transaction **typically disappears entirely** from `getPurchases()`
927
+ - Google Play removes refunded purchases from the purchase history
928
+ - No receipt or transaction will be returned
929
+ - ✅ **SOLUTION:** If a previously-seen purchase is no longer in the list, it was likely refunded
930
+
931
+ 2. **For Subscriptions:**
932
+ - The transaction **may disappear** OR
933
+ - `isActive` will be set to `false`
934
+ - `purchaseState` may be undefined or the transaction won't be returned at all
935
+ - ✅ **SOLUTION:** Track purchases on your server and listen for Google Play real-time developer notifications
936
+
937
+ **Example: Detecting Android refunds**
938
+
939
+ ```typescript
940
+ // Store previously seen purchases in local storage or your database
941
+ const previousPurchases = getPreviouslyStoredPurchases();
942
+
943
+ const { purchases } = await NativePurchases.getPurchases({
944
+ productType: PURCHASE_TYPE.INAPP
945
+ });
946
+
947
+ // Check for missing purchases (likely refunded)
948
+ previousPurchases.forEach(oldPurchase => {
949
+ const stillExists = purchases.find(
950
+ p => p.transactionId === oldPurchase.transactionId
951
+ );
952
+
953
+ if (!stillExists) {
954
+ console.log(`Purchase ${oldPurchase.productIdentifier} no longer exists - likely refunded`);
955
+ revokePremiumAccess(oldPurchase.productIdentifier);
956
+ }
957
+ });
958
+
959
+ // Check current purchases for validity
960
+ purchases.forEach(purchase => {
961
+ const isValid =
962
+ purchase.purchaseState === 'PURCHASED' &&
963
+ purchase.isAcknowledged === true;
964
+
965
+ if (!isValid) {
966
+ console.log('Invalid purchase state detected');
967
+ // Don't grant access
968
+ }
969
+ });
970
+
971
+ // Store current purchases for next comparison
972
+ storePurchases(purchases);
973
+ ```
974
+
975
+ **Sandbox vs Production Behavior:**
976
+ - ⚠️ Sandbox test accounts can make unlimited "purchases" without payment
977
+ - ⚠️ Sandbox refunds are **instant** - purchase disappears immediately
978
+ - ⚠️ Production refunds may take **several hours** before purchase disappears
979
+ - ✅ Testing refunds in production requires real money and real refund requests
980
+
981
+ **IMPORTANT: Without Server-Side Validation:**
982
+
983
+ If you're **not using a backend validator** (not recommended for production), here's what to expect:
984
+
985
+ | Scenario | iOS Behavior | Android Behavior |
986
+ |----------|-------------|------------------|
987
+ | User requests IAP refund | Transaction may still appear in `restorePurchases()`, check `isActive` | Transaction disappears from `getPurchases()` |
988
+ | User cancels subscription | `willCancel: true`, still active until expiration | Transaction remains, check `isActive` and `expirationDate` |
989
+ | Subscription expires naturally | `isActive: false`, `expirationDate` in past | Transaction disappears OR `isActive: false` |
990
+ | User refunds subscription | Transaction remains with `isActive: false` | Transaction may disappear |
991
+
992
+ **RECOMMENDATION: Always implement server-side validation**
993
+ - Listen to Apple's App Store Server Notifications (iOS)
994
+ - Listen to Google Play Real-Time Developer Notifications (Android)
995
+ - Validate receipts/tokens on your server before granting access
996
+ - See the [Backend Validation](#backend-validation) section for implementation
997
+
998
+ ### Sandbox vs Production Differences
999
+
1000
+ #### iOS: Sandbox vs Production
1001
+
1002
+ **Similarities:**
1003
+ - ✅ Transaction structure is identical
1004
+ - ✅ All properties return the same data format
1005
+ - ✅ `receipt` validation works (use sandbox Apple endpoint)
1006
+ - ✅ Refund behavior is consistent
1007
+
1008
+ **Differences:**
1009
+
1010
+ | Aspect | Sandbox | Production |
1011
+ |--------|---------|-----------|
1012
+ | Payment processing | Instant, no real money | Real payment, takes seconds |
1013
+ | Receipt validation endpoint | `https://sandbox.itunes.apple.com/verifyReceipt` | `https://buy.itunes.apple.com/verifyReceipt` |
1014
+ | Subscription duration | Compressed (1 week = 5 minutes) | Real duration (1 week = 7 days) |
1015
+ | Refund processing | Instant (via StoreKit Testing) | Takes hours/days, must contact Apple |
1016
+ | Test user requirements | Sandbox Apple ID required | Real Apple ID |
1017
+ | Transaction IDs | Real format, unique per test | Real format, unique per purchase |
1018
+ | `receipt` data | Valid test receipt | Valid production receipt |
1019
+
1020
+ **Testing Refunds in Sandbox:**
1021
+ 1. Use StoreKit Configuration file (local testing) for instant refunds
1022
+ 2. Or sandbox testing with sandbox Apple ID
1023
+ 3. Refunds are instant and can be tested repeatedly
1024
+ 4. Receipt validation will show refunded status immediately
1025
+
1026
+ #### Android: Sandbox vs Production
1027
+
1028
+ **Similarities:**
1029
+ - ✅ Transaction structure is identical
1030
+ - ✅ All properties return the same data format
1031
+ - ✅ Purchase state values are the same
1032
+
1033
+ **Differences:**
1034
+
1035
+ | Aspect | License Testing (Sandbox) | Production |
1036
+ |--------|--------------------------|-----------|
1037
+ | Payment processing | No payment required | Real payment required |
1038
+ | Purchase token validation | Works with Google Play API | Works with Google Play API |
1039
+ | Transaction IDs | Test format: `GPA.1234-...` | Real format: `GPA.1234-...` |
1040
+ | Refund processing | Instant (test account only) | Takes hours, appears as purchase disappearing |
1041
+ | Test user requirements | Gmail added to license testers | Real Google account |
1042
+ | `purchaseState` values | Same as production | Same as sandbox |
1043
+ | Refund detection | Purchase disappears immediately | Purchase disappears after hours/days |
1044
+
1045
+ **Testing Refunds in Android Sandbox:**
1046
+ 1. **License testers** can make unlimited purchases without payment
1047
+ 2. Refunds are **instant** - purchase disappears from `getPurchases()` immediately
1048
+ 3. Use **Internal Testing** track for most realistic testing
1049
+ 4. Real refunds in production require real purchases and real refund requests via Google Play
1050
+
1051
+ **Key Difference:**
1052
+ - In **sandbox/test**, refunded purchases disappear instantly
1053
+ - In **production**, refunded purchases may remain visible for hours before disappearing
1054
+ - Always implement server-side validation with Google Play Developer API to catch refunds reliably
1055
+
1056
+ ### Recommended Access Control Logic
1057
+
1058
+ Based on the above, here's the recommended approach for each platform and product type:
1059
+
1060
+ ```typescript
1061
+ import { NativePurchases, PURCHASE_TYPE, Transaction } from '@capgo/native-purchases';
1062
+ import { Capacitor } from '@capacitor/core';
1063
+
1064
+ async function checkUserAccess(productId: string, productType: PURCHASE_TYPE): Promise<boolean> {
1065
+ try {
1066
+ const { purchases } = await NativePurchases.getPurchases({ productType });
1067
+ const purchase = purchases.find(p => p.productIdentifier === productId);
1068
+
1069
+ if (!purchase) {
1070
+ return false; // No purchase found
1071
+ }
1072
+
1073
+ const platform = Capacitor.getPlatform();
1074
+
1075
+ if (platform === 'ios') {
1076
+ // iOS Logic
1077
+ if (productType === PURCHASE_TYPE.INAPP) {
1078
+ // For IAP: presence in list + receipt validation
1079
+ // Note: isActive is NOT set for iOS IAP
1080
+ if (!purchase.receipt) return false;
1081
+
1082
+ // IMPORTANT: Validate receipt on server for production
1083
+ const isValid = await validateReceiptOnServer(purchase.receipt);
1084
+ return isValid;
1085
+
1086
+ } else {
1087
+ // For subscriptions: check isActive and expiration
1088
+ // iOS subscriptions ALWAYS have isActive and expirationDate
1089
+ if (purchase.isActive === false) return false;
1090
+ if (purchase.expirationDate) {
1091
+ const expiration = new Date(purchase.expirationDate);
1092
+ if (expiration < new Date()) return false;
1093
+ }
1094
+ return true;
1095
+ }
1096
+
1097
+ } else if (platform === 'android') {
1098
+ // Android Logic
1099
+ if (productType === PURCHASE_TYPE.INAPP) {
1100
+ // For IAP: check purchase state and acknowledgment
1101
+ // Note: Android does NOT set isActive, expirationDate, or originalPurchaseDate
1102
+ const isValid =
1103
+ (purchase.purchaseState === 'PURCHASED' || purchase.purchaseState === '1') &&
1104
+ purchase.isAcknowledged === true;
1105
+
1106
+ if (!isValid) return false;
1107
+
1108
+ // IMPORTANT: Validate purchaseToken on server for production
1109
+ await validatePurchaseTokenOnServer(purchase.purchaseToken);
1110
+ return true;
1111
+
1112
+ } else {
1113
+ // For subscriptions: check purchase state only
1114
+ // Android does NOT set isActive, expirationDate, or originalPurchaseDate
1115
+ // You MUST use Google Play Developer API on your server to get subscription details
1116
+ const isValidState =
1117
+ (purchase.purchaseState === 'PURCHASED' || purchase.purchaseState === '1') &&
1118
+ purchase.isAcknowledged === true;
1119
+
1120
+ if (!isValidState) return false;
1121
+
1122
+ // CRITICAL: Validate subscription status on server with Google Play API
1123
+ // The Purchase object doesn't include expiration dates
1124
+ const serverStatus = await validateAndGetSubscriptionStatus(purchase.purchaseToken);
1125
+ return serverStatus.isActive && serverStatus.expirationDate > new Date();
1126
+ }
1127
+ }
1128
+
1129
+ return false;
1130
+ } catch (error) {
1131
+ console.error('Error checking user access:', error);
1132
+ return false;
1133
+ }
1134
+ }
1135
+
1136
+ // Example usage
1137
+ async function grantAccessBasedOnPurchase() {
1138
+ // Check for premium IAP
1139
+ const hasPremium = await checkUserAccess(
1140
+ 'com.yourapp.premium',
1141
+ PURCHASE_TYPE.INAPP
1142
+ );
1143
+
1144
+ // Check for active subscription
1145
+ const hasSubscription = await checkUserAccess(
1146
+ 'com.yourapp.premium.monthly',
1147
+ PURCHASE_TYPE.SUBS
1148
+ );
1149
+
1150
+ if (hasPremium || hasSubscription) {
1151
+ unlockPremiumFeatures();
1152
+ }
1153
+ }
1154
+ ```
1155
+
1156
+ **Critical Takeaways:**
1157
+ 1. ✅ For **iOS IAP**: `isActive` is NOT set - validate receipt on server
1158
+ 2. ✅ For **iOS Subscriptions**: `isActive` and `expirationDate` ARE set - use them!
1159
+ 3. ✅ For **Android IAP**: Check `purchaseState === "PURCHASED"` (or "1")
1160
+ 4. ✅ For **Android Subscriptions**: `isActive` and `expirationDate` are NOT set - must use Google Play API on server
1161
+ 5. ✅ For **Refunds**: iOS purchases may linger (validate server-side), Android purchases disappear
1162
+ 6. 🔒 **Always implement server-side validation for production apps**
1163
+
487
1164
  ### API Reference
488
1165
 
489
1166
  #### Core Methods
@@ -499,14 +1176,17 @@ await NativePurchases.getProduct({ productIdentifier: 'product_id' });
499
1176
  await NativePurchases.getProducts({ productIdentifiers: ['id1', 'id2'] });
500
1177
 
501
1178
  // Purchase a product
502
- await NativePurchases.purchaseProduct({
503
- productIdentifier: 'product_id',
504
- quantity: 1
1179
+ await NativePurchases.purchaseProduct({
1180
+ productIdentifier: 'product_id',
1181
+ quantity: 1
505
1182
  });
506
1183
 
507
1184
  // Restore previous purchases
508
1185
  await NativePurchases.restorePurchases();
509
1186
 
1187
+ // Open subscription management page
1188
+ await NativePurchases.manageSubscriptions();
1189
+
510
1190
  // Get plugin version
511
1191
  await NativePurchases.getPluginVersion();
512
1192
  ```
@@ -731,6 +1411,7 @@ This approach balances immediate user gratification with proper server-side vali
731
1411
  * [`isBillingSupported()`](#isbillingsupported)
732
1412
  * [`getPluginVersion()`](#getpluginversion)
733
1413
  * [`getPurchases(...)`](#getpurchases)
1414
+ * [`manageSubscriptions()`](#managesubscriptions)
734
1415
  * [`addListener('transactionUpdated', ...)`](#addlistenertransactionupdated-)
735
1416
  * [`removeAllListeners()`](#removealllisteners)
736
1417
  * [Interfaces](#interfaces)
@@ -849,6 +1530,23 @@ This method queries the platform's purchase history for the current user.
849
1530
  --------------------
850
1531
 
851
1532
 
1533
+ ### manageSubscriptions()
1534
+
1535
+ ```typescript
1536
+ manageSubscriptions() => Promise<void>
1537
+ ```
1538
+
1539
+ Opens the platform's native subscription management page.
1540
+ This allows users to view, modify, or cancel their subscriptions.
1541
+
1542
+ - iOS: Opens the App Store subscription management page for the current app
1543
+ - Android: Opens the Google Play subscription management page
1544
+
1545
+ **Since:** 7.10.0
1546
+
1547
+ --------------------
1548
+
1549
+
852
1550
  ### addListener('transactionUpdated', ...)
853
1551
 
854
1552
  ```typescript
@@ -885,26 +1583,26 @@ Remove all registered listeners
885
1583
 
886
1584
  #### Transaction
887
1585
 
888
- | Prop | Type | Description |
889
- | -------------------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
890
- | **`transactionId`** | <code>string</code> | Id associated to the transaction. |
891
- | **`receipt`** | <code>string</code> | Receipt data for validation (iOS only - base64 encoded receipt) |
892
- | **`appAccountToken`** | <code>string \| null</code> | Account token provided during purchase. Works on both platforms and maps to Google Play's ObfuscatedAccountId on Android. |
893
- | **`productIdentifier`** | <code>string</code> | <a href="#product">Product</a> Id associated with the transaction. |
894
- | **`purchaseDate`** | <code>string</code> | Purchase date of the transaction in ISO 8601 format. |
895
- | **`originalPurchaseDate`** | <code>string</code> | Original purchase date of the transaction in ISO 8601 format (for subscriptions). |
896
- | **`expirationDate`** | <code>string</code> | Expiration date of the transaction in ISO 8601 format (for subscriptions). |
897
- | **`isActive`** | <code>boolean</code> | Whether the transaction is still active/valid. |
898
- | **`willCancel`** | <code>boolean \| null</code> | Whether the subscription will be cancelled at the end of the billing cycle, or null if not cancelled. Only available on iOS. |
899
- | **`purchaseState`** | <code>string</code> | Purchase state of the transaction. |
900
- | **`orderId`** | <code>string</code> | Order ID associated with the transaction (Android). |
901
- | **`purchaseToken`** | <code>string</code> | Purchase token associated with the transaction (Android). |
902
- | **`isAcknowledged`** | <code>boolean</code> | Whether the purchase has been acknowledged (Android). |
903
- | **`quantity`** | <code>number</code> | Quantity purchased. |
904
- | **`productType`** | <code>string</code> | <a href="#product">Product</a> type (inapp or subs). |
905
- | **`isTrialPeriod`** | <code>boolean</code> | Whether the transaction is a trial period. |
906
- | **`isInIntroPricePeriod`** | <code>boolean</code> | Whether the transaction is in intro price period. |
907
- | **`isInGracePeriod`** | <code>boolean</code> | Whether the transaction is in grace period. |
1586
+ | Prop | Type | Description | Default | Since |
1587
+ | -------------------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | ----- |
1588
+ | **`transactionId`** | <code>string</code> | Unique identifier for the transaction. | | 1.0.0 |
1589
+ | **`receipt`** | <code>string</code> | Receipt data for validation (base64 encoded StoreKit receipt). 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. | | 1.0.0 |
1590
+ | **`appAccountToken`** | <code>string \| null</code> | 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 ``` | | |
1591
+ | **`productIdentifier`** | <code>string</code> | <a href="#product">Product</a> identifier associated with the transaction. | | 1.0.0 |
1592
+ | **`purchaseDate`** | <code>string</code> | Purchase date of the transaction in ISO 8601 format. | | 1.0.0 |
1593
+ | **`originalPurchaseDate`** | <code>string</code> | 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 |
1594
+ | **`expirationDate`** | <code>string</code> | 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 &gt; now, subscription is active. | | 1.0.0 |
1595
+ | **`isActive`** | <code>boolean</code> | 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. | | 1.0.0 |
1596
+ | **`willCancel`** | <code>boolean \| null</code> | Whether the subscription will be cancelled at the end of the billing cycle. - `true`: User has cancelled but subscription remains active until expiration - `false`: Subscription will auto-renew - `null`: Status unknown or not available | <code>null</code> | 1.0.0 |
1597
+ | **`purchaseState`** | <code>string</code> | Purchase state of the transaction (numeric string value). **Android Values:** - `"1"`: Purchase completed and valid (PURCHASED state) - `"0"`: Payment pending (PENDING state, e.g., cash payment processing) - Other numeric values: Various other states Always check `purchaseState === "1"` on Android to verify a valid purchase. Refunded purchases typically disappear from getPurchases() rather than showing a different state. | | 1.0.0 |
1598
+ | **`orderId`** | <code>string</code> | Order ID associated with the transaction. Use this for server-side verification on Android. This is the Google Play order ID. | | 1.0.0 |
1599
+ | **`purchaseToken`** | <code>string</code> | Purchase token associated with the transaction. Send this to your backend for server-side validation with Google Play Developer API. This is the Android equivalent of iOS's receipt field. | | 1.0.0 |
1600
+ | **`isAcknowledged`** | <code>boolean</code> | Whether the purchase has been acknowledged. Purchases must be acknowledged within 3 days or they will be refunded. This plugin automatically acknowledges purchases. | | 1.0.0 |
1601
+ | **`quantity`** | <code>number</code> | Quantity purchased. | <code>1</code> | 1.0.0 |
1602
+ | **`productType`** | <code>string</code> | <a href="#product">Product</a> type. - `"inapp"`: One-time in-app purchase - `"subs"`: Subscription | | 1.0.0 |
1603
+ | **`isTrialPeriod`** | <code>boolean</code> | Whether the transaction is in a trial period. - `true`: Currently in free trial period - `false`: Not in trial period | | 1.0.0 |
1604
+ | **`isInIntroPricePeriod`** | <code>boolean</code> | Whether the transaction is in an introductory price period. Introductory pricing is a discounted rate, different from a free trial. - `true`: Currently using introductory pricing - `false`: Not in intro period | | 1.0.0 |
1605
+ | **`isInGracePeriod`** | <code>boolean</code> | Whether the transaction is in a grace period. Grace period allows users to fix payment issues while maintaining access. You typically want to continue providing access during this time. - `true`: Subscription payment failed but user still has access - `false`: Not in grace period | | 1.0.0 |
908
1606
 
909
1607
 
910
1608
  #### Product