Невозможно определить срок действия подписки с помощью StoreKit2

Я разрабатываю автоматически возобновляемую подписку на свое приложение 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, а затем обмениваться данными с моим приложением, но я хочу избежать наличия сервера для этого.

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
4
0
308
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Событие «подписка отменена» отсутствует. Когда пользователь отменяет подписку, она продолжается до конца периода подписки, а затем не продлевается.

Вам не нужна вся эта сложная логика дат в вашем методе updatePurchasedProducts.

С StoreKit2 все, что вам нужно сделать, это проверить currentEntitlements для ваших продуктов по подписке. Если идентификатор продукта подписки присутствует, значит, существует активная подписка (или подписка в льготном периоде), и вам следует предоставить соответствующие преимущества. Если идентификатор продукта подписки отсутствует, удалите преимущества.

Вы, конечно, можете сохранить ожидаемую дату истечения срока действия текущей подписки и проверять ее только тогда, когда наступит эта дата, или вы можете просто проверять каждый раз, когда ваше приложение запускается или возвращается на передний план.

Ваше предложение не работает, если вам нужно отобразить «Продлевается ДАТА» или «Срок действия истекает ДАТА» для активной подписки. Поскольку StoreKit2 не сообщает об обновлении подписки (в то время как Product.SubscriptionInfo.RenewalInfo изменяется в транзакции), мы не можем отразить эту отмену в пользовательском интерфейсе.

Hopreeeenjust 19.06.2024 17:41

Ответ, который вы ищете, находится на странице Developer.apple.com/documentation/storekit/product/…

Paulw11 19.06.2024 22:44

Другие вопросы по теме