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()
}
Post
Replies
Boosts
Views
Activity
Same on a 2020 Intel MB Pro 13" on MacOS 12.0.1. Tried uninstalling Xcode (worked for me in the past with similar issues with apps installed via the App Store) but now the App Store still shows it partly downloaded.