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:
// Store transaction for the active subscriptions.
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 {
// Check for previous purchases.
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)
}
}
// Watch for future transactions coming in.
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... 🤞")
}
// Avoid resetting activeSubscriptions if no changes have been made.
if activeSubs != activeSubscriptions {
activeSubscriptions = activeSubs
}
await transaction.finish()
}