Я разрабатываю автоматически возобновляемую подписку на свое приложение SwiftUI. Я могу реагировать на такие события, как обновление и переход на другой уровень подписки, а также на автоматическое продление.
Проблема в том, что я не получаю обновления, когда пользователь отменяет подписку. Сама отмена происходит вне приложения (из «Настройки/Профиль/Подписки» или в Xcode через «Управление транзакциями StoreKit»), поскольку в приложении нет кнопки отмены.
В идеале я хочу хранить в UserDefaults только два логических значения для каждой из моих дополнительных функций, чтобы знать, следует ли мне предоставлять их пользователю или нет. Но в связи с текущей проблемой я думаю вместо логических значений использовать даты истечения срока действия (которые будут обновляться при каждом продлении) и каждый раз проверять их на текущую дату, чтобы узнать, истек ли срок их действия.
Видите, не пропустил ли я что-то в PurchaseManager, чтобы не получать обновления об отмене подписки? И думаете ли вы, что этот обходной путь достаточно хорош, чтобы выполнить работу?
import Foundation
import StoreKit
typealias SkTransaction = StoreKit.Transaction
enum PremiumFeature: String {
case unlimitedWallets = "unlimitedWallets"
case unlimitedReceiptScans = "unlimitedReceiptScans"
}
@MainActor
class PurchaseManager: ObservableObject {
static let productIds: Set<String> = Set(["plus","premium"])
@Published var products: [Product] = []
private var subscriptionManager: SubscriptionManager
private var updates: Task<Void, Never>? = nil
init(subscriptionManager: SubscriptionManager) {
self.subscriptionManager = subscriptionManager
updates = observeTransactionUpdates()
}
deinit {
updates?.cancel()
}
func loadProducts() async throws {
products = try await Product.products(for: PurchaseManager.productIds)
}
func purchase(_ product: Product) async throws {
print("Purcahse initiated for \(product.id)")
let result = try await product.purchase()
switch result {
case let .success(.verified(transaction)):
// Successful purhcase
await transaction.finish()
await updatePurchasedProducts()
case .success(.unverified(_, _)):
// Successful purchase but transaction/receipt can't be verified
// Could be a jailbroken phone
break
case .pending:
// Transaction waiting on SCA (Strong Customer Authentication) or
// approval from Ask to Buy
break
case .userCancelled:
// ^^^
break
@unknown default:
break
}
}
func updatePurchasedProducts() async {
for await result in SkTransaction.currentEntitlements {
guard case .verified(let transaction) = result else {
continue
}
let currentDate = Date()
let isSubscriptionActive = transaction.expirationDate == nil || currentDate <= transaction.expirationDate!
if transaction.revocationDate == nil && isSubscriptionActive {
// Subscription is active
switch transaction.productID {
case "plus":
print("Plus subscription active")
subscriptionManager.unlimitedWallets = true
case "premium":
print("Premium subscription active")
subscriptionManager.unlimitedWallets = true
subscriptionManager.unlimitedReceiptScans = true
default:
print("Unknown product ID \(transaction.productID)")
}
} else {
// Subscription is cancelled or expired
switch transaction.productID {
case "plus":
print("Plus subscription cancelled or expired")
subscriptionManager.unlimitedWallets = false
case "premium":
print("Premium subscription cancelled or expired")
subscriptionManager.unlimitedWallets = false
subscriptionManager.unlimitedReceiptScans = false
default:
print("Unknown product ID \(transaction.productID)")
}
}
await transaction.finish()
}
}
private func observeTransactionUpdates() -> Task<Void, Never> {
Task(priority: .background) { [unowned self] in
for await result in SkTransaction.updates {
print("transaction updated observed")
guard case .verified(let transaction) = result else {
continue
}
await updatePurchasedProducts()
await transaction.finish()
}
}
}
}
Вот как я это называю из @main
/*...*/
ContentView()
.task {
await purchaseManager.updatePurchasedProducts()
do {
try await purchaseManager.loadProducts()
} catch {
print(error)
}
}
/*...*/
Я тестирую подписки только с помощью Xcode и его инструмента «Управление транзакциями StoreKit», поэтому не уверен, что это не просто ошибка в инструменте.
Еще один способ решения этой проблемы, который я нашел в Интернете, заключался в использовании таймеров, но после быстрого исследования выяснилось, что таймеры используются в течение короткого периода времени и ненадежны, особенно в течение периода времени в 1 месяц.
Другой допустимый вариант — настроить сервер, который будет получать обновления из AppStore, а затем обмениваться данными с моим приложением, но я хочу избежать наличия сервера для этого.





Событие «подписка отменена» отсутствует. Когда пользователь отменяет подписку, она продолжается до конца периода подписки, а затем не продлевается.
Вам не нужна вся эта сложная логика дат в вашем методе updatePurchasedProducts.
С StoreKit2 все, что вам нужно сделать, это проверить currentEntitlements для ваших продуктов по подписке. Если идентификатор продукта подписки присутствует, значит, существует активная подписка (или подписка в льготном периоде), и вам следует предоставить соответствующие преимущества. Если идентификатор продукта подписки отсутствует, удалите преимущества.
Вы, конечно, можете сохранить ожидаемую дату истечения срока действия текущей подписки и проверять ее только тогда, когда наступит эта дата, или вы можете просто проверять каждый раз, когда ваше приложение запускается или возвращается на передний план.
Ответ, который вы ищете, находится на странице Developer.apple.com/documentation/storekit/product/…
Ваше предложение не работает, если вам нужно отобразить «Продлевается ДАТА» или «Срок действия истекает ДАТА» для активной подписки. Поскольку StoreKit2 не сообщает об обновлении подписки (в то время как Product.SubscriptionInfo.RenewalInfo изменяется в транзакции), мы не можем отразить эту отмену в пользовательском интерфейсе.