SKDownload's contentURL is not nil but doesn't exist

Viewed 117

I'm building the In App Purchases part of my app using StoreKit and am facing a strange behaviour while purchasing and downloading content hosted by Apple.

This is the code in a nutshell:

SKPaymentQueue.default().add(delegate)
...
SKPaymentQueue.default().add(payment)

Delegate

func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
    transactions.forEach { transaction in
        switch transaction.transactionState {
        case .purchased, .restored:
            if transaction.downloads.isEmpty {
                SKPaymentQueue.default().finishTransaction(transaction)
                didPurchaseProduct(nil)
            } else {
                SKPaymentQueue.default().start(transaction.downloads)
            }

        case .failed:
            SKPaymentQueue.default().finishTransaction(transaction)
            didFailWithError(transaction.error)

        case .deferred: break
        case .purchasing: break
        @unknown default:
            fatalError("Unhandled case \(transaction.transactionState)")
        }
    }
}

func paymentQueue(_ queue: SKPaymentQueue, updatedDownloads downloads: [SKDownload]) {
    downloads.forEach { download in
        switch download.state {
        case .waiting: break
        case .active: break
        case .paused: break
        case .finished:
            didPurchaseProduct(download)
        case .failed: didFailWithError(nil)
        case .cancelled: didFailWithError(nil)
        }
    }
}

Download Processing

do {
    guard let url = download.contentURL else {
        promise(.failure(.noContentURL))
        return
    }

    print("download.contentURL =", url.absoluteString)
    print("download.contentURL exists:", FileManager.default.fileExists(atPath: url.path))

    let path = url.appendingPathComponent("Contents").appendingPathComponent("Content.json")
    let data = try Data(contentsOf: path, options: .mappedIfSafe)
    let package = try JSONDecoder().decode(CardPackage.self, from: data)

    promise(.success(.didProcessDownload(package)))

    SKPaymentQueue.default().finishTransaction(download.transaction)
} catch {
    print(error.localizedDescription)

    promise(.failure(.decodeErrorDidOccur))

    SKPaymentQueue.default().finishTransaction(download.transaction)
}

The problem I am facing is the following. The SKDownload attached to the transaction has a contentURL which is not nil but which correspond to a folder that doesn't exist. These are the printed logs

download.contentURL = file:///private/var/mobile/Containers/Data/Application/EE97399D-974F-417A-AA50-969E3C238693/Library/Caches/StoreKit/13484324297981572924/
download.contentURL exists: false

The file “Content.json” couldn’t be opened because there is no such file.

Does anyone have any idea why this is happening and how to fix it?

1 Answers

I encountered this same issue and I suspect the problem was that at some point in my development of the StoreKit integration I deleted the installed app. When the app was reinstalled it received a new container, and thus a new Caches directory, but the previously unfinished transaction's download still held the url pointing to the old install's container, which of course no longer existed.

There is no way to create a new download for the unfinished transaction.

The solution is to discard the inaccessible download by completing the transaction with

SKPaymentQueue.default().finishTransaction(download.transaction)

and then complete the StoreKit integration by implementing Restore Purchases.

When you restore purchases you will get a new transaction and a new download.

Presumably this is something that should never happen in a production environment, but if it did I guess the only appropriate action is to tell the user something went wrong and instruct them to Restore Purchases.

Related