4

Many the tutorials around StoreKit 2 and even Apple's own Sample Code reference "StoreKit testing in Xcode so you can build and run the sample app without completing any setup in App Store Connect. The project defines in-app products for the StoreKit testing server in the Products.storekit file." I have an app with auto renewing subscriptions already set up in App Store Connect (migrating to StoreKit 2 from SwiftyStoreKit)...how can I set up this StoreKitManager class to check for an active subscription without needing to make a separate Products.plist file? This code below, largely based on Apple's sample code, results in Error Domain=ASDErrorDomain Code=509 "No active account" which is obvious as I can't figure out how to connect my products to the StoreKit 2 logic?

EDIT: here is a gist of my code

import Foundation
import StoreKit

typealias Transaction = StoreKit.Transaction

public enum StoreError: Error {
    case failedVerification
}

@available(watchOSApplicationExtension 8.0, *)
class WatchStoreManager: ObservableObject {
    
    var updateListenerTask: Task<Void, Error>? = nil
    
    init() {
        print("Init called in WatchStoreManager")
        //Start a transaction listener as close to app launch as possible so you don't miss any transactions.
        updateListenerTask = listenForTransactions()
    }
    
    deinit {
        updateListenerTask?.cancel()
    }
    
    func listenForTransactions() -> Task<Void, Error> {
        return Task.detached {
            //Iterate through any transactions that don't come from a direct call to `purchase()`.
            for await result in Transaction.updates {
                do {
                    let transaction = try self.checkVerified(result)
                    
                    print("we have a verified transacction")

                    //Deliver products to the user.
                    //TODO:
                    //await self.updateCustomerProductStatus()

                    //Always finish a transaction.
                    await transaction.finish()
                } catch {
                    //StoreKit has a transaction that fails verification. Don't deliver content to the user.
                    print("Transaction failed verification")
                }
            }
        }
    }
    
    func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
        //Check whether the JWS passes StoreKit verification.
        switch result {
        case .unverified:
            //StoreKit parses the JWS, but it fails verification.
            throw StoreError.failedVerification
        case .verified(let safe):
            //The result is verified. Return the unwrapped value.
            return safe
        }
    }
    
}
GarySabo
  • 5,806
  • 5
  • 49
  • 124
  • Are you running on a real device? Have you logged into App Store on that device with a sandbox account ? – Paulw11 Oct 04 '22 at 21:57
  • Yes running on device and logged on what same Apple ID that I use for sandbox testing in Storekit 1 – GarySabo Oct 05 '22 at 01:11
  • Are you able to actually catch this error? I only see it printed to console - it doesn't get to the catch block – froggomad Nov 06 '22 at 17:20

2 Answers2

0

509: No active account

Means the user isn't signed in to the app store. Go to Settings -> Sign in to Your iPhone and sign in with a valid App Store or sandbox account

Edit: I see you are logged in on your device - that's strange. You mentioned you haven't linked your products. You need to fetch products from the App Store with something similar to the following. But I wouldn't expect you to see that specific error message...

    enum AppProduct: String, CaseIterable, Identifiable {
        case noAds = "MYUNIQUEIDENTIFIER_ESTABLISHEDIN_APPSTORECONNECT"

        var id: String {
            UUID().uuidString
        } // to make it Identifiable for use in a List

        static var allProductIds: [String] {
            return Self.allCases.map { $0.rawValue }
        } // convenience
    }


    @MainActor
    @discardableResult func fetchProducts() async throws -> [Product] {
        let products = try await Product.products(for: AppProduct.allProductIds) // this is the call that fetches products
        guard products.first != nil else { throw PurchaseError.unknown } // custom error
        self._storeProducts = products
        let fetchedProducts: [AppProduct] = products.compactMap {
            let appProduct = AppProduct(rawValue: $0.id)
            return appProduct
        }
        self.fetchedProducts = fetchedProducts
        try await checkPurchased()
        return products
    }

    private func checkPurchased() async throws {
        for product in _storeProducts {
            guard let state = await product.currentEntitlement else { continue }
            let transaction = try self.checkVerified(state)
            //Always finish a transaction.
            await transaction.finish()
        }
    }

I'm setting purchase state in checkVerified when verification passes...

froggomad
  • 1,747
  • 2
  • 17
  • 40
  • Thanks I actually burned a DST on this and they haven't been much help. I can load my products (I edited my question to include a gist of my code). But I am not seeing any active or inactive subscriptions and getting these console logs: `Error enumerating unfinished transactions for first transaction listener: Error Domain=ASDErrorDomain Code=509 "No active account" ...` and `Error enumerating all current transactions: Error Domain=ASDErrorDomain Code=509 "No active account" ` – GarySabo Nov 06 '22 at 18:35
0

For me it was that the Bundle id of my Xcode project didnt exactly match the one of appstoreconnect. I changed it on Xcode and didn't get updated on appstoreconnect.

The weird thing is that my config store file for my local test got sync just fine and everything worked.

In general (for sandbox testing with StoreKit 2) I would check of course: -You have created the app and in-app purchases on appstoreconnect. -You have agreed every agreement on appstoreconnect - Agreements, taxes, etc. -The product identifier and bundle ids match.

And thats what I got for trying to resolve this issue, and seeing other peoples problems and what worked for them. Hope someone get this useful!