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?