Repository: zt-pawer/SwiftGodotIosPlugins
Pull request #26: Added auto-renewable subscription transaction counts to InAppPurchase.
Author: login: SputnikFive
Created: 2025-04-04T22:00:24Z | Merged: 2025-04-17T01:25:00Z

InAppPurchase changes:
- Added callable fetchAutoRenewableTransactionCounts() function.
- Added in_app_purchase_fetch_auto_renewable_transaction_counts signal.
- Added button and functionality to Godot sample project to fetch the auto-renewable transaction counts.
- Updated readme file.
- Above changes will allow tracking rewards granted on each subscription renewal, like monthly rewards, etc., even if the player doesn't log into the game for a long period of time, or if their subscription is no longer active, but they are owed rewards from when they were subscribed previously.
- Moved all emits to the main thread, to prevent crashes with UI changes in a background thread.


Diff:
diff --git a/InAppPurchase/Godot/main.gd b/InAppPurchase/Godot/main.gd
index fb2e254..4b5981c 100644
--- a/InAppPurchase/Godot/main.gd
+++ b/InAppPurchase/Godot/main.gd
@@ -13,6 +13,7 @@ func _ready() -> void:
 		_inapppurchase.in_app_purchase_fetch_success.connect(_on_in_app_purchase_fetch_success)
 		_inapppurchase.in_app_purchase_fetch_error.connect(_on_in_app_purchase_fetch_error)
 		_inapppurchase.in_app_purchase_fetch_active_auto_renewable_subscriptions.connect(_on_in_app_purchase_fetch_active_auto_renewable_subscriptions)
+		_inapppurchase.in_app_purchase_fetch_auto_renewable_transaction_counts.connect(_on_in_app_purchase_fetch_auto_renewable_transaction_counts)
 		_inapppurchase.in_app_purchase_success.connect(_on_in_app_purchase_success)
 		_inapppurchase.in_app_purchase_error.connect(_on_in_app_purchase_error)
 		_inapppurchase.in_app_purchase_restore_success.connect(_on_in_app_purchase_restore_success)
@@ -38,6 +39,7 @@ func _on_in_app_purchase_fetch_success(products: Array[InAppPurchaseProduct]) ->
 	$MarginContainer/VBoxContainer/PurchaseSubscriptionButton.disabled = false
 	$MarginContainer/VBoxContainer/PurchaseSubscriptionNorenewButton.disabled = false
 	$MarginContainer/VBoxContainer/FetchActiveAutoRenewableSubscriptionsButton.disabled = false
+	$MarginContainer/VBoxContainer/FetchAutoRenewableTransactionCountsButton.disabled = false
 
 
 func _on_in_app_purchase_fetch_active_auto_renewable_subscriptions(product_ids: Array[Variant]) -> void:
@@ -48,6 +50,14 @@ func _on_in_app_purchase_fetch_active_auto_renewable_subscriptions(product_ids:
 	status_label.text = "Active subscriptions received: %d" % len(_active_auto_renewable_subscription_product_ids)
 
 
+func _on_in_app_purchase_fetch_auto_renewable_transaction_counts(counts: Dictionary) -> void:
+	print("counts: %s" % counts)
+	if counts:
+		status_label.text = "counts: %s" % counts
+	else:
+		status_label.text = "no transactions"
+
+
 func _on_in_app_purchase_success(message: String) -> void:
 	status_label.text = "Product %s purchased" % message
 
@@ -92,6 +102,10 @@ func _on_fetch_active_auto_renewable_subscriptions_button_pressed() -> void:
 	_inapppurchase.fetchActiveAutoRenewableSubscriptions()
 
 
+func _on_fetch_auto_renewable_transaction_counts_button_pressed() -> void:
+	_inapppurchase.fetchAutoRenewableTransactionCounts()
+
+
 func _on_restore_button_pressed() -> void:
 	_inapppurchase.restorePurchases()
 
diff --git a/InAppPurchase/Godot/main.tscn b/InAppPurchase/Godot/main.tscn
index 8e495d2..eae6daf 100644
--- a/InAppPurchase/Godot/main.tscn
+++ b/InAppPurchase/Godot/main.tscn
@@ -30,24 +30,24 @@ theme_override_constants/separation = 20
 alignment = 1
 
 [node name="LoadProductsButton" type="Button" parent="MarginContainer/VBoxContainer"]
-custom_minimum_size = Vector2(0, 130)
+custom_minimum_size = Vector2(0, 80)
 layout_mode = 2
 text = "Load products"
 
 [node name="PurchaseConsumableButton" type="Button" parent="MarginContainer/VBoxContainer"]
-custom_minimum_size = Vector2(0, 130)
+custom_minimum_size = Vector2(0, 80)
 layout_mode = 2
 disabled = true
 text = "Purchase consumable"
 
 [node name="PurchaseNonConsumableButton" type="Button" parent="MarginContainer/VBoxContainer"]
-custom_minimum_size = Vector2(0, 130)
+custom_minimum_size = Vector2(0, 80)
 layout_mode = 2
 disabled = true
 text = "Purchase non consumable"
 
 [node name="PurchaseSubscriptionButton" type="Button" parent="MarginContainer/VBoxContainer"]
-custom_minimum_size = Vector2(0, 130)
+custom_minimum_size = Vector2(0, 80)
 layout_mode = 2
 disabled = true
 text = "Purchase subscription"
@@ -66,9 +66,16 @@ disabled = true
 text = "Fetch active subscriptions
 auto-renewable"
 
-[node name="RestoreButton" type="Button" parent="MarginContainer/VBoxContainer"]
+[node name="FetchAutoRenewableTransactionCountsButton" type="Button" parent="MarginContainer/VBoxContainer"]
 custom_minimum_size = Vector2(0, 130)
 layout_mode = 2
+disabled = true
+text = "Fetch transaction counts
+auto-renewable"
+
+[node name="RestoreButton" type="Button" parent="MarginContainer/VBoxContainer"]
+custom_minimum_size = Vector2(0, 80)
+layout_mode = 2
 text = "Restore purchases"
 
 [node name="StatusLabel" type="Label" parent="MarginContainer/VBoxContainer"]
@@ -81,4 +88,5 @@ horizontal_alignment = 1
 [connection signal="pressed" from="MarginContainer/VBoxContainer/PurchaseSubscriptionButton" to="." method="_on_purchase_subscription_button_pressed"]
 [connection signal="pressed" from="MarginContainer/VBoxContainer/PurchaseSubscriptionNorenewButton" to="." method="_on_purchase_subscription_norenew_button_pressed"]
 [connection signal="pressed" from="MarginContainer/VBoxContainer/FetchActiveAutoRenewableSubscriptionsButton" to="." method="_on_fetch_active_auto_renewable_subscriptions_button_pressed"]
+[connection signal="pressed" from="MarginContainer/VBoxContainer/FetchAutoRenewableTransactionCountsButton" to="." method="_on_fetch_auto_renewable_transaction_counts_button_pressed"]
 [connection signal="pressed" from="MarginContainer/VBoxContainer/RestoreButton" to="." method="_on_restore_button_pressed"]
diff --git a/InAppPurchase/README.md b/InAppPurchase/README.md
index beceddf..8af4158 100644
--- a/InAppPurchase/README.md
+++ b/InAppPurchase/README.md
@@ -19,6 +19,7 @@ func _ready() -> void:
 		_inapppurchase.in_app_purchase_fetch_success.connect(_on_in_app_purchase_fetch_success)
 		_inapppurchase.in_app_purchase_fetch_error.connect(_on_in_app_purchase_fetch_error)
 		_inapppurchase.in_app_purchase_fetch_active_auto_renewable_subscriptions.connect(_on_in_app_purchase_fetch_active_auto_renewable_subscriptions)
+		_inapppurchase.in_app_purchase_fetch_auto_renewable_transaction_counts.connect(_on_in_app_purchase_fetch_auto_renewable_transaction_counts)
 		_inapppurchase.in_app_purchase_success.connect(_on_in_app_purchase_success)
 		_inapppurchase.in_app_purchase_error.connect(_on_in_app_purchase_error)
 		_inapppurchase.in_app_purchase_restore_success.connect(_on_in_app_purchase_restore_success)
@@ -31,6 +32,7 @@ The Godot method signature required
 func _on_in_app_purchase_fetch_error(error: int, message: String) -> void:
 func _on_in_app_purchase_fetch_success(products: Array[InAppPurchaseProduct]) -> void:
 func _on_in_app_purchase_fetch_active_auto_renewable_subscriptions(product_ids: Array[Variant]) -> void:
+func _on_in_app_purchase_fetch_auto_renewable_transaction_counts(counts: Dictionary) -> void:
 func _on_in_app_purchase_success(message: String) -> void:
 func _on_in_app_purchase_error(error: int, message: String) -> void:
 func _on_in_app_purchase_restore_success(product_ids: Array[Variant]) -> void:
@@ -43,6 +45,7 @@ func _on_in_app_purchase_restore_error(error: int, message: String) -> void:
 - `in_app_purchase_fetch_success` SignalWithArguments<ObjectCollection<InAppPurchaseProduct>>
 - `in_app_purchase_fetch_error` SignalWithArguments<Int,Dictionary>
 - `in_app_purchase_fetch_active_auto_renewable_subscriptions` SignalWithArguments<GArray>
+- `in_app_purchase_fetch_auto_renewable_transaction_counts` SignalWithArguments<GDictionary>
 - `in_app_purchase_success` SignalWithArguments<String>
 - `in_app_purchase_error` SignalWithArguments<Int,Dictionary>
 - `in_app_purchase_restore_success` SignalWithArguments<GArray>
@@ -52,5 +55,6 @@ func _on_in_app_purchase_restore_error(error: int, message: String) -> void:
 
 - `fetchProducts(products: [String])` - Fetch all products given in input, this method **must** be called once before any purchase.
 - `fetchActiveAutoRenewableSubscriptions()` - Fetch all active auto-renewable subscriptions, returning a list of product ids.
+- `fetchAutoRenewableTransactionCounts()` - Fetch all auto-renewable subscription transaction counts. Returns a dictionary, with product ids as the key, and the number of transactions as the value.  Useful for tracking monthly awards, etc.
 - `purchaseProduct(productID: String)` - Purchase a given pruduct.
 - `restorePurchases()` - Restore all the previous purchased products, returning a list of product ids.
diff --git a/InAppPurchase/Swift/Sources/InAppPurchase/InAppPurchase+Product.swift b/InAppPurchase/Swift/Sources/InAppPurchase/InAppPurchase+Product.swift
index 0b923f3..3f62c56 100644
--- a/InAppPurchase/Swift/Sources/InAppPurchase/InAppPurchase+Product.swift
+++ b/InAppPurchase/Swift/Sources/InAppPurchase/InAppPurchase+Product.swift
@@ -90,6 +90,39 @@ extension InAppPurchase {
         }
     }
     
+    internal func fetchAutoRenewableTransactionsAsync(
+        completion: @escaping (Set<Transaction>) -> Void
+    ) {
+        Task {
+            var transactions = Set<Transaction>()
+            // Fetch all entitlements and filter valid transactions
+            for await latestTransaction in Transaction
+                .all
+            {
+                switch latestTransaction
+                {
+                case .verified(let transaction):
+                    // Check for auto-renewable subscriptions
+                    if .autoRenewable == transaction.productType
+                        && transaction.revocationDate == nil
+                        && transaction.isUpgraded == false
+                    {
+                        transactions.insert(transaction)
+                    }
+                case .unverified(
+                    let unverifiedTransaction, let verificationError):
+                    // Handle unverified transactions based on your
+                    // business model.
+                    continue
+
+                }
+            }
+
+            // Call the completion handler with auto-renewable subscription transactions
+            completion(transactions)
+        }
+    }
+    
     internal func restorePurchasesAsync(
         completion: @escaping ([String], InAppPurchaseError?) -> Void
     ) {
diff --git a/InAppPurchase/Swift/Sources/InAppPurchase/InAppPurchase.swift b/InAppPurchase/Swift/Sources/InAppPurchase/InAppPurchase.swift
index 4886f7c..81d90bf 100644
--- a/InAppPurchase/Swift/Sources/InAppPurchase/InAppPurchase.swift
+++ b/InAppPurchase/Swift/Sources/InAppPurchase/InAppPurchase.swift
@@ -58,6 +58,7 @@ class InAppPurchase: Object , ObservableObject {
     var availableProducts: [InAppPurchaseProduct] = []
     var purchasedProductIDs: Set<String> = []
     internal var products_cached: [Product] = []
+    internal var allAutoRenewableSubscriptionTransactions = Set<Transaction>()
 
     // StoreKit's transaction listener
     private var updateListenerTask: Task<Void, Never>?
@@ -73,10 +74,14 @@ class InAppPurchase: Object , ObservableObject {
     /// Error signal during products fetch process
     @Signal var inAppPurchaseFetchError: SignalWithArguments<Int, String>
     /// @Signal
-    /// Success signal during renewable products fetch process
+    /// Success signal during active renewable products fetch process
     @Signal var inAppPurchaseFetchActiveAutoRenewableSubscriptions:
         SignalWithArguments<GArray>
     /// @Signal
+    /// Success signal during renewable transaction counts fetch process
+    @Signal var inAppPurchaseFetchAutoRenewableTransactionCounts:
+        SignalWithArguments<GDictionary>
+    /// @Signal
     /// Error signal during purchase process
     @Signal var inAppPurchaseError: SignalWithArguments<Int, String>
     /// @Signal
@@ -114,15 +119,19 @@ class InAppPurchase: Object , ObservableObject {
             with: products,
             completion: { error in
                 guard error == nil else {
-                    self.inAppPurchaseFetchError.emit(
-                        error!.rawValue, error!.localizedDescription)
+                    DispatchQueue.main.async {
+                        self.inAppPurchaseFetchError.emit(
+                            error!.rawValue, error!.localizedDescription)
+                    }
                     return
                 }
                 var iapProducts = ObjectCollection<InAppPurchaseProduct>()
                 for product in self.products_cached {
                     iapProducts.append(InAppPurchaseProduct(product: product))
                 }
-                self.inAppPurchaseFetchSuccess.emit(iapProducts)
+                DispatchQueue.main.async {
+                    self.inAppPurchaseFetchSuccess.emit(iapProducts)
+                }
             })
     }
 
@@ -134,7 +143,33 @@ class InAppPurchase: Object , ObservableObject {
         fetchActiveAutoRenewableSubscriptionsAsync(completion: { products in
             var productsArray = GArray()
             products.forEach { productsArray.append(Variant($0)) }
-            self.inAppPurchaseFetchActiveAutoRenewableSubscriptions.emit(productsArray)
+            DispatchQueue.main.async {
+                self.inAppPurchaseFetchActiveAutoRenewableSubscriptions.emit(productsArray)
+            }
+        })
+    }
+
+    /// @Callable
+    ///
+    /// Synchronously fetches all auto-renewable subscription transactions (does not block UI, callback when done)
+    @Callable
+    func fetchAutoRenewableTransactionCounts() {
+        fetchAutoRenewableTransactionsAsync(completion: { transactions in
+            
+            // Tally the transaction count for each subscription.
+            self.allAutoRenewableSubscriptionTransactions = self.allAutoRenewableSubscriptionTransactions.union(transactions)
+            var autoRenewableTransactionCounts = [String : Int]()
+            for transaction in self.allAutoRenewableSubscriptionTransactions {
+                let transactionCount = autoRenewableTransactionCounts[transaction.productID] ?? 0
+                autoRenewableTransactionCounts[transaction.productID] = transactionCount + transaction.purchasedQuantity
+            }
+
+            // Convert the dictionary to a GDictionary, and pass it back to Godot via the signal.
+            var countGDictionary = GDictionary()
+            autoRenewableTransactionCounts.forEach { countGDictionary[Variant($0.key)] = Variant($0.value) }
+            DispatchQueue.main.async {
+                self.inAppPurchaseFetchAutoRenewableTransactionCounts.emit(countGDictionary)
+            }
         })
     }
 
@@ -147,11 +182,15 @@ class InAppPurchase: Object , ObservableObject {
             productID,
             completion: { error in
                 guard error == nil else {
-                    self.inAppPurchaseError.emit(
-                        error!.rawValue, error!.localizedDescription)
+                    DispatchQueue.main.async {
+                        self.inAppPurchaseError.emit(
+                            error!.rawValue, error!.localizedDescription)
+                    }
                     return
                 }
-                self.inAppPurchaseSuccess.emit(productID)
+                DispatchQueue.main.async {
+                    self.inAppPurchaseSuccess.emit(productID)
+                }
             })
     }
 
@@ -162,13 +201,17 @@ class InAppPurchase: Object , ObservableObject {
     func restorePurchases() {
         restorePurchasesAsync(completion: { products, error in
             guard error == nil else {
-                self.inAppPurchaseRestoreError.emit(
-                    error!.rawValue, error!.localizedDescription)
+                DispatchQueue.main.async {
+                    self.inAppPurchaseRestoreError.emit(
+                        error!.rawValue, error!.localizedDescription)
+                }
                 return
             }
             var productsArray = GArray()
             products.forEach { productsArray.append(Variant($0)) }
-            self.inAppPurchaseRestoreSuccess.emit(productsArray)
+            DispatchQueue.main.async {
+                self.inAppPurchaseRestoreSuccess.emit(productsArray)
+            }
         })
     }
 
@@ -179,6 +222,13 @@ class InAppPurchase: Object , ObservableObject {
                 switch result {
                 case .verified(let transaction):
                     await self.handleTransaction(transaction)
+                    
+                    // Store any auto-renewable subscription transactions.
+                    if .autoRenewable == transaction.productType
+                        && transaction.revocationDate == nil
+                        && transaction.isUpgraded == false {
+                            self.allAutoRenewableSubscriptionTransactions.insert(transaction)
+                    }
                 case .unverified(_, let error):
                     GD.printErr("Unverified transaction: \(error)")
                 }


Reviews:
user: login: gemini-code-assist[bot]
body: Hello @SputnikFive, I'm Gemini Code Assist[^1]! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

## Summary of Changes
This pull request adds functionality to fetch the transaction counts for auto-renewable subscriptions in the InAppPurchase module. This includes adding a new function `fetchAutoRenewableTransactionCounts()` to the Swift code, a corresponding signal `in_app_purchase_fetch_auto_renewable_transaction_counts` to the Godot side, and a button in the Godot sample project to trigger the new functionality. The changes also update the README file to reflect the new feature.

### Highlights
* **New Functionality**: Introduces `fetchAutoRenewableTransactionCounts()` to fetch the number of transactions for each auto-renewable subscription.
* **Godot Integration**: Adds a new signal `in_app_purchase_fetch_auto_renewable_transaction_counts` and a button in the sample project to demonstrate the new functionality.
* **Transaction Handling**: The Swift code now tallies the transaction count for each subscription and emits a dictionary containing the product IDs and their corresponding transaction counts.

### Changelog
<details>
<summary>Click here to see the changelog</summary>

* **InAppPurchase/Godot/main.gd**
  * Connects the new `in_app_purchase_fetch_auto_renewable_transaction_counts` signal to a handler function.
  * Adds a button to the Godot sample project to fetch auto-renewable transaction counts.
  * Implements the `_on_in_app_purchase_fetch_auto_renewable_transaction_counts` function to display the transaction counts in the status label.
  * Implements the `_on_fetch_auto_renewable_transaction_counts_button_pressed` function to call the new `fetchAutoRenewableTransactionCounts` method.
* **InAppPurchase/Godot/main.tscn**
  * Adds a new button 'FetchAutoRenewableTransactionCountsButton' to the Godot scene.
  * Connects the button's pressed signal to the `_on_fetch_auto_renewable_transaction_counts_button_pressed` method in `main.gd`.
  * Reduces the size of the buttons in the Godot scene.
* **InAppPurchase/README.md**
  * Adds documentation for the new `fetchAutoRenewableTransactionCounts()` function and the `in_app_purchase_fetch_auto_renewable_transaction_counts` signal.
  * Updates the list of available signals and methods.
* **InAppPurchase/Swift/Sources/InAppPurchase/InAppPurchase+Product.swift**
  * Adds `fetchAutoRenewableTransactionsAsync` to fetch all auto-renewable transactions.
* **InAppPurchase/Swift/Sources/InAppPurchase/InAppPurchase.swift**
  * Adds `allAutoRenewableSubscriptionTransactions` to store all auto-renewable transactions.
  * Adds `inAppPurchaseFetchAutoRenewableTransactionCounts` signal.
  * Implements `fetchAutoRenewableTransactionCounts` to fetch and tally auto-renewable subscription transactions and emit the counts via the new signal.
  * Adds logic to store auto-renewable subscription transactions as they occur.

</details>


<details>

<summary><b>Using Gemini Code Assist</b></summary>
<br>

The full guide for Gemini Code Assist can be found on our [documentation page](https://developers.google.com/gemini-code-assist/docs/review-github-code), here are some quick tips.

<b>Invoking Gemini</b>

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either `/gemini <command>` or `@gemini-code-assist <command>`. Below is a summary of the supported commands.

Feature | Command | Description
--- | --- | ---
Code Review | `/gemini review` | Performs a code review for the current pull request in its current state.
Pull Request Summary | `/gemini summary` | Provides a summary of the current pull request in its current state.
Comment | @gemini-code-assist | Responds in comments when explicitly tagged, both in issue comments and review comments.
Help | `/gemini help` | Displays a list of available commands.

<b>Customization</b>

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a `.gemini/` folder in the base of the repository. Detailed instructions can be found [here](https://developers.google.com/gemini-code-assist/docs/customize-gemini-behavior-github).

<b>Limitations & Feedback</b>

Gemini Code Assist is currently in preview and may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with :thumbsup: and :thumbsdown: on @gemini-code-assist comments to provide feedback.

</details>

<b>You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the [Gemini Code Assist IDE Extension](https://cloud.google.com/products/gemini/code-assist).</b>


[^1]: Review the [Privacy Notices](https://policies.google.com/privacy), [Generative AI Prohibited Use Policy](https://policies.google.com/terms/generative-ai/use-policy), [Terms of Service](https://policies.google.com/terms), and learn how to configure Gemini Code Assist in GitHub [here](https://developers.google.com/gemini-code-assist/docs/review-github-code). Gemini can make mistakes, so double check it and [use code with caution](https://support.google.com/legal/answer/13505487).

---

> A count of transactions,
> A monthly reward's reflection,
> Loyalty's true prize.


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T22:00:33Z
user: login: gemini-code-assist[bot]
body: ## Code Review
This pull request introduces functionality to fetch auto-renewable subscription transaction counts, which is a valuable addition for tracking rewards and subscription history. The changes include updates to the Godot demo project and the Swift implementation. Overall, the code is well-structured and addresses the intended functionality.

### Summary of Findings
* **Missing Error Handling**: The `fetchAutoRenewableTransactionsAsync` function in `InAppPurchase+Product.swift` does not handle errors when fetching transactions. This could lead to unhandled exceptions and unexpected behavior.
* **Potential Performance Issue**: The `fetchAutoRenewableTransactionCounts` function in `InAppPurchase.swift` iterates through all transactions stored in `allAutoRenewableSubscriptionTransactions` every time it's called. This could become a performance bottleneck if the number of transactions grows large.
* **Inconsistent Button Sizes**: In `main.tscn`, the `FetchAutoRenewableTransactionCountsButton` has a different `custom_minimum_size` than other buttons. This inconsistency should be addressed for a uniform UI.

### Merge Readiness
The pull request introduces a useful feature and is generally well-implemented. However, the missing error handling in `fetchAutoRenewableTransactionsAsync` and the potential performance issue in `fetchAutoRenewableTransactionCounts` should be addressed before merging. Additionally, the button size inconsistency in `main.tscn` should be fixed. I am unable to approve this pull request, and recommend that others review and approve this code before merging.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T22:00:45Z

Review comments:
diff_hunk: @@ -90,6 +90,39 @@ extension InAppPurchase {
         }
     }
     
+    internal func fetchAutoRenewableTransactionsAsync(
+        completion: @escaping (Set<Transaction>) -> Void
+    ) {
+        Task {
+            var transactions = Set<Transaction>()
+            // Fetch all entitlements and filter valid transactions
+            for await latestTransaction in Transaction
+                .all
+            {
+                switch latestTransaction
+                {
+                case .verified(let transaction):
+                    // Check for auto-renewable subscriptions
+                    if .autoRenewable == transaction.productType
+                        && transaction.revocationDate == nil
+                        && transaction.isUpgraded == false
+                    {
+                        transactions.insert(transaction)
+                    }
+                case .unverified(
+                    let unverifiedTransaction, let verificationError):
+                    // Handle unverified transactions based on your
+                    // business model.
+                    continue
+
+                }
+            }
+
+            // Call the completion handler with auto-renewable subscription transactions
+            completion(transactions)
+        }
+    }
path: InAppPurchase/Swift/Sources/InAppPurchase/InAppPurchase+Product.swift
user: login: gemini-code-assist[bot]
body: ![high](https://img.shields.io/badge/priority-high-orange)

The `fetchAutoRenewableTransactionsAsync` function lacks proper error handling. If `Transaction.all` fails or encounters an error, the function will not handle it, potentially leading to unhandled exceptions and unexpected behavior. Consider adding a `do-catch` block to handle potential errors during transaction fetching.

Also, consider adding logging for unverified transactions to aid in debugging.

```swift
        Task {
            var transactions = Set<Transaction>()
            do {
                // Fetch all entitlements and filter valid transactions
                for await latestTransaction in Transaction
                    .all
                {
                    switch latestTransaction
                    {
                    case .verified(let transaction):
                        // Check for auto-renewable subscriptions
                        if .autoRenewable == transaction.productType
                            && transaction.revocationDate == nil
                            && transaction.isUpgraded == false
                        {
                            transactions.insert(transaction)
                        }
                    case .unverified(
                        let unverifiedTransaction, let verificationError):
                        // Handle unverified transactions based on your
                        // business model.
                        GD.print("Unverified transaction: \(verificationError)")
                        continue

                    }
                }
            } catch {
                GD.printErr("Error fetching transactions: \(error)")
            }

            // Call the completion handler with auto-renewable subscription transactions
            completion(transactions)
        }
```
created_at: 2025-04-04T22:00:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -138,6 +143,28 @@ class InAppPurchase: Object , ObservableObject {
         })
     }
 
+    /// @Callable
+    ///
+    /// Synchronously fetches all auto-renewable subscription transactions (does not block UI, callback when done)
+    @Callable
+    func fetchAutoRenewableTransactionCounts() {
+        fetchAutoRenewableTransactionsAsync(completion: { transactions in
+            
+            // Tally the transaction count for each subscription.
+            self.allAutoRenewableSubscriptionTransactions = self.allAutoRenewableSubscriptionTransactions.union(transactions)
+            var autoRenewableTransactionCounts = [String : Int]()
+            for transaction in self.allAutoRenewableSubscriptionTransactions {
+                let transactionCount = autoRenewableTransactionCounts[transaction.productID] ?? 0
+                autoRenewableTransactionCounts[transaction.productID] = transactionCount + transaction.purchasedQuantity
+            }
path: InAppPurchase/Swift/Sources/InAppPurchase/InAppPurchase.swift
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

The `fetchAutoRenewableTransactionCounts` function iterates through `self.allAutoRenewableSubscriptionTransactions` every time it's called. If the number of transactions becomes large, this could become a performance bottleneck. Consider caching the transaction counts or using a more efficient data structure to store and update the counts.

Consider using a dictionary to store the counts and updating it as new transactions are processed.

```suggestion
        fetchAutoRenewableTransactionsAsync(completion: { transactions in

            // Tally the transaction count for each subscription.
            self.allAutoRenewableSubscriptionTransactions = self.allAutoRenewableSubscriptionTransactions.union(transactions)
            var autoRenewableTransactionCounts = [String : Int]()
            for transaction in self.allAutoRenewableSubscriptionTransactions {
                let transactionCount = autoRenewableTransactionCounts[transaction.productID] ?? 0
                autoRenewableTransactionCounts[transaction.productID] = transactionCount + transaction.purchasedQuantity
            }

```
created_at: 2025-04-04T22:00:45Z
author_association: CONTRIBUTOR

PR comments:
user: login: zt-pawer
created_at: 2025-04-10T01:53:34Z
body: Thank you for all these contributions, I will consolidate these over the weekend.
author_association: OWNER
user: login: SputnikFive
created_at: 2025-04-10T03:34:23Z
body: I needed these changes to allow my game to see if the user has a subscription active, and to give them monthly rewards for past months.  Seems to be working well.  The last change, moving all emits to the main thread, was per getting a crash when removing AdMob banner ads when a subscription is purchased.  It crashed trying to hide a UIView in a background thread.  Moving the emits to the main thread seems to have fixed it.

Happy to contribute.  Let me know if you have any questions!
author_association: NONE