@capgo/native-purchases 7.13.5 → 7.14.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,39 @@ 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 a purchase on Android.
1575
+
1576
+ This method is only needed when you set `autoAcknowledgePurchases: false` in purchaseProduct().
1577
+ Purchases MUST be acknowledged within 3 days or they will be automatically refunded by Google Play.
1578
+
1579
+ **Acknowledgment Options:**
1580
+ 1. **Client-side (this method)**: Call from your app after validation
1581
+ 2. **Server-side (recommended for security)**: Use Google Play Developer API v3
1582
+ - Endpoint: POST https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/products/{productId}/tokens/{token}:acknowledge
1583
+ - Requires OAuth 2.0 authentication with appropriate scopes
1584
+ - See: https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.products/acknowledge
1585
+
1586
+ When to use manual acknowledgment:
1587
+ - Server-side validation: Verify the purchase with your backend before acknowledging
1588
+ - Entitlement delivery: Ensure user receives content/features before acknowledging
1589
+ - Multi-step workflows: Complete all steps before final acknowledgment
1590
+ - Security: Prevent client-side manipulation by handling acknowledgment server-side
1591
+
1592
+ | Param | Type | Description |
1593
+ | ------------- | --------------------------------------- | ----------------------------- |
1594
+ | **`options`** | <code>{ purchaseToken: string; }</code> | - The purchase to acknowledge |
1595
+
1596
+ **Since:** 7.14.0
1597
+
1598
+ --------------------
1599
+
1600
+
1566
1601
  ### addListener('transactionUpdated', ...)
1567
1602
 
1568
1603
  ```typescript
@@ -1583,6 +1618,26 @@ iOS only.
1583
1618
  --------------------
1584
1619
 
1585
1620
 
1621
+ ### addListener('transactionVerificationFailed', ...)
1622
+
1623
+ ```typescript
1624
+ addListener(eventName: 'transactionVerificationFailed', listenerFunc: (payload: TransactionVerificationFailedEvent) => void) => Promise<PluginListenerHandle>
1625
+ ```
1626
+
1627
+ Listen for StoreKit transaction verification failures delivered by Apple's <a href="#transaction">Transaction</a>.updates.
1628
+ Fires when the verification result is unverified.
1629
+ iOS only.
1630
+
1631
+ | Param | Type |
1632
+ | ------------------ | ----------------------------------------------------------------------------------------------------------------------- |
1633
+ | **`eventName`** | <code>'transactionVerificationFailed'</code> |
1634
+ | **`listenerFunc`** | <code>(payload: <a href="#transactionverificationfailedevent">TransactionVerificationFailedEvent</a>) =&gt; void</code> |
1635
+
1636
+ **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</code>
1637
+
1638
+ --------------------
1639
+
1640
+
1586
1641
  ### removeAllListeners()
1587
1642
 
1588
1643
  ```typescript
@@ -1599,28 +1654,34 @@ Remove all registered listeners
1599
1654
 
1600
1655
  #### Transaction
1601
1656
 
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 |
1657
+ | Prop | Type | Description | Default | Since |
1658
+ | -------------------------- | ------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | ------ |
1659
+ | **`transactionId`** | <code>string</code> | Unique identifier for the transaction. | | 1.0.0 |
1660
+ | **`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 |
1661
+ | **`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 |
1662
+ | **`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 ``` | | |
1663
+ | **`productIdentifier`** | <code>string</code> | <a href="#product">Product</a> identifier associated with the transaction. | | 1.0.0 |
1664
+ | **`purchaseDate`** | <code>string</code> | Purchase date of the transaction in ISO 8601 format. | | 1.0.0 |
1665
+ | **`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 |
1666
+ | **`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 |
1667
+ | **`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 |
1668
+ | **`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 |
1669
+ | **`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 |
1670
+ | **`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 |
1671
+ | **`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 |
1672
+ | **`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 |
1673
+ | **`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 |
1674
+ | **`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 |
1675
+ | **`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 |
1676
+ | **`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 |
1677
+ | **`quantity`** | <code>number</code> | Quantity purchased. | <code>1</code> | 1.0.0 |
1678
+ | **`productType`** | <code>string</code> | <a href="#product">Product</a> type. - `"inapp"`: One-time in-app purchase - `"subs"`: Subscription | | 1.0.0 |
1679
+ | **`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 |
1680
+ | **`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 |
1681
+ | **`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 |
1682
+ | **`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 |
1683
+ | **`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 |
1684
+ | **`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
1685
 
1625
1686
 
1626
1687
  #### Product
@@ -1671,6 +1732,14 @@ Remove all registered listeners
1671
1732
  | **`remove`** | <code>() =&gt; Promise&lt;void&gt;</code> |
1672
1733
 
1673
1734
 
1735
+ #### TransactionVerificationFailedEvent
1736
+
1737
+ | Prop | Type | Description | Since |
1738
+ | ------------------- | ------------------- | ----------------------------------------------------------- | ------ |
1739
+ | **`transactionId`** | <code>string</code> | Identifier of the transaction that failed verification. | 7.13.2 |
1740
+ | **`error`** | <code>string</code> | Localized error message describing why verification failed. | 7.13.2 |
1741
+
1742
+
1674
1743
  ### Enums
1675
1744
 
1676
1745
 
@@ -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.14.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
  }