summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2022-11-08 11:05:04 +0100
committerAndrej Mihajlov <and@mullvad.net>2022-11-08 11:05:04 +0100
commitb607ae0e0bec2aa1bfa237af965b1415a50483f8 (patch)
treeadc05d137c3a355b80e18335ae7902a210183bf7
parentdfb13c23d54d201530bc4e8c9d9ade27f5b9ca43 (diff)
parent9648250c7210de8967db8148f040be5d697cab8b (diff)
downloadmullvadvpn-b607ae0e0bec2aa1bfa237af965b1415a50483f8.tar.xz
mullvadvpn-b607ae0e0bec2aa1bfa237af965b1415a50483f8.zip
Merge branch 'reduce-code-duplication'
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj4
-rw-r--r--ios/MullvadVPN/DisplayChainedError.swift16
-rw-r--r--ios/MullvadVPN/StorePaymentManager/SendStoreReceiptOperation.swift109
-rw-r--r--ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift166
-rw-r--r--ios/MullvadVPN/StorePaymentManager/StoreReceipt.swift155
5 files changed, 180 insertions, 270 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index c73d0c820c..3e7b6261aa 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -309,7 +309,6 @@
58FBFBEA291622580020E046 /* MullvadREST.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 06799ABC28F98E1D00ACD94E /* MullvadREST.framework */; };
58FBFBF1291630700020E046 /* DurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FBFBF0291630700020E046 /* DurationTests.swift */; };
58FC040A27B3EE03001C21F0 /* TunnelMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FC040927B3EE03001C21F0 /* TunnelMonitor.swift */; };
- 58FD5BE724192A2C00112C88 /* StoreReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FD5BE624192A2B00112C88 /* StoreReceipt.swift */; };
58FD5BF024238EB300112C88 /* SKProduct+Formatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FD5BEF24238EB300112C88 /* SKProduct+Formatting.swift */; };
58FD5BF42428C67600112C88 /* InAppPurchaseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FD5BF32428C67600112C88 /* InAppPurchaseButton.swift */; };
58FEEB46260A028D00A621A8 /* GeoJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FEEB45260A028D00A621A8 /* GeoJSON.swift */; };
@@ -802,7 +801,6 @@
58FBFBE8291622580020E046 /* ExponentialBackoffTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExponentialBackoffTests.swift; sourceTree = "<group>"; };
58FBFBF0291630700020E046 /* DurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DurationTests.swift; sourceTree = "<group>"; };
58FC040927B3EE03001C21F0 /* TunnelMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelMonitor.swift; sourceTree = "<group>"; };
- 58FD5BE624192A2B00112C88 /* StoreReceipt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreReceipt.swift; sourceTree = "<group>"; };
58FD5BEF24238EB300112C88 /* SKProduct+Formatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SKProduct+Formatting.swift"; sourceTree = "<group>"; };
58FD5BF32428C67600112C88 /* InAppPurchaseButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchaseButton.swift; sourceTree = "<group>"; };
58FEEB45260A028D00A621A8 /* GeoJSON.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeoJSON.swift; sourceTree = "<group>"; };
@@ -1107,7 +1105,6 @@
5846227626E22A7C0035F7C2 /* StorePaymentManagerDelegate.swift */,
58FB865426E8BF3100F188BC /* StorePaymentManagerError.swift */,
5846227226E22A160035F7C2 /* StorePaymentObserver.swift */,
- 58FD5BE624192A2B00112C88 /* StoreReceipt.swift */,
5846227026E229F20035F7C2 /* StoreSubscription.swift */,
);
path = StorePaymentManager;
@@ -2239,7 +2236,6 @@
58677712290976FB006F721F /* SettingsInteractor.swift in Sources */,
58CE5E66224146200008646E /* LoginViewController.swift in Sources */,
58EF580B25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift in Sources */,
- 58FD5BE724192A2C00112C88 /* StoreReceipt.swift in Sources */,
5892A45E265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift in Sources */,
580909D32876D09A0078138D /* RevokedDeviceViewController.swift in Sources */,
5835B7CC233B76CB0096D79F /* TunnelManager.swift in Sources */,
diff --git a/ios/MullvadVPN/DisplayChainedError.swift b/ios/MullvadVPN/DisplayChainedError.swift
index 4bfa351f4f..c1ef9b463e 100644
--- a/ios/MullvadVPN/DisplayChainedError.swift
+++ b/ios/MullvadVPN/DisplayChainedError.swift
@@ -113,7 +113,7 @@ extension StorePaymentManagerError: DisplayChainedError {
case .noAccountSet:
return NSLocalizedString(
"NO_ACCOUNT_SET_ERROR",
- tableName: "AppStorePaymentManager",
+ tableName: "StorePaymentManager",
value: "Internal error: account is not set.",
comment: ""
)
@@ -125,7 +125,7 @@ extension StorePaymentManagerError: DisplayChainedError {
return String(
format: NSLocalizedString(
"INVALID_ACCOUNT_ERROR",
- tableName: "AppStorePaymentManager",
+ tableName: "StorePaymentManager",
value: "Cannot add credit to invalid account.",
comment: ""
), reason
@@ -136,7 +136,7 @@ extension StorePaymentManagerError: DisplayChainedError {
return String(
format: NSLocalizedString(
"VALIDATE_ACCOUNT_ERROR",
- tableName: "AppStorePaymentManager",
+ tableName: "StorePaymentManager",
value: "Failed to validate account token: %@",
comment: ""
), reason
@@ -147,7 +147,7 @@ extension StorePaymentManagerError: DisplayChainedError {
if readReceiptError is StoreReceiptNotFound {
return NSLocalizedString(
"RECEIPT_NOT_FOUND_ERROR",
- tableName: "AppStorePaymentManager",
+ tableName: "StorePaymentManager",
value: "AppStore receipt is not found on disk.",
comment: ""
)
@@ -155,7 +155,7 @@ extension StorePaymentManagerError: DisplayChainedError {
return String(
format: NSLocalizedString(
"REFRESH_RECEIPT_ERROR",
- tableName: "AppStorePaymentManager",
+ tableName: "StorePaymentManager",
value: "Cannot refresh the AppStore receipt: %@",
comment: ""
),
@@ -165,7 +165,7 @@ extension StorePaymentManagerError: DisplayChainedError {
return String(
format: NSLocalizedString(
"READ_RECEIPT_ERROR",
- tableName: "AppStorePaymentManager",
+ tableName: "StorePaymentManager",
value: "Cannot read the AppStore receipt from disk: %@",
comment: ""
),
@@ -177,13 +177,13 @@ extension StorePaymentManagerError: DisplayChainedError {
let reason = restError.errorChainDescription ?? ""
let errorFormat = NSLocalizedString(
"SEND_RECEIPT_ERROR",
- tableName: "AppStorePaymentManager",
+ tableName: "StorePaymentManager",
value: "Failed to send the receipt to server: %@",
comment: ""
)
let recoverySuggestion = NSLocalizedString(
"SEND_RECEIPT_RECOVERY_SUGGESTION",
- tableName: "AppStorePaymentManager",
+ tableName: "StorePaymentManager",
value: "Please retry by using the \"Restore purchases\" button.",
comment: ""
)
diff --git a/ios/MullvadVPN/StorePaymentManager/SendStoreReceiptOperation.swift b/ios/MullvadVPN/StorePaymentManager/SendStoreReceiptOperation.swift
index 0b360a7184..73e9788c2b 100644
--- a/ios/MullvadVPN/StorePaymentManager/SendStoreReceiptOperation.swift
+++ b/ios/MullvadVPN/StorePaymentManager/SendStoreReceiptOperation.swift
@@ -11,29 +11,32 @@ import MullvadLogging
import MullvadREST
import MullvadTypes
import Operations
+import StoreKit
class SendStoreReceiptOperation: ResultOperation<
REST.CreateApplePaymentResponse,
StorePaymentManagerError
-> {
+>, SKRequestDelegate {
private let apiProxy: REST.APIProxy
- private let accountToken: String
+ private let accountNumber: String
+
private let forceRefresh: Bool
private let receiptProperties: [String: Any]?
- private var fetchReceiptTask: Cancellable?
+ private var refreshRequest: SKReceiptRefreshRequest?
+
private var submitReceiptTask: Cancellable?
private let logger = Logger(label: "SendStoreReceiptOperation")
init(
apiProxy: REST.APIProxy,
- accountToken: String,
+ accountNumber: String,
forceRefresh: Bool,
receiptProperties: [String: Any]?,
completionHandler: @escaping CompletionHandler
) {
self.apiProxy = apiProxy
- self.accountToken = accountToken
+ self.accountNumber = accountNumber
self.forceRefresh = forceRefresh
self.receiptProperties = receiptProperties
@@ -45,42 +48,98 @@ class SendStoreReceiptOperation: ResultOperation<
}
override func operationDidCancel() {
- fetchReceiptTask?.cancel()
- fetchReceiptTask = nil
+ refreshRequest?.cancel()
+ refreshRequest = nil
submitReceiptTask?.cancel()
submitReceiptTask = nil
}
override func main() {
- fetchReceiptTask = StoreReceipt.fetch(
- forceRefresh: forceRefresh,
- receiptProperties: receiptProperties
- ) { completion in
- switch completion {
- case let .success(receiptData):
- self.sendReceipt(receiptData)
+ // Pull receipt from AppStore if requested.
+ guard !forceRefresh else {
+ startRefreshRequest()
+ return
+ }
- case let .failure(error):
+ // Read AppStore receipt from disk.
+ do {
+ let data = try readReceiptFromDisk()
+
+ sendReceipt(data)
+ } catch is StoreReceiptNotFound {
+ // Pull receipt from AppStore if it's not cached locally.
+ startRefreshRequest()
+ } catch {
+ logger.error(
+ error: error,
+ message: "Failed to read the AppStore receipt."
+ )
+ finish(completion: .failure(.readReceipt(error)))
+ }
+ }
+
+ // - MARK: SKRequestDelegate
+
+ func requestDidFinish(_ request: SKRequest) {
+ dispatchQueue.async {
+ do {
+ let data = try self.readReceiptFromDisk()
+
+ self.sendReceipt(data)
+ } catch {
self.logger.error(
error: error,
- message: "Failed to fetch the AppStore receipt."
+ message: "Failed to read the AppStore receipt after refresh."
)
self.finish(completion: .failure(.readReceipt(error)))
-
- case .cancelled:
- self.finish(completion: .cancelled)
}
}
}
+ func request(_ request: SKRequest, didFailWithError error: Error) {
+ dispatchQueue.async {
+ self.logger.error(
+ error: error,
+ message: "Failed to refresh the AppStore receipt."
+ )
+ self.finish(completion: .failure(.readReceipt(error)))
+ }
+ }
+
+ // MARK: - Private
+
+ private func startRefreshRequest() {
+ let refreshRequest = SKReceiptRefreshRequest(receiptProperties: receiptProperties)
+ refreshRequest.delegate = self
+ refreshRequest.start()
+
+ self.refreshRequest = refreshRequest
+ }
+
+ private func readReceiptFromDisk() throws -> Data {
+ guard let appStoreReceiptURL = Bundle.main.appStoreReceiptURL else {
+ throw StoreReceiptNotFound()
+ }
+
+ do {
+ return try Data(contentsOf: appStoreReceiptURL)
+ } catch let error as CocoaError
+ where error.code == .fileReadNoSuchFile || error.code == .fileNoSuchFile
+ {
+ throw StoreReceiptNotFound()
+ } catch {
+ throw error
+ }
+ }
+
private func sendReceipt(_ receiptData: Data) {
submitReceiptTask = apiProxy.createApplePayment(
- accountNumber: accountToken,
+ accountNumber: accountNumber,
receiptString: receiptData,
retryStrategy: .noRetry
- ) { result in
- switch result {
+ ) { completion in
+ switch completion {
case let .success(response):
self.logger.info(
"""
@@ -105,3 +164,9 @@ class SendStoreReceiptOperation: ResultOperation<
}
}
}
+
+struct StoreReceiptNotFound: LocalizedError {
+ var errorDescription: String? {
+ return "AppStore receipt file does not exist on disk."
+ }
+}
diff --git a/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift b/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift
index 61b626cd33..fa8963f5b6 100644
--- a/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift
+++ b/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift
@@ -13,7 +13,7 @@ import MullvadTypes
import Operations
import StoreKit
-class StorePaymentManager: NSObject, SKPaymentTransactionObserver {
+final class StorePaymentManager: NSObject, SKPaymentTransactionObserver {
private enum OperationCategory {
static let sendStoreReceipt = "StorePaymentManager.sendStoreReceipt"
static let productsRequest = "StorePaymentManager.productsRequest"
@@ -124,67 +124,38 @@ class StorePaymentManager: NSObject, SKPaymentTransactionObserver {
return operation
}
- func addPayment(_ payment: SKPayment, for accountToken: String) {
- var task: Cancellable?
- let backgroundTaskIdentifier = UIApplication.shared
- .beginBackgroundTask(withName: "Validate account token") {
- task?.cancel()
- }
-
+ func addPayment(_ payment: SKPayment, for accountNumber: String) {
// Validate account token before adding new payment to the queue.
- task = accountsProxy.getAccountData(
- accountNumber: accountToken,
- retryStrategy: .default
- ) { completion in
- dispatchPrecondition(condition: .onQueue(.main))
-
- switch completion {
- case .success:
- self.associateAccountToken(accountToken, and: payment)
- self.paymentQueue.add(payment)
-
- case let .failure(error):
- let event = StorePaymentEvent.failure(
- StorePaymentFailure(
- transaction: nil,
- payment: payment,
- accountNumber: accountToken,
- error: .validateAccount(error)
- )
- )
-
- self.observerList.forEach { observer in
- observer.storePaymentManager(self, didReceiveEvent: event)
- }
-
- case .cancelled:
+ validateAccount(accountNumber: accountNumber) { error in
+ if let error = error {
let event = StorePaymentEvent.failure(
StorePaymentFailure(
transaction: nil,
payment: payment,
- accountNumber: accountToken,
- error: .validateAccount(.network(URLError(.cancelled)))
+ accountNumber: accountNumber,
+ error: error
)
)
self.observerList.forEach { observer in
observer.storePaymentManager(self, didReceiveEvent: event)
}
+ } else {
+ self.associateAccountToken(accountNumber, and: payment)
+ self.paymentQueue.add(payment)
}
-
- UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
}
}
func restorePurchases(
- for accountToken: String,
+ for accountNumber: String,
completionHandler: @escaping (OperationCompletion<
REST.CreateApplePaymentResponse,
StorePaymentManagerError
>) -> Void
) -> Cancellable {
return sendStoreReceipt(
- accountToken: accountToken,
+ accountNumber: accountNumber,
forceRefresh: true,
completionHandler: completionHandler
)
@@ -209,18 +180,59 @@ class StorePaymentManager: NSObject, SKPaymentTransactionObserver {
}
}
+ private func validateAccount(
+ accountNumber: String,
+ completionHandler: @escaping (StorePaymentManagerError?) -> Void
+ ) {
+ let accountOperation = ResultBlockOperation<
+ REST.AccountData,
+ REST.Error
+ >(dispatchQueue: .main) { op in
+ let task = self.accountsProxy.getAccountData(
+ accountNumber: accountNumber,
+ retryStrategy: .default
+ ) { completion in
+ op.finish(completion: completion)
+ }
+
+ op.addCancellationBlock {
+ task.cancel()
+ }
+ }
+
+ accountOperation.addObserver(BackgroundObserver(
+ application: .shared,
+ name: "Validate account number",
+ cancelUponExpiration: false
+ ))
+
+ accountOperation.completionQueue = .main
+ accountOperation.completionHandler = { completion in
+ var error: REST.Error?
+
+ if case .cancelled = completion {
+ error = .network(URLError(.cancelled))
+ } else {
+ error = completion.error
+ }
+
+ completionHandler(error.map { .validateAccount($0) })
+ }
+
+ operationQueue.addOperation(accountOperation)
+ }
+
private func sendStoreReceipt(
- accountToken: String,
+ accountNumber: String,
forceRefresh: Bool,
completionHandler: @escaping (OperationCompletion<
REST.CreateApplePaymentResponse,
StorePaymentManagerError
- >)
- -> Void
+ >) -> Void
) -> Cancellable {
let operation = SendStoreReceiptOperation(
apiProxy: apiProxy,
- accountToken: accountToken,
+ accountNumber: accountNumber,
forceRefresh: forceRefresh,
receiptProperties: nil,
completionHandler: completionHandler
@@ -234,9 +246,7 @@ class StorePaymentManager: NSObject, SKPaymentTransactionObserver {
)
)
- operation.addCondition(
- MutuallyExclusive(category: OperationCategory.sendStoreReceipt)
- )
+ operation.addCondition(MutuallyExclusive(category: OperationCategory.sendStoreReceipt))
operationQueue.addOperation(operation)
@@ -283,37 +293,31 @@ class StorePaymentManager: NSObject, SKPaymentTransactionObserver {
private func didFailPurchase(transaction: SKPaymentTransaction) {
paymentQueue.finishTransaction(transaction)
+ let paymentFailure: StorePaymentFailure
+
if let accountToken = deassociateAccountToken(transaction.payment) {
- let event = StorePaymentEvent.failure(
- StorePaymentFailure(
- transaction: transaction,
- payment: transaction.payment,
- accountNumber: accountToken,
- error: .storePayment(transaction.error!)
- )
+ paymentFailure = StorePaymentFailure(
+ transaction: transaction,
+ payment: transaction.payment,
+ accountNumber: accountToken,
+ error: .storePayment(transaction.error!)
)
-
- observerList.forEach { observer in
- observer.storePaymentManager(self, didReceiveEvent: event)
- }
} else {
- let event = StorePaymentEvent.failure(
- StorePaymentFailure(
- transaction: transaction,
- payment: transaction.payment,
- accountNumber: nil,
- error: .noAccountSet
- )
+ paymentFailure = StorePaymentFailure(
+ transaction: transaction,
+ payment: transaction.payment,
+ accountNumber: nil,
+ error: .noAccountSet
)
+ }
- observerList.forEach { observer in
- observer.storePaymentManager(self, didReceiveEvent: event)
- }
+ observerList.forEach { observer in
+ observer.storePaymentManager(self, didReceiveEvent: .failure(paymentFailure))
}
}
private func didFinishOrRestorePurchase(transaction: SKPaymentTransaction) {
- guard let accountToken = deassociateAccountToken(transaction.payment) else {
+ guard let accountNumber = deassociateAccountToken(transaction.payment) else {
let event = StorePaymentEvent.failure(
StorePaymentFailure(
transaction: transaction,
@@ -329,35 +333,35 @@ class StorePaymentManager: NSObject, SKPaymentTransactionObserver {
return
}
- _ = sendStoreReceipt(accountToken: accountToken, forceRefresh: false) { completion in
+ _ = sendStoreReceipt(accountNumber: accountNumber, forceRefresh: false) { completion in
+ var event: StorePaymentEvent?
+
switch completion {
case let .success(response):
self.paymentQueue.finishTransaction(transaction)
- let event = StorePaymentEvent.finished(StorePaymentCompletion(
+ event = StorePaymentEvent.finished(StorePaymentCompletion(
transaction: transaction,
- accountNumber: accountToken,
+ accountNumber: accountNumber,
serverResponse: response
))
- self.observerList.forEach { observer in
- observer.storePaymentManager(self, didReceiveEvent: event)
- }
-
case let .failure(error):
- let event = StorePaymentEvent.failure(StorePaymentFailure(
+ event = StorePaymentEvent.failure(StorePaymentFailure(
transaction: transaction,
payment: transaction.payment,
- accountNumber: accountToken,
+ accountNumber: accountNumber,
error: error
))
+ case .cancelled:
+ break
+ }
+
+ if let event = event {
self.observerList.forEach { observer in
observer.storePaymentManager(self, didReceiveEvent: event)
}
-
- case .cancelled:
- break
}
}
}
diff --git a/ios/MullvadVPN/StorePaymentManager/StoreReceipt.swift b/ios/MullvadVPN/StorePaymentManager/StoreReceipt.swift
deleted file mode 100644
index 93731ba7f9..0000000000
--- a/ios/MullvadVPN/StorePaymentManager/StoreReceipt.swift
+++ /dev/null
@@ -1,155 +0,0 @@
-//
-// StoreReceipt.swift
-// MullvadVPN
-//
-// Created by pronebird on 11/03/2020.
-// Copyright © 2020 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-import MullvadREST
-import MullvadTypes
-import Operations
-import StoreKit
-
-struct StoreReceiptNotFound: LocalizedError {
- var errorDescription: String? {
- return "AppStore receipt file does not exist on disk."
- }
-}
-
-enum StoreReceipt {
- /// Internal operation queue.
- private static let operationQueue: OperationQueue = {
- let queue = AsyncOperationQueue()
- queue.name = "StoreReceiptQueue"
- queue.maxConcurrentOperationCount = 1
- return queue
- }()
-
- /// Read AppStore receipt from disk or refresh it from AppStore if it's missing.
- /// This call may trigger a sign in with AppStore prompt to appear.
- static func fetch(
- forceRefresh: Bool = false,
- receiptProperties: [String: Any]? = nil,
- completionHandler: @escaping (OperationCompletion<Data, Error>) -> Void
- ) -> Cancellable {
- let operation = FetchAppStoreReceiptOperation(
- forceRefresh: forceRefresh,
- receiptProperties: receiptProperties,
- completionHandler: completionHandler
- )
-
- operation.addObserver(
- BackgroundObserver(
- application: .shared,
- name: "Fetch AppStore receipt",
- cancelUponExpiration: true
- )
- )
-
- operationQueue.addOperation(operation)
-
- return operation
- }
-}
-
-private class FetchAppStoreReceiptOperation: ResultOperation<Data, Error>, SKRequestDelegate {
- private var request: SKReceiptRefreshRequest?
- private let receiptProperties: [String: Any]?
- private let forceRefresh: Bool
-
- init(
- forceRefresh: Bool,
- receiptProperties: [String: Any]?,
- completionHandler: @escaping (Completion) -> Void
- ) {
- self.forceRefresh = forceRefresh
- self.receiptProperties = receiptProperties
-
- super.init(
- dispatchQueue: .main,
- completionQueue: .main,
- completionHandler: completionHandler
- )
- }
-
- override func main() {
- // Pull receipt from AppStore if requested.
- guard !forceRefresh else {
- startRefreshRequest()
- return
- }
-
- // Read AppStore receipt from disk.
- do {
- let data = try readReceiptFromDisk()
-
- finish(completion: .success(data))
- } catch is StoreReceiptNotFound {
- // Pull receipt from AppStore if it's not cached locally.
- startRefreshRequest()
- } catch {
- finish(completion: .failure(error))
- }
- }
-
- override func operationDidCancel() {
- request?.cancel()
- }
-
- // - MARK: SKRequestDelegate
-
- func requestDidFinish(_ request: SKRequest) {
- dispatchQueue.async {
- self.didFinishRefreshRequest(error: nil)
- }
- }
-
- func request(_ request: SKRequest, didFailWithError error: Error) {
- dispatchQueue.async {
- self.didFinishRefreshRequest(error: error)
- }
- }
-
- // - MARK: Private
-
- private func startRefreshRequest() {
- let request = SKReceiptRefreshRequest(receiptProperties: receiptProperties)
- request.delegate = self
- request.start()
-
- self.request = request
- }
-
- private func didFinishRefreshRequest(error: Error?) {
- guard !isCancelled else {
- finish(completion: .cancelled)
- return
- }
-
- if let error = error {
- finish(completion: .failure(error))
- } else {
- let result = Result { try readReceiptFromDisk() }
-
- finish(completion: OperationCompletion(result: result))
- }
- }
-
- private func readReceiptFromDisk() throws -> Data {
- guard let appStoreReceiptURL = Bundle.main.appStoreReceiptURL else {
- throw StoreReceiptNotFound()
- }
-
- do {
- return try Data(contentsOf: appStoreReceiptURL)
- } catch let error as CocoaError
- where error.code == .fileReadNoSuchFile || error.code == .fileNoSuchFile
- {
- throw StoreReceiptNotFound()
- } catch {
- throw error
- }
- }
-}