summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2022-03-25 10:14:40 +0100
committerAndrej Mihajlov <and@mullvad.net>2022-03-25 10:14:40 +0100
commitf05337f58b06401e8642a23c3935d876bfb7e526 (patch)
tree17aa8b64183df84e2110b919833ca52ba949b868
parent3755fd206a8c2220a0f5ebdfc6436338022c32da (diff)
parentd6aa361f5e0fc9280c70548a54c5379ce18c4c1a (diff)
downloadmullvadvpn-f05337f58b06401e8642a23c3935d876bfb7e526.tar.xz
mullvadvpn-f05337f58b06401e8642a23c3935d876bfb7e526.zip
Merge branch 'validate-account-before-purchase'
-rw-r--r--ios/CHANGELOG.md3
-rw-r--r--ios/MullvadVPN/Account.swift2
-rw-r--r--ios/MullvadVPN/AccountViewController.swift4
-rw-r--r--ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift48
-rw-r--r--ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManagerError.swift5
-rw-r--r--ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentObserver.swift4
-rw-r--r--ios/MullvadVPN/DisplayChainedError.swift25
-rw-r--r--ios/MullvadVPN/en.lproj/AppStorePaymentManager.strings6
8 files changed, 78 insertions, 19 deletions
diff --git a/ios/CHANGELOG.md b/ios/CHANGELOG.md
index 6b18d854f1..59e0e12f6f 100644
--- a/ios/CHANGELOG.md
+++ b/ios/CHANGELOG.md
@@ -32,6 +32,9 @@ Line wrap the file at 100 chars. Th
- Delete leftover settings in Keychain during login. WireGuard keys will be removed from
server too if old settings can be read. This is usually the case when uninstalling the app and
then reinstalling it without logging out first.
+- Validate account token before charging user (in-app purchases). Safeguards from trying to add
+ credits on accounts that no longer exist on our backend. Usually the case with newly created
+ accounts that went stale.
## [2022.1] - 2022-02-15
diff --git a/ios/MullvadVPN/Account.swift b/ios/MullvadVPN/Account.swift
index 3d6ac59d29..f6f0ab141a 100644
--- a/ios/MullvadVPN/Account.swift
+++ b/ios/MullvadVPN/Account.swift
@@ -259,7 +259,7 @@ extension Account: AppStorePaymentObserver {
paymentManager.addPaymentObserver(self)
}
- func appStorePaymentManager(_ manager: AppStorePaymentManager, transaction: SKPaymentTransaction, accountToken: String?, didFailWithError error: AppStorePaymentManager.Error) {
+ func appStorePaymentManager(_ manager: AppStorePaymentManager, transaction: SKPaymentTransaction?, payment: SKPayment, accountToken: String?, didFailWithError error: AppStorePaymentManager.Error) {
// no-op
}
diff --git a/ios/MullvadVPN/AccountViewController.swift b/ios/MullvadVPN/AccountViewController.swift
index 34cc20345c..88a34167f5 100644
--- a/ios/MullvadVPN/AccountViewController.swift
+++ b/ios/MullvadVPN/AccountViewController.swift
@@ -319,7 +319,7 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO
// MARK: - AppStorePaymentObserver
- func appStorePaymentManager(_ manager: AppStorePaymentManager, transaction: SKPaymentTransaction, accountToken: String?, didFailWithError error: AppStorePaymentManager.Error) {
+ func appStorePaymentManager(_ manager: AppStorePaymentManager, transaction: SKPaymentTransaction?, payment: SKPayment, accountToken: String?, didFailWithError error: AppStorePaymentManager.Error) {
let alertController = UIAlertController(
title: NSLocalizedString(
"CANNOT_COMPLETE_PURCHASE_ALERT_TITLE",
@@ -343,7 +343,7 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO
alertPresenter.enqueue(alertController, presentingController: self)
- if transaction.payment == pendingPayment {
+ if payment == pendingPayment {
compoundInteractionRestriction.decrease(animated: true)
}
}
diff --git a/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift b/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift
index 709ebff131..8c0567c777 100644
--- a/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift
+++ b/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift
@@ -109,12 +109,33 @@ class AppStorePaymentManager: NSObject, SKPaymentTransactionObserver {
}
func addPayment(_ payment: SKPayment, for accountToken: String) {
- if Thread.isMainThread {
- _addPayment(payment, for: accountToken)
- } else {
- DispatchQueue.main.async {
- self._addPayment(payment, for: accountToken)
+ var cancellableTask: Cancellable?
+ let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "Validate account token") {
+ cancellableTask?.cancel()
+ }
+
+ // Validate account token before adding new payment to the queue.
+ cancellableTask = REST.Client.shared.getAccountExpiry(token: accountToken, retryStrategy: .default) { result in
+ dispatchPrecondition(condition: .onQueue(.main))
+
+ switch result {
+ case .success:
+ self.associateAccountToken(accountToken, and: payment)
+ self.paymentQueue.add(payment)
+
+ case .failure(let error):
+ self.observerList.forEach { observer in
+ observer.appStorePaymentManager(
+ self,
+ transaction: nil,
+ payment: payment,
+ accountToken: accountToken,
+ didFailWithError: .validateAccount(error)
+ )
+ }
}
+
+ UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
}
}
@@ -142,13 +163,6 @@ class AppStorePaymentManager: NSObject, SKPaymentTransactionObserver {
}
}
- private func _addPayment(_ payment: SKPayment, for accountToken: String) {
- assert(Thread.isMainThread)
-
- associateAccountToken(accountToken, and: payment)
- paymentQueue.add(payment)
- }
-
private func sendAppStoreReceipt(accountToken: String, forceRefresh: Bool, completionHandler: @escaping (OperationCompletion<REST.CreateApplePaymentResponse, Error>) -> Void) -> Cancellable {
let operation = SendAppStoreReceiptOperation(restClient: REST.Client.shared, accountToken: accountToken, forceRefresh: forceRefresh, receiptProperties: nil) { completion in
completionHandler(completion)
@@ -207,18 +221,20 @@ class AppStorePaymentManager: NSObject, SKPaymentTransactionObserver {
paymentQueue.finishTransaction(transaction)
if let accountToken = deassociateAccountToken(transaction.payment) {
- observerList.forEach { (observer) in
+ observerList.forEach { observer in
observer.appStorePaymentManager(
self,
transaction: transaction,
+ payment: transaction.payment,
accountToken: accountToken,
didFailWithError: .storePayment(transaction.error!))
}
} else {
- observerList.forEach { (observer) in
+ observerList.forEach { observer in
observer.appStorePaymentManager(
self,
transaction: transaction,
+ payment: transaction.payment,
accountToken: nil,
didFailWithError: .noAccountSet)
}
@@ -227,10 +243,11 @@ class AppStorePaymentManager: NSObject, SKPaymentTransactionObserver {
private func didFinishOrRestorePurchase(transaction: SKPaymentTransaction) {
guard let accountToken = deassociateAccountToken(transaction.payment) else {
- observerList.forEach { (observer) in
+ observerList.forEach { observer in
observer.appStorePaymentManager(
self,
transaction: transaction,
+ payment: transaction.payment,
accountToken: nil,
didFailWithError: .noAccountSet)
}
@@ -255,6 +272,7 @@ class AppStorePaymentManager: NSObject, SKPaymentTransactionObserver {
observer.appStorePaymentManager(
self,
transaction: transaction,
+ payment: transaction.payment,
accountToken: accountToken,
didFailWithError: error)
}
diff --git a/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManagerError.swift b/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManagerError.swift
index 5d395c4e48..5e1be9b6fc 100644
--- a/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManagerError.swift
+++ b/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManagerError.swift
@@ -14,6 +14,9 @@ extension AppStorePaymentManager {
/// Failure to find the account token associated with the transaction.
case noAccountSet
+ /// Failure to validate the account token.
+ case validateAccount(REST.Error)
+
/// Failure to handle payment transaction. Contains error returned by StoreKit.
case storePayment(Swift.Error)
@@ -27,6 +30,8 @@ extension AppStorePaymentManager {
switch self {
case .noAccountSet:
return "Account is not set"
+ case .validateAccount:
+ return "Account validation error"
case .storePayment:
return "Store payment error"
case .readReceipt:
diff --git a/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentObserver.swift b/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentObserver.swift
index 9aa4cc9481..ad37c707c4 100644
--- a/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentObserver.swift
+++ b/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentObserver.swift
@@ -10,9 +10,11 @@ import Foundation
import StoreKit
protocol AppStorePaymentObserver: AnyObject {
+
func appStorePaymentManager(
_ manager: AppStorePaymentManager,
- transaction: SKPaymentTransaction,
+ transaction: SKPaymentTransaction?,
+ payment: SKPayment,
accountToken: String?,
didFailWithError error: AppStorePaymentManager.Error)
diff --git a/ios/MullvadVPN/DisplayChainedError.swift b/ios/MullvadVPN/DisplayChainedError.swift
index 8baef34ece..aab3fce95e 100644
--- a/ios/MullvadVPN/DisplayChainedError.swift
+++ b/ios/MullvadVPN/DisplayChainedError.swift
@@ -343,6 +343,31 @@ extension AppStorePaymentManager.Error: DisplayChainedError {
comment: ""
)
+ case .validateAccount(let restError):
+ let reason = restError.errorChainDescription ?? ""
+
+ if case .server(.invalidAccount) = restError {
+ return String(
+ format: NSLocalizedString(
+ "INVALID_ACCOUNT_ERROR",
+ tableName: "AppStorePaymentManager",
+ value: "Cannot add credit to invalid account.",
+ comment: ""
+ ), reason
+ )
+ } else {
+ let reason = restError.errorChainDescription ?? ""
+
+ return String(
+ format: NSLocalizedString(
+ "VALIDATE_ACCOUNT_ERROR",
+ tableName: "AppStorePaymentManager",
+ value: "Failed to validate account token: %@",
+ comment: ""
+ ), reason
+ )
+ }
+
case .readReceipt(let readReceiptError):
switch readReceiptError {
case .refresh(let storeError):
diff --git a/ios/MullvadVPN/en.lproj/AppStorePaymentManager.strings b/ios/MullvadVPN/en.lproj/AppStorePaymentManager.strings
index 54c9c348c4..a168dfc6ff 100644
--- a/ios/MullvadVPN/en.lproj/AppStorePaymentManager.strings
+++ b/ios/MullvadVPN/en.lproj/AppStorePaymentManager.strings
@@ -1,4 +1,7 @@
/* No comment provided by engineer. */
+"INVALID_ACCOUNT_ERROR" = "Cannot add credit to invalid account.";
+
+/* No comment provided by engineer. */
"NO_ACCOUNT_SET_ERROR" = "Internal error: account is not set";
/* No comment provided by engineer. */
@@ -15,3 +18,6 @@
/* No comment provided by engineer. */
"SEND_RECEIPT_RECOVERY_SUGGESTION" = "Please retry by using the \"Restore purchases\" button.";
+
+/* No comment provided by engineer. */
+"VALIDATE_ACCOUNT_ERROR" = "Failed to validate account token: %@";