Reiteration of Transaction verification

36 Views Asked by At

Trying to reduce the amount of transactions IDs. Currently, it seems to check the transaction ID every second. I want to avoid that.

here is my code:

import SwiftUI
import StoreKit

import OSLog

private let logger = Logger(subsystem: "PassStatus", category: "PassStatus")

struct ContentView: View {
    
    @State private var showScriptionView: Bool = false
    @State private var status: EntitlementTaskState<PassStatus> = .loading
    @State private var presentingSubscriptionSheet = false
    
    @Environment(PassStatusModel.self) var passStatusModel: PassStatusModel
    @Environment(\.passIDs) private var passIDs
    

    
    var body: some View {
        NavigationView {
            List {
                Section {
                    planView
                    // Show the option button if user does not have a plan.
                    if passStatusModel.passStatus == .notSubscribed {
                        Button {
                            self.showScriptionView = true
                        } label: {
                            Text("View Options")
                        }
                    }
                } header: {
                    Text("SUBSCRIPTION")
                } footer: {
                    if passStatusModel.passStatus != .notSubscribed {
                        Text("Flower Movie+ Plan: \(String(describing: passStatusModel.passStatus.description))")
                    }
                }
            }
            .navigationTitle("Account")
            .sheet(isPresented: $showScriptionView, content: {
                SubscriptionShopView()
            })
            
        }
        .manageSubscriptionsSheet(
            isPresented: $presentingSubscriptionSheet,
            subscriptionGroupID: passIDs.group
        )
        .onAppear(perform: {
            ProductSubscription.createSharedInstance()
        })
        .subscriptionStatusTask(for: passIDs.group) { taskStatus in
            logger.info("Checking subscription status")
            self.status = await taskStatus.map { statuses in
                await ProductSubscription.shared.status(
                    for: statuses,
                    ids: passIDs
                )
            }
            switch self.status {
            case .failure(let error):
                passStatusModel.passStatus = .notSubscribed
                print("Failed to check subscription status: \(error)")
            case .success(let status):
                passStatusModel.passStatus = status
            case .loading: break
            @unknown default: break
            }
            logger.info("Finished checking subscription status")
        }
        .task {
            logger.info("Starting tasks to observe transaction updates")
            await ProductSubscription.shared.observeTransactionUpdates()
            await ProductSubscription.shared.checkForUnfinishedTransactions()
            logger.info("Finished checking for unfinished transactions")
        }
    }
}

#Preview {
    ContentView()
        .environment(PassStatusModel())
}


extension ContentView {
    @ViewBuilder
    var planView: some View {
        VStack(alignment: .leading, spacing: 3) {
            Text(passStatusModel.passStatus == .notSubscribed ? "Flower Movie+": "Flower Movie+ Plan: \(passStatusModel.passStatus.description)")
                .font(.system(size: 17))
            Text(passStatusModel.passStatus == .notSubscribed ? "Subscription to unlock all streaming videos, enjoy Blu-ray 4K quality, and watch offline.": "Enjoy all streaming Blu-ray 4K quality videos, and watch offline.")
                .font(.system(size: 15))
                .foregroundStyle(.gray)
            if passStatusModel.passStatus != .notSubscribed {
                Button("Handle Subscription \(Image(systemName: "chevron.forward"))") {
                    self.presentingSubscriptionSheet = true
                }
            }
        }
    }
}

This was taken from here: https://betterprogramming.pub/meet-storekit-subscriptionstoreview-in-ios-17-bdbe7a827a9

but it is similar to: https://github.com/apple/sample-backyard-birds, which was created by apple.

Unsure where the problem is to avoid such iteration of transaction IDs.

I copy and pasted the same stack from apple. Here is the code associated with my current problem.

@Observable class PassStatusModel {
    var passStatus: PassStatus = .notSubscribed
}

enum PassStatus: Comparable, Hashable {
    case notSubscribed
    case monthly
    case quarterly
    case yearly
    
    init?(productID: Product.ID, ids: PassIdentifiers) {
        switch productID {
        case ids.monthly: self = .monthly
        case ids.quarterly: self = .quarterly
        case ids.yearly: self = .yearly
        default: return nil
        }
    }
    
    var description: String {
        switch self {
        case .notSubscribed:
            "Not Subscribed"
        case .monthly:
            "Monthly"
        case .quarterly:
            "Quarterly"
        case .yearly:
            "Yearly"
        }
    }
}

import OSLog
import Foundation
import StoreKit

actor ProductSubscription {
    private let logger = Logger(
        subsystem: "Meet SubscriptionView",
        category: "Product Subscription"
    )
    
    private var updatesTask: Task<Void, Never>?
        
    private init() {}
    
    private(set) static var shared: ProductSubscription!
        
    static func createSharedInstance() {
        shared = ProductSubscription()
    }
    
    // Subscription Only Handle Here.
    func status(for statuses: [Product.SubscriptionInfo.Status], ids: PassIdentifiers) -> PassStatus {
        let effectiveStatus = statuses.max { lhs, rhs in
            let lhsStatus = PassStatus(
                productID: lhs.transaction.unsafePayloadValue.productID,
                ids: ids
            ) ?? .notSubscribed
            let rhsStatus = PassStatus(
                productID: rhs.transaction.unsafePayloadValue.productID,
                ids: ids
            ) ?? .notSubscribed
            return lhsStatus < rhsStatus
        }
        guard let effectiveStatus else {
            return .notSubscribed
        }
        
        let transaction: Transaction
        switch effectiveStatus.transaction {
        case .verified(let t):
            logger.debug("""
            Transaction ID \(t.id) for \(t.productID) is verified
            """)
            transaction = t
        
        case .unverified(let t, let error):
            // Log failure and do not give access
            logger.error("""
            Transaction ID \(t.id) for \(t.productID) is unverified: \(error)
            """)
            return .notSubscribed
        }
        
        return PassStatus(productID: transaction.productID, ids: ids) ?? .notSubscribed
    }
    
    
}

// To discard this warning:
//  Making a purchase without listening for
//  transaction updates risks missing successful purchases.
//  Create a Task to iterate Transaction.updates at launch.
extension ProductSubscription {
    // For other in-app purchase use this method to check for status.
    func process(transaction verificationResult: VerificationResult<Transaction>) async {
        do {
            let unsafeTransaction = verificationResult.unsafePayloadValue
            logger.log("""
            Processing transaction ID \(unsafeTransaction.id) for \
            \(unsafeTransaction.productID)
            """)
        }
        
        let transaction: Transaction
        switch verificationResult {
        case .verified(let t):
            logger.debug("""
            Transaction ID \(t.id) for \(t.productID) is verified
            """)
            transaction = t
        case .unverified(let t, let error):
            // Log failure and ignore unverified transactions
            logger.error("""
            Transaction ID \(t.id) for \(t.productID) is unverified: \(error)
            """)
            return
        }
        
        await transaction.finish()
        
    }
    
    func checkForUnfinishedTransactions() async {
        logger.debug("Checking for unfinished transactions")
        for await transaction in Transaction.unfinished {
            let unsafeTransaction = transaction.unsafePayloadValue
            logger.log("""
            Processing unfinished transaction ID \(unsafeTransaction.id) for \
            \(unsafeTransaction.productID)
            """)
            Task.detached(priority: .background) {
                await self.process(transaction: transaction)
            }
        }
        logger.debug("Finished checking for unfinished transactions")
    }
    
    func observeTransactionUpdates() {
        self.updatesTask = Task { [weak self] in
            self?.logger.debug("Observing transaction updates")
            for await update in Transaction.updates {
                guard let self else { break }
                await self.process(transaction: update)
            }
        }
    }
}

struct PassIdentifiers {
    var group: String
    
    var monthly: String
    var quarterly: String
    var yearly: String
}

extension EnvironmentValues {
    private enum PassIDsKey: EnvironmentKey {
        static var defaultValue = PassIdentifiers(
            group: "506F71A6",
            //group: "21408633",
            monthly: "com.pass.monthly",
            quarterly: "com.pass.quarterly",
            yearly: "com.pass.yearly"
        )
    }
    
    var passIDs: PassIdentifiers {
        get { self[PassIDsKey.self] }
        set { self[PassIDsKey.self] = newValue }
    }
}


The biggest difference I see is that Apple uses SwiftData and this code does not. Is it necessary to use SwiftData for something like this?

0

There are 0 best solutions below