Post

Replies

Boosts

Views

Activity

Reply to Detect a subscription has expired
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() }
Apr ’24