Skip to content

Commit

Permalink
ErrorUtils.missingReceiptFileError: added receipt URL userInfo co…
Browse files Browse the repository at this point in the history
…ntext (#2650)

Added `rc_receipt_url` and `rc_receipt_file_exists`. This is useful when
debugging issues like #2558.
This goes from this:
> [Purchases] - ERROR: 💰 Product purchase for 'rc_1099_1w' failed with
error: Error Domain=RevenueCat.ErrorCode Code=9 "The receipt is
missing." UserInfo={NSLocalizedDescription=The receipt is missing.,
readable_error_code=MISSING_RECEIPT_FILE,
source_function=handlePurchasedTransaction(_:data:completion:),
source_file=RevenueCat/TransactionPoster.swift:96}

To this:
> [Purchases] - ERROR: 💰 Product purchase for 'rc_1099_1w' failed with
error: Error Domain=RevenueCat.ErrorCode Code=9 "The receipt is
missing." UserInfo={NSLocalizedDescription=The receipt is missing.,
readable_error_code=MISSING_RECEIPT_FILE,
source_function=handlePurchasedTransaction(_:data:completion:),
rc_receipt_url=file:///.../Build/Products/Debug/...app/Contents/_MASReceipt/receipt,
rc_receipt_file_exists=true,
source_file=RevenueCat/TransactionPoster.swift:96}

In this case, we can see the file does exist, so it must be empty.
  • Loading branch information
NachoSoto authored Jun 14, 2023
1 parent 442b683 commit e2534f0
Show file tree
Hide file tree
Showing 13 changed files with 145 additions and 88 deletions.
10 changes: 6 additions & 4 deletions Sources/Error Handling/BackendError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ enum BackendError: Error, Equatable {
case networkError(NetworkError)
case missingAppUserID(Source)
case emptySubscriberAttributes(Source)
case missingReceiptFile(Source)
case missingReceiptFile(URL?, Source)
case missingTransactionProductIdentifier(Source)
case missingCachedCustomerInfo(Source)
case unexpectedBackendResponse(UnexpectedBackendResponseError, extraContext: String?, Source)
Expand Down Expand Up @@ -49,9 +49,10 @@ extension BackendError {
}

static func missingReceiptFile(
_ receiptURL: URL?,
file: String = #fileID, function: String = #function, line: UInt = #line
) -> Self {
return .missingReceiptFile(.init(file: file, function: function, line: line))
return .missingReceiptFile(receiptURL, .init(file: file, function: function, line: line))
}

static func missingCachedCustomerInfo(
Expand Down Expand Up @@ -89,8 +90,9 @@ extension BackendError: PurchasesErrorConvertible {
functionName: source.function,
line: source.line)

case let .missingReceiptFile(source):
return ErrorUtils.missingReceiptFileError(fileName: source.file,
case let .missingReceiptFile(receiptURL, source):
return ErrorUtils.missingReceiptFileError(receiptURL,
fileName: source.file,
functionName: source.function,
line: source.line)

Expand Down
13 changes: 13 additions & 0 deletions Sources/Error Handling/ErrorUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,22 @@ enum ErrorUtils {
* sandbox or if there are no previous purchases.
*/
static func missingReceiptFileError(
_ receiptURL: URL?,
fileName: String = #fileID, functionName: String = #function, line: UInt = #line
) -> PurchasesError {
let fileExists: Bool = {
if let receiptURL = receiptURL {
return FileManager.default.fileExists(atPath: receiptURL.path)
} else {
return false
}
}()

return error(with: ErrorCode.missingReceiptFileError,
extraUserInfo: [
"rc_receipt_url": receiptURL?.absoluteString ?? "<null>",
"rc_receipt_file_exists": fileExists
],
fileName: fileName, functionName: functionName, line: line)
}

Expand Down
8 changes: 4 additions & 4 deletions Sources/Purchasing/Purchases/PurchasesOrchestrator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -233,10 +233,10 @@ final class PurchasesOrchestrator {
return
}

receiptFetcher.receiptData(refreshPolicy: .onlyIfEmpty) { receiptData in
receiptFetcher.receiptData(refreshPolicy: .onlyIfEmpty) { receiptData, receiptURL in
guard let receiptData = receiptData,
!receiptData.isEmpty else {
completion(.failure(ErrorUtils.missingReceiptFileError()))
completion(.failure(ErrorUtils.missingReceiptFileError(receiptURL)))
return
}

Expand Down Expand Up @@ -944,7 +944,7 @@ private extension PurchasesOrchestrator {

// Refresh the receipt and post to backend, this will allow the transactions to be transferred.
// https://rev.cat/apple-restoring-purchased-products
self.receiptFetcher.receiptData(refreshPolicy: receiptRefreshPolicy) { receiptData in
self.receiptFetcher.receiptData(refreshPolicy: receiptRefreshPolicy) { receiptData, receiptURL in
guard let receiptData = receiptData,
!receiptData.isEmpty else {
if self.systemInfo.isSandbox {
Expand All @@ -953,7 +953,7 @@ private extension PurchasesOrchestrator {

if let completion = completion {
self.operationDispatcher.dispatchOnMainThread {
completion(.failure(ErrorUtils.missingReceiptFileError()))
completion(.failure(ErrorUtils.missingReceiptFileError(receiptURL)))
}
}
return
Expand Down
4 changes: 2 additions & 2 deletions Sources/Purchasing/Purchases/TransactionPoster.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ final class TransactionPoster: TransactionPosterType {
completion: @escaping CustomerAPI.CustomerInfoResponseHandler) {
self.receiptFetcher.receiptData(
refreshPolicy: self.refreshRequestPolicy(forProductIdentifier: transaction.productIdentifier)
) { receiptData in
) { receiptData, receiptURL in
if let receiptData = receiptData, !receiptData.isEmpty {
self.fetchProductsAndPostReceipt(
transaction: transaction,
Expand All @@ -93,7 +93,7 @@ final class TransactionPoster: TransactionPosterType {
)
} else {
self.handleReceiptPost(withTransaction: transaction,
result: .failure(.missingReceiptFile()),
result: .failure(.missingReceiptFile(receiptURL)),
subscriberAttributes: nil,
completion: completion)
}
Expand Down
27 changes: 15 additions & 12 deletions Sources/Purchasing/ReceiptFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ class ReceiptFetcher {
self.clock = clock
}

func receiptData(refreshPolicy: ReceiptRefreshPolicy, completion: @escaping (Data?) -> Void) {
func receiptData(refreshPolicy: ReceiptRefreshPolicy, completion: @escaping (Data?, URL?) -> Void) {
let receiptURL = self.receiptURL

switch refreshPolicy {
case .always:
if self.shouldThrottleRefreshRequest() {
Expand All @@ -61,7 +63,7 @@ class ReceiptFetcher {
Logger.debug(Strings.receipt.refreshing_empty_receipt)
self.refreshReceipt(completion)
} else {
completion(receiptData)
completion(receiptData, receiptURL)
}

case let .retryUntilProductIsFound(productIdentifier, maximumRetries, sleepDuration):
Expand All @@ -77,14 +79,14 @@ class ReceiptFetcher {
}

case .never:
completion(self.receiptData())
completion(self.receiptData(), receiptURL)
}
}

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *)
func receiptData(refreshPolicy: ReceiptRefreshPolicy) async -> Data? {
return await withCheckedContinuation { continuation in
self.receiptData(refreshPolicy: refreshPolicy) { result in
self.receiptData(refreshPolicy: refreshPolicy) { result, _ in
continuation.resume(returning: result)
}
}
Expand Down Expand Up @@ -152,20 +154,20 @@ private extension ReceiptFetcher {
}
}

func refreshReceipt(_ completion: @escaping (Data) -> Void) {
func refreshReceipt(_ completion: @escaping (Data, URL?) -> Void) {
self.lastReceiptRefreshRequest.value = self.clock.now

self.requestFetcher.fetchReceiptData {
completion(self.receiptData() ?? Data())
completion(self.receiptData() ?? Data(), self.receiptURL)
}
}

/// `async` version of `refreshReceipt(_:)`
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *)
func refreshReceipt() async -> Data {
func refreshReceipt() async -> (Data, URL?) {
await withCheckedContinuation { continuation in
self.refreshReceipt {
continuation.resume(returning: $0)
continuation.resume(returning: ($0, $1))
}
}
}
Expand All @@ -176,13 +178,14 @@ private extension ReceiptFetcher {
untilProductIsFound productIdentifier: String,
maximumRetries: Int,
sleepDuration: DispatchTimeInterval
) async -> Data {
) async -> (Data, URL?) {
var retries = 0
var data: Data = .init()
var receiptURL: URL?

repeat {
retries += 1
data = await self.refreshReceipt()
(data, receiptURL) = await self.refreshReceipt()

if !data.isEmpty {
do {
Expand All @@ -192,7 +195,7 @@ private extension ReceiptFetcher {
}.value

if receipt.containsActivePurchase(forProductIdentifier: productIdentifier) {
return data
return (data, receiptURL)
} else {
Logger.appleWarning(Strings.receipt.local_receipt_missing_purchase(
receipt,
Expand All @@ -208,7 +211,7 @@ private extension ReceiptFetcher {
try? await Task.sleep(nanoseconds: UInt64(sleepDuration.nanoseconds))
} while retries <= maximumRetries && !Task.isCancelled

return data
return (data, receiptURL)
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ class TrialOrIntroPriceEligibilityChecker: TrialOrIntroPriceEligibilityCheckerTy
completion: @escaping ReceiveIntroEligibilityBlock) {
// We don't want to refresh receipts because it will likely prompt the user for their credentials,
// and intro eligibility is triggered programmatically.
self.receiptFetcher.receiptData(refreshPolicy: .never) { data in
self.receiptFetcher.receiptData(refreshPolicy: .never) { data, _ in
if #available(iOS 12.0, macOS 10.14, tvOS 12.0, watchOS 6.2, *),
let data = data {
self.sk1CheckEligibility(with: data,
Expand Down
2 changes: 1 addition & 1 deletion Tests/StoreKitUnitTests/PurchasesOrchestratorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -844,7 +844,7 @@ class PurchasesOrchestratorTests: StoreKitConfigTestCase {

self.setUpStoreKit2Listener()

let expectedError: BackendError = .missingReceiptFile()
let expectedError: BackendError = .missingReceiptFile(nil)

self.customerInfoManager.stubbedCachedCustomerInfoResult = self.mockCustomerInfo
self.backend.stubbedPostReceiptResult = .failure(expectedError)
Expand Down
2 changes: 1 addition & 1 deletion Tests/UnitTests/Misc/PurchasesDiagnosticsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ class PurchasesDiagnosticsTests: TestCase {
// MARK: - Errors

func testUnknownError() {
let underlyingError = ErrorUtils.missingReceiptFileError()
let underlyingError = ErrorUtils.missingReceiptFileError(nil)
let error = PurchasesDiagnostics.Error.unknown(underlyingError)

expect(error.errorUserInfo[NSUnderlyingErrorKey] as? NSError).to(matchError(underlyingError))
Expand Down
19 changes: 10 additions & 9 deletions Tests/UnitTests/Mocks/MockReceiptFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,20 @@ class MockReceiptFetcher: ReceiptFetcher {
var receiptDataTimesCalled = 0
var receiptDataReceivedRefreshPolicy: ReceiptRefreshPolicy?
var mockReceiptData: Data = .init(1...3)
var mockReceiptURL: URL?

override func receiptData(refreshPolicy: ReceiptRefreshPolicy, completion: @escaping ((Data?) -> Void)) {
receiptDataReceivedRefreshPolicy = refreshPolicy
receiptDataCalled = true
receiptDataTimesCalled += 1
if shouldReturnReceipt {
if shouldReturnZeroBytesReceipt {
completion(Data())
override func receiptData(refreshPolicy: ReceiptRefreshPolicy, completion: @escaping ((Data?, URL?) -> Void)) {
self.receiptDataReceivedRefreshPolicy = refreshPolicy
self.receiptDataCalled = true
self.receiptDataTimesCalled += 1
if self.shouldReturnReceipt {
if self.shouldReturnZeroBytesReceipt {
completion(Data(), self.mockReceiptURL)
} else {
completion(self.mockReceiptData)
completion(self.mockReceiptData, self.mockReceiptURL)
}
} else {
completion(nil)
completion(nil, self.mockReceiptURL)
}
}

Expand Down
2 changes: 1 addition & 1 deletion Tests/UnitTests/Networking/BackendErrorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class BackendErrorTests: BaseErrorTests {
}

func testMissingReceiptFile() {
let error: BackendError = .missingReceiptFile()
let error: BackendError = .missingReceiptFile(nil)

verifyPurchasesError(error, expectedCode: .missingReceiptFileError)
}
Expand Down
42 changes: 42 additions & 0 deletions Tests/UnitTests/Purchasing/ErrorUtilsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,31 @@ class ErrorUtilsTests: TestCase {
super.tearDown()
}

func testReceiptErrorWithNoURL() {
let error = ErrorUtils.missingReceiptFileError(nil)
expect(error).to(matchError(ErrorCode.missingReceiptFileError))
expect(error.userInfo["rc_receipt_url"] as? String) == "<null>"
expect(error.userInfo["rc_receipt_file_exists"] as? Bool) == false
}

func testReceiptErrorWithMissingReceipt() {
let url = URL(string: "file://does_not_exist")!

let error = ErrorUtils.missingReceiptFileError(url)
expect(error).to(matchError(ErrorCode.missingReceiptFileError))
expect(error.userInfo["rc_receipt_url"] as? String) == url.absoluteString
expect(error.userInfo["rc_receipt_file_exists"] as? Bool) == false
}

func testReceiptErrorWithEmptyReceipt() {
let url = Self.createEmptyFile()

let error = ErrorUtils.missingReceiptFileError(url)
expect(error).to(matchError(ErrorCode.missingReceiptFileError))
expect(error.userInfo["rc_receipt_url"] as? String) == url.absoluteString
expect(error.userInfo["rc_receipt_file_exists"] as? Bool) == true
}

func testPublicErrorsCanBeConvertedToErrorCode() throws {
let error = ErrorUtils.customerInfoError().asPublicError
let errorCode = try XCTUnwrap(error as? ErrorCode, "Error couldn't be converted to ErrorCode")
Expand Down Expand Up @@ -267,3 +292,20 @@ class ErrorUtilsTests: TestCase {
}

}

private extension ErrorUtilsTests {

static func createEmptyFile() -> URL {
let fileManager = FileManager.default
let url = fileManager.temporaryDirectory.appendingPathComponent(UUID().uuidString)

expect(fileManager.createFile(atPath: url.path, contents: nil, attributes: nil))
.to(
beTrue(),
description: "Failed creating file"
)

return url
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class TransactionPosterTests: TestCase {
}

func testHandlePurchasedTransactionWithMissingReceipt() throws {
self.receiptFetcher.mockReceiptURL = URL(string: "file://receipt_file")!
self.receiptFetcher.shouldReturnReceipt = false

let result = try self.handleTransaction(
Expand All @@ -47,7 +48,7 @@ class TransactionPosterTests: TestCase {
)
)
expect(result).to(beFailure())
expect(result.error) == BackendError.missingReceiptFile()
expect(result.error) == BackendError.missingReceiptFile(self.receiptFetcher.mockReceiptURL)
}

func testHandlePurchasedTransaction() throws {
Expand Down
Loading

0 comments on commit e2534f0

Please sign in to comment.