@capgo/native-purchases 7.13.5 → 7.15.0

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
@@ -1428,7 +1428,9 @@ This approach balances immediate user gratification with proper server-side vali
1428
1428
  * [`getPluginVersion()`](#getpluginversion)
1429
1429
  * [`getPurchases(...)`](#getpurchases)
1430
1430
  * [`manageSubscriptions()`](#managesubscriptions)
1431
+ * [`acknowledgePurchase(...)`](#acknowledgepurchase)
1431
1432
  * [`addListener('transactionUpdated', ...)`](#addlistenertransactionupdated-)
1433
+ * [`addListener('transactionVerificationFailed', ...)`](#addlistenertransactionverificationfailed-)
1432
1434
  * [`removeAllListeners()`](#removealllisteners)
1433
1435
  * [Interfaces](#interfaces)
1434
1436
  * [Enums](#enums)
@@ -1452,14 +1454,14 @@ Restores a user's previous and links their appUserIDs to any user's also using
1452
1454
  ### purchaseProduct(...)
1453
1455
 
1454
1456
  ```typescript
1455
- purchaseProduct(options: { productIdentifier: string; planIdentifier?: string; productType?: PURCHASE_TYPE; quantity?: number; appAccountToken?: string; isConsumable?: boolean; }) => Promise<Transaction>
1457
+ purchaseProduct(options: { productIdentifier: string; planIdentifier?: string; productType?: PURCHASE_TYPE; quantity?: number; appAccountToken?: string; isConsumable?: boolean; autoAcknowledgePurchases?: boolean; }) => Promise<Transaction>
1456
1458
  ```
1457
1459
 
1458
1460
  Started purchase process for the given product.
1459
1461
 
1460
- | Param | Type | Description |
1461
- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- |
1462
- | **`options`** | <code>{ productIdentifier: string; planIdentifier?: string; productType?: <a href="#purchase_type">PURCHASE_TYPE</a>; quantity?: number; appAccountToken?: string; isConsumable?: boolean; }</code> | - The product to purchase |
1462
+ | Param | Type | Description |
1463
+ | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- |
1464
+ | **`options`** | <code>{ productIdentifier: string; planIdentifier?: string; productType?: <a href="#purchase_type">PURCHASE_TYPE</a>; quantity?: number; appAccountToken?: string; isConsumable?: boolean; autoAcknowledgePurchases?: boolean; }</code> | - The product to purchase |
1463
1465
 
1464
1466
  **Returns:** <code>Promise&lt;<a href="#transaction">Transaction</a>&gt;</code>
1465
1467
 
@@ -1563,6 +1565,51 @@ This allows users to view, modify, or cancel their subscriptions.
1563
1565
  --------------------
1564
1566
 
1565
1567
 
1568
+ ### acknowledgePurchase(...)
1569
+
1570
+ ```typescript
1571
+ acknowledgePurchase(options: { purchaseToken: string; }) => Promise<void>
1572
+ ```
1573
+
1574
+ Manually acknowledge/finish a purchase transaction.
1575
+
1576
+ This method is only needed when you set `autoAcknowledgePurchases: false` in purchaseProduct().
1577
+
1578
+ **Platform Behavior:**
1579
+ - **Android**: Acknowledges the purchase with Google Play. Must be called within 3 days or the purchase will be refunded.
1580
+ - **iOS**: Finishes the transaction with StoreKit 2. Unfinished transactions remain in the queue and may block future purchases.
1581
+
1582
+ **Acknowledgment Options:**
1583
+
1584
+ **1. Client-side (this method)**: Call from your app after validation
1585
+ ```typescript
1586
+ await NativePurchases.acknowledgePurchase({
1587
+ purchaseToken: transaction.purchaseToken // Android: purchaseToken, iOS: transactionId
1588
+ });
1589
+ ```
1590
+
1591
+ **2. Server-side (Android only, recommended for security)**: Use Google Play Developer API v3
1592
+ - Endpoint: `POST https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/products/{productId}/tokens/{token}:acknowledge`
1593
+ - Requires OAuth 2.0 authentication with appropriate scopes
1594
+ - See: https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.products/acknowledge
1595
+ - For subscriptions: Use `/purchases/subscriptions/{subscriptionId}/tokens/{token}:acknowledge` instead
1596
+ - Note: iOS has no server-side finish API
1597
+
1598
+ **When to use manual acknowledgment:**
1599
+ - Server-side validation: Verify the purchase with your backend before acknowledging
1600
+ - Entitlement delivery: Ensure user receives content/features before acknowledging
1601
+ - Multi-step workflows: Complete all steps before final acknowledgment
1602
+ - Security: Prevent client-side manipulation by handling acknowledgment server-side (Android only)
1603
+
1604
+ | Param | Type | Description |
1605
+ | ------------- | --------------------------------------- | ----------------------------- |
1606
+ | **`options`** | <code>{ purchaseToken: string; }</code> | - The purchase to acknowledge |
1607
+
1608
+ **Since:** 7.14.0
1609
+
1610
+ --------------------
1611
+
1612
+
1566
1613
  ### addListener('transactionUpdated', ...)
1567
1614
 
1568
1615
  ```typescript
@@ -1583,6 +1630,26 @@ iOS only.
1583
1630
  --------------------
1584
1631
 
1585
1632
 
1633
+ ### addListener('transactionVerificationFailed', ...)
1634
+
1635
+ ```typescript
1636
+ addListener(eventName: 'transactionVerificationFailed', listenerFunc: (payload: TransactionVerificationFailedEvent) => void) => Promise<PluginListenerHandle>
1637
+ ```
1638
+
1639
+ Listen for StoreKit transaction verification failures delivered by Apple's <a href="#transaction">Transaction</a>.updates.
1640
+ Fires when the verification result is unverified.
1641
+ iOS only.
1642
+
1643
+ | Param | Type |
1644
+ | ------------------ | ----------------------------------------------------------------------------------------------------------------------- |
1645
+ | **`eventName`** | <code>'transactionVerificationFailed'</code> |
1646
+ | **`listenerFunc`** | <code>(payload: <a href="#transactionverificationfailedevent">TransactionVerificationFailedEvent</a>) =&gt; void</code> |
1647
+
1648
+ **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</code>
1649
+
1650
+ --------------------
1651
+
1652
+
1586
1653
  ### removeAllListeners()
1587
1654
 
1588
1655
  ```typescript
@@ -1599,28 +1666,34 @@ Remove all registered listeners
1599
1666
 
1600
1667
  #### Transaction
1601
1668
 
1602
- | Prop | Type | Description | Default | Since |
1603
- | -------------------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | ------ |
1604
- | **`transactionId`** | <code>string</code> | Unique identifier for the transaction. | | 1.0.0 |
1605
- | **`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 |
1606
- | **`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 ``` | | |
1607
- | **`productIdentifier`** | <code>string</code> | <a href="#product">Product</a> identifier associated with the transaction. | | 1.0.0 |
1608
- | **`purchaseDate`** | <code>string</code> | Purchase date of the transaction in ISO 8601 format. | | 1.0.0 |
1609
- | **`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 |
1610
- | **`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 |
1611
- | **`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 |
1612
- | **`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 |
1613
- | **`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 |
1614
- | **`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 |
1615
- | **`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 |
1616
- | **`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 |
1617
- | **`quantity`** | <code>number</code> | Quantity purchased. | <code>1</code> | 1.0.0 |
1618
- | **`productType`** | <code>string</code> | <a href="#product">Product</a> type. - `"inapp"`: One-time in-app purchase - `"subs"`: Subscription | | 1.0.0 |
1619
- | **`ownershipType`** | <code>'purchased' \| 'familyShared'</code> | Indicates how the user obtained access to the product. - `"purchased"`: The user purchased the product directly - `"familyShared"`: The user has access through Family Sharing (another family member purchased it) This property is useful for: - Detecting family sharing usage for analytics - Implementing different features/limits for family-shared vs. directly purchased products - Understanding your user acquisition channels | | 7.12.8 |
1620
- | **`environment`** | <code>'Sandbox' \| 'Production' \| 'Xcode'</code> | Indicates the server environment where the transaction was processed. - `"Sandbox"`: <a href="#transaction">Transaction</a> belongs to testing in the sandbox environment - `"Production"`: <a href="#transaction">Transaction</a> belongs to a customer in the production environment - `"Xcode"`: <a href="#transaction">Transaction</a> from StoreKit Testing in Xcode This property is useful for: - Debugging and identifying test vs. production purchases - Analytics and reporting (filtering out sandbox transactions) - Server-side validation (knowing which Apple endpoint to use) - Preventing test purchases from affecting production metrics | | 7.12.8 |
1621
- | **`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 |
1622
- | **`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 |
1623
- | **`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 |
1669
+ | Prop | Type | Description | Default | Since |
1670
+ | -------------------------- | ------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | ------ |
1671
+ | **`transactionId`** | <code>string</code> | Unique identifier for the transaction. | | 1.0.0 |
1672
+ | **`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 |
1673
+ | **`jwsRepresentation`** | <code>string</code> | StoreKit 2 JSON Web Signature (JWS) payload describing the verified transaction. 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. <a href="#transaction">Transaction</a>.updates). | | 7.13.2 |
1674
+ | **`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 ``` | | |
1675
+ | **`productIdentifier`** | <code>string</code> | <a href="#product">Product</a> identifier associated with the transaction. | | 1.0.0 |
1676
+ | **`purchaseDate`** | <code>string</code> | Purchase date of the transaction in ISO 8601 format. | | 1.0.0 |
1677
+ | **`isUpgraded`** | <code>boolean</code> | 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 |
1678
+ | **`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 |
1679
+ | **`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 |
1680
+ | **`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 |
1681
+ | **`revocationDate`** | <code>string</code> | Date the transaction was revoked/refunded, in ISO 8601 format. Present when Apple revokes access due to an issue (e.g., refund or developer issue). | | 7.13.2 |
1682
+ | **`revocationReason`** | <code>'developerIssue' \| 'other' \| 'unknown'</code> | Reason why Apple revoked the transaction. Possible values: - `"developerIssue"`: Developer-initiated refund or issue - `"other"`: Apple-initiated (customer refund, billing problem, etc.) - `"unknown"`: StoreKit didn't report a specific reason | | 7.13.2 |
1683
+ | **`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 |
1684
+ | **`subscriptionState`** | <code>'unknown' \| 'subscribed' \| 'expired' \| 'revoked' \| 'inGracePeriod' \| 'inBillingRetryPeriod'</code> | Current subscription state reported by StoreKit. Possible values: - `"subscribed"`: Auto-renewing and in good standing - `"expired"`: Lapsed with no access - `"revoked"`: Access removed due to refund or issue - `"inGracePeriod"`: Payment issue but still in grace access window - `"inBillingRetryPeriod"`: StoreKit retrying failed billing - `"unknown"`: StoreKit did not report a state | | 7.13.2 |
1685
+ | **`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 |
1686
+ | **`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 |
1687
+ | **`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 |
1688
+ | **`isAcknowledged`** | <code>boolean</code> | Whether the purchase has been acknowledged. Purchases must be acknowledged within 3 days or they will be refunded. By default, this plugin automatically acknowledges purchases unless you set `autoAcknowledgePurchases: false` in purchaseProduct(). | | 1.0.0 |
1689
+ | **`quantity`** | <code>number</code> | Quantity purchased. | <code>1</code> | 1.0.0 |
1690
+ | **`productType`** | <code>string</code> | <a href="#product">Product</a> type. - `"inapp"`: One-time in-app purchase - `"subs"`: Subscription | | 1.0.0 |
1691
+ | **`ownershipType`** | <code>'purchased' \| 'familyShared'</code> | Indicates how the user obtained access to the product. - `"purchased"`: The user purchased the product directly - `"familyShared"`: The user has access through Family Sharing (another family member purchased it) This property is useful for: - Detecting family sharing usage for analytics - Implementing different features/limits for family-shared vs. directly purchased products - Understanding your user acquisition channels | | 7.12.8 |
1692
+ | **`environment`** | <code>'Sandbox' \| 'Production' \| 'Xcode'</code> | Indicates the server environment where the transaction was processed. - `"Sandbox"`: <a href="#transaction">Transaction</a> belongs to testing in the sandbox environment - `"Production"`: <a href="#transaction">Transaction</a> belongs to a customer in the production environment - `"Xcode"`: <a href="#transaction">Transaction</a> from StoreKit Testing in Xcode This property is useful for: - Debugging and identifying test vs. production purchases - Analytics and reporting (filtering out sandbox transactions) - Server-side validation (knowing which Apple endpoint to use) - Preventing test purchases from affecting production metrics | | 7.12.8 |
1693
+ | **`transactionReason`** | <code>'unknown' \| 'purchase' \| 'renewal'</code> | Reason StoreKit generated the transaction. - `"purchase"`: Initial purchase that user made manually - `"renewal"`: Automatically generated renewal for an auto-renewable subscription - `"unknown"`: StoreKit did not return a reason | | 7.13.2 |
1694
+ | **`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 |
1695
+ | **`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 |
1696
+ | **`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 |
1624
1697
 
1625
1698
 
1626
1699
  #### Product
@@ -1671,6 +1744,14 @@ Remove all registered listeners
1671
1744
  | **`remove`** | <code>() =&gt; Promise&lt;void&gt;</code> |
1672
1745
 
1673
1746
 
1747
+ #### TransactionVerificationFailedEvent
1748
+
1749
+ | Prop | Type | Description | Since |
1750
+ | ------------------- | ------------------- | ----------------------------------------------------------- | ------ |
1751
+ | **`transactionId`** | <code>string</code> | Identifier of the transaction that failed verification. | 7.13.2 |
1752
+ | **`error`** | <code>string</code> | Localized error message describing why verification failed. | 7.13.2 |
1753
+
1754
+
1674
1755
  ### Enums
1675
1756
 
1676
1757
 
@@ -39,7 +39,7 @@ import org.json.JSONArray;
39
39
  @CapacitorPlugin(name = "NativePurchases")
40
40
  public class NativePurchasesPlugin extends Plugin {
41
41
 
42
- private final String pluginVersion = "7.13.5";
42
+ private final String pluginVersion = "7.15.0";
43
43
  public static final String TAG = "NativePurchases";
44
44
  private static final Phaser semaphoreReady = new Phaser(1);
45
45
  private BillingClient billingClient;
@@ -121,6 +121,9 @@ public class NativePurchasesPlugin extends Plugin {
121
121
  if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
122
122
  Log.d(TAG, "Purchase state is PURCHASED");
123
123
  boolean isConsumable = purchaseCall != null && purchaseCall.getBoolean("isConsumable", false);
124
+ boolean autoAcknowledge = purchaseCall != null ? purchaseCall.getBoolean("autoAcknowledgePurchases", true) : true;
125
+ Log.d(TAG, "Auto-acknowledge enabled: " + autoAcknowledge);
126
+
124
127
  PurchaseAction action = PurchaseActionDecider.decide(isConsumable, purchase);
125
128
 
126
129
  AccountIdentifiers accountIdentifiers = purchase.getAccountIdentifiers();
@@ -134,8 +137,12 @@ public class NativePurchasesPlugin extends Plugin {
134
137
  billingClient.consumeAsync(consumeParams, this::onConsumeResponse);
135
138
  break;
136
139
  case ACKNOWLEDGE:
137
- Log.d(TAG, "Purchase not acknowledged, acknowledging...");
138
- acknowledgePurchase(purchase.getPurchaseToken());
140
+ if (autoAcknowledge) {
141
+ Log.d(TAG, "Purchase not acknowledged, auto-acknowledging...");
142
+ acknowledgePurchase(purchase.getPurchaseToken());
143
+ } else {
144
+ Log.d(TAG, "Purchase not acknowledged, but auto-acknowledge is disabled. Developer must manually acknowledge.");
145
+ }
139
146
  break;
140
147
  case NONE:
141
148
  default:
@@ -316,6 +323,7 @@ public class NativePurchasesPlugin extends Plugin {
316
323
  String appAccountToken = call.getString("appAccountToken");
317
324
  final String accountIdentifier = appAccountToken != null && !appAccountToken.isEmpty() ? appAccountToken : null;
318
325
  boolean isConsumable = call.getBoolean("isConsumable", false);
326
+ boolean autoAcknowledgePurchases = call.getBoolean("autoAcknowledgePurchases", true);
319
327
 
320
328
  Log.d(TAG, "Product identifier: " + productIdentifier);
321
329
  Log.d(TAG, "Plan identifier: " + planIdentifier);
@@ -323,6 +331,7 @@ public class NativePurchasesPlugin extends Plugin {
323
331
  Log.d(TAG, "Quantity: " + quantity);
324
332
  Log.d(TAG, "Account identifier provided: " + (accountIdentifier != null ? "[REDACTED]" : "none"));
325
333
  Log.d(TAG, "Is consumable: " + isConsumable);
334
+ Log.d(TAG, "Auto-acknowledge purchases: " + autoAcknowledgePurchases);
326
335
 
327
336
  // cannot use quantity, because it's done in native modal
328
337
  Log.d("CapacitorPurchases", "purchaseProduct: " + productIdentifier);
@@ -357,6 +366,7 @@ public class NativePurchasesPlugin extends Plugin {
357
366
  }
358
367
 
359
368
  call.getData().put("isConsumable", isConsumable);
369
+ call.getData().put("autoAcknowledgePurchases", autoAcknowledgePurchases);
360
370
 
361
371
  // For subscriptions, always use the productIdentifier (subscription ID) to query
362
372
  // The planIdentifier is used later when setting the offer token
@@ -956,4 +966,50 @@ public class NativePurchasesPlugin extends Plugin {
956
966
  call.reject("Failed to open subscription management page", e);
957
967
  }
958
968
  }
969
+
970
+ @PluginMethod
971
+ public void acknowledgePurchase(PluginCall call) {
972
+ Log.d(TAG, "acknowledgePurchase() called");
973
+ String purchaseToken = call.getString("purchaseToken");
974
+
975
+ if (purchaseToken == null || purchaseToken.isEmpty()) {
976
+ Log.d(TAG, "Error: purchaseToken is empty");
977
+ call.reject("purchaseToken is required");
978
+ return;
979
+ }
980
+
981
+ Log.d(TAG, "Manually acknowledging purchase with token: " + purchaseToken);
982
+ this.initBillingClient(null);
983
+
984
+ try {
985
+ AcknowledgePurchaseParams acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
986
+ .setPurchaseToken(purchaseToken)
987
+ .build();
988
+
989
+ billingClient.acknowledgePurchase(
990
+ acknowledgePurchaseParams,
991
+ new AcknowledgePurchaseResponseListener() {
992
+ @Override
993
+ public void onAcknowledgePurchaseResponse(@NonNull BillingResult billingResult) {
994
+ Log.d(TAG, "onAcknowledgePurchaseResponse() called");
995
+ Log.d(TAG, "Acknowledge result: " + billingResult.getResponseCode() + " - " + billingResult.getDebugMessage());
996
+
997
+ if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
998
+ Log.d(TAG, "Purchase acknowledged successfully");
999
+ closeBillingClient();
1000
+ call.resolve();
1001
+ } else {
1002
+ Log.d(TAG, "Purchase acknowledgment failed");
1003
+ closeBillingClient();
1004
+ call.reject("Failed to acknowledge purchase: " + billingResult.getDebugMessage());
1005
+ }
1006
+ }
1007
+ }
1008
+ );
1009
+ } catch (Exception e) {
1010
+ Log.d(TAG, "Exception during acknowledgePurchase: " + e.getMessage());
1011
+ closeBillingClient();
1012
+ call.reject(e.getMessage());
1013
+ }
1014
+ }
959
1015
  }