I've run into the same while testing in the simulator. Here's how I got my app to work as I wanted in the simulator.
I have an array that keeps track of any active subscription transactions. Whenever it is set, it checks the expiration date of all items and removes them if they have expired.
I also have a method that runs through the activeSubscriptions array and does the same thing (I know... similar code in two places... I'm a horrible coder). I call this every time the app enters the foreground. If there are any items in the activeSubscriptions array, I unlock the paid features. If activeSubscriptions.isEmpty then I lock my paid features.
When I finalize a transaction I also check to see if it should be added or removed (if it exists) from the activeSubscriptions array. This way I don't unnecessarily add (and then immediately remove) any transactions that I receive from Transactions.currentEntitlements that have occurred and then already expired. Yes this is unlikely since the subscription period is normally a month or a year instead of as little as 10 seconds in the simulator but I like to cover all bases.
My activeSubscriptions array:
| |
| var activeSubscriptions: [StoreKit.Transaction] = [] { |
| didSet { |
| var unlockApp = false |
| |
| for transaction in activeSubscriptions { |
| if let expirationDate = transaction.expirationDate { |
| if expirationDate < Date.now { |
| logger.info("activeSubscriptions didSet is removing transaction \(transaction.id) becuase it has an expiration date of \(expirationDate). It is currently \(Date.now).") |
| activeSubscriptions.removeAll(where: {transaction.id == $0.id}) |
| } |
| } |
| } |
| |
| if activeSubscriptions.isEmpty { |
| unlockApp = false |
| } else { |
| unlockApp = true |
| } |
| |
| if appUnlocked != unlockApp { |
| objectWillChange.send() |
| appUnlocked = unlockApp |
| } |
| |
| logger.info("There are \(self.activeSubscriptions.count) subscriptions in activeSubscriptions. The app is \(self.appUnlocked ? "unlocked" : "locked").") |
| |
| for subscription in activeSubscriptions { |
| logger.info("Transaction id \(subscription.id) is in activeSubscriptions.") |
| } |
| } |
| } |
My listener method:
| func monitorTransactions() async { |
| |
| for await entitlement in Transaction.currentEntitlements { |
| print("monitorTransactions is checking entitlement \(String(describing: try? entitlement.payloadValue.id))") |
| if case let .verified(transaction) = entitlement { |
| await finalize(transaction) |
| } |
| } |
| |
| |
| for await update in Transaction.updates { |
| if let transaction = try? update.payloadValue { |
| print("monitorTransactions is updating transaction \(transaction.id)") |
| await finalize(transaction) |
| } |
| } |
| } |
My check for expired subscriptions (this is what I run whenever my app enters the foreground).
| func validateActiveSubscriptions(_ note: Notification) { |
| var activeSubs = activeSubscriptions |
| |
| for transaction in activeSubs { |
| if let expirationDate = transaction.expirationDate { |
| if expirationDate < Date.now { |
| logger.info("Removing transaction \(transaction.id) becuase it has an expiration date of \(expirationDate). It is currently \(Date.now).") |
| activeSubs.removeAll(where: {transaction.id == $0.id}) |
| } |
| } |
| } |
| |
| if activeSubs != activeSubscriptions { |
| activeSubscriptions = activeSubs |
| } |
| } |
And finally my transaction finalizer...
| @MainActor |
| func finalize(_ transaction: Transaction) async { |
| var activeSubs = activeSubscriptions |
| var dispositionOfTransaction: addOrRemove = .doNothing |
| |
| enum addOrRemove { |
| case doNothing |
| case addToActiveSubscriptions |
| case removeFromActiveSubscriptions |
| } |
| |
| logger.info("Finalizing \(transaction.id)") |
| |
| if Self.unlockIDs.contains(transaction.productID) { |
| if transaction.expirationDate == nil { |
| logger.info("Adding \(transaction.id) because it has no expiration date.") |
| dispositionOfTransaction = .addToActiveSubscriptions |
| } else if let expirationDate = transaction.expirationDate { |
| if expirationDate > Date.now { |
| logger.info("Adding \(transaction.id) because its expiration date is in the future.") |
| dispositionOfTransaction = .addToActiveSubscriptions |
| } |
| else { |
| logger.info("Not adding (i.e. removing) \(transaction.id) because it has expired.") |
| dispositionOfTransaction = .removeFromActiveSubscriptions |
| } |
| } |
| } else { |
| logger.info("Removing transaction \(transaction.id) for product \(transaction.productID) because it is not included in \(Self.unlockIDs)") |
| dispositionOfTransaction = .removeFromActiveSubscriptions |
| } |
| |
| if transaction.revocationDate != nil { |
| logger.info("Removing transaction id \(transaction.productID) because it has a revocation date.") |
| dispositionOfTransaction = .removeFromActiveSubscriptions |
| } |
| |
| switch dispositionOfTransaction { |
| case .addToActiveSubscriptions: |
| activeSubs.append(transaction) |
| case .removeFromActiveSubscriptions: |
| activeSubs.removeAll(where: {transaction.id == $0.id}) |
| case .doNothing: |
| logger.warning("Transaction \(transaction.id) was not categorized! This should never happen... 🤞") |
| } |
| |
| |
| if activeSubs != activeSubscriptions { |
| activeSubscriptions = activeSubs |
| } |
| |
| await transaction.finish() |
| } |