From 28a16e06d24770bdbde8e12fe401877de8f5cf6b Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Wed, 18 Oct 2023 15:00:06 +0200 Subject: [PATCH 01/96] Rename shortLivedToken -> authToken --- .../SubscriptionPagesUserScript.swift | 2 +- .../Sources/Account/AccountManager.swift | 42 +++++++++---------- .../AccountKeychainStorage.swift | 24 +++++------ .../AccountStorage/AccountStorage.swift | 4 +- .../DebugMenu/SubscriptionDebugMenu.swift | 2 +- 5 files changed, 37 insertions(+), 37 deletions(-) diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 4ebe7f8406..8118fbd198 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -117,7 +117,7 @@ struct SubscriptionPagesUseEmailFeature: Subfeature { } func getSubscription(params: Any, original: WKScriptMessage) async throws -> Encodable? { - let token = AccountManager().shortLivedToken ?? "" + let token = AccountManager().authToken ?? "" let subscription = Subscription(token: token) return subscription } diff --git a/LocalPackages/Account/Sources/Account/AccountManager.swift b/LocalPackages/Account/Sources/Account/AccountManager.swift index b03c409908..cb219ccfcd 100644 --- a/LocalPackages/Account/Sources/Account/AccountManager.swift +++ b/LocalPackages/Account/Sources/Account/AccountManager.swift @@ -42,12 +42,12 @@ public class AccountManager { self.storage = storage } - public var token: String? { + public var authToken: String? { do { - return try storage.getToken() + return try storage.getAuthToken() } catch { if let error = error as? AccountKeychainAccessError { - delegate?.accountManagerKeychainAccessFailed(accessType: .getToken, error: error) + delegate?.accountManagerKeychainAccessFailed(accessType: .getAuthToken, error: error) } else { assertionFailure("Expected AccountKeychainAccessError") } @@ -55,13 +55,13 @@ public class AccountManager { return nil } } - - public var shortLivedToken: String? { + + public var token: String? { do { - return try storage.getShortLivedToken() + return try storage.getToken() } catch { if let error = error as? AccountKeychainAccessError { - delegate?.accountManagerKeychainAccessFailed(accessType: .getShortLivedToken, error: error) + delegate?.accountManagerKeychainAccessFailed(accessType: .getToken, error: error) } else { assertionFailure("Expected AccountKeychainAccessError") } @@ -98,12 +98,12 @@ public class AccountManager { } } - public func storeShortLivedToken(token: String) { + public func storeAuthToken(token: String) { do { - try storage.store(shortLivedToken: token) + try storage.store(authToken: token) } catch { if let error = error as? AccountKeychainAccessError { - delegate?.accountManagerKeychainAccessFailed(accessType: .storeToken, error: error) + delegate?.accountManagerKeychainAccessFailed(accessType: .storeAuthToken, error: error) } else { assertionFailure("Expected AccountKeychainAccessError") } @@ -168,38 +168,38 @@ public class AccountManager { guard let jwsRepresentation = await PurchaseManager.mostRecentTransaction() else { return } // Do the store login to get short-lived token - let shortLivedToken: String + let authToken: String switch await AuthService.storeLogin(signature: jwsRepresentation) { case .success(let response): - shortLivedToken = response.authToken + authToken = response.authToken case .failure(let error): os_log("AccountManager error: %{public}@", log: .error, error.localizedDescription) return } - storeShortLivedToken(token: shortLivedToken) - exchangeTokenAndRefreshEntitlements(with: shortLivedToken) + storeAuthToken(token: authToken) + exchangeTokenAndRefreshEntitlements(with: authToken) } } } - public func exchangeTokenAndRefreshEntitlements(with shortLivedToken: String) { + public func exchangeTokenAndRefreshEntitlements(with authToken: String) { Task { // Exchange short-lived token to a long-lived one - let longLivedToken: String - switch await AuthService.getAccessToken(token: shortLivedToken) { + let accessToken: String + switch await AuthService.getAccessToken(token: authToken) { case .success(let response): - longLivedToken = response.accessToken + accessToken = response.accessToken case .failure(let error): os_log("AccountManager error: %{public}@", log: .error, error.localizedDescription) return } // Fetch entitlements and account details and store the data - switch await AuthService.validateToken(accessToken: longLivedToken) { + switch await AuthService.validateToken(accessToken: accessToken) { case .success(let response): - self.storeShortLivedToken(token: shortLivedToken) - self.storeAccount(token: longLivedToken, + self.storeAuthToken(token: authToken) + self.storeAccount(token: accessToken, email: response.account.email, externalID: response.account.externalID) diff --git a/LocalPackages/Account/Sources/Account/AccountStorage/AccountKeychainStorage.swift b/LocalPackages/Account/Sources/Account/AccountStorage/AccountKeychainStorage.swift index a9c823dae4..1f54d265b0 100644 --- a/LocalPackages/Account/Sources/Account/AccountStorage/AccountKeychainStorage.swift +++ b/LocalPackages/Account/Sources/Account/AccountStorage/AccountKeychainStorage.swift @@ -21,8 +21,8 @@ import Foundation public enum AccountKeychainAccessType: String { case getToken case storeToken - case getShortLivedToken - case storeShortLivedToken + case getAuthToken + case storeAuthToken case getEmail case storeEmail case getExternalID @@ -52,20 +52,20 @@ public class AccountKeychainStorage: AccountStorage { public init() {} - public func getToken() throws -> String? { - try Self.getString(forField: .token) + public func getAuthToken() throws -> String? { + try Self.getString(forField: .authToken) } - public func store(token: String) throws { - try Self.set(string: token, forField: .token) + public func store(authToken: String) throws { + try Self.set(string: authToken, forField: .authToken) } - public func getShortLivedToken() throws -> String? { - try Self.getString(forField: .shortLivedToken) + public func getToken() throws -> String? { + try Self.getString(forField: .token) } - public func store(shortLivedToken: String) throws { - try Self.set(string: shortLivedToken, forField: .shortLivedToken) + public func store(token: String) throws { + try Self.set(string: token, forField: .token) } public func getEmail() throws -> String? { @@ -93,8 +93,8 @@ public class AccountKeychainStorage: AccountStorage { } public func clearAuthenticationState() throws { + try Self.deleteItem(forField: .authToken) try Self.deleteItem(forField: .token) - try Self.deleteItem(forField: .shortLivedToken) try Self.deleteItem(forField: .email) try Self.deleteItem(forField: .externalID) } @@ -108,8 +108,8 @@ private extension AccountKeychainStorage { multiple accounts/tokens at the same time */ enum AccountKeychainField: String, CaseIterable { + case authToken = "account.authToken" case token = "account.token" - case shortLivedToken = "account.shortLivedToken" case email = "account.email" case externalID = "account.external_id" diff --git a/LocalPackages/Account/Sources/Account/AccountStorage/AccountStorage.swift b/LocalPackages/Account/Sources/Account/AccountStorage/AccountStorage.swift index c91fd9c956..b6640ee87b 100644 --- a/LocalPackages/Account/Sources/Account/AccountStorage/AccountStorage.swift +++ b/LocalPackages/Account/Sources/Account/AccountStorage/AccountStorage.swift @@ -19,10 +19,10 @@ import Foundation public protocol AccountStorage: AnyObject { + func getAuthToken() throws -> String? + func store(authToken: String) throws func getToken() throws -> String? func store(token: String) throws - func getShortLivedToken() throws -> String? - func store(shortLivedToken: String) throws func getEmail() throws -> String? func store(email: String?) throws func getExternalID() throws -> String? diff --git a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift index ae2ddfe3ce..5b6fb99792 100644 --- a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift +++ b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift @@ -65,7 +65,7 @@ public final class SubscriptionDebugMenu: NSMenuItem { func showAccountDetails() { let title = accountManager.isSignedIn ? "Authenticated" : "Not Authenticated" let message = accountManager.isSignedIn ? ["Token: \(accountManager.token ?? "")", - "Short-lived Token: \(accountManager.shortLivedToken ?? "")", + "Short-lived Token: \(accountManager.authToken ?? "")", "Email: \(accountManager.email ?? "")", "ExternalID: \(accountManager.externalID ?? "")"].joined(separator: "\n") : nil showAlert(title: title, message: message) From 2d34f752c7435b9b636f62ed96f89ff3c969f072 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Wed, 18 Oct 2023 15:11:46 +0200 Subject: [PATCH 02/96] Rename longLivedToken -> accessToken --- .../Account/Sources/Account/AccountManager.swift | 12 ++++++------ .../AccountStorage/AccountKeychainStorage.swift | 16 ++++++++-------- .../Account/AccountStorage/AccountStorage.swift | 4 ++-- .../DebugMenu/SubscriptionDebugMenu.swift | 4 ++-- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/LocalPackages/Account/Sources/Account/AccountManager.swift b/LocalPackages/Account/Sources/Account/AccountManager.swift index cb219ccfcd..8f6dd0dd5d 100644 --- a/LocalPackages/Account/Sources/Account/AccountManager.swift +++ b/LocalPackages/Account/Sources/Account/AccountManager.swift @@ -35,7 +35,7 @@ public class AccountManager { public weak var delegate: AccountManagerKeychainAccessDelegate? public var isSignedIn: Bool { - return token != nil + return accessToken != nil } public init(storage: AccountStorage = AccountKeychainStorage()) { @@ -56,12 +56,12 @@ public class AccountManager { } } - public var token: String? { + public var accessToken: String? { do { - return try storage.getToken() + return try storage.getAccessToken() } catch { if let error = error as? AccountKeychainAccessError { - delegate?.accountManagerKeychainAccessFailed(accessType: .getToken, error: error) + delegate?.accountManagerKeychainAccessFailed(accessType: .getAccessToken, error: error) } else { assertionFailure("Expected AccountKeychainAccessError") } @@ -113,10 +113,10 @@ public class AccountManager { public func storeAccount(token: String, email: String?, externalID: String?) { os_log("AccountManager: storeAccount token: %@ email: %@ externalID:%@", log: .account, token, email ?? "nil", externalID ?? "nil") do { - try storage.store(token: token) + try storage.store(accessToken: token) } catch { if let error = error as? AccountKeychainAccessError { - delegate?.accountManagerKeychainAccessFailed(accessType: .storeToken, error: error) + delegate?.accountManagerKeychainAccessFailed(accessType: .storeAccessToken, error: error) } else { assertionFailure("Expected AccountKeychainAccessError") } diff --git a/LocalPackages/Account/Sources/Account/AccountStorage/AccountKeychainStorage.swift b/LocalPackages/Account/Sources/Account/AccountStorage/AccountKeychainStorage.swift index 1f54d265b0..7ce5157651 100644 --- a/LocalPackages/Account/Sources/Account/AccountStorage/AccountKeychainStorage.swift +++ b/LocalPackages/Account/Sources/Account/AccountStorage/AccountKeychainStorage.swift @@ -19,10 +19,10 @@ import Foundation public enum AccountKeychainAccessType: String { - case getToken - case storeToken case getAuthToken case storeAuthToken + case getAccessToken + case storeAccessToken case getEmail case storeEmail case getExternalID @@ -60,12 +60,12 @@ public class AccountKeychainStorage: AccountStorage { try Self.set(string: authToken, forField: .authToken) } - public func getToken() throws -> String? { - try Self.getString(forField: .token) + public func getAccessToken() throws -> String? { + try Self.getString(forField: .accessToken) } - public func store(token: String) throws { - try Self.set(string: token, forField: .token) + public func store(accessToken: String) throws { + try Self.set(string: accessToken, forField: .accessToken) } public func getEmail() throws -> String? { @@ -94,7 +94,7 @@ public class AccountKeychainStorage: AccountStorage { public func clearAuthenticationState() throws { try Self.deleteItem(forField: .authToken) - try Self.deleteItem(forField: .token) + try Self.deleteItem(forField: .accessToken) try Self.deleteItem(forField: .email) try Self.deleteItem(forField: .externalID) } @@ -109,7 +109,7 @@ private extension AccountKeychainStorage { */ enum AccountKeychainField: String, CaseIterable { case authToken = "account.authToken" - case token = "account.token" + case accessToken = "account.accessToken" case email = "account.email" case externalID = "account.external_id" diff --git a/LocalPackages/Account/Sources/Account/AccountStorage/AccountStorage.swift b/LocalPackages/Account/Sources/Account/AccountStorage/AccountStorage.swift index b6640ee87b..06b5e05cb5 100644 --- a/LocalPackages/Account/Sources/Account/AccountStorage/AccountStorage.swift +++ b/LocalPackages/Account/Sources/Account/AccountStorage/AccountStorage.swift @@ -21,8 +21,8 @@ import Foundation public protocol AccountStorage: AnyObject { func getAuthToken() throws -> String? func store(authToken: String) throws - func getToken() throws -> String? - func store(token: String) throws + func getAccessToken() throws -> String? + func store(accessToken: String) throws func getEmail() throws -> String? func store(email: String?) throws func getExternalID() throws -> String? diff --git a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift index 5b6fb99792..f165a90207 100644 --- a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift +++ b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift @@ -64,7 +64,7 @@ public final class SubscriptionDebugMenu: NSMenuItem { @objc func showAccountDetails() { let title = accountManager.isSignedIn ? "Authenticated" : "Not Authenticated" - let message = accountManager.isSignedIn ? ["Token: \(accountManager.token ?? "")", + let message = accountManager.isSignedIn ? ["Token: \(accountManager.accessToken ?? "")", "Short-lived Token: \(accountManager.authToken ?? "")", "Email: \(accountManager.email ?? "")", "ExternalID: \(accountManager.externalID ?? "")"].joined(separator: "\n") : nil @@ -74,7 +74,7 @@ public final class SubscriptionDebugMenu: NSMenuItem { @objc func validateToken() { Task { - guard let token = accountManager.token else { return } + guard let token = accountManager.accessToken else { return } switch await AuthService.validateToken(accessToken: token) { case .success(let response): showAlert(title: "Validate token", message: "\(response)") From 11d1b070aff19b4c9cdd77b50c34a7e2bd7eebfa Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Wed, 18 Oct 2023 15:47:11 +0200 Subject: [PATCH 03/96] Tweak naming --- LocalPackages/Account/Sources/Account/AccountManager.swift | 6 +++--- .../Subscription/DebugMenu/SubscriptionDebugMenu.swift | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/LocalPackages/Account/Sources/Account/AccountManager.swift b/LocalPackages/Account/Sources/Account/AccountManager.swift index 8f6dd0dd5d..c232068321 100644 --- a/LocalPackages/Account/Sources/Account/AccountManager.swift +++ b/LocalPackages/Account/Sources/Account/AccountManager.swift @@ -178,14 +178,14 @@ public class AccountManager { } storeAuthToken(token: authToken) - exchangeTokenAndRefreshEntitlements(with: authToken) + exchangeTokensAndRefreshEntitlements(with: authToken) } } } - public func exchangeTokenAndRefreshEntitlements(with authToken: String) { + public func exchangeTokensAndRefreshEntitlements(with authToken: String) { Task { - // Exchange short-lived token to a long-lived one + // Exchange short-lived auth token to a long-lived access token let accessToken: String switch await AuthService.getAccessToken(token: authToken) { case .success(let response): diff --git a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift index f165a90207..37caa97f78 100644 --- a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift +++ b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift @@ -64,8 +64,8 @@ public final class SubscriptionDebugMenu: NSMenuItem { @objc func showAccountDetails() { let title = accountManager.isSignedIn ? "Authenticated" : "Not Authenticated" - let message = accountManager.isSignedIn ? ["Token: \(accountManager.accessToken ?? "")", - "Short-lived Token: \(accountManager.authToken ?? "")", + let message = accountManager.isSignedIn ? ["AuthToken: \(accountManager.authToken ?? "")", + "AccessToken: \(accountManager.accessToken ?? "")", "Email: \(accountManager.email ?? "")", "ExternalID: \(accountManager.externalID ?? "")"].joined(separator: "\n") : nil showAlert(title: title, message: message) From 38e07b71d958f87e35a762b23de3d29589856396 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Wed, 18 Oct 2023 22:12:46 +0200 Subject: [PATCH 04/96] Move away from storing externalID --- .../App/DuckDuckGoPrivacyPro.xcconfig | 2 +- .../SubscriptionPagesUserScript.swift | 2 +- .../Sources/Account/AccountManager.swift | 36 ++++--------------- .../AccountKeychainStorage.swift | 14 -------- .../AccountStorage/AccountStorage.swift | 2 -- .../Sources/Purchase/PurchaseManager.swift | 2 +- .../DebugMenu/DebugPurchaseModel.swift | 23 ++++++++---- .../DebugMenu/SubscriptionDebugMenu.swift | 5 ++- 8 files changed, 29 insertions(+), 57 deletions(-) diff --git a/Configuration/App/DuckDuckGoPrivacyPro.xcconfig b/Configuration/App/DuckDuckGoPrivacyPro.xcconfig index 0258b28b45..c51373fcc9 100644 --- a/Configuration/App/DuckDuckGoPrivacyPro.xcconfig +++ b/Configuration/App/DuckDuckGoPrivacyPro.xcconfig @@ -21,5 +21,5 @@ #include "DuckDuckGo.xcconfig" -FEATURE_FLAGS = FEEDBACK NETWORK_PROTECTION SPARKLE SUBSCRIPTION +FEATURE_FLAGS = FEEDBACK SPARKLE SUBSCRIPTION PRODUCT_NAME = $(PRODUCT_NAME_PREFIX) Privacy Pro diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 8118fbd198..87e45805d3 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -128,7 +128,7 @@ struct SubscriptionPagesUseEmailFeature: Subfeature { return nil } - AccountManager().exchangeTokenAndRefreshEntitlements(with: subscriptionValues.token) + AccountManager().exchangeTokensAndRefreshEntitlements(with: subscriptionValues.token) return nil } diff --git a/LocalPackages/Account/Sources/Account/AccountManager.swift b/LocalPackages/Account/Sources/Account/AccountManager.swift index c232068321..42833f5fa3 100644 --- a/LocalPackages/Account/Sources/Account/AccountManager.swift +++ b/LocalPackages/Account/Sources/Account/AccountManager.swift @@ -84,20 +84,6 @@ public class AccountManager { } } - public var externalID: String? { - do { - return try storage.getExternalID() - } catch { - if let error = error as? AccountKeychainAccessError { - delegate?.accountManagerKeychainAccessFailed(accessType: .getExternalID, error: error) - } else { - assertionFailure("Expected AccountKeychainAccessError") - } - - return nil - } - } - public func storeAuthToken(token: String) { do { try storage.store(authToken: token) @@ -110,8 +96,8 @@ public class AccountManager { } } - public func storeAccount(token: String, email: String?, externalID: String?) { - os_log("AccountManager: storeAccount token: %@ email: %@ externalID:%@", log: .account, token, email ?? "nil", externalID ?? "nil") + public func storeAccount(token: String, email: String?) { + os_log("AccountManager: storeAccount token: %@ email: %@ externalID:%@", log: .account, token, email ?? "nil") do { try storage.store(accessToken: token) } catch { @@ -132,16 +118,6 @@ public class AccountManager { } } - do { - try storage.store(externalID: externalID) - } catch { - if let error = error as? AccountKeychainAccessError { - delegate?.accountManagerKeychainAccessFailed(accessType: .storeExternalID, error: error) - } else { - assertionFailure("Expected AccountKeychainAccessError") - } - } - NotificationCenter.default.post(name: .accountDidSignIn, object: self, userInfo: nil) } @@ -165,7 +141,10 @@ public class AccountManager { if #available(macOS 12.0, *) { Task { // Fetch most recent purchase - guard let jwsRepresentation = await PurchaseManager.mostRecentTransaction() else { return } + guard let jwsRepresentation = await PurchaseManager.mostRecentTransaction() else { + os_log("No transactions", log: .error) + return + } // Do the store login to get short-lived token let authToken: String @@ -200,8 +179,7 @@ public class AccountManager { case .success(let response): self.storeAuthToken(token: authToken) self.storeAccount(token: accessToken, - email: response.account.email, - externalID: response.account.externalID) + email: response.account.email) case .failure(let error): os_log("AccountManager error: %{public}@", log: .error, error.localizedDescription) diff --git a/LocalPackages/Account/Sources/Account/AccountStorage/AccountKeychainStorage.swift b/LocalPackages/Account/Sources/Account/AccountStorage/AccountKeychainStorage.swift index 7ce5157651..9b5590186a 100644 --- a/LocalPackages/Account/Sources/Account/AccountStorage/AccountKeychainStorage.swift +++ b/LocalPackages/Account/Sources/Account/AccountStorage/AccountKeychainStorage.swift @@ -80,23 +80,10 @@ public class AccountKeychainStorage: AccountStorage { } } - public func getExternalID() throws -> String? { - try Self.getString(forField: .externalID) - } - - public func store(externalID: String?) throws { - if let externalID = externalID, !externalID.isEmpty { - try Self.set(string: externalID, forField: .externalID) - } else { - try Self.deleteItem(forField: .externalID) - } - } - public func clearAuthenticationState() throws { try Self.deleteItem(forField: .authToken) try Self.deleteItem(forField: .accessToken) try Self.deleteItem(forField: .email) - try Self.deleteItem(forField: .externalID) } } @@ -111,7 +98,6 @@ private extension AccountKeychainStorage { case authToken = "account.authToken" case accessToken = "account.accessToken" case email = "account.email" - case externalID = "account.external_id" var keyValue: String { (Bundle.main.bundleIdentifier ?? "com.duckduckgo") + "." + rawValue diff --git a/LocalPackages/Account/Sources/Account/AccountStorage/AccountStorage.swift b/LocalPackages/Account/Sources/Account/AccountStorage/AccountStorage.swift index 06b5e05cb5..3f3466ec93 100644 --- a/LocalPackages/Account/Sources/Account/AccountStorage/AccountStorage.swift +++ b/LocalPackages/Account/Sources/Account/AccountStorage/AccountStorage.swift @@ -25,7 +25,5 @@ public protocol AccountStorage: AnyObject { func store(accessToken: String) throws func getEmail() throws -> String? func store(email: String?) throws - func getExternalID() throws -> String? - func store(externalID: String?) throws func clearAuthenticationState() throws } diff --git a/LocalPackages/Purchase/Sources/Purchase/PurchaseManager.swift b/LocalPackages/Purchase/Sources/Purchase/PurchaseManager.swift index 9c2b94ce8c..771f41f18f 100644 --- a/LocalPackages/Purchase/Sources/Purchase/PurchaseManager.swift +++ b/LocalPackages/Purchase/Sources/Purchase/PurchaseManager.swift @@ -144,7 +144,7 @@ public final class PurchaseManager: ObservableObject { @MainActor public static func mostRecentTransaction() async -> String? { - print(" -- [PurchaseManager] updatePurchasedProducts()") + print(" -- [PurchaseManager] mostRecentTransaction()") var transactions: [VerificationResult] = [] diff --git a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseModel.swift b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseModel.swift index 7b5acc1eda..3967a1b66c 100644 --- a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseModel.swift +++ b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseModel.swift @@ -36,15 +36,26 @@ public final class DebugPurchaseModel: ObservableObject { func purchase(_ product: Product) { print("Attempting purchase: \(product.displayName)") - if let externalID = AccountManager().externalID { - print("ExternalID: \(externalID)") - manager.purchase(product, customUUID: externalID) - } else { - Task { + Task { + guard let token = AccountManager().accessToken else { return } + + var externalID: String? + + switch await AuthService.validateToken(accessToken: token) { + case .success(let response): + externalID = response.account.externalID + case .failure(let error): + print("Error: \(error)") + return + } + + if let externalID { + manager.purchase(product, customUUID: externalID) + } else { switch await AuthService.createAccount() { case .success(let response): manager.purchase(product, customUUID: response.externalID) - AccountManager().exchangeTokenAndRefreshEntitlements(with: response.authToken) + AccountManager().exchangeTokensAndRefreshEntitlements(with: response.authToken) case .failure(let error): print("Error: \(error)") return diff --git a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift index 37caa97f78..7a6110c1f4 100644 --- a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift +++ b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift @@ -53,7 +53,7 @@ public final class SubscriptionDebugMenu: NSMenuItem { @objc func simulateSubscriptionActiveState() { - accountManager.storeAccount(token: "fake-token", email: "fake@email.com", externalID: "fake-externalID") + accountManager.storeAccount(token: "fake-token", email: "fake@email.com") } @objc @@ -66,8 +66,7 @@ public final class SubscriptionDebugMenu: NSMenuItem { let title = accountManager.isSignedIn ? "Authenticated" : "Not Authenticated" let message = accountManager.isSignedIn ? ["AuthToken: \(accountManager.authToken ?? "")", "AccessToken: \(accountManager.accessToken ?? "")", - "Email: \(accountManager.email ?? "")", - "ExternalID: \(accountManager.externalID ?? "")"].joined(separator: "\n") : nil + "Email: \(accountManager.email ?? "")"].joined(separator: "\n") : nil showAlert(title: title, message: message) } From 6b00835f04f045547e49ede3c72532cd54b18cb0 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Thu, 19 Oct 2023 13:56:49 +0200 Subject: [PATCH 05/96] API for checking entitlements --- .../Account/Sources/Account/AccountManager.swift | 16 ++++++++++++++++ .../DebugMenu/SubscriptionDebugMenu.swift | 12 ++++++++++++ 2 files changed, 28 insertions(+) diff --git a/LocalPackages/Account/Sources/Account/AccountManager.swift b/LocalPackages/Account/Sources/Account/AccountManager.swift index 42833f5fa3..1907b3d0e5 100644 --- a/LocalPackages/Account/Sources/Account/AccountManager.swift +++ b/LocalPackages/Account/Sources/Account/AccountManager.swift @@ -137,6 +137,22 @@ public class AccountManager { // MARK: - + public func hasEntitlement(for name: String) async -> Bool { + guard let accessToken else { return false } + + switch await AuthService.validateToken(accessToken: accessToken) { + case .success(let response): + let entitlements = response.account.entitlements + return entitlements.contains { entitlement in + entitlement.name == name + } + + case .failure(let error): + os_log("AccountManager error: %{public}@", log: .error, error.localizedDescription) + return false + } + } + public func signInByRestoringPastPurchases() { if #available(macOS 12.0, *) { Task { diff --git a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift index 7a6110c1f4..14a59dd2ba 100644 --- a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift +++ b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift @@ -42,6 +42,7 @@ public final class SubscriptionDebugMenu: NSMenuItem { menu.addItem(NSMenuItem(title: "Show account details", action: #selector(showAccountDetails), target: self)) menu.addItem(.separator()) menu.addItem(NSMenuItem(title: "Validate Token", action: #selector(validateToken), target: self)) + menu.addItem(NSMenuItem(title: "Check Entitlements", action: #selector(checkEntitlements), target: self)) menu.addItem(NSMenuItem(title: "Restore Subscription from App Store transaction", action: #selector(restorePurchases), target: self)) menu.addItem(.separator()) if #available(macOS 12.0, *) { @@ -83,6 +84,17 @@ public final class SubscriptionDebugMenu: NSMenuItem { } } + @objc + func checkEntitlements() { + Task { + + for entitlementName in ["fake", "dummy1", "dummy2", "dummy3"] { + let result = await AccountManager().hasEntitlement(for: entitlementName) + print("Entitlement check for \(entitlementName): \(result)") + } + } + } + @objc func restorePurchases(_ sender: Any?) { accountManager.signInByRestoringPastPurchases() From e50d90630a3aec2d50159caaf5e62c54ff6b2ab8 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Thu, 19 Oct 2023 15:26:10 +0200 Subject: [PATCH 06/96] API for checking available purchases on the App Store --- .../Sources/Purchase/PurchaseManager.swift | 13 +++++++- .../DebugMenu/SubscriptionDebugMenu.swift | 30 ++++++++++++++++++- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/LocalPackages/Purchase/Sources/Purchase/PurchaseManager.swift b/LocalPackages/Purchase/Sources/Purchase/PurchaseManager.swift index 771f41f18f..9dc7563f46 100644 --- a/LocalPackages/Purchase/Sources/Purchase/PurchaseManager.swift +++ b/LocalPackages/Purchase/Sources/Purchase/PurchaseManager.swift @@ -28,7 +28,6 @@ public enum StoreError: Error { } @available(macOS 12.0, *) -@MainActor public final class PurchaseManager: ObservableObject { static let productIdentifiers = ["subscription.1week", "subscription.1month", "subscription.1year", @@ -55,6 +54,18 @@ public final class PurchaseManager: ObservableObject { storefrontChanges?.cancel() } + @MainActor + public func hasProductsAvailable() async -> Bool { + do { + let availableProducts = try await Product.products(for: Self.productIdentifiers) + print(" -- [PurchaseManager] updateAvailableProducts(): fetched \(availableProducts.count)") + return !availableProducts.isEmpty + } catch { + print("Error fetching available products: \(error)") + return false + } + } + @MainActor func restorePurchases() { Task { diff --git a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift index 14a59dd2ba..ca8bc64266 100644 --- a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift +++ b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift @@ -18,12 +18,22 @@ import AppKit import Account +import Purchase public final class SubscriptionDebugMenu: NSMenuItem { var currentViewController: () -> NSViewController? private let accountManager = AccountManager() + private var _purchaseManager: Any? + @available(macOS 12.0, *) + fileprivate var purchaseManager: PurchaseManager { + if _purchaseManager == nil { + _purchaseManager = PurchaseManager() + } + return _purchaseManager as! PurchaseManager + } + required init(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -43,6 +53,9 @@ public final class SubscriptionDebugMenu: NSMenuItem { menu.addItem(.separator()) menu.addItem(NSMenuItem(title: "Validate Token", action: #selector(validateToken), target: self)) menu.addItem(NSMenuItem(title: "Check Entitlements", action: #selector(checkEntitlements), target: self)) + if #available(macOS 12.0, *) { + menu.addItem(NSMenuItem(title: "Check Purchase Products Availability", action: #selector(checkProductsAvailability), target: self)) + } menu.addItem(NSMenuItem(title: "Restore Subscription from App Store transaction", action: #selector(restorePurchases), target: self)) menu.addItem(.separator()) if #available(macOS 12.0, *) { @@ -87,11 +100,26 @@ public final class SubscriptionDebugMenu: NSMenuItem { @objc func checkEntitlements() { Task { + var results: [String] = [] for entitlementName in ["fake", "dummy1", "dummy2", "dummy3"] { let result = await AccountManager().hasEntitlement(for: entitlementName) - print("Entitlement check for \(entitlementName): \(result)") + let resultSummary = "Entitlement check for \(entitlementName): \(result)" + results.append(resultSummary) + print(resultSummary) } + + showAlert(title: "Check Entitlements", message: results.joined(separator: "\n")) + } + } + + @available(macOS 12.0, *) + @objc + func checkProductsAvailability() { + Task { + let result = await purchaseManager.hasProductsAvailable() + showAlert(title: "Check App Store Product Availability", + message: "Can purchase: \(result ? "YES" : "NO")") } } From 233e77aabfe5b9f5dfe5fc6e67914670f5e37f72 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Tue, 31 Oct 2023 16:19:50 +0100 Subject: [PATCH 07/96] Restrict subscription userscript to DDG and test domain --- DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 87e45805d3..8d25818999 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -81,7 +81,10 @@ struct SubscriptionPagesUseEmailFeature: Subfeature { var featureName = "useSubscription" - var messageOriginPolicy: MessageOriginPolicy = .all + var messageOriginPolicy: MessageOriginPolicy = .only(rules: [ + .exact(hostname: "duckduckgo.com"), + .exact(hostname: "abrown.duckduckgo.com") + ]) func handler(forMethodNamed methodName: String) -> Subfeature.Handler? { switch methodName { From ced340ea7073013de39708041b274068a1e65f7d Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Tue, 31 Oct 2023 16:20:26 +0100 Subject: [PATCH 08/96] Remove leftover --- .../Tab/UserScripts/SubscriptionPagesUserScript.swift | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 8d25818999..60bf60e795 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -108,17 +108,6 @@ struct SubscriptionPagesUseEmailFeature: Subfeature { let token: String } - struct EmailProtectionValues: Codable { - enum CodingKeys: String, CodingKey { - case token - case user - case cohort - } - let token: String - let user: String - let cohort: String - } - func getSubscription(params: Any, original: WKScriptMessage) async throws -> Encodable? { let token = AccountManager().authToken ?? "" let subscription = Subscription(token: token) From 3fd6283161190be02aea28c581e1e41a78ef0e72 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Thu, 2 Nov 2023 10:05:15 +0100 Subject: [PATCH 09/96] Error alerts test --- .../DebugMenu/SubscriptionDebugMenu.swift | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift index ca8bc64266..572dbce7af 100644 --- a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift +++ b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift @@ -61,7 +61,9 @@ public final class SubscriptionDebugMenu: NSMenuItem { if #available(macOS 12.0, *) { menu.addItem(NSMenuItem(title: "Purchase Subscription from App Store", action: #selector(showPurchaseView), target: self)) } - + menu.addItem(.separator()) + menu.addItem(NSMenuItem(title: "Error message #1", action: #selector(testError1), target: self)) + menu.addItem(NSMenuItem(title: "Error message #2", action: #selector(testError2), target: self)) return menu }() @@ -128,6 +130,29 @@ public final class SubscriptionDebugMenu: NSMenuItem { accountManager.signInByRestoringPastPurchases() } + @objc + func testError1(_ sender: Any?) { + Task { @MainActor in + let alert = NSAlert.init() + alert.messageText = "Something Went Wrong" + alert.informativeText = "The App Store was not able to process your purchase. Please try again later." + alert.addButton(withTitle: "OK") + alert.runModal() + } + } + + @objc + func testError2(_ sender: Any?) { + Task { @MainActor in + let alert = NSAlert.init() + alert.messageText = "Subscription Not Found" + alert.informativeText = "The subscription associated with this Apple ID is no longer active." + alert.addButton(withTitle: "View Plans") + alert.addButton(withTitle: "Cancel") + alert.runModal() + } + } + @IBAction func showPurchaseView(_ sender: Any?) { if #available(macOS 12.0, *) { currentViewController()?.presentAsSheet(DebugPurchaseViewController()) From c7567310e7718520d7d9fd84cd0b2cf82c5439f9 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 6 Nov 2023 13:26:02 +0100 Subject: [PATCH 10/96] Check entitlements in settigns --- .../PreferencesSubscriptionModel.swift | 9 +++++++++ .../PreferencesSubscriptionView.swift | 19 +++++++++++++++---- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionModel.swift b/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionModel.swift index 8a026ae284..5d6258242a 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionModel.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionModel.swift @@ -33,6 +33,7 @@ extension URL { public final class PreferencesSubscriptionModel: ObservableObject { @Published var isSignedIn: Bool = false + @Published var hasEntitlements: Bool = false var sheetModel: SubscriptionAccessModel private let accountManager: AccountManager @@ -96,6 +97,14 @@ public final class PreferencesSubscriptionModel: ObservableObject { func openFAQ() { actionHandler.openURL(.subscriptionFAQ) } + + @MainActor + func fetchEntitlements() { + print("Entitlements!") + Task { + self.hasEntitlements = await AccountManager().hasEntitlement(for: "dummy1") + } + } } public final class PreferencesSubscriptionActionHandlers { diff --git a/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionView.swift b/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionView.swift index a27aadf551..5ad394c8b3 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionView.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionView.swift @@ -84,6 +84,10 @@ public struct PreferencesSubscriptionView: View { } .fixedSize() } + .onAppear { + model.fetchEntitlements() + } + } else { UniversalHeaderView { Image("subscription-inactive-icon", bundle: .module) @@ -108,7 +112,8 @@ public struct PreferencesSubscriptionView: View { title: UserText.vpnServiceTitle, description: UserText.vpnServiceDescription, buttonName: model.isSignedIn ? "Manage" : nil, - buttonAction: { model.openVPN() }) + buttonAction: { model.openVPN() }, + enabled: model.hasEntitlements) Divider() .foregroundColor(Color.secondary) @@ -117,7 +122,8 @@ public struct PreferencesSubscriptionView: View { title: UserText.personalInformationRemovalServiceTitle, description: UserText.personalInformationRemovalServiceDescription, buttonName: model.isSignedIn ? "View" : nil, - buttonAction: { model.openPersonalInformationRemoval() }) + buttonAction: { model.openPersonalInformationRemoval() }, + enabled: model.hasEntitlements) Divider() .foregroundColor(Color.secondary) @@ -126,7 +132,8 @@ public struct PreferencesSubscriptionView: View { title: UserText.identityTheftRestorationServiceTitle, description: UserText.identityTheftRestorationServiceDescription, buttonName: model.isSignedIn ? "View" : nil, - buttonAction: { model.openIdentityTheftRestoration() }) + buttonAction: { model.openIdentityTheftRestoration() }, + enabled: model.hasEntitlements) } .padding(10) .roundedBorder() @@ -177,13 +184,15 @@ public struct SectionView: View { public var description: String public var buttonName: String? public var buttonAction: (() -> Void)? + public var enabled: Bool - public init(iconName: String, title: String, description: String, buttonName: String? = nil, buttonAction: (() -> Void)? = nil) { + public init(iconName: String, title: String, description: String, buttonName: String? = nil, buttonAction: (() -> Void)? = nil, enabled: Bool = true) { self.iconName = iconName self.title = title self.description = description self.buttonName = buttonName self.buttonAction = buttonAction + self.enabled = enabled } public var body: some View { @@ -215,6 +224,8 @@ public struct SectionView: View { } } .padding(.vertical, 7) + .disabled(!enabled) + .opacity(enabled ? 1.0 : 0.6) } } From 5befeaf777edef502611076cbbbba1de7dfa8d69 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Tue, 7 Nov 2023 12:05:48 +0100 Subject: [PATCH 11/96] Shared APIService code and SubscriptionService --- .../Account/Sources/Account/AuthService.swift | 187 ------------------ .../Account/Sources/Account/Logging.swift | 7 + .../Sources/Account/Services/APIService.swift | 110 +++++++++++ .../Account/Services/AuthService.swift | 106 ++++++++++ .../Services/SubscriptionService.swift | 44 +++++ .../DebugMenu/SubscriptionDebugMenu.swift | 15 ++ 6 files changed, 282 insertions(+), 187 deletions(-) delete mode 100644 LocalPackages/Account/Sources/Account/AuthService.swift create mode 100644 LocalPackages/Account/Sources/Account/Services/APIService.swift create mode 100644 LocalPackages/Account/Sources/Account/Services/AuthService.swift create mode 100644 LocalPackages/Account/Sources/Account/Services/SubscriptionService.swift diff --git a/LocalPackages/Account/Sources/Account/AuthService.swift b/LocalPackages/Account/Sources/Account/AuthService.swift deleted file mode 100644 index eaedf386e8..0000000000 --- a/LocalPackages/Account/Sources/Account/AuthService.swift +++ /dev/null @@ -1,187 +0,0 @@ -// -// AuthService.swift -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import Common - -public struct AuthService { - - public enum Error: Swift.Error { - case decodingError - case encodingError - case serverError(description: String) - case unknownServerError - case connectionError - } - - private static let baseURL = URL(string: "https://quackdev.duckduckgo.com/api/auth")! - - private static let session = { - let configuration = URLSessionConfiguration.ephemeral - return URLSession(configuration: configuration) - }() - - // MARK: - - - public static func getAccessToken(token: String) async -> Result { - await executeAPICall(method: "GET", endpoint: "access-token", headers: makeAuthorizationHeader(for: token)) - } - - public struct AccessTokenResponse: Decodable { - public let accessToken: String - } - - // MARK: - - - public static func validateToken(accessToken: String) async -> Result { - await executeAPICall(method: "GET", endpoint: "validate-token", headers: makeAuthorizationHeader(for: accessToken)) - } - - // swiftlint:disable nesting - public struct ValidateTokenResponse: Decodable { - public let account: Account - - public struct Account: Decodable { - public let email: String? - let entitlements: [Entitlement] - public let externalID: String - - enum CodingKeys: String, CodingKey { - case email, entitlements, externalID = "externalId" // no underscores due to keyDecodingStrategy = .convertFromSnakeCase - } - } - - struct Entitlement: Decodable { - let id: Int - let name: String - let product: String - } - } - // swiftlint:enable nesting - - // MARK: - - - public static func createAccount() async -> Result { - await executeAPICall(method: "POST", endpoint: "account/create") - } - - public struct CreateAccountResponse: Decodable { - public let authToken: String - public let externalID: String - public let status: String - - enum CodingKeys: String, CodingKey { - case authToken = "authToken", externalID = "externalId", status // no underscores due to keyDecodingStrategy = .convertFromSnakeCase - } - } - - // MARK: - - - public static func storeLogin(signature: String) async -> Result { - let bodyDict = ["signature": signature, - "store": "apple_app_store"] - - guard let bodyData = try? JSONEncoder().encode(bodyDict) else { return .failure(.encodingError) } - return await executeAPICall(method: "POST", endpoint: "store-login", body: bodyData) - } - - public struct StoreLoginResponse: Decodable { - public let authToken: String - public let email: String - public let externalID: String - public let id: Int - public let status: String - - enum CodingKeys: String, CodingKey { - case authToken = "authToken", email, externalID = "externalId", id, status // no underscores due to keyDecodingStrategy = .convertFromSnakeCase - } - } - - // MARK: - Private API - - private static func executeAPICall(method: String, endpoint: String, headers: [String: String]? = nil, body: Data? = nil) async -> Result where T: Decodable { - let request = makeAPIRequest(method: method, endpoint: endpoint, headers: headers, body: body) - - do { - let (data, urlResponse) = try await session.data(for: request) - - printDebugInfo(method: method, endpoint: endpoint, data: data, response: urlResponse) - - if let httpResponse = urlResponse as? HTTPURLResponse, (200..<300).contains(httpResponse.statusCode) { - if let decodedResponse = decode(T.self, from: data) { - return .success(decodedResponse) - } else { - return .failure(.decodingError) - } - } else { - if let decodedResponse = decode(ErrorResponse.self, from: data) { - let errorDescription = [method, endpoint, urlResponse.httpStatusCodeAsString ?? "", decodedResponse.error].joined(separator: " ") - return .failure(.serverError(description: errorDescription)) - } else { - return .failure(.unknownServerError) - } - } - } catch { - os_log("AuthService error: %{public}@", log: .error, error.localizedDescription) - return .failure(.connectionError) - } - } - - struct ErrorResponse: Decodable { - let error: String - } - - private static func makeAPIRequest(method: String, endpoint: String, headers: [String: String]?, body: Data?) -> URLRequest { - let url = baseURL.appendingPathComponent(endpoint) - var request = URLRequest(url: url) - request.httpMethod = method - if let headers = headers { - request.allHTTPHeaderFields = headers - } - if let body = body { - request.httpBody = body - } - - return request - } - - private static func decode(_: T.Type, from data: Data) -> T? where T: Decodable { - let decoder = JSONDecoder() - decoder.keyDecodingStrategy = .convertFromSnakeCase - - return try? decoder.decode(T.self, from: data) - } - - private static func printDebugInfo(method: String, endpoint: String, data: Data, response: URLResponse) { - let statusCode = (response as? HTTPURLResponse)!.statusCode - let stringData = String(data: data, encoding: .utf8) ?? "" - os_log("[%d] %{public}@ /%{public}@ :: %{public}@", log: .authService, statusCode, method, endpoint, stringData) - } - - private static func makeAuthorizationHeader(for token: String) -> [String: String] { - ["Authorization": "Bearer " + token] - } -} - -extension URLResponse { - - var httpStatusCodeAsString: String? { - guard let httpStatusCode = (self as? HTTPURLResponse)?.statusCode else { return nil } - return String(httpStatusCode) - } -} diff --git a/LocalPackages/Account/Sources/Account/Logging.swift b/LocalPackages/Account/Sources/Account/Logging.swift index 890c38a846..b15ec8e1fa 100644 --- a/LocalPackages/Account/Sources/Account/Logging.swift +++ b/LocalPackages/Account/Sources/Account/Logging.swift @@ -29,6 +29,9 @@ struct Logging { fileprivate static let authServiceLoggingEnabled = true fileprivate static let authService: OSLog = OSLog(subsystem: subsystem, category: "Account : AuthService") + fileprivate static let subscriptionServiceLoggingEnabled = true + fileprivate static let subscriptionService: OSLog = OSLog(subsystem: subsystem, category: "Account : SubscriptionService") + fileprivate static let errorsLoggingEnabled = true fileprivate static let error: OSLog = OSLog(subsystem: subsystem, category: "Account : Errors") } @@ -43,6 +46,10 @@ extension OSLog { Logging.authServiceLoggingEnabled ? Logging.authService : .disabled } + public static var subscriptionService: OSLog { + Logging.subscriptionServiceLoggingEnabled ? Logging.subscriptionService : .disabled + } + public static var error: OSLog { Logging.errorsLoggingEnabled ? Logging.error : .disabled } diff --git a/LocalPackages/Account/Sources/Account/Services/APIService.swift b/LocalPackages/Account/Sources/Account/Services/APIService.swift new file mode 100644 index 0000000000..124a8fbf27 --- /dev/null +++ b/LocalPackages/Account/Sources/Account/Services/APIService.swift @@ -0,0 +1,110 @@ +// +// APIService.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Common + +public enum APIServiceError: Swift.Error { + case decodingError + case encodingError + case serverError(description: String) + case unknownServerError + case connectionError +} + +struct ErrorResponse: Decodable { + let error: String +} + +public protocol APIService { + static var logger: OSLog { get } + static var baseURL: URL { get } + static var session: URLSession { get } + static func executeAPICall(method: String, endpoint: String, headers: [String: String]?, body: Data?) async -> Result where T: Decodable +} + +public extension APIService { + + static func executeAPICall(method: String, endpoint: String, headers: [String: String]? = nil, body: Data? = nil) async -> Result where T: Decodable { + let request = makeAPIRequest(method: method, endpoint: endpoint, headers: headers, body: body) + + do { + let (data, urlResponse) = try await session.data(for: request) + + printDebugInfo(method: method, endpoint: endpoint, data: data, response: urlResponse) + + if let httpResponse = urlResponse as? HTTPURLResponse, (200..<300).contains(httpResponse.statusCode) { + if let decodedResponse = decode(T.self, from: data) { + return .success(decodedResponse) + } else { + return .failure(.decodingError) + } + } else { + if let decodedResponse = decode(ErrorResponse.self, from: data) { + let errorDescription = [method, endpoint, urlResponse.httpStatusCodeAsString ?? "", decodedResponse.error].joined(separator: " ") + return .failure(.serverError(description: errorDescription)) + } else { + return .failure(.unknownServerError) + } + } + } catch { + os_log("Service error: %{public}@", log: .error, error.localizedDescription) + return .failure(.connectionError) + } + } + + private static func makeAPIRequest(method: String, endpoint: String, headers: [String: String]?, body: Data?) -> URLRequest { + let url = baseURL.appendingPathComponent(endpoint) + var request = URLRequest(url: url) + request.httpMethod = method + if let headers = headers { + request.allHTTPHeaderFields = headers + } + if let body = body { + request.httpBody = body + } + + return request + } + + private static func decode(_: T.Type, from data: Data) -> T? where T: Decodable { + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + decoder.dateDecodingStrategy = .millisecondsSince1970 + + return try? decoder.decode(T.self, from: data) + } + + private static func printDebugInfo(method: String, endpoint: String, data: Data, response: URLResponse) { + let statusCode = (response as? HTTPURLResponse)!.statusCode + let stringData = String(data: data, encoding: .utf8) ?? "" + os_log("[%d] %{public}@ /%{public}@ :: %{public}@", log: logger, statusCode, method, endpoint, stringData) + } + + static func makeAuthorizationHeader(for token: String) -> [String: String] { + ["Authorization": "Bearer " + token] + } +} + +extension URLResponse { + + var httpStatusCodeAsString: String? { + guard let httpStatusCode = (self as? HTTPURLResponse)?.statusCode else { return nil } + return String(httpStatusCode) + } +} diff --git a/LocalPackages/Account/Sources/Account/Services/AuthService.swift b/LocalPackages/Account/Sources/Account/Services/AuthService.swift new file mode 100644 index 0000000000..5c5f85c6e6 --- /dev/null +++ b/LocalPackages/Account/Sources/Account/Services/AuthService.swift @@ -0,0 +1,106 @@ +// +// AuthService.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Common + +public struct AuthService: APIService { + + public static let logger: OSLog = .authService + public static let session = { + let configuration = URLSessionConfiguration.ephemeral + return URLSession(configuration: configuration) + }() + public static let baseURL = URL(string: "https://quackdev.duckduckgo.com/api/auth")! + + // MARK: - + + public static func getAccessToken(token: String) async -> Result { + await executeAPICall(method: "GET", endpoint: "access-token", headers: makeAuthorizationHeader(for: token)) + } + + public struct AccessTokenResponse: Decodable { + public let accessToken: String + } + + // MARK: - + + public static func validateToken(accessToken: String) async -> Result { + await executeAPICall(method: "GET", endpoint: "validate-token", headers: makeAuthorizationHeader(for: accessToken)) + } + + // swiftlint:disable nesting + public struct ValidateTokenResponse: Decodable { + public let account: Account + + public struct Account: Decodable { + public let email: String? + let entitlements: [Entitlement] + public let externalID: String + + enum CodingKeys: String, CodingKey { + case email, entitlements, externalID = "externalId" // no underscores due to keyDecodingStrategy = .convertFromSnakeCase + } + } + + struct Entitlement: Decodable { + let id: Int + let name: String + let product: String + } + } + // swiftlint:enable nesting + + // MARK: - + + public static func createAccount() async -> Result { + await executeAPICall(method: "POST", endpoint: "account/create") + } + + public struct CreateAccountResponse: Decodable { + public let authToken: String + public let externalID: String + public let status: String + + enum CodingKeys: String, CodingKey { + case authToken = "authToken", externalID = "externalId", status // no underscores due to keyDecodingStrategy = .convertFromSnakeCase + } + } + + // MARK: - + + public static func storeLogin(signature: String) async -> Result { + let bodyDict = ["signature": signature, + "store": "apple_app_store"] + + guard let bodyData = try? JSONEncoder().encode(bodyDict) else { return .failure(.encodingError) } + return await executeAPICall(method: "POST", endpoint: "store-login", body: bodyData) + } + + public struct StoreLoginResponse: Decodable { + public let authToken: String + public let email: String + public let externalID: String + public let id: Int + public let status: String + + enum CodingKeys: String, CodingKey { + case authToken = "authToken", email, externalID = "externalId", id, status // no underscores due to keyDecodingStrategy = .convertFromSnakeCase + } + } +} diff --git a/LocalPackages/Account/Sources/Account/Services/SubscriptionService.swift b/LocalPackages/Account/Sources/Account/Services/SubscriptionService.swift new file mode 100644 index 0000000000..ad34078a27 --- /dev/null +++ b/LocalPackages/Account/Sources/Account/Services/SubscriptionService.swift @@ -0,0 +1,44 @@ +// +// SubscriptionService.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Common + +public struct SubscriptionService: APIService { + + public static let logger: OSLog = .subscriptionService + public static let session = { + let configuration = URLSessionConfiguration.ephemeral + return URLSession(configuration: configuration) + }() + public static let baseURL = URL(string: "https://subscriptions-dev.duckduckgo.com/api")! + + // MARK: - + + public static func getSubscriptionInfo(token: String) async -> Result { + await executeAPICall(method: "GET", endpoint: "subscription", headers: makeAuthorizationHeader(for: token)) + } + + public struct GetSubscriptionInfoResponse: Decodable { + public let productId: String + public let startedAt: Date + public let expiresOrRenewsAt: Date + public let platform: String + public let status: String + } +} diff --git a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift index 572dbce7af..677127e080 100644 --- a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift +++ b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift @@ -31,6 +31,7 @@ public final class SubscriptionDebugMenu: NSMenuItem { if _purchaseManager == nil { _purchaseManager = PurchaseManager() } + // swiftlint:disable:next force_cast return _purchaseManager as! PurchaseManager } @@ -53,6 +54,7 @@ public final class SubscriptionDebugMenu: NSMenuItem { menu.addItem(.separator()) menu.addItem(NSMenuItem(title: "Validate Token", action: #selector(validateToken), target: self)) menu.addItem(NSMenuItem(title: "Check Entitlements", action: #selector(checkEntitlements), target: self)) + menu.addItem(NSMenuItem(title: "Get Subscription Info", action: #selector(getSubscriptionInfo), target: self)) if #available(macOS 12.0, *) { menu.addItem(NSMenuItem(title: "Check Purchase Products Availability", action: #selector(checkProductsAvailability), target: self)) } @@ -115,6 +117,19 @@ public final class SubscriptionDebugMenu: NSMenuItem { } } + @objc + func getSubscriptionInfo() { + Task { + guard let token = accountManager.accessToken else { return } + switch await SubscriptionService.getSubscriptionInfo(token: token) { + case .success(let response): + showAlert(title: "Subscription info", message: "\(response)") + case .failure(let error): + showAlert(title: "Subscription info", message: "\(error)") + } + } + } + @available(macOS 12.0, *) @objc func checkProductsAvailability() { From 81be15901009839c34632f165ba18ad9b80498c2 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Tue, 14 Nov 2023 15:07:53 +0100 Subject: [PATCH 12/96] Core of the purchase flow --- .../SubscriptionPagesUserScript.swift | 120 +++++++++++++++++- DuckDuckGo/Tab/UserScripts/UserScripts.swift | 2 +- .../Sources/Purchase/PurchaseManager.swift | 22 ++-- .../DebugMenu/DebugPurchaseModel.swift | 16 +-- .../DebugMenu/SubscriptionDebugMenu.swift | 10 ++ .../PreferencesSubscriptionModel.swift | 2 +- 6 files changed, 147 insertions(+), 25 deletions(-) diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 60bf60e795..cfb9074345 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -74,10 +74,10 @@ extension SubscriptionPagesUserScript: WKScriptMessageHandler { } /// -/// Use Email sub-feature +/// Use Subscription sub-feature /// -struct SubscriptionPagesUseEmailFeature: Subfeature { - weak var broker: UserScriptMessageBroker? +final class SubscriptionPagesUseSubscriptionFeature: Subfeature { + var broker: UserScriptMessageBroker? var featureName = "useSubscription" @@ -86,11 +86,19 @@ struct SubscriptionPagesUseEmailFeature: Subfeature { .exact(hostname: "abrown.duckduckgo.com") ]) + func with(broker: UserScriptMessageBroker) { + self.broker = broker + } + func handler(forMethodNamed methodName: String) -> Subfeature.Handler? { switch methodName { case "getSubscription": return getSubscription case "setSubscription": return setSubscription case "backToSettings": return backToSettings + case "getSubscriptionOptions": return getSubscriptionOptions + case "subscriptionSelected": return subscriptionSelected + case "activateSubscription": return activateSubscription + case "featureSelected": return featureSelected default: return nil } @@ -131,6 +139,112 @@ struct SubscriptionPagesUseEmailFeature: Subfeature { return nil } + + func getSubscriptionOptions(params: Any, original: WKScriptMessage) async throws -> Encodable? { + struct SubscriptionOptions: Encodable { + let platform: String + let options: [SubscriptionOption] + let features: [SubscriptionFeature] + } + + struct SubscriptionOption: Encodable { + let id: String + let cost: SubscriptionCost + + struct SubscriptionCost: Encodable { + let displayPrice: String + let recurrence: String + } + } + + enum SubscriptionFeatureName: String, CaseIterable { + case privateBrowsing = "private-browsing" + case privateSearch = "private-search" + case emailProtection = "email-protection" + case appTrackingProtection = "app-tracking-protection" + case vpn = "vpn" + case personalInformationRemoval = "personal-information-removal" + case identityTheftRestoration = "identity-theft-restoration" + } + + struct SubscriptionFeature: Encodable { + let name: String + } + + let subscriptionOptions = [SubscriptionOption(id: "bundle_1", cost: .init(displayPrice: "$9.99", recurrence: "monthly")), + SubscriptionOption(id: "bundle_2", cost: .init(displayPrice: "$99.99", recurrence: "yearly"))] + + let message = SubscriptionOptions(platform: "macos", + options: subscriptionOptions, + features: SubscriptionFeatureName.allCases.map { SubscriptionFeature(name: $0.rawValue) }) + + return message + } + + func subscriptionSelected(params: Any, original: WKScriptMessage) async throws -> Encodable? { + struct SubscriptionSelection: Decodable { + let id: String + } + + struct PurchaseUpdate: Codable { + let type: String + } + + let message = original + + guard let subscriptionSelection: SubscriptionSelection = DecodableHelper.decode(from: params) else { + assertionFailure("SubscriptionPagesUserScript: expected JSON representation of SubscriptionSelection") + return nil + } + + print("Selected: \(subscriptionSelection.id)") + + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + guard let webview = message.webView else { + print("No WebView") + return + } + +// self.broker?.push(method: "onPurchaseUpdate", params: PurchaseUpdate(type: "completed"), for: self, into: webview) + + print("Completed!") + self.pushAction(method: .onPurchaseUpdate, webView: original.webView!, params: PurchaseUpdate(type: "completed")) + + } + + return nil + } + + func activateSubscription(params: Any, original: WKScriptMessage) async throws -> Encodable? { + print(">>> Selected to activate a subscription -- show the activation settings screen") + return nil + } + + func featureSelected(params: Any, original: WKScriptMessage) async throws -> Encodable? { + struct FeatureSelection: Codable { + let feature: String + } + + guard let featureSelection: FeatureSelection = DecodableHelper.decode(from: params) else { + assertionFailure("SubscriptionPagesUserScript: expected JSON representation of FeatureSelection") + return nil + } + + print(">>> Selected a feature -- show the corresponding UI", featureSelection) + return nil + } + + enum SubscribeActionName: String { + case onPurchaseUpdate + } + + func pushAction(method: SubscribeActionName, webView: WKWebView, params: Encodable) { + let broker = UserScriptMessageBroker(context: SubscriptionPagesUserScript.context, requiresRunInPageContentWorld: true ) + + print(">>> Pushing into WebView:", method.rawValue, String(describing: params)) + broker.push(method: method.rawValue, params: params, for: self, into: webView) + } + } #endif diff --git a/DuckDuckGo/Tab/UserScripts/UserScripts.swift b/DuckDuckGo/Tab/UserScripts/UserScripts.swift index 0b801bd3e3..75f54035b9 100644 --- a/DuckDuckGo/Tab/UserScripts/UserScripts.swift +++ b/DuckDuckGo/Tab/UserScripts/UserScripts.swift @@ -87,7 +87,7 @@ final class UserScripts: UserScriptsProvider { } #if SUBSCRIPTION - subscriptionPagesUserScript.registerSubfeature(delegate: SubscriptionPagesUseEmailFeature()) + subscriptionPagesUserScript.registerSubfeature(delegate: SubscriptionPagesUseSubscriptionFeature()) userScripts.append(subscriptionPagesUserScript) #endif } diff --git a/LocalPackages/Purchase/Sources/Purchase/PurchaseManager.swift b/LocalPackages/Purchase/Sources/Purchase/PurchaseManager.swift index 9dc7563f46..82785da31f 100644 --- a/LocalPackages/Purchase/Sources/Purchase/PurchaseManager.swift +++ b/LocalPackages/Purchase/Sources/Purchase/PurchaseManager.swift @@ -67,22 +67,20 @@ public final class PurchaseManager: ObservableObject { } @MainActor - func restorePurchases() { - Task { - do { - purchaseQueue.removeAll() + public func restorePurchases() async { + do { + purchaseQueue.removeAll() - print("Before AppStore.sync()") + print("Before AppStore.sync()") - try await AppStore.sync() + try await AppStore.sync() - print("After AppStore.sync()") + print("After AppStore.sync()") - await updatePurchasedProducts() - await updateAvailableProducts() - } catch { - print("AppStore.sync error: \(error)") - } + await updatePurchasedProducts() + await updateAvailableProducts() + } catch { + print("AppStore.sync error: \(error)") } } diff --git a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseModel.swift b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseModel.swift index 3967a1b66c..2381086108 100644 --- a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseModel.swift +++ b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseModel.swift @@ -37,16 +37,16 @@ public final class DebugPurchaseModel: ObservableObject { print("Attempting purchase: \(product.displayName)") Task { - guard let token = AccountManager().accessToken else { return } - var externalID: String? - switch await AuthService.validateToken(accessToken: token) { - case .success(let response): - externalID = response.account.externalID - case .failure(let error): - print("Error: \(error)") - return + if let token = AccountManager().accessToken { + switch await AuthService.validateToken(accessToken: token) { + case .success(let response): + externalID = response.account.externalID + case .failure(let error): + print("Error: \(error)") + return + } } if let externalID { diff --git a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift index 677127e080..da148adb4b 100644 --- a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift +++ b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift @@ -57,6 +57,7 @@ public final class SubscriptionDebugMenu: NSMenuItem { menu.addItem(NSMenuItem(title: "Get Subscription Info", action: #selector(getSubscriptionInfo), target: self)) if #available(macOS 12.0, *) { menu.addItem(NSMenuItem(title: "Check Purchase Products Availability", action: #selector(checkProductsAvailability), target: self)) + menu.addItem(NSMenuItem(title: "Restore App Store purchases (requires sign-in)", action: #selector(restoreAppStore), target: self)) } menu.addItem(NSMenuItem(title: "Restore Subscription from App Store transaction", action: #selector(restorePurchases), target: self)) menu.addItem(.separator()) @@ -130,10 +131,19 @@ public final class SubscriptionDebugMenu: NSMenuItem { } } + @available(macOS 12.0, *) + @objc + func restoreAppStore() { + Task { + await purchaseManager.restorePurchases() + } + } + @available(macOS 12.0, *) @objc func checkProductsAvailability() { Task { + let result = await purchaseManager.hasProductsAvailable() showAlert(title: "Check App Store Product Availability", message: "Can purchase: \(result ? "YES" : "NO")") diff --git a/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionModel.swift b/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionModel.swift index 5d6258242a..42dced9f7d 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionModel.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionModel.swift @@ -22,7 +22,7 @@ import Account extension URL { static var purchaseSubscription: URL { - URL(string: "https://duckduckgo.com/about")! + URL(string: "https://abrown.duckduckgo.com/subscriptions/welcome")! } static var subscriptionFAQ: URL { From fd231db2aaf8a02300fa70ea6e5568fbb4ec1ccd Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Thu, 16 Nov 2023 09:28:39 +0100 Subject: [PATCH 13/96] Refresh available products on entering settings --- .../Preferences/View/PreferencesViewController.swift | 12 ++++++++++++ .../DebugMenu/PurchaseInProgressViewController.swift | 8 ++++++++ 2 files changed, 20 insertions(+) create mode 100644 LocalPackages/Subscription/Sources/Subscription/DebugMenu/PurchaseInProgressViewController.swift diff --git a/DuckDuckGo/Preferences/View/PreferencesViewController.swift b/DuckDuckGo/Preferences/View/PreferencesViewController.swift index f7920d8f60..d365a48000 100644 --- a/DuckDuckGo/Preferences/View/PreferencesViewController.swift +++ b/DuckDuckGo/Preferences/View/PreferencesViewController.swift @@ -20,6 +20,10 @@ import AppKit import SwiftUI import Combine +#if SUBSCRIPTION +import Purchase +#endif + final class PreferencesViewController: NSViewController { weak var delegate: BrowserTabSelectionDelegate? @@ -60,6 +64,14 @@ final class PreferencesViewController: NSViewController { .sink { [weak self] identifier in self?.delegate?.selectedPreferencePane(identifier) } + +#if SUBSCRIPTION + if #available(macOS 12.0, *) { + Task { + await PurchaseManager.shared.updateAvailableProducts() + } + } +#endif } override func viewWillDisappear() { diff --git a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/PurchaseInProgressViewController.swift b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/PurchaseInProgressViewController.swift new file mode 100644 index 0000000000..25723e0511 --- /dev/null +++ b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/PurchaseInProgressViewController.swift @@ -0,0 +1,8 @@ +// +// File.swift +// +// +// Created by Michał Smaga on 15/11/2023. +// + +import Foundation From 8b8c5306f89e0bd706d2dbee173bbdcba8b74e5e Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Thu, 16 Nov 2023 09:29:55 +0100 Subject: [PATCH 14/96] Complete purchase flow --- .../SubscriptionPagesUserScript.swift | 133 ++++++++++++++++-- .../Sources/Account/AccountManager.swift | 65 +++++++++ .../Sources/Purchase/PurchaseManager.swift | 78 ++++++---- .../DebugMenu/DebugPurchaseModel.swift | 4 +- .../PurchaseInProgressViewController.swift | 75 +++++++++- 5 files changed, 305 insertions(+), 50 deletions(-) diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index cfb9074345..eb562168b5 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -26,6 +26,8 @@ import Navigation import WebKit import UserScript import Account +import Purchase +import Subscription public extension Notification.Name { static let subscriptionPageCloseAndOpenPreferences = Notification.Name("com.duckduckgo.subscriptionPage.CloseAndOpenPreferences") @@ -171,8 +173,19 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { let name: String } - let subscriptionOptions = [SubscriptionOption(id: "bundle_1", cost: .init(displayPrice: "$9.99", recurrence: "monthly")), - SubscriptionOption(id: "bundle_2", cost: .init(displayPrice: "$99.99", recurrence: "yearly"))] + let subscriptionOptions: [SubscriptionOption] + + if #available(macOS 12.0, *) { + let monthly = PurchaseManager.shared.availableProducts.first(where: { $0.id.contains("1month") }) + let yearly = PurchaseManager.shared.availableProducts.first(where: { $0.id.contains("1year") }) + + guard let monthly, let yearly else { return nil } + + subscriptionOptions = [SubscriptionOption(id: monthly.id, cost: .init(displayPrice: monthly.displayPrice, recurrence: "monthly")), + SubscriptionOption(id: yearly.id, cost: .init(displayPrice: yearly.displayPrice, recurrence: "yearly"))] + } else { + return nil + } let message = SubscriptionOptions(platform: "macos", options: subscriptionOptions, @@ -192,29 +205,114 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { let message = original - guard let subscriptionSelection: SubscriptionSelection = DecodableHelper.decode(from: params) else { - assertionFailure("SubscriptionPagesUserScript: expected JSON representation of SubscriptionSelection") - return nil - } + if #available(macOS 12.0, *) { + guard let subscriptionSelection: SubscriptionSelection = DecodableHelper.decode(from: params) else { + assertionFailure("SubscriptionPagesUserScript: expected JSON representation of SubscriptionSelection") + return nil + } + + print("Selected: \(subscriptionSelection.id)") - print("Selected: \(subscriptionSelection.id)") + await showProgress(with: "Purchase in progress...") - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - guard let webview = message.webView else { - print("No WebView") - return + // Hide after some time in case nothing happens + /* + DispatchQueue.main.asyncAfter(deadline: .now() + 60) { + print("hiding it since nothing happened!") + self.hideProgress() } + */ + + await PurchaseManager.shared.restorePurchases() + + var externalID = await AccountManager().asyncSignInByRestoringPastPurchases() -// self.broker?.push(method: "onPurchaseUpdate", params: PurchaseUpdate(type: "completed"), for: self, into: webview) + if externalID == "error" { + print("No past transactions or account or both?") - print("Completed!") - self.pushAction(method: .onPurchaseUpdate, webView: original.webView!, params: PurchaseUpdate(type: "completed")) + switch await AuthService.createAccount() { + case .success(let response): + AccountManager().exchangeTokensAndRefreshEntitlements(with: response.authToken) + externalID = response.externalID + case .failure(let error): + print("Error: \(error)") + return nil + } + } + + guard externalID != "error" else { + print("still some error") + await hideProgress() + return nil + } + // Has account purchase then + print("Has account \(externalID)") + + // rework to wrap and make a purchase with identifier + if let product = PurchaseManager.shared.availableProducts.first(where: { $0.id == subscriptionSelection.id }) { + let purchaseResult = await PurchaseManager.shared.purchase(product, customUUID: externalID) + + if purchaseResult == "ok" { + // purchase ok now wait for the entitlements + await updateProgressTitle("Completing purchase...") + + print("[Loop start]") + var count = 0 + var hasEntitlements = false + + repeat { + print("Attempt \(count)") + let entitlements = await AccountManager().fetchEntitlements() + hasEntitlements = !entitlements.isEmpty + + if !hasEntitlements { + count += 1 + try await Task.sleep(seconds: 2) + } else { + print("Got entitlements!") + } + } while !hasEntitlements && count < 15 + print("[Loop end]") + + // Done + await hideProgress() + DispatchQueue.main.async { + self.pushAction(method: .onPurchaseUpdate, webView: original.webView!, params: PurchaseUpdate(type: "completed")) + } + } else { + print("Something went wrong, reason: \(purchaseResult)") + await hideProgress() + } + } } return nil } + private weak var purchaseInProgressViewController: PurchaseInProgressViewController? + + @MainActor + private func showProgress(with title: String) { + guard purchaseInProgressViewController == nil else { return } + let progressVC = PurchaseInProgressViewController(title: title) + WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.presentAsSheet(progressVC) + purchaseInProgressViewController = progressVC + } + + @MainActor + private func updateProgressTitle(_ title: String) { + guard let purchaseInProgressViewController else { return } + purchaseInProgressViewController.updateTitleText(title) + } + + @MainActor + private func hideProgress() { + guard let purchaseInProgressViewController else { return } + WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.dismiss(purchaseInProgressViewController) + self.purchaseInProgressViewController = nil + } + func activateSubscription(params: Any, original: WKScriptMessage) async throws -> Encodable? { print(">>> Selected to activate a subscription -- show the activation settings screen") return nil @@ -247,4 +345,11 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { } +extension Task where Success == Never, Failure == Never { + static func sleep(seconds: Double) async throws { + let duration = UInt64(seconds * 1_000_000_000) + try await Task.sleep(nanoseconds: duration) + } +} + #endif diff --git a/LocalPackages/Account/Sources/Account/AccountManager.swift b/LocalPackages/Account/Sources/Account/AccountManager.swift index 1907b3d0e5..38e84b0620 100644 --- a/LocalPackages/Account/Sources/Account/AccountManager.swift +++ b/LocalPackages/Account/Sources/Account/AccountManager.swift @@ -153,6 +153,20 @@ public class AccountManager { } } + public func fetchEntitlements() async -> [String] { + guard let accessToken else { return [] } + + switch await AuthService.validateToken(accessToken: accessToken) { + case .success(let response): + let entitlements = response.account.entitlements + return entitlements.map { $0.name } + + case .failure(let error): + os_log("AccountManager error: %{public}@", log: .error, error.localizedDescription) + return [] + } + } + public func signInByRestoringPastPurchases() { if #available(macOS 12.0, *) { Task { @@ -203,4 +217,55 @@ public class AccountManager { } } } + + public func asyncSignInByRestoringPastPurchases() async -> String { + if #available(macOS 12.0, *) { + // Fetch most recent purchase + guard let jwsRepresentation = await PurchaseManager.mostRecentTransaction() else { + os_log("No transactions", log: .error) + return "error" + } + + // Do the store login to get short-lived token + let authToken: String + switch await AuthService.storeLogin(signature: jwsRepresentation) { + case .success(let response): + authToken = response.authToken + case .failure(let error): + os_log("AccountManager error: %{public}@", log: .error, error.localizedDescription) + return "error" + } + + storeAuthToken(token: authToken) + return await asyncExchangeTokensAndRefreshEntitlements(with: authToken) + } + + return "" + } + + public func asyncExchangeTokensAndRefreshEntitlements(with authToken: String) async -> String { + // Exchange short-lived auth token to a long-lived access token + let accessToken: String + switch await AuthService.getAccessToken(token: authToken) { + case .success(let response): + accessToken = response.accessToken + case .failure(let error): + os_log("AccountManager error: %{public}@", log: .error, error.localizedDescription) + return "error" + } + + // Fetch entitlements and account details and store the data + switch await AuthService.validateToken(accessToken: accessToken) { + case .success(let response): + self.storeAuthToken(token: authToken) + self.storeAccount(token: accessToken, + email: response.account.email) + + return response.account.externalID + + case .failure(let error): + os_log("AccountManager error: %{public}@", log: .error, error.localizedDescription) + return "error" + } + } } diff --git a/LocalPackages/Purchase/Sources/Purchase/PurchaseManager.swift b/LocalPackages/Purchase/Sources/Purchase/PurchaseManager.swift index 82785da31f..5225ea2780 100644 --- a/LocalPackages/Purchase/Sources/Purchase/PurchaseManager.swift +++ b/LocalPackages/Purchase/Sources/Purchase/PurchaseManager.swift @@ -102,7 +102,7 @@ public final class PurchaseManager: ObservableObject { } @MainActor - public func fetchAvailableProducts() async -> [Product] { + func fetchAvailableProducts() async -> [Product] { print(" -- [PurchaseManager] fetchAvailableProducts()") do { @@ -157,6 +157,11 @@ public final class PurchaseManager: ObservableObject { var transactions: [VerificationResult] = [] + +// if let result = await Transaction.latest(for: "subscription.1month") { +// transactions.append(result) +// } + for await result in Transaction.all { transactions.append(result) } @@ -167,46 +172,59 @@ public final class PurchaseManager: ObservableObject { } @MainActor - public func purchase(_ product: Product, customUUID: String) { + public func purchase(_ product: Product, customUUID: String) async -> String { print(" -- [PurchaseManager] buy: \(product.displayName) (customUUID: \(customUUID))") print("purchaseQueue append!") purchaseQueue.append(product.id) - Task { - print(" -- [PurchaseManager] starting await task") + print(" -- [PurchaseManager] starting purchase") - var options: Set = Set() + var options: Set = Set() - if let token = UUID(uuidString: customUUID) { - options.insert(.appAccountToken(token)) - } + if let token = UUID(uuidString: customUUID) { + options.insert(.appAccountToken(token)) + } else { + print("Wrong UUID") + return "error" + } - let result = try await product.purchase(options: options) - print(" -- [PurchaseManager] receiving await task result") - purchaseQueue.removeAll() - print("purchaseQueue removeAll!") + let result: Product.PurchaseResult + do { + result = try await product.purchase(options: options) + } catch { + print("error \(error)") + return "error" + } - switch result { - case let .success(.verified(transaction)): - // Successful purchase - await transaction.finish() - await self.updatePurchasedProducts() - case let .success(.unverified(_, error)): - // Successful purchase but transaction/receipt can't be verified - // Could be a jailbroken phone - print("Error: \(error.localizedDescription)") - case .pending: - // Transaction waiting on SCA (Strong Customer Authentication) or - // approval from Ask to Buy - break - case .userCancelled: - break - @unknown default: - break - } + print(" -- [PurchaseManager] purchase complete") + + purchaseQueue.removeAll() + print("purchaseQueue removeAll!") + + switch result { + case let .success(.verified(transaction)): + // Successful purchase + await transaction.finish() + await self.updatePurchasedProducts() + return "ok" + case let .success(.unverified(_, error)): + // Successful purchase but transaction/receipt can't be verified + // Could be a jailbroken phone + print("Error: \(error.localizedDescription)") + return "error" + case .pending: + // Transaction waiting on SCA (Strong Customer Authentication) or + // approval from Ask to Buy + break + case .userCancelled: + return "cancelled" + @unknown default: + return "unknown" } + + return "unknown" } private func checkVerified(_ result: VerificationResult) throws -> T { diff --git a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseModel.swift b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseModel.swift index 2381086108..c33347b880 100644 --- a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseModel.swift +++ b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseModel.swift @@ -50,11 +50,11 @@ public final class DebugPurchaseModel: ObservableObject { } if let externalID { - manager.purchase(product, customUUID: externalID) + await manager.purchase(product, customUUID: externalID) } else { switch await AuthService.createAccount() { case .success(let response): - manager.purchase(product, customUUID: response.externalID) + await manager.purchase(product, customUUID: response.externalID) AccountManager().exchangeTokensAndRefreshEntitlements(with: response.authToken) case .failure(let error): print("Error: \(error)") diff --git a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/PurchaseInProgressViewController.swift b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/PurchaseInProgressViewController.swift index 25723e0511..5a3babbf5b 100644 --- a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/PurchaseInProgressViewController.swift +++ b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/PurchaseInProgressViewController.swift @@ -1,8 +1,75 @@ // -// File.swift -// +// PurchaseInProgressViewController.swift // -// Created by Michał Smaga on 15/11/2023. +// Copyright © 2023 DuckDuckGo. All rights reserved. // +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import AppKit +import SwiftUI + +public final class PurchaseInProgressViewController: NSViewController { + + private var purchaseInProgressView: PurchaseInProgressView? + private var viewModel: PurchaseInProgressViewModel + + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public init(title: String) { + self.viewModel = PurchaseInProgressViewModel(title: title) + super.init(nibName: nil, bundle: nil) + } + + public override func loadView() { + + let purchaseInProgressView = PurchaseInProgressView(viewModel: viewModel) + let hostingView = NSHostingView(rootView: purchaseInProgressView) + + self.purchaseInProgressView = purchaseInProgressView + + view = NSView(frame: NSRect(x: 0, y: 0, width: 360, height: 160)) + hostingView.frame = view.bounds + hostingView.autoresizingMask = [.height, .width] + hostingView.translatesAutoresizingMaskIntoConstraints = true + + view.addSubview(hostingView) + } + + public func updateTitleText(_ text: String) { + self.viewModel.title = text + } +} + +final class PurchaseInProgressViewModel: ObservableObject { + @Published var title: String + + init(title: String) { + self.title = title + } +} + +struct PurchaseInProgressView: View { + + @ObservedObject var viewModel: PurchaseInProgressViewModel -import Foundation + public var body: some View { + VStack { + Text(viewModel.title).font(.title) + Spacer().frame(height: 32) + ActivityIndicator(isAnimating: .constant(true), style: .spinning) + } + } +} From b8313e91172abf9640366f0cfcb48921414129e5 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Thu, 16 Nov 2023 11:01:12 +0100 Subject: [PATCH 15/96] Rework AccountManager API to async --- .../View/PreferencesRootView.swift | 4 +- .../SubscriptionPagesUserScript.swift | 16 ++-- .../Sources/Account/AccountManager.swift | 73 ++----------------- .../DebugMenu/DebugPurchaseModel.swift | 12 +-- .../DebugMenu/SubscriptionDebugMenu.swift | 6 +- 5 files changed, 27 insertions(+), 84 deletions(-) diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index 6d76f5e5ea..97dbe7ae82 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -105,7 +105,9 @@ extension Preferences { }) let sheetActionHandler = SubscriptionAccessActionHandlers(restorePurchases: { - AccountManager().signInByRestoringPastPurchases() + Task { + await AccountManager().signInByRestoringPastPurchases() + } }, openURLHandler: { url in WindowControllersManager.shared.show(url: url, newTab: true) }, goToSyncPreferences: { diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index eb562168b5..66b1557140 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -130,7 +130,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { return nil } - AccountManager().exchangeTokensAndRefreshEntitlements(with: subscriptionValues.token) + _ = await AccountManager().exchangeTokensAndRefreshEntitlements(with: subscriptionValues.token) return nil } @@ -225,15 +225,15 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { await PurchaseManager.shared.restorePurchases() - var externalID = await AccountManager().asyncSignInByRestoringPastPurchases() + var externalID = await AccountManager().signInByRestoringPastPurchases() if externalID == "error" { print("No past transactions or account or both?") switch await AuthService.createAccount() { case .success(let response): - AccountManager().exchangeTokensAndRefreshEntitlements(with: response.authToken) externalID = response.externalID + externalID = await AccountManager().exchangeTokensAndRefreshEntitlements(with: response.authToken) case .failure(let error): print("Error: \(error)") return nil @@ -278,7 +278,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { // Done await hideProgress() DispatchQueue.main.async { - self.pushAction(method: .onPurchaseUpdate, webView: original.webView!, params: PurchaseUpdate(type: "completed")) + self.pushAction(method: .onPurchaseUpdate, webView: message.webView!, params: PurchaseUpdate(type: "completed")) } } else { print("Something went wrong, reason: \(purchaseResult)") @@ -317,21 +317,21 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { print(">>> Selected to activate a subscription -- show the activation settings screen") return nil } - + func featureSelected(params: Any, original: WKScriptMessage) async throws -> Encodable? { struct FeatureSelection: Codable { let feature: String } - + guard let featureSelection: FeatureSelection = DecodableHelper.decode(from: params) else { assertionFailure("SubscriptionPagesUserScript: expected JSON representation of FeatureSelection") return nil } - + print(">>> Selected a feature -- show the corresponding UI", featureSelection) return nil } - + enum SubscribeActionName: String { case onPurchaseUpdate } diff --git a/LocalPackages/Account/Sources/Account/AccountManager.swift b/LocalPackages/Account/Sources/Account/AccountManager.swift index 38e84b0620..e021c5249e 100644 --- a/LocalPackages/Account/Sources/Account/AccountManager.swift +++ b/LocalPackages/Account/Sources/Account/AccountManager.swift @@ -55,7 +55,7 @@ public class AccountManager { return nil } } - + public var accessToken: String? { do { return try storage.getAccessToken() @@ -138,19 +138,7 @@ public class AccountManager { // MARK: - public func hasEntitlement(for name: String) async -> Bool { - guard let accessToken else { return false } - - switch await AuthService.validateToken(accessToken: accessToken) { - case .success(let response): - let entitlements = response.account.entitlements - return entitlements.contains { entitlement in - entitlement.name == name - } - - case .failure(let error): - os_log("AccountManager error: %{public}@", log: .error, error.localizedDescription) - return false - } + await fetchEntitlements().contains(name) } public func fetchEntitlements() async -> [String] { @@ -167,58 +155,7 @@ public class AccountManager { } } - public func signInByRestoringPastPurchases() { - if #available(macOS 12.0, *) { - Task { - // Fetch most recent purchase - guard let jwsRepresentation = await PurchaseManager.mostRecentTransaction() else { - os_log("No transactions", log: .error) - return - } - - // Do the store login to get short-lived token - let authToken: String - switch await AuthService.storeLogin(signature: jwsRepresentation) { - case .success(let response): - authToken = response.authToken - case .failure(let error): - os_log("AccountManager error: %{public}@", log: .error, error.localizedDescription) - return - } - - storeAuthToken(token: authToken) - exchangeTokensAndRefreshEntitlements(with: authToken) - } - } - } - - public func exchangeTokensAndRefreshEntitlements(with authToken: String) { - Task { - // Exchange short-lived auth token to a long-lived access token - let accessToken: String - switch await AuthService.getAccessToken(token: authToken) { - case .success(let response): - accessToken = response.accessToken - case .failure(let error): - os_log("AccountManager error: %{public}@", log: .error, error.localizedDescription) - return - } - - // Fetch entitlements and account details and store the data - switch await AuthService.validateToken(accessToken: accessToken) { - case .success(let response): - self.storeAuthToken(token: authToken) - self.storeAccount(token: accessToken, - email: response.account.email) - - case .failure(let error): - os_log("AccountManager error: %{public}@", log: .error, error.localizedDescription) - return - } - } - } - - public func asyncSignInByRestoringPastPurchases() async -> String { + public func signInByRestoringPastPurchases() async -> String { if #available(macOS 12.0, *) { // Fetch most recent purchase guard let jwsRepresentation = await PurchaseManager.mostRecentTransaction() else { @@ -237,13 +174,13 @@ public class AccountManager { } storeAuthToken(token: authToken) - return await asyncExchangeTokensAndRefreshEntitlements(with: authToken) + return await exchangeTokensAndRefreshEntitlements(with: authToken) } return "" } - public func asyncExchangeTokensAndRefreshEntitlements(with authToken: String) async -> String { + public func exchangeTokensAndRefreshEntitlements(with authToken: String) async -> String { // Exchange short-lived auth token to a long-lived access token let accessToken: String switch await AuthService.getAccessToken(token: authToken) { diff --git a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseModel.swift b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseModel.swift index c33347b880..e2b66e98b3 100644 --- a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseModel.swift +++ b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseModel.swift @@ -24,11 +24,13 @@ import Account @available(macOS 12.0, *) public final class DebugPurchaseModel: ObservableObject { - var manager: PurchaseManager + var purchaseManager: PurchaseManager + var accountManager: AccountManager = AccountManager() + @Published var subscriptions: [SubscriptionRowModel] init(manager: PurchaseManager, subscriptions: [SubscriptionRowModel] = []) { - self.manager = manager + self.purchaseManager = manager self.subscriptions = subscriptions } @@ -50,12 +52,12 @@ public final class DebugPurchaseModel: ObservableObject { } if let externalID { - await manager.purchase(product, customUUID: externalID) + _ = await purchaseManager.purchase(product, customUUID: externalID) } else { switch await AuthService.createAccount() { case .success(let response): - await manager.purchase(product, customUUID: response.externalID) - AccountManager().exchangeTokensAndRefreshEntitlements(with: response.authToken) + _ = await purchaseManager.purchase(product, customUUID: response.externalID) + _ = await accountManager.exchangeTokensAndRefreshEntitlements(with: response.authToken) case .failure(let error): print("Error: \(error)") return diff --git a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift index da148adb4b..95ef0bfc3c 100644 --- a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift +++ b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift @@ -106,7 +106,7 @@ public final class SubscriptionDebugMenu: NSMenuItem { func checkEntitlements() { Task { var results: [String] = [] - + for entitlementName in ["fake", "dummy1", "dummy2", "dummy3"] { let result = await AccountManager().hasEntitlement(for: entitlementName) let resultSummary = "Entitlement check for \(entitlementName): \(result)" @@ -152,7 +152,9 @@ public final class SubscriptionDebugMenu: NSMenuItem { @objc func restorePurchases(_ sender: Any?) { - accountManager.signInByRestoringPastPurchases() + Task { + await accountManager.signInByRestoringPastPurchases() + } } @objc From 7ce8193cffe1e1fd5382ac0fa842eaa1f4468369 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Thu, 16 Nov 2023 11:10:07 +0100 Subject: [PATCH 16/96] Decouple Account and Purchase packages --- .../View/PreferencesRootView.swift | 7 ++-- .../SubscriptionPagesUserScript.swift | 3 +- LocalPackages/Account/Package.swift | 2 -- .../Sources/Account/AccountManager.swift | 33 +++++++------------ .../DebugMenu/SubscriptionDebugMenu.swift | 7 ++-- 5 files changed, 23 insertions(+), 29 deletions(-) diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index 97dbe7ae82..a72ac843a8 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -105,8 +105,11 @@ extension Preferences { }) let sheetActionHandler = SubscriptionAccessActionHandlers(restorePurchases: { - Task { - await AccountManager().signInByRestoringPastPurchases() + if #available(macOS 12.0, *) { + Task { + guard let jwsRepresentation = await PurchaseManager.mostRecentTransaction() else { return } + await AccountManager().signInByRestoringPastPurchases(from: jwsRepresentation) + } } }, openURLHandler: { url in WindowControllersManager.shared.show(url: url, newTab: true) diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 66b1557140..058df588e9 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -225,7 +225,8 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { await PurchaseManager.shared.restorePurchases() - var externalID = await AccountManager().signInByRestoringPastPurchases() + guard let jwsRepresentation = await PurchaseManager.mostRecentTransaction() else { return "" } + var externalID = await AccountManager().signInByRestoringPastPurchases(from: jwsRepresentation) if externalID == "error" { print("No past transactions or account or both?") diff --git a/LocalPackages/Account/Package.swift b/LocalPackages/Account/Package.swift index a736d21ae8..3da49fc4d3 100644 --- a/LocalPackages/Account/Package.swift +++ b/LocalPackages/Account/Package.swift @@ -13,14 +13,12 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "81.4.0"), - .package(path: "../Purchase") ], targets: [ .target( name: "Account", dependencies: [ .product(name: "BrowserServicesKit", package: "BrowserServicesKit"), - .product(name: "Purchase", package: "Purchase") ]), .testTarget( name: "AccountTests", diff --git a/LocalPackages/Account/Sources/Account/AccountManager.swift b/LocalPackages/Account/Sources/Account/AccountManager.swift index e021c5249e..b46229bbfc 100644 --- a/LocalPackages/Account/Sources/Account/AccountManager.swift +++ b/LocalPackages/Account/Sources/Account/AccountManager.swift @@ -17,7 +17,6 @@ // import Foundation -import Purchase import Common public extension Notification.Name { @@ -155,29 +154,19 @@ public class AccountManager { } } - public func signInByRestoringPastPurchases() async -> String { - if #available(macOS 12.0, *) { - // Fetch most recent purchase - guard let jwsRepresentation = await PurchaseManager.mostRecentTransaction() else { - os_log("No transactions", log: .error) - return "error" - } - - // Do the store login to get short-lived token - let authToken: String - switch await AuthService.storeLogin(signature: jwsRepresentation) { - case .success(let response): - authToken = response.authToken - case .failure(let error): - os_log("AccountManager error: %{public}@", log: .error, error.localizedDescription) - return "error" - } - - storeAuthToken(token: authToken) - return await exchangeTokensAndRefreshEntitlements(with: authToken) + public func signInByRestoringPastPurchases(from lastTransactionJWSRepresentation: String) async -> String { + // Do the store login to get short-lived token + let authToken: String + switch await AuthService.storeLogin(signature: lastTransactionJWSRepresentation) { + case .success(let response): + authToken = response.authToken + case .failure(let error): + os_log("AccountManager error: %{public}@", log: .error, error.localizedDescription) + return "error" } - return "" + storeAuthToken(token: authToken) + return await exchangeTokensAndRefreshEntitlements(with: authToken) } public func exchangeTokensAndRefreshEntitlements(with authToken: String) async -> String { diff --git a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift index 95ef0bfc3c..02c8db8795 100644 --- a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift +++ b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift @@ -152,8 +152,11 @@ public final class SubscriptionDebugMenu: NSMenuItem { @objc func restorePurchases(_ sender: Any?) { - Task { - await accountManager.signInByRestoringPastPurchases() + if #available(macOS 12.0, *) { + Task { + guard let jwsRepresentation = await PurchaseManager.mostRecentTransaction() else { return } + _ = await accountManager.signInByRestoringPastPurchases(from: jwsRepresentation) + } } } From 796dd94f19e2a74b186288ea1f564a229c14ee4e Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Thu, 16 Nov 2023 12:14:40 +0100 Subject: [PATCH 17/96] Extract AppStore purchase flow --- .../SubscriptionPagesUserScript.swift | 71 ++-------- .../PurchaseFlows/AppStorePurchaseFlow.swift | 126 ++++++++++++++++++ 2 files changed, 138 insertions(+), 59 deletions(-) create mode 100644 LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 058df588e9..faab2a3067 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -215,7 +215,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { await showProgress(with: "Purchase in progress...") - // Hide after some time in case nothing happens + // Hide it after some time in case nothing happens /* DispatchQueue.main.asyncAfter(deadline: .now() + 60) { print("hiding it since nothing happened!") @@ -223,68 +223,21 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { } */ - await PurchaseManager.shared.restorePurchases() - - guard let jwsRepresentation = await PurchaseManager.mostRecentTransaction() else { return "" } - var externalID = await AccountManager().signInByRestoringPastPurchases(from: jwsRepresentation) + switch await AppStorePurchaseFlow.purchaseSubscription(with: subscriptionSelection.id) { + case .success: + break + case .failure: + return nil + } - if externalID == "error" { - print("No past transactions or account or both?") + await updateProgressTitle("Completing purchase...") - switch await AuthService.createAccount() { - case .success(let response): - externalID = response.externalID - externalID = await AccountManager().exchangeTokensAndRefreshEntitlements(with: response.authToken) - case .failure(let error): - print("Error: \(error)") - return nil - } - } + await AppStorePurchaseFlow.checkForEntitlements(wait: 2.0, retry: 15) - guard externalID != "error" else { - print("still some error") - await hideProgress() - return nil - } + await hideProgress() - // Has account purchase then - print("Has account \(externalID)") - - // rework to wrap and make a purchase with identifier - if let product = PurchaseManager.shared.availableProducts.first(where: { $0.id == subscriptionSelection.id }) { - let purchaseResult = await PurchaseManager.shared.purchase(product, customUUID: externalID) - - if purchaseResult == "ok" { - // purchase ok now wait for the entitlements - await updateProgressTitle("Completing purchase...") - - print("[Loop start]") - var count = 0 - var hasEntitlements = false - - repeat { - print("Attempt \(count)") - let entitlements = await AccountManager().fetchEntitlements() - hasEntitlements = !entitlements.isEmpty - - if !hasEntitlements { - count += 1 - try await Task.sleep(seconds: 2) - } else { - print("Got entitlements!") - } - } while !hasEntitlements && count < 15 - print("[Loop end]") - - // Done - await hideProgress() - DispatchQueue.main.async { - self.pushAction(method: .onPurchaseUpdate, webView: message.webView!, params: PurchaseUpdate(type: "completed")) - } - } else { - print("Something went wrong, reason: \(purchaseResult)") - await hideProgress() - } + DispatchQueue.main.async { + self.pushAction(method: .onPurchaseUpdate, webView: message.webView!, params: PurchaseUpdate(type: "completed")) } } diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift new file mode 100644 index 0000000000..5d7ad49a22 --- /dev/null +++ b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift @@ -0,0 +1,126 @@ +// +// AppStorePurchaseFlow.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import StoreKit +import Purchase +import Account + +public final class AppStorePurchaseFlow { + + public enum PurchaseResult { + case ok + } + + public enum Error: Swift.Error { + case noPastTransactions + case accountCreationUnsuccessful + case purchaseUnsuccessful + case somethingWentWrong + } + + public static func purchaseSubscription(with identifier: String) async -> Result { + if #available(macOS 12.0, *) { + // Trigger sign in pop-up + await PurchaseManager.shared.restorePurchases() + + // Get externalID from current account based on past purchases or create a new one + guard let externalID = await getUsersExternalID() else { return .failure(.accountCreationUnsuccessful) } + + // Make the purchase + switch await makePurchase(identifier, externalID: externalID) { + case true: + return .success(.ok) + case false: + return .failure(.purchaseUnsuccessful) + } + } + + return .failure(.somethingWentWrong) + } + + @available(macOS 12.0, *) + private static func getUsersExternalID() async -> String? { + var externalID: String? + + // Try fetching most recent + if let jwsRepresentation = await PurchaseManager.mostRecentTransaction() { + externalID = await AccountManager().signInByRestoringPastPurchases(from: jwsRepresentation) + } + + if externalID == "error" { + print("No past transactions or account or both?") + + switch await AuthService.createAccount() { + case .success(let response): + externalID = response.externalID + _ = await AccountManager().exchangeTokensAndRefreshEntitlements(with: response.authToken) + case .failure(let error): + print("Error: \(error)") + externalID = nil + } + } + + return externalID + } + + @available(macOS 12.0, *) + private static func makePurchase(_ identifier: String, externalID: String) async -> Bool { + // rework to wrap and make a purchase with identifier + if let product = PurchaseManager.shared.availableProducts.first(where: { $0.id == identifier }) { + let purchaseResult = await PurchaseManager.shared.purchase(product, customUUID: externalID) + + if purchaseResult == "ok" { + return true + } else { + print("Something went wrong, reason: \(purchaseResult)") + return false + } + } + + return false + } + + @discardableResult + public static func checkForEntitlements(wait second: Double, retry times: Int) async -> Bool { + var count = 0 + var hasEntitlements = false + + repeat { + print("Attempt \(count)") + hasEntitlements = await !AccountManager().fetchEntitlements().isEmpty + + if hasEntitlements { + print("Got entitlements!") + break + } else { + count += 1 + try? await Task.sleep(seconds: 2) + } + } while !hasEntitlements && count < 15 + + return hasEntitlements + } +} + +extension Task where Success == Never, Failure == Never { + static func sleep(seconds: Double) async throws { + let duration = UInt64(seconds * 1_000_000_000) + try await Task.sleep(nanoseconds: duration) + } +} From 38de072faa9c11fc0d9e39d044fb1779d1d42451 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Thu, 16 Nov 2023 13:28:29 +0100 Subject: [PATCH 18/96] Update sync Apple ID method return result --- .../SubscriptionPagesUserScript.swift | 1 + .../Sources/Purchase/PurchaseManager.swift | 6 +- .../DebugMenu/DebugPurchaseModel.swift | 25 +------- .../DebugMenu/SubscriptionDebugMenu.swift | 6 +- .../PurchaseFlows/AppStorePurchaseFlow.swift | 39 ++++++------ .../PurchaseFlows/AppStoreRestoreFlow.swift | 59 +++++++++++++++++++ 6 files changed, 87 insertions(+), 49 deletions(-) create mode 100644 LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStoreRestoreFlow.swift diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index faab2a3067..ed546f9473 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -227,6 +227,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { case .success: break case .failure: + await hideProgress() return nil } diff --git a/LocalPackages/Purchase/Sources/Purchase/PurchaseManager.swift b/LocalPackages/Purchase/Sources/Purchase/PurchaseManager.swift index 5225ea2780..ea1fb05702 100644 --- a/LocalPackages/Purchase/Sources/Purchase/PurchaseManager.swift +++ b/LocalPackages/Purchase/Sources/Purchase/PurchaseManager.swift @@ -67,7 +67,8 @@ public final class PurchaseManager: ObservableObject { } @MainActor - public func restorePurchases() async { + @discardableResult + public func syncAppleIDAccount() async -> Result { do { purchaseQueue.removeAll() @@ -79,8 +80,11 @@ public final class PurchaseManager: ObservableObject { await updatePurchasedProducts() await updateAvailableProducts() + + return .success(()) } catch { print("AppStore.sync error: \(error)") + return .failure(error) } } diff --git a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseModel.swift b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseModel.swift index e2b66e98b3..6f0227c87b 100644 --- a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseModel.swift +++ b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseModel.swift @@ -39,30 +39,7 @@ public final class DebugPurchaseModel: ObservableObject { print("Attempting purchase: \(product.displayName)") Task { - var externalID: String? - - if let token = AccountManager().accessToken { - switch await AuthService.validateToken(accessToken: token) { - case .success(let response): - externalID = response.account.externalID - case .failure(let error): - print("Error: \(error)") - return - } - } - - if let externalID { - _ = await purchaseManager.purchase(product, customUUID: externalID) - } else { - switch await AuthService.createAccount() { - case .success(let response): - _ = await purchaseManager.purchase(product, customUUID: response.externalID) - _ = await accountManager.exchangeTokensAndRefreshEntitlements(with: response.authToken) - case .failure(let error): - print("Error: \(error)") - return - } - } + await AppStorePurchaseFlow.purchaseSubscription(with: product.id) } } } diff --git a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift index 02c8db8795..6aada18c04 100644 --- a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift +++ b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift @@ -57,11 +57,11 @@ public final class SubscriptionDebugMenu: NSMenuItem { menu.addItem(NSMenuItem(title: "Get Subscription Info", action: #selector(getSubscriptionInfo), target: self)) if #available(macOS 12.0, *) { menu.addItem(NSMenuItem(title: "Check Purchase Products Availability", action: #selector(checkProductsAvailability), target: self)) - menu.addItem(NSMenuItem(title: "Restore App Store purchases (requires sign-in)", action: #selector(restoreAppStore), target: self)) } menu.addItem(NSMenuItem(title: "Restore Subscription from App Store transaction", action: #selector(restorePurchases), target: self)) menu.addItem(.separator()) if #available(macOS 12.0, *) { + menu.addItem(NSMenuItem(title: "Sync App Store AppleID Account (re- sign-in)", action: #selector(syncAppleIDAccount), target: self)) menu.addItem(NSMenuItem(title: "Purchase Subscription from App Store", action: #selector(showPurchaseView), target: self)) } menu.addItem(.separator()) @@ -133,9 +133,9 @@ public final class SubscriptionDebugMenu: NSMenuItem { @available(macOS 12.0, *) @objc - func restoreAppStore() { + func syncAppleIDAccount() { Task { - await purchaseManager.restorePurchases() + await purchaseManager.syncAppleIDAccount() } } diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift index 5d7ad49a22..39c22b73a9 100644 --- a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift @@ -21,40 +21,38 @@ import StoreKit import Purchase import Account +@available(macOS 12.0, *) public final class AppStorePurchaseFlow { - public enum PurchaseResult { - case ok - } - public enum Error: Swift.Error { + case appStoreAuthenticationFailed case noPastTransactions case accountCreationUnsuccessful case purchaseUnsuccessful case somethingWentWrong } - public static func purchaseSubscription(with identifier: String) async -> Result { - if #available(macOS 12.0, *) { - // Trigger sign in pop-up - await PurchaseManager.shared.restorePurchases() + public static func purchaseSubscription(with identifier: String) async -> Result { + // Trigger sign in pop-up + switch await PurchaseManager.shared.syncAppleIDAccount() { + case .success: + break + case .failure: + return .failure(.appStoreAuthenticationFailed) + } - // Get externalID from current account based on past purchases or create a new one - guard let externalID = await getUsersExternalID() else { return .failure(.accountCreationUnsuccessful) } + // Get externalID from current account based on past purchases or create a new one + guard let externalID = await getUsersExternalID() else { return .failure(.accountCreationUnsuccessful) } - // Make the purchase - switch await makePurchase(identifier, externalID: externalID) { - case true: - return .success(.ok) - case false: - return .failure(.purchaseUnsuccessful) - } + // Make the purchase + switch await makePurchase(identifier, externalID: externalID) { + case true: + return .success(()) + case false: + return .failure(.purchaseUnsuccessful) } - - return .failure(.somethingWentWrong) } - @available(macOS 12.0, *) private static func getUsersExternalID() async -> String? { var externalID: String? @@ -79,7 +77,6 @@ public final class AppStorePurchaseFlow { return externalID } - @available(macOS 12.0, *) private static func makePurchase(_ identifier: String, externalID: String) async -> Bool { // rework to wrap and make a purchase with identifier if let product = PurchaseManager.shared.availableProducts.first(where: { $0.id == identifier }) { diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStoreRestoreFlow.swift b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStoreRestoreFlow.swift new file mode 100644 index 0000000000..b657404f05 --- /dev/null +++ b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStoreRestoreFlow.swift @@ -0,0 +1,59 @@ +// +// AppStorePurchaseFlow.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import StoreKit +import Purchase +import Account + +@available(macOS 12.0, *) +public final class AppStoreRestoreFlow { + + public enum Success { + case ok + } + + public enum Error: Swift.Error { + case missingAccountOrTransactions + case userCancelled + case somethingWentWrong + } + + public static func restoreAccountFromAppleID() async -> Result { + +// // Try fetching most recent +// if let jwsRepresentation = await PurchaseManager.mostRecentTransaction() { +// externalID = await AccountManager().signInByRestoringPastPurchases(from: jwsRepresentation) +// } +// +// if externalID == "error" { +// print("No past transactions or account or both?") +// +// switch await AuthService.createAccount() { +// case .success(let response): +// externalID = response.externalID +// _ = await AccountManager().exchangeTokensAndRefreshEntitlements(with: response.authToken) +// case .failure(let error): +// print("Error: \(error)") +// externalID = nil +// } +// } + + return .success(.ok) + } +} From cc4a3f42d6cb89b28fc8fdfbdfbf4a791b3c2c3b Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Thu, 16 Nov 2023 13:59:26 +0100 Subject: [PATCH 19/96] Further tweaking result types --- .../SubscriptionPagesUserScript.swift | 2 +- .../Sources/Account/AccountManager.swift | 9 ++-- .../Sources/Purchase/PurchaseManager.swift | 5 -- .../PurchaseFlows/AppStorePurchaseFlow.swift | 46 ++++++++----------- 4 files changed, 26 insertions(+), 36 deletions(-) diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index ed546f9473..a4fc94ec20 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -130,7 +130,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { return nil } - _ = await AccountManager().exchangeTokensAndRefreshEntitlements(with: subscriptionValues.token) + await AccountManager().exchangeTokensAndRefreshEntitlements(with: subscriptionValues.token) return nil } diff --git a/LocalPackages/Account/Sources/Account/AccountManager.swift b/LocalPackages/Account/Sources/Account/AccountManager.swift index b46229bbfc..3ecf853a01 100644 --- a/LocalPackages/Account/Sources/Account/AccountManager.swift +++ b/LocalPackages/Account/Sources/Account/AccountManager.swift @@ -154,7 +154,8 @@ public class AccountManager { } } - public func signInByRestoringPastPurchases(from lastTransactionJWSRepresentation: String) async -> String { + @discardableResult + public func signInByRestoringPastPurchases(from lastTransactionJWSRepresentation: String) async -> Result { // Do the store login to get short-lived token let authToken: String switch await AuthService.storeLogin(signature: lastTransactionJWSRepresentation) { @@ -162,11 +163,13 @@ public class AccountManager { authToken = response.authToken case .failure(let error): os_log("AccountManager error: %{public}@", log: .error, error.localizedDescription) - return "error" + return .failure(error) } storeAuthToken(token: authToken) - return await exchangeTokensAndRefreshEntitlements(with: authToken) + + let result = await exchangeTokensAndRefreshEntitlements(with: authToken) + return .success(result) } public func exchangeTokensAndRefreshEntitlements(with authToken: String) async -> String { diff --git a/LocalPackages/Purchase/Sources/Purchase/PurchaseManager.swift b/LocalPackages/Purchase/Sources/Purchase/PurchaseManager.swift index ea1fb05702..d2a17f9518 100644 --- a/LocalPackages/Purchase/Sources/Purchase/PurchaseManager.swift +++ b/LocalPackages/Purchase/Sources/Purchase/PurchaseManager.swift @@ -161,11 +161,6 @@ public final class PurchaseManager: ObservableObject { var transactions: [VerificationResult] = [] - -// if let result = await Transaction.latest(for: "subscription.1month") { -// transactions.append(result) -// } - for await result in Transaction.all { transactions.append(result) } diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift index 39c22b73a9..b566be7e3c 100644 --- a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift @@ -26,8 +26,8 @@ public final class AppStorePurchaseFlow { public enum Error: Swift.Error { case appStoreAuthenticationFailed - case noPastTransactions - case accountCreationUnsuccessful + case authenticatingWithTransactionFailed + case accountCreationFailed case purchaseUnsuccessful case somethingWentWrong } @@ -41,40 +41,32 @@ public final class AppStorePurchaseFlow { return .failure(.appStoreAuthenticationFailed) } - // Get externalID from current account based on past purchases or create a new one - guard let externalID = await getUsersExternalID() else { return .failure(.accountCreationUnsuccessful) } - - // Make the purchase - switch await makePurchase(identifier, externalID: externalID) { - case true: - return .success(()) - case false: - return .failure(.purchaseUnsuccessful) - } - } - - private static func getUsersExternalID() async -> String? { - var externalID: String? + let externalID: String // Try fetching most recent if let jwsRepresentation = await PurchaseManager.mostRecentTransaction() { - externalID = await AccountManager().signInByRestoringPastPurchases(from: jwsRepresentation) - } - - if externalID == "error" { - print("No past transactions or account or both?") - + switch await AccountManager().signInByRestoringPastPurchases(from: jwsRepresentation) { + case .success(let existingExternalID): + externalID = existingExternalID + case .failure: + return .failure(.authenticatingWithTransactionFailed) + } + } else { switch await AuthService.createAccount() { case .success(let response): - externalID = response.externalID - _ = await AccountManager().exchangeTokensAndRefreshEntitlements(with: response.authToken) + externalID = await AccountManager().exchangeTokensAndRefreshEntitlements(with: response.authToken) case .failure(let error): - print("Error: \(error)") - externalID = nil + return .failure(.accountCreationFailed) } } - return externalID + // Make the purchase + switch await makePurchase(identifier, externalID: externalID) { + case true: + return .success(()) + case false: + return .failure(.purchaseUnsuccessful) + } } private static func makePurchase(_ identifier: String, externalID: String) async -> Bool { From 4616b6934a04175f4f8de087ac38fa45dab52149 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Thu, 16 Nov 2023 14:18:33 +0100 Subject: [PATCH 20/96] Further tweaking result types again --- .../SubscriptionPagesUserScript.swift | 8 -------- .../Sources/Account/AccountManager.swift | 12 ++++++------ .../PurchaseFlows/AppStorePurchaseFlow.swift | 3 ++- .../PurchaseFlows/AppStoreRestoreFlow.swift | 18 +----------------- 4 files changed, 9 insertions(+), 32 deletions(-) diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index a4fc94ec20..3238281f57 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -297,14 +297,6 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { print(">>> Pushing into WebView:", method.rawValue, String(describing: params)) broker.push(method: method.rawValue, params: params, for: self, into: webView) } - -} - -extension Task where Success == Never, Failure == Never { - static func sleep(seconds: Double) async throws { - let duration = UInt64(seconds * 1_000_000_000) - try await Task.sleep(nanoseconds: duration) - } } #endif diff --git a/LocalPackages/Account/Sources/Account/AccountManager.swift b/LocalPackages/Account/Sources/Account/AccountManager.swift index 3ecf853a01..4353947e81 100644 --- a/LocalPackages/Account/Sources/Account/AccountManager.swift +++ b/LocalPackages/Account/Sources/Account/AccountManager.swift @@ -168,11 +168,11 @@ public class AccountManager { storeAuthToken(token: authToken) - let result = await exchangeTokensAndRefreshEntitlements(with: authToken) - return .success(result) + return await exchangeTokensAndRefreshEntitlements(with: authToken) } - public func exchangeTokensAndRefreshEntitlements(with authToken: String) async -> String { + @discardableResult + public func exchangeTokensAndRefreshEntitlements(with authToken: String) async -> Result { // Exchange short-lived auth token to a long-lived access token let accessToken: String switch await AuthService.getAccessToken(token: authToken) { @@ -180,7 +180,7 @@ public class AccountManager { accessToken = response.accessToken case .failure(let error): os_log("AccountManager error: %{public}@", log: .error, error.localizedDescription) - return "error" + return .failure(error) } // Fetch entitlements and account details and store the data @@ -190,11 +190,11 @@ public class AccountManager { self.storeAccount(token: accessToken, email: response.account.email) - return response.account.externalID + return .success(response.account.externalID) case .failure(let error): os_log("AccountManager error: %{public}@", log: .error, error.localizedDescription) - return "error" + return .failure(error) } } } diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift index b566be7e3c..ab443156ce 100644 --- a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift @@ -54,7 +54,8 @@ public final class AppStorePurchaseFlow { } else { switch await AuthService.createAccount() { case .success(let response): - externalID = await AccountManager().exchangeTokensAndRefreshEntitlements(with: response.authToken) + externalID = response.externalID + await AccountManager().exchangeTokensAndRefreshEntitlements(with: response.authToken) case .failure(let error): return .failure(.accountCreationFailed) } diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStoreRestoreFlow.swift b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStoreRestoreFlow.swift index b657404f05..ea0f957d9d 100644 --- a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStoreRestoreFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStoreRestoreFlow.swift @@ -36,23 +36,7 @@ public final class AppStoreRestoreFlow { public static func restoreAccountFromAppleID() async -> Result { -// // Try fetching most recent -// if let jwsRepresentation = await PurchaseManager.mostRecentTransaction() { -// externalID = await AccountManager().signInByRestoringPastPurchases(from: jwsRepresentation) -// } -// -// if externalID == "error" { -// print("No past transactions or account or both?") -// -// switch await AuthService.createAccount() { -// case .success(let response): -// externalID = response.externalID -// _ = await AccountManager().exchangeTokensAndRefreshEntitlements(with: response.authToken) -// case .failure(let error): -// print("Error: \(error)") -// externalID = nil -// } -// } + return .success(.ok) } From 07171986d3015e718a5ac63ec61645bdd0dd0eda Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Thu, 16 Nov 2023 15:58:18 +0100 Subject: [PATCH 21/96] Purchase flow errors --- .../Sources/Purchase/PurchaseManager.swift | 38 ++++++++++++------- .../PurchaseFlows/AppStorePurchaseFlow.swift | 35 +++++------------ 2 files changed, 35 insertions(+), 38 deletions(-) diff --git a/LocalPackages/Purchase/Sources/Purchase/PurchaseManager.swift b/LocalPackages/Purchase/Sources/Purchase/PurchaseManager.swift index d2a17f9518..c3099eb067 100644 --- a/LocalPackages/Purchase/Sources/Purchase/PurchaseManager.swift +++ b/LocalPackages/Purchase/Sources/Purchase/PurchaseManager.swift @@ -27,6 +27,16 @@ public enum StoreError: Error { case failedVerification } +enum PurchaseManagerError: Error { + case productNotFound + case externalIDisNotAValidUUID + case purchaseFailed + case transactionCannotBeVerified + case transactionPendingAuthentication + case purchaseCancelledByUser + case unknownError +} + @available(macOS 12.0, *) public final class PurchaseManager: ObservableObject { @@ -170,9 +180,14 @@ public final class PurchaseManager: ObservableObject { return transactions.first?.jwsRepresentation } + + @MainActor - public func purchase(_ product: Product, customUUID: String) async -> String { - print(" -- [PurchaseManager] buy: \(product.displayName) (customUUID: \(customUUID))") + public func purchaseSubscription(with identifier: String, externalID: String) async -> Result { + + guard let product = availableProducts.first(where: { $0.id == identifier }) else { return .failure(PurchaseManagerError.productNotFound) } + + print(" -- [PurchaseManager] buy: \(product.displayName) (customUUID: \(externalID))") print("purchaseQueue append!") purchaseQueue.append(product.id) @@ -181,20 +196,19 @@ public final class PurchaseManager: ObservableObject { var options: Set = Set() - if let token = UUID(uuidString: customUUID) { + if let token = UUID(uuidString: externalID) { options.insert(.appAccountToken(token)) } else { print("Wrong UUID") - return "error" + return .failure(PurchaseManagerError.externalIDisNotAValidUUID) } - let result: Product.PurchaseResult do { result = try await product.purchase(options: options) } catch { print("error \(error)") - return "error" + return .failure(PurchaseManagerError.purchaseFailed) } print(" -- [PurchaseManager] purchase complete") @@ -207,23 +221,21 @@ public final class PurchaseManager: ObservableObject { // Successful purchase await transaction.finish() await self.updatePurchasedProducts() - return "ok" + return .success(()) case let .success(.unverified(_, error)): // Successful purchase but transaction/receipt can't be verified // Could be a jailbroken phone print("Error: \(error.localizedDescription)") - return "error" + return .failure(PurchaseManagerError.transactionCannotBeVerified) case .pending: // Transaction waiting on SCA (Strong Customer Authentication) or // approval from Ask to Buy - break + return .failure(PurchaseManagerError.transactionPendingAuthentication) case .userCancelled: - return "cancelled" + return .failure(PurchaseManagerError.purchaseCancelledByUser) @unknown default: - return "unknown" + return .failure(PurchaseManagerError.unknownError) } - - return "unknown" } private func checkVerified(_ result: VerificationResult) throws -> T { diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift index ab443156ce..845f80ac7f 100644 --- a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift @@ -43,8 +43,9 @@ public final class AppStorePurchaseFlow { let externalID: String - // Try fetching most recent + // Check for past transactions most recent if let jwsRepresentation = await PurchaseManager.mostRecentTransaction() { + // Attempt sign in using purchase history switch await AccountManager().signInByRestoringPastPurchases(from: jwsRepresentation) { case .success(let existingExternalID): externalID = existingExternalID @@ -52,6 +53,7 @@ public final class AppStorePurchaseFlow { return .failure(.authenticatingWithTransactionFailed) } } else { + // No history, create new account switch await AuthService.createAccount() { case .success(let response): externalID = response.externalID @@ -62,47 +64,30 @@ public final class AppStorePurchaseFlow { } // Make the purchase - switch await makePurchase(identifier, externalID: externalID) { - case true: + switch await PurchaseManager.shared.purchaseSubscription(with: identifier, externalID: externalID) { + case .success: return .success(()) - case false: + case .failure(let error): + print("Something went wrong, reason: \(error)") return .failure(.purchaseUnsuccessful) } } - private static func makePurchase(_ identifier: String, externalID: String) async -> Bool { - // rework to wrap and make a purchase with identifier - if let product = PurchaseManager.shared.availableProducts.first(where: { $0.id == identifier }) { - let purchaseResult = await PurchaseManager.shared.purchase(product, customUUID: externalID) - - if purchaseResult == "ok" { - return true - } else { - print("Something went wrong, reason: \(purchaseResult)") - return false - } - } - - return false - } - @discardableResult - public static func checkForEntitlements(wait second: Double, retry times: Int) async -> Bool { + public static func checkForEntitlements(wait waitTime: Double, retry retryCount: Int) async -> Bool { var count = 0 var hasEntitlements = false repeat { - print("Attempt \(count)") hasEntitlements = await !AccountManager().fetchEntitlements().isEmpty if hasEntitlements { - print("Got entitlements!") break } else { count += 1 - try? await Task.sleep(seconds: 2) + try? await Task.sleep(seconds: waitTime) } - } while !hasEntitlements && count < 15 + } while !hasEntitlements && count < retryCount return hasEntitlements } From 5805873cee5f740dc6e2e5d01ac7c3e5a55087e2 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Thu, 16 Nov 2023 16:15:20 +0100 Subject: [PATCH 22/96] Clean up --- .../Tab/UserScripts/SubscriptionPagesUserScript.swift | 3 ++- .../Subscription/PurchaseFlows/AppStorePurchaseFlow.swift | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 3238281f57..4e22103636 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -226,7 +226,8 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { switch await AppStorePurchaseFlow.purchaseSubscription(with: subscriptionSelection.id) { case .success: break - case .failure: + case .failure(let error): + print("Purchase failed: \(error)") await hideProgress() return nil } diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift index 845f80ac7f..195482b56d 100644 --- a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift @@ -28,7 +28,7 @@ public final class AppStorePurchaseFlow { case appStoreAuthenticationFailed case authenticatingWithTransactionFailed case accountCreationFailed - case purchaseUnsuccessful + case purchaseFailed case somethingWentWrong } @@ -58,7 +58,7 @@ public final class AppStorePurchaseFlow { case .success(let response): externalID = response.externalID await AccountManager().exchangeTokensAndRefreshEntitlements(with: response.authToken) - case .failure(let error): + case .failure: return .failure(.accountCreationFailed) } } @@ -69,7 +69,7 @@ public final class AppStorePurchaseFlow { return .success(()) case .failure(let error): print("Something went wrong, reason: \(error)") - return .failure(.purchaseUnsuccessful) + return .failure(.purchaseFailed) } } From 5730c23cd07a65fd73c097f1ef3394b2f50ad547 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 17 Nov 2023 11:17:31 +0100 Subject: [PATCH 23/96] Present subscription access view from the offer and onboarding pages --- .../SubscriptionPagesUserScript.swift | 36 ++++++++++++++++--- .../SubscriptionAccessViewController.swift | 22 +++++------- 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 4e22103636..25c4442d15 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -199,10 +199,6 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { let id: String } - struct PurchaseUpdate: Codable { - let type: String - } - let message = original if #available(macOS 12.0, *) { @@ -271,6 +267,34 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { func activateSubscription(params: Any, original: WKScriptMessage) async throws -> Encodable? { print(">>> Selected to activate a subscription -- show the activation settings screen") + + let message = original + + Task { @MainActor in + let actionHandlers = SubscriptionAccessActionHandlers( + restorePurchases: { + if #available(macOS 12.0, *) { + Task { + guard let jwsRepresentation = await PurchaseManager.mostRecentTransaction() else { return } + switch await AccountManager().signInByRestoringPastPurchases(from: jwsRepresentation) { + case .success: + message.webView?.reload() + case .failure: + break + } + } + } + }, + openURLHandler: { url in + WindowControllersManager.shared.show(url: url, newTab: true) + }, goToSyncPreferences: { + WindowControllersManager.shared.show(url: URL(string: "about:preferences/sync")!, newTab: true) + }) + + let vc = SubscriptionAccessViewController(actionHandlers: actionHandlers) + WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.presentAsSheet(vc) + } + return nil } @@ -292,6 +316,10 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { case onPurchaseUpdate } + struct PurchaseUpdate: Codable { + let type: String + } + func pushAction(method: SubscribeActionName, webView: WKWebView, params: Encodable) { let broker = UserScriptMessageBroker(context: SubscriptionPagesUserScript.context, requiresRunInPageContentWorld: true ) diff --git a/LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/SubscriptionAccessViewController.swift b/LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/SubscriptionAccessViewController.swift index 0e01f67ff2..78f15951cf 100644 --- a/LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/SubscriptionAccessViewController.swift +++ b/LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/SubscriptionAccessViewController.swift @@ -17,31 +17,27 @@ // import AppKit +import Account import SwiftUI public final class SubscriptionAccessViewController: NSViewController { + private let accountManager: AccountManager + private var actionHandlers: SubscriptionAccessActionHandlers + public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - public init() { + public init(accountManager: AccountManager = AccountManager(), actionHandlers: SubscriptionAccessActionHandlers) { + self.accountManager = accountManager + self.actionHandlers = actionHandlers super.init(nibName: nil, bundle: nil) } public override func loadView() { - let actionHandlers = SubscriptionAccessActionHandlers( - restorePurchases: { - // restore purchases - }, - openURLHandler: { _ in - // open URL here - }, goToSyncPreferences: { - // go to sync - }) - - let model = ActivateSubscriptionAccessModel(actionHandlers: actionHandlers) -// let model = ShareSubscriptionAccessModel(actionHandlers: actionHandlers) + let isSignedIn = accountManager.isSignedIn + let model: SubscriptionAccessModel = isSignedIn ? ShareSubscriptionAccessModel(actionHandlers: actionHandlers, email: accountManager.email) : ActivateSubscriptionAccessModel(actionHandlers: actionHandlers) let subscriptionAccessView = SubscriptionAccessView(model: model, dismiss: { [weak self] in From a7c009b448d048b4db39743ed8efdc84df83d37b Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 20 Nov 2023 10:29:17 +0100 Subject: [PATCH 24/96] Gather subscription URLs in single place --- .../NavigationBar/View/MoreOptionsMenu.swift | 4 ++ .../View/NavigationBarViewController.swift | 6 ++- .../PreferencesSubscriptionModel.swift | 11 ----- .../ActivateSubscriptionAccessModel.swift | 7 --- .../Model/ShareSubscriptionAccessModel.swift | 11 ----- .../Subscription/URL+Subscription.swift | 44 +++++++++++++++++++ 6 files changed, 53 insertions(+), 30 deletions(-) create mode 100644 LocalPackages/Subscription/Sources/Subscription/URL+Subscription.swift diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index 78bb8abd07..3941102f28 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -27,6 +27,7 @@ import NetworkProtection #if SUBSCRIPTION import Account +import Purchase #endif protocol OptionsButtonMenuDelegate: AnyObject { @@ -232,6 +233,9 @@ final class MoreOptionsMenu: NSMenu { #if SUBSCRIPTION @objc func openSubscriptionPreferences(_ sender: NSMenuItem) { + if #available(macOS 12.0, *) { + _ = PurchaseManager.shared + } actionDelegate?.optionsButtonMenuRequestedSubscriptionPreferences(self) } #endif diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index be8f20b356..9e1ae3b93e 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -26,6 +26,10 @@ import NetworkProtection import NetworkProtectionUI #endif +#if SUBSCRIPTION +import Subscription +#endif + // swiftlint:disable:next type_body_length final class NavigationBarViewController: NSViewController { @@ -944,7 +948,7 @@ extension NavigationBarViewController: OptionsButtonMenuDelegate { #if SUBSCRIPTION func optionsButtonMenuRequestedSubscriptionPreferences(_ menu: NSMenu) { - WindowControllersManager.shared.showPreferencesTab(withSelectedPane: .subscription) + WindowControllersManager.shared.show(url: .purchaseSubscription, newTab: true) } #endif diff --git a/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionModel.swift b/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionModel.swift index 42dced9f7d..f07764db61 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionModel.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionModel.swift @@ -19,17 +19,6 @@ import Foundation import Account -extension URL { - - static var purchaseSubscription: URL { - URL(string: "https://abrown.duckduckgo.com/subscriptions/welcome")! - } - - static var subscriptionFAQ: URL { - URL(string: "https://duckduckgo.com/about")! - } -} - public final class PreferencesSubscriptionModel: ObservableObject { @Published var isSignedIn: Bool = false diff --git a/LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/Model/ActivateSubscriptionAccessModel.swift b/LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/Model/ActivateSubscriptionAccessModel.swift index 4b1e015989..cc7ce4ac98 100644 --- a/LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/Model/ActivateSubscriptionAccessModel.swift +++ b/LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/Model/ActivateSubscriptionAccessModel.swift @@ -18,13 +18,6 @@ import Foundation -extension URL { - - static var activateSubscriptionViaEmail: URL { - URL(string: "https://abrown.duckduckgo.com/subscriptions/activate")! - } -} - public final class ActivateSubscriptionAccessModel: SubscriptionAccessModel { public var actionHandlers: SubscriptionAccessActionHandlers public var title = UserText.activateModalTitle diff --git a/LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/Model/ShareSubscriptionAccessModel.swift b/LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/Model/ShareSubscriptionAccessModel.swift index 5f84e14c39..6a1068bd35 100644 --- a/LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/Model/ShareSubscriptionAccessModel.swift +++ b/LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/Model/ShareSubscriptionAccessModel.swift @@ -18,17 +18,6 @@ import Foundation -extension URL { - - static var addEmailToSubscription: URL { - URL(string: "https://abrown.duckduckgo.com/subscriptions/add-email")! - } - - static var manageSubscriptionEmail: URL { - URL(string: "https://abrown.duckduckgo.com/subscriptions/manage")! - } -} - public final class ShareSubscriptionAccessModel: SubscriptionAccessModel { public var title = UserText.shareModalTitle public var description = UserText.shareModalDescription diff --git a/LocalPackages/Subscription/Sources/Subscription/URL+Subscription.swift b/LocalPackages/Subscription/Sources/Subscription/URL+Subscription.swift new file mode 100644 index 0000000000..dbb018b53e --- /dev/null +++ b/LocalPackages/Subscription/Sources/Subscription/URL+Subscription.swift @@ -0,0 +1,44 @@ +// +// URL+Subscription.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +public extension URL { + + static var purchaseSubscription: URL { + URL(string: "https://abrown.duckduckgo.com/subscriptions/welcome")! + } + + static var subscriptionFAQ: URL { + URL(string: "https://duckduckgo.com/about")! + } + + + // MARK: - Subscription Email + static var activateSubscriptionViaEmail: URL { + URL(string: "https://abrown.duckduckgo.com/subscriptions/activate")! + } + + static var addEmailToSubscription: URL { + URL(string: "https://abrown.duckduckgo.com/subscriptions/add-email")! + } + + static var manageSubscriptionEmail: URL { + URL(string: "https://abrown.duckduckgo.com/subscriptions/manage")! + } +} From b853d93f21221d22029ec04c394ed058d1a60858 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 20 Nov 2023 10:48:17 +0100 Subject: [PATCH 25/96] Initialize and fetch subscription products add app launch --- DuckDuckGo/AppDelegate/AppDelegate.swift | 12 ++++++++++++ DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift | 3 --- .../Preferences/View/PreferencesViewController.swift | 8 -------- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/DuckDuckGo/AppDelegate/AppDelegate.swift b/DuckDuckGo/AppDelegate/AppDelegate.swift index ead4a26ade..eeedf9f1f8 100644 --- a/DuckDuckGo/AppDelegate/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate/AppDelegate.swift @@ -33,6 +33,10 @@ import UserNotifications import NetworkProtection #endif +#if SUBSCRIPTION +import Purchase +#endif + @MainActor final class AppDelegate: NSObject, NSApplicationDelegate, FileDownloadManagerDelegate { @@ -223,6 +227,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate, FileDownloadManagerDel #if DBP DataBrokerProtectionManager.shared.runOperationsAndStartSchedulerIfPossible() #endif + +#if SUBSCRIPTION + if #available(macOS 12.0, *) { + Task { + await PurchaseManager.shared.updateAvailableProducts() + } + } +#endif } func applicationDidBecomeActive(_ notification: Notification) { diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index 3941102f28..4ec3206d00 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -233,9 +233,6 @@ final class MoreOptionsMenu: NSMenu { #if SUBSCRIPTION @objc func openSubscriptionPreferences(_ sender: NSMenuItem) { - if #available(macOS 12.0, *) { - _ = PurchaseManager.shared - } actionDelegate?.optionsButtonMenuRequestedSubscriptionPreferences(self) } #endif diff --git a/DuckDuckGo/Preferences/View/PreferencesViewController.swift b/DuckDuckGo/Preferences/View/PreferencesViewController.swift index d365a48000..e231be8580 100644 --- a/DuckDuckGo/Preferences/View/PreferencesViewController.swift +++ b/DuckDuckGo/Preferences/View/PreferencesViewController.swift @@ -64,14 +64,6 @@ final class PreferencesViewController: NSViewController { .sink { [weak self] identifier in self?.delegate?.selectedPreferencePane(identifier) } - -#if SUBSCRIPTION - if #available(macOS 12.0, *) { - Task { - await PurchaseManager.shared.updateAvailableProducts() - } - } -#endif } override func viewWillDisappear() { From b26b976058814f5ab6c0dd9d268dab9a8690c917 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 20 Nov 2023 11:32:51 +0100 Subject: [PATCH 26/96] Rename isSignedIn to isUserAuthenticated --- .../NavigationBar/View/MoreOptionsMenu.swift | 10 +++++----- .../View/NavigationBarViewController.swift | 2 +- .../Sources/Account/AccountManager.swift | 2 +- .../DebugMenu/SubscriptionDebugMenu.swift | 4 ++-- .../PreferencesSubscriptionModel.swift | 18 +++++++++--------- .../PreferencesSubscriptionView.swift | 8 ++++---- .../SubscriptionAccessViewController.swift | 3 +-- 7 files changed, 23 insertions(+), 24 deletions(-) diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index 4ec3206d00..c2abe7608a 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -48,7 +48,7 @@ protocol OptionsButtonMenuDelegate: AnyObject { func optionsButtonMenuRequestedDataBrokerProtection(_ menu: NSMenu) #endif #if SUBSCRIPTION - func optionsButtonMenuRequestedSubscriptionPreferences(_ menu: NSMenu) + func optionsButtonMenuRequestedSubscriptionPurchasePage(_ menu: NSMenu) #endif } @@ -232,8 +232,8 @@ final class MoreOptionsMenu: NSMenu { } #if SUBSCRIPTION - @objc func openSubscriptionPreferences(_ sender: NSMenuItem) { - actionDelegate?.optionsButtonMenuRequestedSubscriptionPreferences(self) + @objc func openSubscriptionPurchasePage(_ sender: NSMenuItem) { + actionDelegate?.optionsButtonMenuRequestedSubscriptionPurchasePage(self) } #endif @@ -295,7 +295,7 @@ final class MoreOptionsMenu: NSMenu { var items: [NSMenuItem] = [] #if SUBSCRIPTION - items.append(contentsOf: AccountManager().isSignedIn ? makeActiveSubscriptionItems() : makeInactiveSubscriptionItems()) + items.append(contentsOf: AccountManager().isUserAuthenticated ? makeActiveSubscriptionItems() : makeInactiveSubscriptionItems()) #else items.append(contentsOf: makeActiveSubscriptionItems()) // this only adds NETP and DBP (if enabled) #endif @@ -355,7 +355,7 @@ final class MoreOptionsMenu: NSMenu { #if SUBSCRIPTION private func makeInactiveSubscriptionItems() -> [NSMenuItem] { let privacyProItem = NSMenuItem(title: "", - action: #selector(openSubscriptionPreferences(_:)), + action: #selector(openSubscriptionPurchasePage(_:)), keyEquivalent: "") .targetting(self) .withImage(NSImage(named: "SubscriptionIcon")) diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index 9e1ae3b93e..d5666a99fd 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -947,7 +947,7 @@ extension NavigationBarViewController: OptionsButtonMenuDelegate { } #if SUBSCRIPTION - func optionsButtonMenuRequestedSubscriptionPreferences(_ menu: NSMenu) { + func optionsButtonMenuRequestedSubscriptionPurchasePage(_ menu: NSMenu) { WindowControllersManager.shared.show(url: .purchaseSubscription, newTab: true) } #endif diff --git a/LocalPackages/Account/Sources/Account/AccountManager.swift b/LocalPackages/Account/Sources/Account/AccountManager.swift index 4353947e81..c397ccc2a1 100644 --- a/LocalPackages/Account/Sources/Account/AccountManager.swift +++ b/LocalPackages/Account/Sources/Account/AccountManager.swift @@ -33,7 +33,7 @@ public class AccountManager { private let storage: AccountStorage public weak var delegate: AccountManagerKeychainAccessDelegate? - public var isSignedIn: Bool { + public var isUserAuthenticated: Bool { return accessToken != nil } diff --git a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift index 6aada18c04..ab3d2c565d 100644 --- a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift +++ b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift @@ -82,8 +82,8 @@ public final class SubscriptionDebugMenu: NSMenuItem { @objc func showAccountDetails() { - let title = accountManager.isSignedIn ? "Authenticated" : "Not Authenticated" - let message = accountManager.isSignedIn ? ["AuthToken: \(accountManager.authToken ?? "")", + let title = accountManager.isUserAuthenticated ? "Authenticated" : "Not Authenticated" + let message = accountManager.isUserAuthenticated ? ["AuthToken: \(accountManager.authToken ?? "")", "AccessToken: \(accountManager.accessToken ?? "")", "Email: \(accountManager.email ?? "")"].joined(separator: "\n") : nil showAlert(title: title, message: message) diff --git a/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionModel.swift b/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionModel.swift index f07764db61..66fe272d3b 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionModel.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionModel.swift @@ -21,7 +21,7 @@ import Account public final class PreferencesSubscriptionModel: ObservableObject { - @Published var isSignedIn: Bool = false + @Published var isUserAuthenticated: Bool = false @Published var hasEntitlements: Bool = false var sheetModel: SubscriptionAccessModel @@ -34,22 +34,22 @@ public final class PreferencesSubscriptionModel: ObservableObject { self.actionHandler = actionHandler self.sheetActionHandler = sheetActionHandler - let isSignedIn = accountManager.isSignedIn - self.isSignedIn = isSignedIn - sheetModel = isSignedIn ? ShareSubscriptionAccessModel(actionHandlers: sheetActionHandler, email: accountManager.email) : ActivateSubscriptionAccessModel(actionHandlers: sheetActionHandler) + let isUserAuthenticated = accountManager.isUserAuthenticated + self.isUserAuthenticated = isUserAuthenticated + sheetModel = isUserAuthenticated ? ShareSubscriptionAccessModel(actionHandlers: sheetActionHandler, email: accountManager.email) : ActivateSubscriptionAccessModel(actionHandlers: sheetActionHandler) NotificationCenter.default.addObserver(forName: .accountDidSignIn, object: nil, queue: .main) { _ in - self.updateSignInState(true) + self.updateUserAuthenticatedState(true) } NotificationCenter.default.addObserver(forName: .accountDidSignOut, object: nil, queue: .main) { _ in - self.updateSignInState(false) + self.updateUserAuthenticatedState(false) } } - private func updateSignInState(_ isSignedIn: Bool) { - self.isSignedIn = isSignedIn - sheetModel = isSignedIn ? ShareSubscriptionAccessModel(actionHandlers: sheetActionHandler, email: accountManager.email) : ActivateSubscriptionAccessModel(actionHandlers: sheetActionHandler) + private func updateUserAuthenticatedState(_ isUserAuthenticated: Bool) { + self.isUserAuthenticated = isUserAuthenticated + sheetModel = isUserAuthenticated ? ShareSubscriptionAccessModel(actionHandlers: sheetActionHandler, email: accountManager.email) : ActivateSubscriptionAccessModel(actionHandlers: sheetActionHandler) } @MainActor diff --git a/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionView.swift b/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionView.swift index 5ad394c8b3..a9c9fac687 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionView.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionView.swift @@ -64,7 +64,7 @@ public struct PreferencesSubscriptionView: View { .frame(height: 20) VStack { - if model.isSignedIn { + if model.isUserAuthenticated { UniversalHeaderView { Image("subscription-active-icon", bundle: .module) .padding(4) @@ -111,7 +111,7 @@ public struct PreferencesSubscriptionView: View { SectionView(iconName: "vpn-service-icon", title: UserText.vpnServiceTitle, description: UserText.vpnServiceDescription, - buttonName: model.isSignedIn ? "Manage" : nil, + buttonName: model.isUserAuthenticated ? "Manage" : nil, buttonAction: { model.openVPN() }, enabled: model.hasEntitlements) @@ -121,7 +121,7 @@ public struct PreferencesSubscriptionView: View { SectionView(iconName: "pir-service-icon", title: UserText.personalInformationRemovalServiceTitle, description: UserText.personalInformationRemovalServiceDescription, - buttonName: model.isSignedIn ? "View" : nil, + buttonName: model.isUserAuthenticated ? "View" : nil, buttonAction: { model.openPersonalInformationRemoval() }, enabled: model.hasEntitlements) @@ -131,7 +131,7 @@ public struct PreferencesSubscriptionView: View { SectionView(iconName: "itr-service-icon", title: UserText.identityTheftRestorationServiceTitle, description: UserText.identityTheftRestorationServiceDescription, - buttonName: model.isSignedIn ? "View" : nil, + buttonName: model.isUserAuthenticated ? "View" : nil, buttonAction: { model.openIdentityTheftRestoration() }, enabled: model.hasEntitlements) } diff --git a/LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/SubscriptionAccessViewController.swift b/LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/SubscriptionAccessViewController.swift index 78f15951cf..adb061d663 100644 --- a/LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/SubscriptionAccessViewController.swift +++ b/LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/SubscriptionAccessViewController.swift @@ -36,8 +36,7 @@ public final class SubscriptionAccessViewController: NSViewController { } public override func loadView() { - let isSignedIn = accountManager.isSignedIn - let model: SubscriptionAccessModel = isSignedIn ? ShareSubscriptionAccessModel(actionHandlers: actionHandlers, email: accountManager.email) : ActivateSubscriptionAccessModel(actionHandlers: actionHandlers) + let model: SubscriptionAccessModel = accountManager.isUserAuthenticated ? ShareSubscriptionAccessModel(actionHandlers: actionHandlers, email: accountManager.email) : ActivateSubscriptionAccessModel(actionHandlers: actionHandlers) let subscriptionAccessView = SubscriptionAccessView(model: model, dismiss: { [weak self] in From 046c23246d0af07ab051db0be0b9b0af05a583ef Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 20 Nov 2023 11:40:54 +0100 Subject: [PATCH 27/96] Rename exchangeTokensAndRefreshEntitlements to exchangeAndStoreTokens --- DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift | 2 +- LocalPackages/Account/Sources/Account/AccountManager.swift | 4 ++-- .../Subscription/PurchaseFlows/AppStorePurchaseFlow.swift | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 25c4442d15..6c74a82e80 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -130,7 +130,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { return nil } - await AccountManager().exchangeTokensAndRefreshEntitlements(with: subscriptionValues.token) + await AccountManager().exchangeAndStoreTokens(with: subscriptionValues.token) return nil } diff --git a/LocalPackages/Account/Sources/Account/AccountManager.swift b/LocalPackages/Account/Sources/Account/AccountManager.swift index c397ccc2a1..8611b196e0 100644 --- a/LocalPackages/Account/Sources/Account/AccountManager.swift +++ b/LocalPackages/Account/Sources/Account/AccountManager.swift @@ -168,11 +168,11 @@ public class AccountManager { storeAuthToken(token: authToken) - return await exchangeTokensAndRefreshEntitlements(with: authToken) + return await exchangeAndStoreTokens(with: authToken) } @discardableResult - public func exchangeTokensAndRefreshEntitlements(with authToken: String) async -> Result { + public func exchangeAndStoreTokens(with authToken: String) async -> Result { // Exchange short-lived auth token to a long-lived access token let accessToken: String switch await AuthService.getAccessToken(token: authToken) { diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift index 195482b56d..ede29509c5 100644 --- a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift @@ -57,7 +57,7 @@ public final class AppStorePurchaseFlow { switch await AuthService.createAccount() { case .success(let response): externalID = response.externalID - await AccountManager().exchangeTokensAndRefreshEntitlements(with: response.authToken) + await AccountManager().exchangeAndStoreTokens(with: response.authToken) case .failure: return .failure(.accountCreationFailed) } From b5201b740df5288eaf99c696c87439d89bc2af83 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 20 Nov 2023 11:46:03 +0100 Subject: [PATCH 28/96] Remove unnecessary call --- LocalPackages/Account/Sources/Account/AccountManager.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/LocalPackages/Account/Sources/Account/AccountManager.swift b/LocalPackages/Account/Sources/Account/AccountManager.swift index 8611b196e0..0bbd61fadf 100644 --- a/LocalPackages/Account/Sources/Account/AccountManager.swift +++ b/LocalPackages/Account/Sources/Account/AccountManager.swift @@ -166,8 +166,6 @@ public class AccountManager { return .failure(error) } - storeAuthToken(token: authToken) - return await exchangeAndStoreTokens(with: authToken) } From adac3fb55e10810d481650436ebf4064026265d1 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 20 Nov 2023 15:31:03 +0100 Subject: [PATCH 29/96] Extract restore flow to AppStoreRestoreFlow --- .../View/PreferencesRootView.swift | 3 +- .../SubscriptionPagesUserScript.swift | 3 +- .../Sources/Account/AccountManager.swift | 15 --------- .../DebugMenu/SubscriptionDebugMenu.swift | 3 +- .../PurchaseFlows/AppStorePurchaseFlow.swift | 30 +++++++++--------- .../PurchaseFlows/AppStoreRestoreFlow.swift | 31 +++++++++++++------ 6 files changed, 39 insertions(+), 46 deletions(-) diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index a72ac843a8..b5d2b158c1 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -107,8 +107,7 @@ extension Preferences { let sheetActionHandler = SubscriptionAccessActionHandlers(restorePurchases: { if #available(macOS 12.0, *) { Task { - guard let jwsRepresentation = await PurchaseManager.mostRecentTransaction() else { return } - await AccountManager().signInByRestoringPastPurchases(from: jwsRepresentation) + await AppStoreRestoreFlow.restoreAccountFromPastPurchase() } } }, openURLHandler: { url in diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 6c74a82e80..e1f66e9766 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -275,8 +275,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { restorePurchases: { if #available(macOS 12.0, *) { Task { - guard let jwsRepresentation = await PurchaseManager.mostRecentTransaction() else { return } - switch await AccountManager().signInByRestoringPastPurchases(from: jwsRepresentation) { + switch await AppStoreRestoreFlow.restoreAccountFromPastPurchase() { case .success: message.webView?.reload() case .failure: diff --git a/LocalPackages/Account/Sources/Account/AccountManager.swift b/LocalPackages/Account/Sources/Account/AccountManager.swift index 0bbd61fadf..0a2bcac8dc 100644 --- a/LocalPackages/Account/Sources/Account/AccountManager.swift +++ b/LocalPackages/Account/Sources/Account/AccountManager.swift @@ -154,21 +154,6 @@ public class AccountManager { } } - @discardableResult - public func signInByRestoringPastPurchases(from lastTransactionJWSRepresentation: String) async -> Result { - // Do the store login to get short-lived token - let authToken: String - switch await AuthService.storeLogin(signature: lastTransactionJWSRepresentation) { - case .success(let response): - authToken = response.authToken - case .failure(let error): - os_log("AccountManager error: %{public}@", log: .error, error.localizedDescription) - return .failure(error) - } - - return await exchangeAndStoreTokens(with: authToken) - } - @discardableResult public func exchangeAndStoreTokens(with authToken: String) async -> Result { // Exchange short-lived auth token to a long-lived access token diff --git a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift index ab3d2c565d..bd3049de9c 100644 --- a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift +++ b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift @@ -154,8 +154,7 @@ public final class SubscriptionDebugMenu: NSMenuItem { func restorePurchases(_ sender: Any?) { if #available(macOS 12.0, *) { Task { - guard let jwsRepresentation = await PurchaseManager.mostRecentTransaction() else { return } - _ = await accountManager.signInByRestoringPastPurchases(from: jwsRepresentation) + await AppStoreRestoreFlow.restoreAccountFromPastPurchase() } } } diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift index ede29509c5..78487b1476 100644 --- a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift @@ -44,23 +44,23 @@ public final class AppStorePurchaseFlow { let externalID: String // Check for past transactions most recent - if let jwsRepresentation = await PurchaseManager.mostRecentTransaction() { - // Attempt sign in using purchase history - switch await AccountManager().signInByRestoringPastPurchases(from: jwsRepresentation) { - case .success(let existingExternalID): - externalID = existingExternalID - case .failure: + switch await AppStoreRestoreFlow.restoreAccountFromPastPurchase() { + case .success(let existingExternalID): + externalID = existingExternalID + case .failure(let error): + switch error { + case .missingAccountOrTransactions: + // No history, create new account + switch await AuthService.createAccount() { + case .success(let response): + externalID = response.externalID + await AccountManager().exchangeAndStoreTokens(with: response.authToken) + case .failure: + return .failure(.accountCreationFailed) + } + default: return .failure(.authenticatingWithTransactionFailed) } - } else { - // No history, create new account - switch await AuthService.createAccount() { - case .success(let response): - externalID = response.externalID - await AccountManager().exchangeAndStoreTokens(with: response.authToken) - case .failure: - return .failure(.accountCreationFailed) - } } // Make the purchase diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStoreRestoreFlow.swift b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStoreRestoreFlow.swift index ea0f957d9d..d628c7e17b 100644 --- a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStoreRestoreFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStoreRestoreFlow.swift @@ -24,20 +24,31 @@ import Account @available(macOS 12.0, *) public final class AppStoreRestoreFlow { - public enum Success { - case ok - } - public enum Error: Swift.Error { case missingAccountOrTransactions - case userCancelled + case pastTransactionAuthenticationFailure + case accessTokenObtainingError case somethingWentWrong } - public static func restoreAccountFromAppleID() async -> Result { - - - - return .success(.ok) + public static func restoreAccountFromPastPurchase() async -> Result { + guard let lastTransactionJWSRepresentation = await PurchaseManager.mostRecentTransaction() else { return .failure(.missingAccountOrTransactions) } + + // Do the store login to get short-lived token + let authToken: String + + switch await AuthService.storeLogin(signature: lastTransactionJWSRepresentation) { + case .success(let response): + authToken = response.authToken + case .failure: + return .failure(.pastTransactionAuthenticationFailure) + } + + switch await AccountManager().exchangeAndStoreTokens(with: authToken) { + case .success(let externalID): + return .success(externalID) + case .failure: + return .failure(.accessTokenObtainingError) + } } } From 69b793976f1abb0afeb7833b7600e924f7563446 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Tue, 21 Nov 2023 13:13:22 +0100 Subject: [PATCH 30/96] Pass email PAT if present when creating an account --- .../Tab/UserScripts/SubscriptionPagesUserScript.swift | 4 +++- .../Account/Sources/Account/Services/AuthService.swift | 10 ++++++++-- .../Subscription/DebugMenu/DebugPurchaseModel.swift | 2 +- .../PurchaseFlows/AppStorePurchaseFlow.swift | 4 ++-- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index e1f66e9766..6da80e1a0d 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -219,7 +219,9 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { } */ - switch await AppStorePurchaseFlow.purchaseSubscription(with: subscriptionSelection.id) { + let emailAccessToken = try? EmailManager().getToken() + + switch await AppStorePurchaseFlow.purchaseSubscription(with: subscriptionSelection.id, emailAccessToken: emailAccessToken) { case .success: break case .failure(let error): diff --git a/LocalPackages/Account/Sources/Account/Services/AuthService.swift b/LocalPackages/Account/Sources/Account/Services/AuthService.swift index 5c5f85c6e6..33a18262ad 100644 --- a/LocalPackages/Account/Sources/Account/Services/AuthService.swift +++ b/LocalPackages/Account/Sources/Account/Services/AuthService.swift @@ -68,8 +68,14 @@ public struct AuthService: APIService { // MARK: - - public static func createAccount() async -> Result { - await executeAPICall(method: "POST", endpoint: "account/create") + public static func createAccount(emailAccessToken: String?) async -> Result { + var headers: [String: String]? + + if let emailAccessToken { + headers = makeAuthorizationHeader(for: emailAccessToken) + } + + return await executeAPICall(method: "POST", endpoint: "account/create", headers: headers) } public struct CreateAccountResponse: Decodable { diff --git a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseModel.swift b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseModel.swift index 6f0227c87b..7299d9fd10 100644 --- a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseModel.swift +++ b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseModel.swift @@ -39,7 +39,7 @@ public final class DebugPurchaseModel: ObservableObject { print("Attempting purchase: \(product.displayName)") Task { - await AppStorePurchaseFlow.purchaseSubscription(with: product.id) + await AppStorePurchaseFlow.purchaseSubscription(with: product.id, emailAccessToken: nil) } } } diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift index 78487b1476..982b7438f3 100644 --- a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift @@ -32,7 +32,7 @@ public final class AppStorePurchaseFlow { case somethingWentWrong } - public static func purchaseSubscription(with identifier: String) async -> Result { + public static func purchaseSubscription(with identifier: String, emailAccessToken: String?) async -> Result { // Trigger sign in pop-up switch await PurchaseManager.shared.syncAppleIDAccount() { case .success: @@ -51,7 +51,7 @@ public final class AppStorePurchaseFlow { switch error { case .missingAccountOrTransactions: // No history, create new account - switch await AuthService.createAccount() { + switch await AuthService.createAccount(emailAccessToken: emailAccessToken) { case .success(let response): externalID = response.externalID await AccountManager().exchangeAndStoreTokens(with: response.authToken) From 73600269cb2826aebddbfb37670b98e08f791a55 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Tue, 21 Nov 2023 16:42:25 +0100 Subject: [PATCH 31/96] Refresh account details after changing email --- .../UserScripts/SubscriptionPagesUserScript.swift | 2 ++ .../Account/Sources/Account/AccountManager.swift | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 6da80e1a0d..63a3c709fb 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -135,6 +135,8 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { } func backToSettings(params: Any, original: WKScriptMessage) async throws -> Encodable? { + await AccountManager().refreshAccountData() + DispatchQueue.main.async { NotificationCenter.default.post(name: .subscriptionPageCloseAndOpenPreferences, object: self) } diff --git a/LocalPackages/Account/Sources/Account/AccountManager.swift b/LocalPackages/Account/Sources/Account/AccountManager.swift index 0a2bcac8dc..ebb217d437 100644 --- a/LocalPackages/Account/Sources/Account/AccountManager.swift +++ b/LocalPackages/Account/Sources/Account/AccountManager.swift @@ -180,4 +180,16 @@ public class AccountManager { return .failure(error) } } + + public func refreshAccountData() async { + guard let accessToken else { return } + + switch await AuthService.validateToken(accessToken: accessToken) { + case .success(let response): + self.storeAccount(token: accessToken, + email: response.account.email) + case .failure(let error): + break + } + } } From f61b53a95abfeac4963a1b0289d4f1d0d2646342 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Tue, 21 Nov 2023 16:44:29 +0100 Subject: [PATCH 32/96] Clean up account details after failed/cancelled purchase --- .../Subscription/PurchaseFlows/AppStorePurchaseFlow.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift index 982b7438f3..b2d0cbe997 100644 --- a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift @@ -69,6 +69,7 @@ public final class AppStorePurchaseFlow { return .success(()) case .failure(let error): print("Something went wrong, reason: \(error)") + AccountManager().signOut() return .failure(.purchaseFailed) } } From 85625c309d28de14ff29b0c16c689531f537cf03 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Wed, 22 Nov 2023 12:47:15 +0100 Subject: [PATCH 33/96] Refine how error description is built --- LocalPackages/Account/Sources/Account/Services/APIService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LocalPackages/Account/Sources/Account/Services/APIService.swift b/LocalPackages/Account/Sources/Account/Services/APIService.swift index 124a8fbf27..5a7d8f0d1b 100644 --- a/LocalPackages/Account/Sources/Account/Services/APIService.swift +++ b/LocalPackages/Account/Sources/Account/Services/APIService.swift @@ -56,7 +56,7 @@ public extension APIService { } } else { if let decodedResponse = decode(ErrorResponse.self, from: data) { - let errorDescription = [method, endpoint, urlResponse.httpStatusCodeAsString ?? "", decodedResponse.error].joined(separator: " ") + let errorDescription = "[\(endpoint)] \(urlResponse.httpStatusCodeAsString ?? ""): \(decodedResponse.error)" return .failure(.serverError(description: errorDescription)) } else { return .failure(.unknownServerError) From 769a22b197a0d06ca9267b62ead5ebbb9d2533f0 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Wed, 22 Nov 2023 12:47:56 +0100 Subject: [PATCH 34/96] Flow for validating and refreshing short lived auth token for email management --- .../SubscriptionPagesUserScript.swift | 21 ++++++++++++++++--- .../Sources/Account/AccountManager.swift | 2 +- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 63a3c709fb..009c18cdbf 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -119,9 +119,24 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { } func getSubscription(params: Any, original: WKScriptMessage) async throws -> Encodable? { - let token = AccountManager().authToken ?? "" - let subscription = Subscription(token: token) - return subscription + var authToken = AccountManager().authToken ?? "" + + // Check if auth token if still valid + if case let .failure(error) = await AuthService.validateToken(accessToken: authToken) { + print(error) + + if #available(macOS 12.0, *) { + // In case of invalid token attempt store based authentication to obtain a new one + guard let lastTransactionJWSRepresentation = await PurchaseManager.mostRecentTransaction() else { return nil } + + if case let .success(response) = await AuthService.storeLogin(signature: lastTransactionJWSRepresentation) { + authToken = response.authToken + AccountManager().storeAuthToken(token: authToken) + } + } + } + + return Subscription(token: authToken) } func setSubscription(params: Any, original: WKScriptMessage) async throws -> Encodable? { diff --git a/LocalPackages/Account/Sources/Account/AccountManager.swift b/LocalPackages/Account/Sources/Account/AccountManager.swift index ebb217d437..a6d8676c7c 100644 --- a/LocalPackages/Account/Sources/Account/AccountManager.swift +++ b/LocalPackages/Account/Sources/Account/AccountManager.swift @@ -188,7 +188,7 @@ public class AccountManager { case .success(let response): self.storeAccount(token: accessToken, email: response.account.email) - case .failure(let error): + case .failure: break } } From 9e2252fc6c3284ff6e6fee2bac36105633c1c916 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Wed, 22 Nov 2023 16:10:26 +0100 Subject: [PATCH 35/96] Move auth token refresh to when email management flows are launched --- .../SubscriptionPagesUserScript.swift | 16 ------ .../Sources/Account/AccountManager.swift | 32 +++++++++-- .../AccountKeychainStorage.swift | 14 +++++ .../AccountStorage/AccountStorage.swift | 2 + .../DebugMenu/SubscriptionDebugMenu.swift | 2 +- .../PreferencesSubscriptionModel.swift | 13 ++++- .../AppStoreAccountManagementFlow.swift | 57 +++++++++++++++++++ .../Model/ShareSubscriptionAccessModel.swift | 10 +++- .../SubscriptionAccessViewController.swift | 11 +++- 9 files changed, 129 insertions(+), 28 deletions(-) create mode 100644 LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStoreAccountManagementFlow.swift diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 009c18cdbf..faaff96aa5 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -120,22 +120,6 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { func getSubscription(params: Any, original: WKScriptMessage) async throws -> Encodable? { var authToken = AccountManager().authToken ?? "" - - // Check if auth token if still valid - if case let .failure(error) = await AuthService.validateToken(accessToken: authToken) { - print(error) - - if #available(macOS 12.0, *) { - // In case of invalid token attempt store based authentication to obtain a new one - guard let lastTransactionJWSRepresentation = await PurchaseManager.mostRecentTransaction() else { return nil } - - if case let .success(response) = await AuthService.storeLogin(signature: lastTransactionJWSRepresentation) { - authToken = response.authToken - AccountManager().storeAuthToken(token: authToken) - } - } - } - return Subscription(token: authToken) } diff --git a/LocalPackages/Account/Sources/Account/AccountManager.swift b/LocalPackages/Account/Sources/Account/AccountManager.swift index a6d8676c7c..fb8261ca46 100644 --- a/LocalPackages/Account/Sources/Account/AccountManager.swift +++ b/LocalPackages/Account/Sources/Account/AccountManager.swift @@ -83,6 +83,20 @@ public class AccountManager { } } + public var externalID: String? { + do { + return try storage.getExternalID() + } catch { + if let error = error as? AccountKeychainAccessError { + delegate?.accountManagerKeychainAccessFailed(accessType: .getExternalID, error: error) + } else { + assertionFailure("Expected AccountKeychainAccessError") + } + + return nil + } + } + public func storeAuthToken(token: String) { do { try storage.store(authToken: token) @@ -95,8 +109,7 @@ public class AccountManager { } } - public func storeAccount(token: String, email: String?) { - os_log("AccountManager: storeAccount token: %@ email: %@ externalID:%@", log: .account, token, email ?? "nil") + public func storeAccount(token: String, email: String?, externalID: String?) { do { try storage.store(accessToken: token) } catch { @@ -117,6 +130,15 @@ public class AccountManager { } } + do { + try storage.store(externalID: externalID) + } catch { + if let error = error as? AccountKeychainAccessError { + delegate?.accountManagerKeychainAccessFailed(accessType: .storeExternalID, error: error) + } else { + assertionFailure("Expected AccountKeychainAccessError") + } + } NotificationCenter.default.post(name: .accountDidSignIn, object: self, userInfo: nil) } @@ -171,7 +193,8 @@ public class AccountManager { case .success(let response): self.storeAuthToken(token: authToken) self.storeAccount(token: accessToken, - email: response.account.email) + email: response.account.email, + externalID: response.account.externalID) return .success(response.account.externalID) @@ -187,7 +210,8 @@ public class AccountManager { switch await AuthService.validateToken(accessToken: accessToken) { case .success(let response): self.storeAccount(token: accessToken, - email: response.account.email) + email: response.account.email, + externalID: response.account.externalID) case .failure: break } diff --git a/LocalPackages/Account/Sources/Account/AccountStorage/AccountKeychainStorage.swift b/LocalPackages/Account/Sources/Account/AccountStorage/AccountKeychainStorage.swift index 9b5590186a..19596f764e 100644 --- a/LocalPackages/Account/Sources/Account/AccountStorage/AccountKeychainStorage.swift +++ b/LocalPackages/Account/Sources/Account/AccountStorage/AccountKeychainStorage.swift @@ -72,6 +72,18 @@ public class AccountKeychainStorage: AccountStorage { try Self.getString(forField: .email) } + public func getExternalID() throws -> String? { + try Self.getString(forField: .externalID) + } + + public func store(externalID: String?) throws { + if let externalID = externalID, !externalID.isEmpty { + try Self.set(string: externalID, forField: .externalID) + } else { + try Self.deleteItem(forField: .externalID) + } + } + public func store(email: String?) throws { if let email = email, !email.isEmpty { try Self.set(string: email, forField: .email) @@ -84,6 +96,7 @@ public class AccountKeychainStorage: AccountStorage { try Self.deleteItem(forField: .authToken) try Self.deleteItem(forField: .accessToken) try Self.deleteItem(forField: .email) + try Self.deleteItem(forField: .externalID) } } @@ -98,6 +111,7 @@ private extension AccountKeychainStorage { case authToken = "account.authToken" case accessToken = "account.accessToken" case email = "account.email" + case externalID = "account.external_id" var keyValue: String { (Bundle.main.bundleIdentifier ?? "com.duckduckgo") + "." + rawValue diff --git a/LocalPackages/Account/Sources/Account/AccountStorage/AccountStorage.swift b/LocalPackages/Account/Sources/Account/AccountStorage/AccountStorage.swift index 3f3466ec93..06b5e05cb5 100644 --- a/LocalPackages/Account/Sources/Account/AccountStorage/AccountStorage.swift +++ b/LocalPackages/Account/Sources/Account/AccountStorage/AccountStorage.swift @@ -25,5 +25,7 @@ public protocol AccountStorage: AnyObject { func store(accessToken: String) throws func getEmail() throws -> String? func store(email: String?) throws + func getExternalID() throws -> String? + func store(externalID: String?) throws func clearAuthenticationState() throws } diff --git a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift index bd3049de9c..f54e106c5b 100644 --- a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift +++ b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift @@ -72,7 +72,7 @@ public final class SubscriptionDebugMenu: NSMenuItem { @objc func simulateSubscriptionActiveState() { - accountManager.storeAccount(token: "fake-token", email: "fake@email.com") + accountManager.storeAccount(token: "fake-token", email: "fake@email.com", externalID: "123") } @objc diff --git a/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionModel.swift b/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionModel.swift index 66fe272d3b..b94ea34da2 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionModel.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionModel.swift @@ -23,7 +23,7 @@ public final class PreferencesSubscriptionModel: ObservableObject { @Published var isUserAuthenticated: Bool = false @Published var hasEntitlements: Bool = false - var sheetModel: SubscriptionAccessModel + lazy var sheetModel: SubscriptionAccessModel = makeSubscriptionAccessModel() private let accountManager: AccountManager private var actionHandler: PreferencesSubscriptionActionHandlers @@ -36,7 +36,6 @@ public final class PreferencesSubscriptionModel: ObservableObject { let isUserAuthenticated = accountManager.isUserAuthenticated self.isUserAuthenticated = isUserAuthenticated - sheetModel = isUserAuthenticated ? ShareSubscriptionAccessModel(actionHandlers: sheetActionHandler, email: accountManager.email) : ActivateSubscriptionAccessModel(actionHandlers: sheetActionHandler) NotificationCenter.default.addObserver(forName: .accountDidSignIn, object: nil, queue: .main) { _ in self.updateUserAuthenticatedState(true) @@ -47,9 +46,17 @@ public final class PreferencesSubscriptionModel: ObservableObject { } } + private func makeSubscriptionAccessModel() -> SubscriptionAccessModel { + if accountManager.isUserAuthenticated { + ShareSubscriptionAccessModel(actionHandlers: sheetActionHandler, email: accountManager.email) + } else { + ActivateSubscriptionAccessModel(actionHandlers: sheetActionHandler) + } + } + private func updateUserAuthenticatedState(_ isUserAuthenticated: Bool) { self.isUserAuthenticated = isUserAuthenticated - sheetModel = isUserAuthenticated ? ShareSubscriptionAccessModel(actionHandlers: sheetActionHandler, email: accountManager.email) : ActivateSubscriptionAccessModel(actionHandlers: sheetActionHandler) + sheetModel = makeSubscriptionAccessModel() } @MainActor diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStoreAccountManagementFlow.swift b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStoreAccountManagementFlow.swift new file mode 100644 index 0000000000..ab951e46f1 --- /dev/null +++ b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStoreAccountManagementFlow.swift @@ -0,0 +1,57 @@ +// +// AppStoreAccountManagementFlow.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import StoreKit +import Purchase +import Account + +public final class AppStoreAccountManagementFlow { + + public enum Error: Swift.Error { + case noPastTransaction + case authenticatingWithTransactionFailed + } + + @discardableResult + public static func refreshAuthTokenIfNeeded() async -> Result { + var authToken = AccountManager().authToken ?? "" + + // Check if auth token if still valid + if case let .failure(error) = await AuthService.validateToken(accessToken: authToken) { + print(error) + + if #available(macOS 12.0, *) { + // In case of invalid token attempt store based authentication to obtain a new one + guard let lastTransactionJWSRepresentation = await PurchaseManager.mostRecentTransaction() else { return .failure(.noPastTransaction) } + + switch await AuthService.storeLogin(signature: lastTransactionJWSRepresentation) { + case .success(let response): + if response.externalID == AccountManager().externalID { + authToken = response.authToken + AccountManager().storeAuthToken(token: authToken) + } + case .failure: + return .failure(.authenticatingWithTransactionFailed) + } + } + } + + return .success(authToken) + } +} diff --git a/LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/Model/ShareSubscriptionAccessModel.swift b/LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/Model/ShareSubscriptionAccessModel.swift index 6a1068bd35..8ece29fe02 100644 --- a/LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/Model/ShareSubscriptionAccessModel.swift +++ b/LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/Model/ShareSubscriptionAccessModel.swift @@ -63,7 +63,15 @@ public final class ShareSubscriptionAccessModel: SubscriptionAccessModel { case .appleID: actionHandlers.restorePurchases() case .email: - actionHandlers.openURLHandler(hasEmail ? .manageSubscriptionEmail : .addEmailToSubscription) + let url: URL = hasEmail ? .manageSubscriptionEmail : .addEmailToSubscription + + Task { + await AppStoreAccountManagementFlow.refreshAuthTokenIfNeeded() + + DispatchQueue.main.async { + self.actionHandlers.openURLHandler(url) + } + } case .sync: actionHandlers.goToSyncPreferences() } diff --git a/LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/SubscriptionAccessViewController.swift b/LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/SubscriptionAccessViewController.swift index adb061d663..4b96a8764e 100644 --- a/LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/SubscriptionAccessViewController.swift +++ b/LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/SubscriptionAccessViewController.swift @@ -36,9 +36,7 @@ public final class SubscriptionAccessViewController: NSViewController { } public override func loadView() { - let model: SubscriptionAccessModel = accountManager.isUserAuthenticated ? ShareSubscriptionAccessModel(actionHandlers: actionHandlers, email: accountManager.email) : ActivateSubscriptionAccessModel(actionHandlers: actionHandlers) - - let subscriptionAccessView = SubscriptionAccessView(model: model, + let subscriptionAccessView = SubscriptionAccessView(model: makeSubscriptionAccessModel(), dismiss: { [weak self] in guard let self = self else { return } self.presentingViewController?.dismiss(self) @@ -55,4 +53,11 @@ public final class SubscriptionAccessViewController: NSViewController { view.addSubview(hostingView) } + private func makeSubscriptionAccessModel() -> SubscriptionAccessModel { + if accountManager.isUserAuthenticated { + ShareSubscriptionAccessModel(actionHandlers: actionHandlers, email: accountManager.email) + } else { + ActivateSubscriptionAccessModel(actionHandlers: actionHandlers) + } + } } From 651eb88d19657c939b64f63949e7abe954017fbd Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Thu, 23 Nov 2023 09:00:19 +0100 Subject: [PATCH 36/96] Foundation for alerts --- .../SubscriptionPagesUserScript.swift | 21 +++++++++- .../Subscription/NSAlert+Subscription.swift | 41 +++++++++++++++++++ .../PurchaseFlows/AppStorePurchaseFlow.swift | 5 ++- 3 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 LocalPackages/Subscription/Sources/Subscription/NSAlert+Subscription.swift diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index faaff96aa5..1d83036533 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -135,7 +135,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { func backToSettings(params: Any, original: WKScriptMessage) async throws -> Encodable? { await AccountManager().refreshAccountData() - + DispatchQueue.main.async { NotificationCenter.default.post(name: .subscriptionPageCloseAndOpenPreferences, object: self) } @@ -268,6 +268,25 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { self.purchaseInProgressViewController = nil } + @MainActor + private func showSubscriptionFoundAlert() { + guard let window = WindowControllersManager.shared.lastKeyMainWindowController?.window else { + assertionFailure("No window") + return + } + + let alert = NSAlert.subscriptionFoundAlert() + alert.beginSheetModal(for: window, completionHandler: { response in + if case .alertFirstButtonReturn = response { + // restore + } else { + // clear + } + + print("Restore action") + }) + } + func activateSubscription(params: Any, original: WKScriptMessage) async throws -> Encodable? { print(">>> Selected to activate a subscription -- show the activation settings screen") diff --git a/LocalPackages/Subscription/Sources/Subscription/NSAlert+Subscription.swift b/LocalPackages/Subscription/Sources/Subscription/NSAlert+Subscription.swift new file mode 100644 index 0000000000..2c1459144c --- /dev/null +++ b/LocalPackages/Subscription/Sources/Subscription/NSAlert+Subscription.swift @@ -0,0 +1,41 @@ +// +// NSAlert+Subscription.swift +// +// Copyright © 2021 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import AppKit + +public extension NSAlert { + + static func somethingWentWrong() -> NSAlert { + let alert = NSAlert() + alert.messageText = "Something Went Wrong" + alert.informativeText = "The App Store was not able to process your purchase. Please try again later." + alert.addButton(withTitle: "OK") + alert.runModal() + return alert + } + + static func subscriptionFoundAlert() -> NSAlert { + let alert = NSAlert() + alert.messageText = "Subscription Found" + alert.informativeText = "We found a subscription associated with this Apple ID." + alert.addButton(withTitle: "Restore") + alert.addButton(withTitle: "Cancel") + return alert + } +} diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift index b2d0cbe997..fe95be5c49 100644 --- a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift @@ -26,13 +26,14 @@ public final class AppStorePurchaseFlow { public enum Error: Swift.Error { case appStoreAuthenticationFailed + case activeSubscriptionAlreadyPresent case authenticatingWithTransactionFailed case accountCreationFailed case purchaseFailed case somethingWentWrong } - public static func purchaseSubscription(with identifier: String, emailAccessToken: String?) async -> Result { + public static func purchaseSubscription(with subscriptionIdentifier: String, emailAccessToken: String?) async -> Result { // Trigger sign in pop-up switch await PurchaseManager.shared.syncAppleIDAccount() { case .success: @@ -64,7 +65,7 @@ public final class AppStorePurchaseFlow { } // Make the purchase - switch await PurchaseManager.shared.purchaseSubscription(with: identifier, externalID: externalID) { + switch await PurchaseManager.shared.purchaseSubscription(with: subscriptionIdentifier, externalID: externalID) { case .success: return .success(()) case .failure(let error): From ebf1b44dea572666a9049e7bfb64b56a380caba0 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 24 Nov 2023 17:32:09 +0100 Subject: [PATCH 37/96] Support Stripe purchase flow --- .../App/DuckDuckGoPrivacyPro.xcconfig | 2 +- .../SubscriptionPagesUserScript.swift | 74 ++++++++-------- .../Services/SubscriptionService.swift | 17 ++++ .../PurchaseFlows/PurchaseFlow.swift | 72 +++++++++++++++ .../PurchaseFlows/StripePurchaseFlow.swift | 87 +++++++++++++++++++ 5 files changed, 215 insertions(+), 37 deletions(-) create mode 100644 LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/PurchaseFlow.swift create mode 100644 LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/StripePurchaseFlow.swift diff --git a/Configuration/App/DuckDuckGoPrivacyPro.xcconfig b/Configuration/App/DuckDuckGoPrivacyPro.xcconfig index c51373fcc9..e49e19d954 100644 --- a/Configuration/App/DuckDuckGoPrivacyPro.xcconfig +++ b/Configuration/App/DuckDuckGoPrivacyPro.xcconfig @@ -21,5 +21,5 @@ #include "DuckDuckGo.xcconfig" -FEATURE_FLAGS = FEEDBACK SPARKLE SUBSCRIPTION +FEATURE_FLAGS = FEEDBACK SPARKLE SUBSCRIPTION NOSTRIPE PRODUCT_NAME = $(PRODUCT_NAME_PREFIX) Privacy Pro diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 1d83036533..e7b71bfea4 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -101,6 +101,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { case "subscriptionSelected": return subscriptionSelected case "activateSubscription": return activateSubscription case "featureSelected": return featureSelected + case "completeStripePayment": return completeStripePayment default: return nil } @@ -119,7 +120,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { } func getSubscription(params: Any, original: WKScriptMessage) async throws -> Encodable? { - var authToken = AccountManager().authToken ?? "" + let authToken = AccountManager().authToken ?? "" return Subscription(token: authToken) } @@ -144,36 +145,15 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { } func getSubscriptionOptions(params: Any, original: WKScriptMessage) async throws -> Encodable? { - struct SubscriptionOptions: Encodable { - let platform: String - let options: [SubscriptionOption] - let features: [SubscriptionFeature] - } - - struct SubscriptionOption: Encodable { - let id: String - let cost: SubscriptionCost - - struct SubscriptionCost: Encodable { - let displayPrice: String - let recurrence: String - } - } - - enum SubscriptionFeatureName: String, CaseIterable { - case privateBrowsing = "private-browsing" - case privateSearch = "private-search" - case emailProtection = "email-protection" - case appTrackingProtection = "app-tracking-protection" - case vpn = "vpn" - case personalInformationRemoval = "personal-information-removal" - case identityTheftRestoration = "identity-theft-restoration" - } - - struct SubscriptionFeature: Encodable { - let name: String +#if STRIPE + switch await StripePurchaseFlow.subscriptionOptions() { + case .success(let subscriptionOptions): + return subscriptionOptions + case .failure: + // TODO: handle errors + return nil } - +#else let subscriptionOptions: [SubscriptionOption] if #available(macOS 12.0, *) { @@ -188,11 +168,12 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { return nil } - let message = SubscriptionOptions(platform: "macos", - options: subscriptionOptions, - features: SubscriptionFeatureName.allCases.map { SubscriptionFeature(name: $0.rawValue) }) + subscriptionOptions = SubscriptionOptions(platform: "macos", + options: subscriptionOptions, + features: SubscriptionFeatureName.allCases.map { SubscriptionFeature(name: $0.rawValue) }) - return message + return subscriptionOptions +#endif } func subscriptionSelected(params: Any, original: WKScriptMessage) async throws -> Encodable? { @@ -202,6 +183,15 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { let message = original +#if STRIPE + switch await StripePurchaseFlow.prepareSubscriptionPurchase(emailAccessToken: "passemailaccesstokenhere") { + case .success(let purchaseUpdate): + await pushPurchaseUpdate(webView: message.webView!, purchaseUpdate: purchaseUpdate) + case .failure: + // TODO: handle errors + return nil + } +#else if #available(macOS 12.0, *) { guard let subscriptionSelection: SubscriptionSelection = DecodableHelper.decode(from: params) else { assertionFailure("SubscriptionPagesUserScript: expected JSON representation of SubscriptionSelection") @@ -241,6 +231,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { self.pushAction(method: .onPurchaseUpdate, webView: message.webView!, params: PurchaseUpdate(type: "completed")) } } +#endif return nil } @@ -333,12 +324,23 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { return nil } + func completeStripePayment(params: Any, original: WKScriptMessage) async throws -> Encodable? { + print(">>> completeStripePayment") + + await showProgress(with: "Completing purchase...") + await StripePurchaseFlow.completeSubscriptionPurchase() + await hideProgress() + + return [String: String]() // cannot be nil + } + enum SubscribeActionName: String { case onPurchaseUpdate } - struct PurchaseUpdate: Codable { - let type: String + @MainActor + func pushPurchaseUpdate(webView: WKWebView, purchaseUpdate: PurchaseUpdate) async { + pushAction(method: .onPurchaseUpdate, webView: webView, params: purchaseUpdate) } func pushAction(method: SubscribeActionName, webView: WKWebView, params: Encodable) { diff --git a/LocalPackages/Account/Sources/Account/Services/SubscriptionService.swift b/LocalPackages/Account/Sources/Account/Services/SubscriptionService.swift index ad34078a27..877a45f245 100644 --- a/LocalPackages/Account/Sources/Account/Services/SubscriptionService.swift +++ b/LocalPackages/Account/Sources/Account/Services/SubscriptionService.swift @@ -41,4 +41,21 @@ public struct SubscriptionService: APIService { public let platform: String public let status: String } + + // MARK: - + + public static func getProducts() async -> Result { + await executeAPICall(method: "GET", endpoint: "products") + } + + public typealias GetProductsResponse = [GetProductsItem] + + public struct GetProductsItem: Decodable { + public let productId: String + public let productLabel: String + public let billingPeriod: String + public let price: String + public let currency: String + } + } diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/PurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/PurchaseFlow.swift new file mode 100644 index 0000000000..d1a666bde2 --- /dev/null +++ b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/PurchaseFlow.swift @@ -0,0 +1,72 @@ +// +// AppStorePurchaseFlow.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +protocol PurchaseFlow { + +} + +public struct SubscriptionOptions: Encodable { + let platform: String + let options: [SubscriptionOption] + let features: [SubscriptionFeature] +} + +public struct SubscriptionOption: Encodable { + let id: String + let cost: SubscriptionOptionCost +} + +struct SubscriptionOptionCost: Encodable { + let displayPrice: String + let recurrence: String +} + +public struct SubscriptionFeature: Encodable { + let name: String +} + +// MARK: - + +public enum SubscriptionFeatureName: String, CaseIterable { + case privateBrowsing = "private-browsing" + case privateSearch = "private-search" + case emailProtection = "email-protection" + case appTrackingProtection = "app-tracking-protection" + case vpn = "vpn" + case personalInformationRemoval = "personal-information-removal" + case identityTheftRestoration = "identity-theft-restoration" +} + +public enum SubscriptionPlatformName: String { + case macos + case stripe +} + +// MARK: - + +public struct PurchaseUpdate: Codable { + let type: String + let token: String? + + public init(type: String, token: String? = nil) { + self.type = type + self.token = token + } +} diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/StripePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/StripePurchaseFlow.swift new file mode 100644 index 0000000000..b82123c7c5 --- /dev/null +++ b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/StripePurchaseFlow.swift @@ -0,0 +1,87 @@ +// +// StripePurchaseFlow.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import StoreKit +import Account + +public final class StripePurchaseFlow { + + public enum Error: Swift.Error { + case noProductsFound + case accountCreationFailed + } + + public static func subscriptionOptions() async -> Result { + + guard case let .success(products) = await SubscriptionService.getProducts(), !products.isEmpty else { return .failure(.noProductsFound) } + + let currency = products.first?.currency ?? "USD" + + let formatter = NumberFormatter() + formatter.numberStyle = .currency + formatter.locale = Locale(identifier: "en_US@currency=\(currency)") + + let options: [SubscriptionOption] = products.map { + var displayPrice = "\($0.price) \($0.currency)" + + if let price = Float($0.price), let formattedPrice = formatter.string(from: price as NSNumber) { + displayPrice = formattedPrice + } + + let cost = SubscriptionOptionCost(displayPrice: displayPrice, recurrence: $0.billingPeriod.lowercased()) + + return SubscriptionOption(id: $0.productId, + cost: cost) + } + + let features = SubscriptionFeatureName.allCases.map { SubscriptionFeature(name: $0.rawValue) } + + return .success(SubscriptionOptions(platform: SubscriptionPlatformName.stripe.rawValue, + options: options, + features: features)) + } + + public static func prepareSubscriptionPurchase(emailAccessToken: String?) async -> Result { + + var token: String = "" + + switch await AuthService.createAccount(emailAccessToken: nil) { + case .success(let response): + token = response.authToken + AccountManager().storeAuthToken(token: response.authToken) + case .failure: + return .failure(.accountCreationFailed) + } + + return .success(PurchaseUpdate(type: "redirect", token: token)) + } + + public static func completeSubscriptionPurchase() async { + let accountManager = AccountManager() + + if let authToken = accountManager.authToken { + print("Exchanging token") + await accountManager.exchangeAndStoreTokens(with: authToken) + } + + if #available(macOS 12.0, *) { + await AppStorePurchaseFlow.checkForEntitlements(wait: 2.0, retry: 5) + } + } +} From cabb412e80ad1ba4f8fd7e647e262e6479b5c0f8 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 24 Nov 2023 18:43:25 +0100 Subject: [PATCH 38/96] Adapt App Store purchase flow --- .../SubscriptionPagesUserScript.swift | 44 ++++++++----------- .../PurchaseFlows/AppStorePurchaseFlow.swift | 32 ++++++++++++++ 2 files changed, 51 insertions(+), 25 deletions(-) diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index e7b71bfea4..fa66357652 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -154,25 +154,16 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { return nil } #else - let subscriptionOptions: [SubscriptionOption] - if #available(macOS 12.0, *) { - let monthly = PurchaseManager.shared.availableProducts.first(where: { $0.id.contains("1month") }) - let yearly = PurchaseManager.shared.availableProducts.first(where: { $0.id.contains("1year") }) - - guard let monthly, let yearly else { return nil } - - subscriptionOptions = [SubscriptionOption(id: monthly.id, cost: .init(displayPrice: monthly.displayPrice, recurrence: "monthly")), - SubscriptionOption(id: yearly.id, cost: .init(displayPrice: yearly.displayPrice, recurrence: "yearly"))] - } else { - return nil + switch await AppStorePurchaseFlow.subscriptionOptions() { + case .success(let subscriptionOptions): + return subscriptionOptions + case .failure: + // TODO: handle errors + return nil + } } - - subscriptionOptions = SubscriptionOptions(platform: "macos", - options: subscriptionOptions, - features: SubscriptionFeatureName.allCases.map { SubscriptionFeature(name: $0.rawValue) }) - - return subscriptionOptions + return nil #endif } @@ -186,7 +177,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { #if STRIPE switch await StripePurchaseFlow.prepareSubscriptionPurchase(emailAccessToken: "passemailaccesstokenhere") { case .success(let purchaseUpdate): - await pushPurchaseUpdate(webView: message.webView!, purchaseUpdate: purchaseUpdate) + await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: purchaseUpdate) case .failure: // TODO: handle errors return nil @@ -216,6 +207,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { case .success: break case .failure(let error): + // TODO: handle errors print("Purchase failed: \(error)") await hideProgress() return nil @@ -223,13 +215,15 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { await updateProgressTitle("Completing purchase...") - await AppStorePurchaseFlow.checkForEntitlements(wait: 2.0, retry: 15) + switch await AppStorePurchaseFlow.completeSubscriptionPurchase() { + case .success(let purchaseUpdate): + await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: purchaseUpdate) + case .failure: + // TODO: handle errors + return nil + } await hideProgress() - - DispatchQueue.main.async { - self.pushAction(method: .onPurchaseUpdate, webView: message.webView!, params: PurchaseUpdate(type: "completed")) - } } #endif @@ -339,8 +333,8 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { } @MainActor - func pushPurchaseUpdate(webView: WKWebView, purchaseUpdate: PurchaseUpdate) async { - pushAction(method: .onPurchaseUpdate, webView: webView, params: purchaseUpdate) + func pushPurchaseUpdate(originalMessage: WKScriptMessage, purchaseUpdate: PurchaseUpdate) async { + pushAction(method: .onPurchaseUpdate, webView: originalMessage.webView!, params: purchaseUpdate) } func pushAction(method: SubscribeActionName, webView: WKWebView, params: Encodable) { diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift index fe95be5c49..d490a0d0cc 100644 --- a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift @@ -25,14 +25,38 @@ import Account public final class AppStorePurchaseFlow { public enum Error: Swift.Error { + case noProductsFound + case appStoreAuthenticationFailed case activeSubscriptionAlreadyPresent case authenticatingWithTransactionFailed case accountCreationFailed case purchaseFailed + + case missingEntitlements + case somethingWentWrong } + public static func subscriptionOptions() async -> Result { + + let products = PurchaseManager.shared.availableProducts + + let monthly = products.first(where: { $0.id.contains("1month") }) + let yearly = products.first(where: { $0.id.contains("1year") }) + + guard let monthly, let yearly else { return .failure(.noProductsFound) } + + let options = [SubscriptionOption(id: monthly.id, cost: .init(displayPrice: monthly.displayPrice, recurrence: "monthly")), + SubscriptionOption(id: yearly.id, cost: .init(displayPrice: yearly.displayPrice, recurrence: "yearly"))] + + let features = SubscriptionFeatureName.allCases.map { SubscriptionFeature(name: $0.rawValue) } + + return .success(SubscriptionOptions(platform: SubscriptionPlatformName.macos.rawValue, + options: options, + features: features)) + } + public static func purchaseSubscription(with subscriptionIdentifier: String, emailAccessToken: String?) async -> Result { // Trigger sign in pop-up switch await PurchaseManager.shared.syncAppleIDAccount() { @@ -75,6 +99,14 @@ public final class AppStorePurchaseFlow { } } + @discardableResult + public static func completeSubscriptionPurchase() async -> Result { + + let result = await checkForEntitlements(wait: 2.0, retry: 15) + + return result ? .success(PurchaseUpdate(type: "completed")) : .failure(.missingEntitlements) + } + @discardableResult public static func checkForEntitlements(wait waitTime: Double, retry retryCount: Int) async -> Bool { var count = 0 From efa3bc026466bc726efd4730b4c00420278c6184 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 27 Nov 2023 10:28:38 +0100 Subject: [PATCH 39/96] Add todo --- .../Subscription/PurchaseFlows/AppStorePurchaseFlow.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift index d490a0d0cc..829d829edb 100644 --- a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift @@ -72,6 +72,7 @@ public final class AppStorePurchaseFlow { switch await AppStoreRestoreFlow.restoreAccountFromPastPurchase() { case .success(let existingExternalID): externalID = existingExternalID + // TODO: Check if has active subscription case .failure(let error): switch error { case .missingAccountOrTransactions: From cb9981f3b68742212fba7b740a61c88f0ef0f3e8 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 27 Nov 2023 10:49:52 +0100 Subject: [PATCH 40/96] Handle error during purchase --- .../SubscriptionPagesUserScript.swift | 55 +++++++++++-------- .../Subscription/NSAlert+Subscription.swift | 3 +- .../PurchaseFlows/AppStorePurchaseFlow.swift | 2 +- 3 files changed, 35 insertions(+), 25 deletions(-) diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index fa66357652..ea0f2accc2 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -207,8 +207,10 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { case .success: break case .failure(let error): - // TODO: handle errors - print("Purchase failed: \(error)") + if error != .appStoreAuthenticationFailed { + await showSomethingWentWrongAlert() + } + await hideProgress() return nil } @@ -219,7 +221,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { case .success(let purchaseUpdate): await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: purchaseUpdate) case .failure: - // TODO: handle errors + // TODO: handle errors - missing entitlements on post purchase check return nil } @@ -253,25 +255,6 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { self.purchaseInProgressViewController = nil } - @MainActor - private func showSubscriptionFoundAlert() { - guard let window = WindowControllersManager.shared.lastKeyMainWindowController?.window else { - assertionFailure("No window") - return - } - - let alert = NSAlert.subscriptionFoundAlert() - alert.beginSheetModal(for: window, completionHandler: { response in - if case .alertFirstButtonReturn = response { - // restore - } else { - // clear - } - - print("Restore action") - }) - } - func activateSubscription(params: Any, original: WKScriptMessage) async throws -> Encodable? { print(">>> Selected to activate a subscription -- show the activation settings screen") @@ -328,6 +311,8 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { return [String: String]() // cannot be nil } + // MARK: Push actions + enum SubscribeActionName: String { case onPurchaseUpdate } @@ -343,6 +328,32 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { print(">>> Pushing into WebView:", method.rawValue, String(describing: params)) broker.push(method: method.rawValue, params: params, for: self, into: webView) } + + // MARK: Alerts + + @MainActor + private func showSomethingWentWrongAlert() { + guard let window = WindowControllersManager.shared.lastKeyMainWindowController?.window else { return } + + let alert = NSAlert.somethingWentWrongAlert() + alert.beginSheetModal(for: window) + } + + @MainActor + private func showSubscriptionFoundAlert() { + guard let window = WindowControllersManager.shared.lastKeyMainWindowController?.window else { return } + + let alert = NSAlert.subscriptionFoundAlert() + alert.beginSheetModal(for: window, completionHandler: { response in + if case .alertFirstButtonReturn = response { + // restore + } else { + // clear + } + + print("Restore action") + }) + } } #endif diff --git a/LocalPackages/Subscription/Sources/Subscription/NSAlert+Subscription.swift b/LocalPackages/Subscription/Sources/Subscription/NSAlert+Subscription.swift index 2c1459144c..7599e20d24 100644 --- a/LocalPackages/Subscription/Sources/Subscription/NSAlert+Subscription.swift +++ b/LocalPackages/Subscription/Sources/Subscription/NSAlert+Subscription.swift @@ -21,12 +21,11 @@ import AppKit public extension NSAlert { - static func somethingWentWrong() -> NSAlert { + static func somethingWentWrongAlert() -> NSAlert { let alert = NSAlert() alert.messageText = "Something Went Wrong" alert.informativeText = "The App Store was not able to process your purchase. Please try again later." alert.addButton(withTitle: "OK") - alert.runModal() return alert } diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift index 829d829edb..1c5d78d246 100644 --- a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift @@ -62,7 +62,7 @@ public final class AppStorePurchaseFlow { switch await PurchaseManager.shared.syncAppleIDAccount() { case .success: break - case .failure: + case .failure(let error): return .failure(.appStoreAuthenticationFailed) } From 31756a4996285842fc02c7d0df823e24fdef4a24 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 27 Nov 2023 14:48:59 +0100 Subject: [PATCH 41/96] Edge cases for restore flow --- .../View/PreferencesRootView.swift | 44 +++++++++++++++- .../SubscriptionPagesUserScript.swift | 51 ++++++++++++++++--- .../Subscription/NSAlert+Subscription.swift | 19 +++++++ .../PurchaseFlows/AppStoreRestoreFlow.swift | 2 + .../Subscription/URL+Subscription.swift | 9 +++- 5 files changed, 113 insertions(+), 12 deletions(-) diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index b5d2b158c1..c625e7d83b 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -95,7 +95,7 @@ extension Preferences { let actionHandler = PreferencesSubscriptionActionHandlers(openURL: { url in WindowControllersManager.shared.show(url: url, newTab: true) }, manageSubscriptionInAppStore: { - NSWorkspace.shared.open(URL(string: "macappstores://apps.apple.com/account/subscriptions")!) + NSWorkspace.shared.open(.manageSubscriptionsInAppStoreAppURL) }, openVPN: { print("openVPN") }, openPersonalInformationRemoval: { @@ -107,7 +107,20 @@ extension Preferences { let sheetActionHandler = SubscriptionAccessActionHandlers(restorePurchases: { if #available(macOS 12.0, *) { Task { - await AppStoreRestoreFlow.restoreAccountFromPastPurchase() + guard case .success = await PurchaseManager.shared.syncAppleIDAccount() else { return } + + guard case .success = await AppStoreRestoreFlow.restoreAccountFromPastPurchase() else { + self.showSubscriptionNotFoundAlert() + return + } + + guard let token = AccountManager().accessToken else { return } + + if case .success(let response) = await SubscriptionService.getSubscriptionInfo(token: token) { + if response.status == "Expired" { + self.showSubscriptionInactiveAlert() + } + } } } }, openURLHandler: { url in @@ -119,6 +132,31 @@ extension Preferences { let model = PreferencesSubscriptionModel(actionHandler: actionHandler, sheetActionHandler: sheetActionHandler) return Subscription.PreferencesSubscriptionView(model: model) } + + @MainActor + private func showSubscriptionNotFoundAlert() { + guard let window = WindowControllersManager.shared.lastKeyMainWindowController?.window else { return } + + let alert = NSAlert.subscriptionNotFoundAlert() + alert.beginSheetModal(for: window, completionHandler: { response in + if case .alertFirstButtonReturn = response { + WindowControllersManager.shared.show(url: .purchaseSubscription, newTab: true) + } + }) + } + + @MainActor + private func showSubscriptionInactiveAlert() { + guard let window = WindowControllersManager.shared.lastKeyMainWindowController?.window else { return } + + let alert = NSAlert.subscriptionInactiveAlert() + alert.beginSheetModal(for: window, completionHandler: { response in + if case .alertFirstButtonReturn = response { + WindowControllersManager.shared.show(url: .purchaseSubscription, newTab: true) + AccountManager().signOut() + } + }) + } #endif } } @@ -134,3 +172,5 @@ struct SyncView: View { } } + + diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index ea0f2accc2..8003c205f2 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -150,7 +150,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { case .success(let subscriptionOptions): return subscriptionOptions case .failure: - // TODO: handle errors + // TODO: handle errors - no products found return nil } #else @@ -159,7 +159,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { case .success(let subscriptionOptions): return subscriptionOptions case .failure: - // TODO: handle errors + // TODO: handle errors - no products found return nil } } @@ -179,7 +179,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { case .success(let purchaseUpdate): await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: purchaseUpdate) case .failure: - // TODO: handle errors + // TODO: handle errors - failed prepare purchae return nil } #else @@ -265,12 +265,22 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { restorePurchases: { if #available(macOS 12.0, *) { Task { - switch await AppStoreRestoreFlow.restoreAccountFromPastPurchase() { - case .success: - message.webView?.reload() - case .failure: - break + guard case .success = await PurchaseManager.shared.syncAppleIDAccount() else { return } + + guard case .success = await AppStoreRestoreFlow.restoreAccountFromPastPurchase() else { + self.showSubscriptionNotFoundAlert() + return + } + + guard let token = AccountManager().accessToken else { return } + + if case .success(let response) = await SubscriptionService.getSubscriptionInfo(token: token) { + if response.status == "Expired" { + self.showSubscriptionInactiveAlert() + } } + + message.webView?.reload() } } }, @@ -339,6 +349,31 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { alert.beginSheetModal(for: window) } + @MainActor + private func showSubscriptionNotFoundAlert() { + guard let window = WindowControllersManager.shared.lastKeyMainWindowController?.window else { return } + + let alert = NSAlert.subscriptionNotFoundAlert() + alert.beginSheetModal(for: window, completionHandler: { response in + if case .alertFirstButtonReturn = response { + WindowControllersManager.shared.show(url: .purchaseSubscription, newTab: true) + } + }) + } + + @MainActor + private func showSubscriptionInactiveAlert() { + guard let window = WindowControllersManager.shared.lastKeyMainWindowController?.window else { return } + + let alert = NSAlert.subscriptionInactiveAlert() + alert.beginSheetModal(for: window, completionHandler: { response in + if case .alertFirstButtonReturn = response { + WindowControllersManager.shared.show(url: .purchaseSubscription, newTab: true) + AccountManager().signOut() + } + }) + } + @MainActor private func showSubscriptionFoundAlert() { guard let window = WindowControllersManager.shared.lastKeyMainWindowController?.window else { return } diff --git a/LocalPackages/Subscription/Sources/Subscription/NSAlert+Subscription.swift b/LocalPackages/Subscription/Sources/Subscription/NSAlert+Subscription.swift index 7599e20d24..2d6ba34a76 100644 --- a/LocalPackages/Subscription/Sources/Subscription/NSAlert+Subscription.swift +++ b/LocalPackages/Subscription/Sources/Subscription/NSAlert+Subscription.swift @@ -29,6 +29,25 @@ public extension NSAlert { return alert } + static func subscriptionNotFoundAlert() -> NSAlert { + let alert = NSAlert() + alert.messageText = "Subscription Not Found" + alert.informativeText = "We couldn’t find a subscription associated with this Apple ID." + alert.addButton(withTitle: "View Plans") + alert.addButton(withTitle: "Cancel") + return alert + } + + static func subscriptionInactiveAlert() -> NSAlert { + let alert = NSAlert() + alert.messageText = "Subscription Not Found" + alert.informativeText = "The subscription associated with this Apple ID is no longer active." + alert.addButton(withTitle: "View Plans") + alert.addButton(withTitle: "Cancel") + return alert + } + + static func subscriptionFoundAlert() -> NSAlert { let alert = NSAlert() alert.messageText = "Subscription Found" diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStoreRestoreFlow.swift b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStoreRestoreFlow.swift index d628c7e17b..218d727f5b 100644 --- a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStoreRestoreFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStoreRestoreFlow.swift @@ -24,6 +24,8 @@ import Account @available(macOS 12.0, *) public final class AppStoreRestoreFlow { +// typealias Success = (externalID: String, isActive: Bool, first:String) + public enum Error: Swift.Error { case missingAccountOrTransactions case pastTransactionAuthenticationFailure diff --git a/LocalPackages/Subscription/Sources/Subscription/URL+Subscription.swift b/LocalPackages/Subscription/Sources/Subscription/URL+Subscription.swift index dbb018b53e..78061d4a93 100644 --- a/LocalPackages/Subscription/Sources/Subscription/URL+Subscription.swift +++ b/LocalPackages/Subscription/Sources/Subscription/URL+Subscription.swift @@ -19,7 +19,7 @@ import Foundation public extension URL { - + static var purchaseSubscription: URL { URL(string: "https://abrown.duckduckgo.com/subscriptions/welcome")! } @@ -28,7 +28,6 @@ public extension URL { URL(string: "https://duckduckgo.com/about")! } - // MARK: - Subscription Email static var activateSubscriptionViaEmail: URL { URL(string: "https://abrown.duckduckgo.com/subscriptions/activate")! @@ -41,4 +40,10 @@ public extension URL { static var manageSubscriptionEmail: URL { URL(string: "https://abrown.duckduckgo.com/subscriptions/manage")! } + + // MARK: - App Store app manage subscription URL + + static var manageSubscriptionsInAppStoreAppURL: URL { + URL(string: "macappstores://apps.apple.com/account/subscriptions")! + } } From 966e5616330a4bc04a66ab8b9b5fb1ce68c46a95 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 27 Nov 2023 23:38:13 +0100 Subject: [PATCH 42/96] Check for active subscription when restoring --- .../View/PreferencesRootView.swift | 19 ++++++----- .../SubscriptionPagesUserScript.swift | 34 +++++++++++++------ .../Sources/Purchase/PurchaseManager.swift | 13 +++++++ .../PurchaseFlows/AppStorePurchaseFlow.swift | 15 ++------ .../PurchaseFlows/AppStoreRestoreFlow.swift | 24 ++++++++++--- 5 files changed, 69 insertions(+), 36 deletions(-) diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index c625e7d83b..840a9e0519 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -109,17 +109,18 @@ extension Preferences { Task { guard case .success = await PurchaseManager.shared.syncAppleIDAccount() else { return } - guard case .success = await AppStoreRestoreFlow.restoreAccountFromPastPurchase() else { - self.showSubscriptionNotFoundAlert() - return - } - - guard let token = AccountManager().accessToken else { return } - - if case .success(let response) = await SubscriptionService.getSubscriptionInfo(token: token) { - if response.status == "Expired" { + switch await AppStoreRestoreFlow.restoreAccountFromPastPurchase() { + case .success(let success): + if !success.isActive { self.showSubscriptionInactiveAlert() } + case .failure(let error): + switch error { + case .missingAccountOrTransactions: + self.showSubscriptionNotFoundAlert() + default: + break + } } } } diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 8003c205f2..353aa74b0a 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -184,6 +184,12 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { } #else if #available(macOS 12.0, *) { + defer { + Task { + await hideProgress() + } + } + guard let subscriptionSelection: SubscriptionSelection = DecodableHelper.decode(from: params) else { assertionFailure("SubscriptionPagesUserScript: expected JSON representation of SubscriptionSelection") return nil @@ -193,6 +199,21 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { await showProgress(with: "Purchase in progress...") + // Trigger sign in pop-up + switch await PurchaseManager.shared.syncAppleIDAccount() { + case .success: + break + case .failure(let error): + return nil + } + + // Check for active subscriptions + if await PurchaseManager.hasActiveSubscription() { + print("hasActiveSubscription: TRUE") + await showSubscriptionFoundAlert() + return nil + } + // Hide it after some time in case nothing happens /* DispatchQueue.main.asyncAfter(deadline: .now() + 60) { @@ -207,11 +228,6 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { case .success: break case .failure(let error): - if error != .appStoreAuthenticationFailed { - await showSomethingWentWrongAlert() - } - - await hideProgress() return nil } @@ -224,8 +240,6 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { // TODO: handle errors - missing entitlements on post purchase check return nil } - - await hideProgress() } #endif @@ -381,12 +395,10 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { let alert = NSAlert.subscriptionFoundAlert() alert.beginSheetModal(for: window, completionHandler: { response in if case .alertFirstButtonReturn = response { - // restore + print("Restore action") } else { - // clear + print("Cancel and do nothing") } - - print("Restore action") }) } } diff --git a/LocalPackages/Purchase/Sources/Purchase/PurchaseManager.swift b/LocalPackages/Purchase/Sources/Purchase/PurchaseManager.swift index c3099eb067..a219dc4d04 100644 --- a/LocalPackages/Purchase/Sources/Purchase/PurchaseManager.swift +++ b/LocalPackages/Purchase/Sources/Purchase/PurchaseManager.swift @@ -180,7 +180,20 @@ public final class PurchaseManager: ObservableObject { return transactions.first?.jwsRepresentation } + @MainActor + public static func hasActiveSubscription() async -> Bool { + print(" -- [PurchaseManager] hasActiveSubscription()") + + var transactions: [VerificationResult] = [] + + for await result in Transaction.currentEntitlements { + transactions.append(result) + } + print(" -- [PurchaseManager] hasActiveSubscription(): fetched \(transactions.count) transactions") + + return !transactions.isEmpty + } @MainActor public func purchaseSubscription(with identifier: String, externalID: String) async -> Result { diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift index 1c5d78d246..d5c2287ae6 100644 --- a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift @@ -27,7 +27,6 @@ public final class AppStorePurchaseFlow { public enum Error: Swift.Error { case noProductsFound - case appStoreAuthenticationFailed case activeSubscriptionAlreadyPresent case authenticatingWithTransactionFailed case accountCreationFailed @@ -58,21 +57,13 @@ public final class AppStorePurchaseFlow { } public static func purchaseSubscription(with subscriptionIdentifier: String, emailAccessToken: String?) async -> Result { - // Trigger sign in pop-up - switch await PurchaseManager.shared.syncAppleIDAccount() { - case .success: - break - case .failure(let error): - return .failure(.appStoreAuthenticationFailed) - } - let externalID: String // Check for past transactions most recent switch await AppStoreRestoreFlow.restoreAccountFromPastPurchase() { - case .success(let existingExternalID): - externalID = existingExternalID - // TODO: Check if has active subscription + case .success(let success): + guard !success.isActive else { return .failure(.activeSubscriptionAlreadyPresent)} + externalID = success.externalID case .failure(let error): switch error { case .missingAccountOrTransactions: diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStoreRestoreFlow.swift b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStoreRestoreFlow.swift index 218d727f5b..5d0c83fd05 100644 --- a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStoreRestoreFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStoreRestoreFlow.swift @@ -24,16 +24,17 @@ import Account @available(macOS 12.0, *) public final class AppStoreRestoreFlow { -// typealias Success = (externalID: String, isActive: Bool, first:String) + public typealias Success = (externalID: String, isActive: Bool) public enum Error: Swift.Error { case missingAccountOrTransactions case pastTransactionAuthenticationFailure case accessTokenObtainingError +// case subscriptionExpired case somethingWentWrong } - public static func restoreAccountFromPastPurchase() async -> Result { + public static func restoreAccountFromPastPurchase() async -> Result { guard let lastTransactionJWSRepresentation = await PurchaseManager.mostRecentTransaction() else { return .failure(.missingAccountOrTransactions) } // Do the store login to get short-lived token @@ -46,11 +47,26 @@ public final class AppStoreRestoreFlow { return .failure(.pastTransactionAuthenticationFailure) } + let externalID: String + switch await AccountManager().exchangeAndStoreTokens(with: authToken) { - case .success(let externalID): - return .success(externalID) + case .success(let existingExternalID): + externalID = existingExternalID case .failure: return .failure(.accessTokenObtainingError) } + + let accessToken = AccountManager().accessToken ?? "" + var isActive = false + + switch await SubscriptionService.getSubscriptionInfo(token: accessToken) { + case .success(let response): + isActive = response.status != "Expired" && response.status != "Inactive" + case .failure: + return .failure(.somethingWentWrong) + } + + // TOOD: Fix this by probably splitting/changing result of exchangeAndStoreTokens + return .success((externalID: externalID, isActive: isActive)) } } From 07741133514ee097c179e5f70943a60d45af2c61 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 27 Nov 2023 23:48:42 +0100 Subject: [PATCH 43/96] Clean the logic --- .../Preferences/View/PreferencesRootView.swift | 4 +--- .../UserScripts/SubscriptionPagesUserScript.swift | 15 ++++++--------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index 840a9e0519..65843ecf58 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -112,6 +112,7 @@ extension Preferences { switch await AppStoreRestoreFlow.restoreAccountFromPastPurchase() { case .success(let success): if !success.isActive { + AccountManager().signOut() self.showSubscriptionInactiveAlert() } case .failure(let error): @@ -154,7 +155,6 @@ extension Preferences { alert.beginSheetModal(for: window, completionHandler: { response in if case .alertFirstButtonReturn = response { WindowControllersManager.shared.show(url: .purchaseSubscription, newTab: true) - AccountManager().signOut() } }) } @@ -173,5 +173,3 @@ struct SyncView: View { } } - - diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 353aa74b0a..19566b4b27 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -281,19 +281,16 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { Task { guard case .success = await PurchaseManager.shared.syncAppleIDAccount() else { return } - guard case .success = await AppStoreRestoreFlow.restoreAccountFromPastPurchase() else { - self.showSubscriptionNotFoundAlert() - return - } - - guard let token = AccountManager().accessToken else { return } - - if case .success(let response) = await SubscriptionService.getSubscriptionInfo(token: token) { - if response.status == "Expired" { + switch await AppStoreRestoreFlow.restoreAccountFromPastPurchase() { + case .success(let success): + if !success.isActive { self.showSubscriptionInactiveAlert() } + case .failure: + self.showSubscriptionNotFoundAlert() } + message.webView?.reload() } } From 1574e1a3838b6d535469aff196996e24fe952a2b Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 27 Nov 2023 23:58:18 +0100 Subject: [PATCH 44/96] Final tweaks to the restore during purchase --- .../SubscriptionPagesUserScript.swift | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 19566b4b27..1e26622590 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -203,14 +203,14 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { switch await PurchaseManager.shared.syncAppleIDAccount() { case .success: break - case .failure(let error): + case .failure: return nil } // Check for active subscriptions if await PurchaseManager.hasActiveSubscription() { print("hasActiveSubscription: TRUE") - await showSubscriptionFoundAlert() + await showSubscriptionFoundAlert(originalMessage: message) return nil } @@ -227,7 +227,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { switch await AppStorePurchaseFlow.purchaseSubscription(with: subscriptionSelection.id, emailAccessToken: emailAccessToken) { case .success: break - case .failure(let error): + case .failure: return nil } @@ -290,7 +290,6 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { self.showSubscriptionNotFoundAlert() } - message.webView?.reload() } } @@ -386,15 +385,18 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { } @MainActor - private func showSubscriptionFoundAlert() { + private func showSubscriptionFoundAlert(originalMessage: WKScriptMessage) { guard let window = WindowControllersManager.shared.lastKeyMainWindowController?.window else { return } let alert = NSAlert.subscriptionFoundAlert() alert.beginSheetModal(for: window, completionHandler: { response in if case .alertFirstButtonReturn = response { - print("Restore action") - } else { - print("Cancel and do nothing") + if #available(macOS 12.0, *) { + Task { + _ = await AppStoreRestoreFlow.restoreAccountFromPastPurchase() + originalMessage.webView?.reload() + } + } } }) } From b4d34c1cc173e2c8d62e8bd523fd57acbdeac6a9 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Tue, 28 Nov 2023 00:33:16 +0100 Subject: [PATCH 45/96] Fetch and show date in the settings --- .../PreferencesSubscriptionModel.swift | 18 ++++++++++++++++++ .../PreferencesSubscriptionView.swift | 3 ++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionModel.swift b/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionModel.swift index b94ea34da2..6d860ff757 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionModel.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionModel.swift @@ -23,6 +23,7 @@ public final class PreferencesSubscriptionModel: ObservableObject { @Published var isUserAuthenticated: Bool = false @Published var hasEntitlements: Bool = false + @Published var subscriptionDetails: String? lazy var sheetModel: SubscriptionAccessModel = makeSubscriptionAccessModel() private let accountManager: AccountManager @@ -99,6 +100,23 @@ public final class PreferencesSubscriptionModel: ObservableObject { print("Entitlements!") Task { self.hasEntitlements = await AccountManager().hasEntitlement(for: "dummy1") + + guard let token = accountManager.accessToken else { return } + + if case .success(let response) = await SubscriptionService.getSubscriptionInfo(token: token) { + if response.expiresOrRenewsAt < Date() { + AccountManager().signOut() + return + } + + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .short + + let stringDate = dateFormatter.string(from: response.expiresOrRenewsAt) + + self.subscriptionDetails = "Your monthly Privacy Pro subscription renews on \(stringDate)." + } } } } diff --git a/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionView.swift b/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionView.swift index a9c9fac687..40333dbafc 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionView.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionView.swift @@ -70,7 +70,8 @@ public struct PreferencesSubscriptionView: View { .padding(4) } content: { TextMenuItemHeader(text: UserText.preferencesSubscriptionActiveHeader) - TextMenuItemCaption(text: UserText.preferencesSubscriptionActiveCaption) + TextMenuItemCaption(text: model.subscriptionDetails ?? UserText.preferencesSubscriptionActiveCaption) +// TextMenuItemCaption(text: UserText.preferencesSubscriptionActiveCaption) } buttons: { Button(UserText.addToAnotherDeviceButton) { showingSheet.toggle() } From 49dc0ed61d3879e7d1d877ef6e3b9a3426aabb8e Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Tue, 28 Nov 2023 00:50:04 +0100 Subject: [PATCH 46/96] Merge in Purchase package with Account --- DuckDuckGo.xcodeproj/project.pbxproj | 9 ------ DuckDuckGo/AppDelegate/AppDelegate.swift | 2 +- .../NavigationBar/View/MoreOptionsMenu.swift | 1 - .../View/PreferencesRootView.swift | 1 - .../View/PreferencesViewController.swift | 4 --- .../SubscriptionPagesUserScript.swift | 1 - .../Sources/Account}/PurchaseManager.swift | 0 LocalPackages/Purchase/.gitignore | 9 ------ LocalPackages/Purchase/Package.swift | 24 --------------- .../Tests/PurchaseTests/PurchaseTests.swift | 29 ------------------- LocalPackages/Subscription/Package.swift | 2 -- .../DebugMenu/DebugPurchaseModel.swift | 1 - .../DebugPurchaseViewController.swift | 2 +- .../DebugMenu/SubscriptionDebugMenu.swift | 1 - .../AppStoreAccountManagementFlow.swift | 1 - .../PurchaseFlows/AppStorePurchaseFlow.swift | 1 - .../PurchaseFlows/AppStoreRestoreFlow.swift | 1 - 17 files changed, 2 insertions(+), 87 deletions(-) rename LocalPackages/{Purchase/Sources/Purchase => Account/Sources/Account}/PurchaseManager.swift (100%) delete mode 100644 LocalPackages/Purchase/.gitignore delete mode 100644 LocalPackages/Purchase/Package.swift delete mode 100644 LocalPackages/Purchase/Tests/PurchaseTests/PurchaseTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 70e5b82935..0cac4d5de0 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2612,7 +2612,6 @@ 4B957BDC2AC7AE700062CA31 /* SwiftUIExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = 4B9579372AC7AE700062CA31 /* SwiftUIExtensions */; }; 4B957BDD2AC7AE700062CA31 /* UserScript in Frameworks */ = {isa = PBXBuildFile; productRef = 4B9579302AC7AE700062CA31 /* UserScript */; }; 4B957BDE2AC7AE700062CA31 /* Configuration in Frameworks */ = {isa = PBXBuildFile; productRef = 4B9579322AC7AE700062CA31 /* Configuration */; }; - 4B957BDF2AC7AE700062CA31 /* Purchase in Frameworks */ = {isa = PBXBuildFile; productRef = 4B9579422AC7AE700062CA31 /* Purchase */; }; 4B957BE02AC7AE700062CA31 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 4B95793A2AC7AE700062CA31 /* Lottie */; }; 4B957BE12AC7AE700062CA31 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = 4B9579402AC7AE700062CA31 /* Subscription */; }; 4B957BE22AC7AE700062CA31 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 4B9579292AC7AE700062CA31 /* Sparkle */; }; @@ -3841,7 +3840,6 @@ 1E7E2E932902AC0E00C01B54 /* PrivacyDashboardPermissionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardPermissionHandler.swift; sourceTree = ""; }; 1E862A852A9FBD7000F84D4B /* Account */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Account; sourceTree = ""; }; 1E862A882A9FC01200F84D4B /* Subscription */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Subscription; sourceTree = ""; }; - 1EC88CA22AC1DE82003A4471 /* Purchase */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Purchase; sourceTree = ""; }; 3106AD75287F000600159FE5 /* CookieConsentUserPermissionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieConsentUserPermissionViewController.swift; sourceTree = ""; }; 310E79BE294A19A8007C49E8 /* FireproofingReferenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FireproofingReferenceTests.swift; sourceTree = ""; }; 311B262628E73E0A00FD181A /* TabShadowConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabShadowConfig.swift; sourceTree = ""; }; @@ -5033,7 +5031,6 @@ 4B957BDC2AC7AE700062CA31 /* SwiftUIExtensions in Frameworks */, 4B957BDD2AC7AE700062CA31 /* UserScript in Frameworks */, 4B957BDE2AC7AE700062CA31 /* Configuration in Frameworks */, - 4B957BDF2AC7AE700062CA31 /* Purchase in Frameworks */, 4B957BE02AC7AE700062CA31 /* Lottie in Frameworks */, 4B957BE12AC7AE700062CA31 /* Subscription in Frameworks */, 4B957BE22AC7AE700062CA31 /* Sparkle in Frameworks */, @@ -5468,7 +5465,6 @@ 9DB6E7222AA0DA7A00A17F3C /* LoginItems */, 7B25FE322AD12C990012AFAB /* NetworkProtectionMac */, 4BE15DB12A0B0DD500898243 /* PixelKit */, - 1EC88CA22AC1DE82003A4471 /* Purchase */, 378F44E229B4B7B600899924 /* SwiftUIExtensions */, 37BA812B29B3CB8A0053F1A3 /* SyncUI */, 1E862A882A9FC01200F84D4B /* Subscription */, @@ -8819,7 +8815,6 @@ 4B95793F2AC7AE700062CA31 /* LoginItems */, 4B9579402AC7AE700062CA31 /* Subscription */, 4B9579412AC7AE700062CA31 /* Account */, - 4B9579422AC7AE700062CA31 /* Purchase */, 7BF770642AD6CA14001C9182 /* PixelKit */, ); productName = DuckDuckGo; @@ -14616,10 +14611,6 @@ isa = XCSwiftPackageProductDependency; productName = Account; }; - 4B9579422AC7AE700062CA31 /* Purchase */ = { - isa = XCSwiftPackageProductDependency; - productName = Purchase; - }; 7BF7705E2AD6C999001C9182 /* PixelKit */ = { isa = XCSwiftPackageProductDependency; productName = PixelKit; diff --git a/DuckDuckGo/AppDelegate/AppDelegate.swift b/DuckDuckGo/AppDelegate/AppDelegate.swift index eeedf9f1f8..3e7a379c6b 100644 --- a/DuckDuckGo/AppDelegate/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate/AppDelegate.swift @@ -34,7 +34,7 @@ import NetworkProtection #endif #if SUBSCRIPTION -import Purchase +import Account #endif @MainActor diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index c2abe7608a..bbea90d0a1 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -27,7 +27,6 @@ import NetworkProtection #if SUBSCRIPTION import Account -import Purchase #endif protocol OptionsButtonMenuDelegate: AnyObject { diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index 65843ecf58..20ba6bdd87 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -22,7 +22,6 @@ import SyncUI #if SUBSCRIPTION import Account -import Purchase import Subscription #endif diff --git a/DuckDuckGo/Preferences/View/PreferencesViewController.swift b/DuckDuckGo/Preferences/View/PreferencesViewController.swift index e231be8580..f7920d8f60 100644 --- a/DuckDuckGo/Preferences/View/PreferencesViewController.swift +++ b/DuckDuckGo/Preferences/View/PreferencesViewController.swift @@ -20,10 +20,6 @@ import AppKit import SwiftUI import Combine -#if SUBSCRIPTION -import Purchase -#endif - final class PreferencesViewController: NSViewController { weak var delegate: BrowserTabSelectionDelegate? diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 1e26622590..0a8d327edb 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -26,7 +26,6 @@ import Navigation import WebKit import UserScript import Account -import Purchase import Subscription public extension Notification.Name { diff --git a/LocalPackages/Purchase/Sources/Purchase/PurchaseManager.swift b/LocalPackages/Account/Sources/Account/PurchaseManager.swift similarity index 100% rename from LocalPackages/Purchase/Sources/Purchase/PurchaseManager.swift rename to LocalPackages/Account/Sources/Account/PurchaseManager.swift diff --git a/LocalPackages/Purchase/.gitignore b/LocalPackages/Purchase/.gitignore deleted file mode 100644 index 3b29812086..0000000000 --- a/LocalPackages/Purchase/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -.DS_Store -/.build -/Packages -/*.xcodeproj -xcuserdata/ -DerivedData/ -.swiftpm/config/registries.json -.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata -.netrc diff --git a/LocalPackages/Purchase/Package.swift b/LocalPackages/Purchase/Package.swift deleted file mode 100644 index 1152d0810e..0000000000 --- a/LocalPackages/Purchase/Package.swift +++ /dev/null @@ -1,24 +0,0 @@ -// swift-tools-version: 5.8 -// The swift-tools-version declares the minimum version of Swift required to build this package. - -import PackageDescription - -let package = Package( - name: "Purchase", - platforms: [ .macOS(.v11) ], - products: [ - .library( - name: "Purchase", - targets: ["Purchase"]), - ], - dependencies: [ - ], - targets: [ - .target( - name: "Purchase", - dependencies: []), - .testTarget( - name: "PurchaseTests", - dependencies: ["Purchase"]), - ] -) diff --git a/LocalPackages/Purchase/Tests/PurchaseTests/PurchaseTests.swift b/LocalPackages/Purchase/Tests/PurchaseTests/PurchaseTests.swift deleted file mode 100644 index 9c6b73d0a1..0000000000 --- a/LocalPackages/Purchase/Tests/PurchaseTests/PurchaseTests.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// PurchaseTests.swift -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import XCTest -@testable import Purchase - -final class PurchaseTests: XCTestCase { - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. - XCTAssertEqual(Purchase().text, "Hello, World!") - } -} diff --git a/LocalPackages/Subscription/Package.swift b/LocalPackages/Subscription/Package.swift index 1f9f0afea6..263aa023fe 100644 --- a/LocalPackages/Subscription/Package.swift +++ b/LocalPackages/Subscription/Package.swift @@ -13,7 +13,6 @@ let package = Package( ], dependencies: [ .package(path: "../Account"), - .package(path: "../Purchase"), .package(path: "../SwiftUIExtensions") ], targets: [ @@ -21,7 +20,6 @@ let package = Package( name: "Subscription", dependencies: [ .product(name: "Account", package: "Account"), - .product(name: "Purchase", package: "Purchase"), .product(name: "SwiftUIExtensions", package: "SwiftUIExtensions") ], resources: [ diff --git a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseModel.swift b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseModel.swift index 7299d9fd10..b23195b5a0 100644 --- a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseModel.swift +++ b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseModel.swift @@ -18,7 +18,6 @@ import Foundation import StoreKit -import Purchase import Account @available(macOS 12.0, *) diff --git a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseViewController.swift b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseViewController.swift index dd2eae869c..bfc5af3d9f 100644 --- a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseViewController.swift +++ b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseViewController.swift @@ -20,7 +20,7 @@ import AppKit import SwiftUI import Combine import StoreKit -import Purchase +import Account @available(macOS 12.0, *) public final class DebugPurchaseViewController: NSViewController { diff --git a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift index f54e106c5b..ce6029faa2 100644 --- a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift +++ b/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift @@ -18,7 +18,6 @@ import AppKit import Account -import Purchase public final class SubscriptionDebugMenu: NSMenuItem { diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStoreAccountManagementFlow.swift b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStoreAccountManagementFlow.swift index ab951e46f1..4eee62b914 100644 --- a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStoreAccountManagementFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStoreAccountManagementFlow.swift @@ -18,7 +18,6 @@ import Foundation import StoreKit -import Purchase import Account public final class AppStoreAccountManagementFlow { diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift index d5c2287ae6..6fda6ec4ce 100644 --- a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift @@ -18,7 +18,6 @@ import Foundation import StoreKit -import Purchase import Account @available(macOS 12.0, *) diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStoreRestoreFlow.swift b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStoreRestoreFlow.swift index 5d0c83fd05..d9c61ea02b 100644 --- a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStoreRestoreFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStoreRestoreFlow.swift @@ -18,7 +18,6 @@ import Foundation import StoreKit -import Purchase import Account @available(macOS 12.0, *) From 6f5bba14db60ce529fb26d1b9fddca6554e6b982 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Tue, 28 Nov 2023 20:07:10 +0100 Subject: [PATCH 47/96] Remove last trace of Purchase framework from the main target --- DuckDuckGo.xcodeproj/project.pbxproj | 7 ------- 1 file changed, 7 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 0cac4d5de0..a375f3471f 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -86,7 +86,6 @@ 1E2AE4CA2ACB21A000684E0A /* NetworkProtectionRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF15D82ABB8A7F0083F6DF /* NetworkProtectionRemoteMessage.swift */; }; 1E2AE4CB2ACB21C800684E0A /* HardwareModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9579202AC687170062CA31 /* HardwareModel.swift */; }; 1E2AE4CC2ACB224A00684E0A /* NetworkProtectionRemoteMessagingRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF15DC2ABB8CFC0083F6DF /* NetworkProtectionRemoteMessagingRequest.swift */; }; - 1E3ED4FD2AC1E0290075F60F /* Purchase in Frameworks */ = {isa = PBXBuildFile; productRef = 1E3ED4FC2AC1E0290075F60F /* Purchase */; }; 1E7E2E9029029A2A00C01B54 /* ContentBlockingRulesUpdateObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7E2E8F29029A2A00C01B54 /* ContentBlockingRulesUpdateObserver.swift */; }; 1E7E2E9229029F9B00C01B54 /* WebsiteBreakageReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7E2E9129029F9B00C01B54 /* WebsiteBreakageReporter.swift */; }; 1E7E2E942902AC0E00C01B54 /* PrivacyDashboardPermissionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7E2E932902AC0E00C01B54 /* PrivacyDashboardPermissionHandler.swift */; }; @@ -5075,7 +5074,6 @@ 378F44E429B4BDE900899924 /* SwiftUIExtensions in Frameworks */, 1E950E432912A10D0051A99B /* UserScript in Frameworks */, CBC83E3629B63D380008E19C /* Configuration in Frameworks */, - 1E3ED4FD2AC1E0290075F60F /* Purchase in Frameworks */, 4B2AAAF529E70DEA0026AFC0 /* Lottie in Frameworks */, AA06B6B72672AF8100F541C5 /* Sparkle in Frameworks */, 7BF7705F2AD6C999001C9182 /* PixelKit in Frameworks */, @@ -8899,7 +8897,6 @@ EE7295E22A545B9A008C0991 /* NetworkProtection */, 9DB6E7232AA0DC5800A17F3C /* LoginItems */, 1EC88CA02AC1DD63003A4471 /* Account */, - 1E3ED4FC2AC1E0290075F60F /* Purchase */, 7BF7705E2AD6C999001C9182 /* PixelKit */, ); productName = DuckDuckGo; @@ -14241,10 +14238,6 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = Common; }; - 1E3ED4FC2AC1E0290075F60F /* Purchase */ = { - isa = XCSwiftPackageProductDependency; - productName = Purchase; - }; 1E950E3E2912A10D0051A99B /* ContentBlocking */ = { isa = XCSwiftPackageProductDependency; package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; From 26d12c47bae4b9dd0cdcddad61fa6eaed04d7e35 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Tue, 28 Nov 2023 20:12:22 +0100 Subject: [PATCH 48/96] Remove Accounts from the main target --- DuckDuckGo.xcodeproj/project.pbxproj | 7 ------- 1 file changed, 7 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index a375f3471f..c34121dcae 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -92,7 +92,6 @@ 1E950E3F2912A10D0051A99B /* ContentBlocking in Frameworks */ = {isa = PBXBuildFile; productRef = 1E950E3E2912A10D0051A99B /* ContentBlocking */; }; 1E950E412912A10D0051A99B /* PrivacyDashboard in Frameworks */ = {isa = PBXBuildFile; productRef = 1E950E402912A10D0051A99B /* PrivacyDashboard */; }; 1E950E432912A10D0051A99B /* UserScript in Frameworks */ = {isa = PBXBuildFile; productRef = 1E950E422912A10D0051A99B /* UserScript */; }; - 1EC88CA12AC1DD63003A4471 /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 1EC88CA02AC1DD63003A4471 /* Account */; }; 3106AD76287F000600159FE5 /* CookieConsentUserPermissionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3106AD75287F000600159FE5 /* CookieConsentUserPermissionViewController.swift */; }; 310E79BF294A19A8007C49E8 /* FireproofingReferenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 310E79BE294A19A8007C49E8 /* FireproofingReferenceTests.swift */; }; 311B262728E73E0A00FD181A /* TabShadowConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 311B262628E73E0A00FD181A /* TabShadowConfig.swift */; }; @@ -5068,7 +5067,6 @@ 9DB6E7242AA0DC5800A17F3C /* LoginItems in Frameworks */, EE7295E32A545B9A008C0991 /* NetworkProtection in Frameworks */, 9807F645278CA16F00E1547B /* BrowserServicesKit in Frameworks */, - 1EC88CA12AC1DD63003A4471 /* Account in Frameworks */, 987799ED299998B1005D8EB6 /* Bookmarks in Frameworks */, 1E950E3F2912A10D0051A99B /* ContentBlocking in Frameworks */, 378F44E429B4BDE900899924 /* SwiftUIExtensions in Frameworks */, @@ -8896,7 +8894,6 @@ 4B4D60B02A0C83B900BCD287 /* NetworkProtectionUI */, EE7295E22A545B9A008C0991 /* NetworkProtection */, 9DB6E7232AA0DC5800A17F3C /* LoginItems */, - 1EC88CA02AC1DD63003A4471 /* Account */, 7BF7705E2AD6C999001C9182 /* PixelKit */, ); productName = DuckDuckGo; @@ -14253,10 +14250,6 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = UserScript; }; - 1EC88CA02AC1DD63003A4471 /* Account */ = { - isa = XCSwiftPackageProductDependency; - productName = Account; - }; 31929F7F2A4C4CFF0084EA89 /* TrackerRadarKit */ = { isa = XCSwiftPackageProductDependency; package = 31929F802A4C4CFF0084EA89 /* XCRemoteSwiftPackageReference "TrackerRadarKit" */; From fc8b9716ee430d898d60be5ec5fce35a80c56dab Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Tue, 28 Nov 2023 20:23:08 +0100 Subject: [PATCH 49/96] Rename Subscription to SubscriptionUI --- DuckDuckGo.xcodeproj/project.pbxproj | 18 +++++++++--------- DuckDuckGo/Menus/MainMenu.swift | 2 +- .../View/NavigationBarViewController.swift | 2 +- .../View/PreferencesRootView.swift | 4 ++-- .../SubscriptionPagesUserScript.swift | 2 +- .../.gitignore | 0 .../Package.swift | 12 ++++++------ .../DebugMenu/DebugPurchaseModel.swift | 0 .../DebugMenu/DebugPurchaseView.swift | 0 .../DebugPurchaseViewController.swift | 0 .../PurchaseInProgressViewController.swift | 0 .../DebugMenu/SubscriptionDebugMenu.swift | 0 .../Extensions/RoundedBorder.swift | 0 .../NSAlert+Subscription.swift | 0 .../PreferencesSubscriptionModel.swift | 0 .../PreferencesSubscriptionView.swift | 0 .../AppStoreAccountManagementFlow.swift | 0 .../PurchaseFlows/AppStorePurchaseFlow.swift | 0 .../PurchaseFlows/AppStoreRestoreFlow.swift | 0 .../PurchaseFlows/PurchaseFlow.swift | 0 .../PurchaseFlows/StripePurchaseFlow.swift | 0 .../BadgeBackground.colorset/Contents.json | 0 .../Colors/Contents.json | 0 .../Colors/TextPrimary.colorset/Contents.json | 0 .../TextSecondary.colorset/Contents.json | 0 .../Subscription.xcassets/Contents.json | 0 .../Subscription.xcassets/Icons/Contents.json | 0 .../apple-id-icon.imageset/Contents.json | 0 .../apple-id-icon.imageset/apple-id-icon.pdf | Bin .../Icons/email-icon.imageset/Contents.json | 0 .../Icons/email-icon.imageset/email-icon.pdf | Bin .../itr-service-icon.imageset/Contents.json | 0 .../itr-service-icon.pdf | Bin .../pir-service-icon.imageset/Contents.json | 0 .../pir-service-icon.pdf | Bin .../Contents.json | 0 .../subscription-active-icon.pdf | Bin .../Contents.json | 0 .../subscription-inactive-icon.pdf | Bin .../Icons/sync-icon.imageset/Contents.json | 0 .../Icons/sync-icon.imageset/sync-icon.pdf | Bin .../vpn-service-icon.imageset/Contents.json | 0 .../vpn-service-icon.pdf | Bin .../Images/Contents.json | 0 .../Placeholder-96x64.imageset/Contents.json | 0 .../Placeholder-96x64.pdf | Bin .../Model/AccessChannel.swift | 0 .../ActivateSubscriptionAccessModel.swift | 0 .../Model/ShareSubscriptionAccessModel.swift | 0 .../Model/SubscriptionAccessModel.swift | 0 .../SubscriptionAccessRow.swift | 0 .../SubscriptionAccessView.swift | 0 .../SubscriptionAccessViewController.swift | 0 .../SubscriptionUI}/URL+Subscription.swift | 0 .../Sources/SubscriptionUI}/UserText.swift | 0 .../SubscriptionTests.swift | 4 ++-- 56 files changed, 22 insertions(+), 22 deletions(-) rename LocalPackages/{Subscription => SubscriptionUI}/.gitignore (100%) rename LocalPackages/{Subscription => SubscriptionUI}/Package.swift (75%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/DebugMenu/DebugPurchaseModel.swift (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/DebugMenu/DebugPurchaseView.swift (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/DebugMenu/DebugPurchaseViewController.swift (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/DebugMenu/PurchaseInProgressViewController.swift (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/DebugMenu/SubscriptionDebugMenu.swift (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/Extensions/RoundedBorder.swift (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/NSAlert+Subscription.swift (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/Preferences/PreferencesSubscriptionModel.swift (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/Preferences/PreferencesSubscriptionView.swift (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/PurchaseFlows/AppStoreAccountManagementFlow.swift (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/PurchaseFlows/AppStorePurchaseFlow.swift (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/PurchaseFlows/AppStoreRestoreFlow.swift (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/PurchaseFlows/PurchaseFlow.swift (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/PurchaseFlows/StripePurchaseFlow.swift (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/Resources/Subscription.xcassets/Colors/BadgeBackground.colorset/Contents.json (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/Resources/Subscription.xcassets/Colors/Contents.json (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/Resources/Subscription.xcassets/Colors/TextPrimary.colorset/Contents.json (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/Resources/Subscription.xcassets/Colors/TextSecondary.colorset/Contents.json (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/Resources/Subscription.xcassets/Contents.json (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/Resources/Subscription.xcassets/Icons/Contents.json (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/Resources/Subscription.xcassets/Icons/apple-id-icon.imageset/Contents.json (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/Resources/Subscription.xcassets/Icons/apple-id-icon.imageset/apple-id-icon.pdf (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/Resources/Subscription.xcassets/Icons/email-icon.imageset/Contents.json (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/Resources/Subscription.xcassets/Icons/email-icon.imageset/email-icon.pdf (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/Resources/Subscription.xcassets/Icons/itr-service-icon.imageset/Contents.json (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/Resources/Subscription.xcassets/Icons/itr-service-icon.imageset/itr-service-icon.pdf (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/Resources/Subscription.xcassets/Icons/pir-service-icon.imageset/Contents.json (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/Resources/Subscription.xcassets/Icons/pir-service-icon.imageset/pir-service-icon.pdf (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/Resources/Subscription.xcassets/Icons/subscription-active-icon.imageset/Contents.json (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/Resources/Subscription.xcassets/Icons/subscription-active-icon.imageset/subscription-active-icon.pdf (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/Resources/Subscription.xcassets/Icons/subscription-inactive-icon.imageset/Contents.json (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/Resources/Subscription.xcassets/Icons/subscription-inactive-icon.imageset/subscription-inactive-icon.pdf (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/Resources/Subscription.xcassets/Icons/sync-icon.imageset/Contents.json (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/Resources/Subscription.xcassets/Icons/sync-icon.imageset/sync-icon.pdf (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/Resources/Subscription.xcassets/Icons/vpn-service-icon.imageset/Contents.json (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/Resources/Subscription.xcassets/Icons/vpn-service-icon.imageset/vpn-service-icon.pdf (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/Resources/Subscription.xcassets/Images/Contents.json (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/Resources/Subscription.xcassets/Images/Placeholder-96x64.imageset/Contents.json (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/Resources/Subscription.xcassets/Images/Placeholder-96x64.imageset/Placeholder-96x64.pdf (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/SubscriptionAccessView/Model/AccessChannel.swift (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/SubscriptionAccessView/Model/ActivateSubscriptionAccessModel.swift (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/SubscriptionAccessView/Model/ShareSubscriptionAccessModel.swift (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/SubscriptionAccessView/Model/SubscriptionAccessModel.swift (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/SubscriptionAccessView/SubscriptionAccessRow.swift (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/SubscriptionAccessView/SubscriptionAccessView.swift (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/SubscriptionAccessView/SubscriptionAccessViewController.swift (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/URL+Subscription.swift (100%) rename LocalPackages/{Subscription/Sources/Subscription => SubscriptionUI/Sources/SubscriptionUI}/UserText.swift (100%) rename LocalPackages/{Subscription/Tests/SubscriptionTests => SubscriptionUI/Tests/SubscriptionUITests}/SubscriptionTests.swift (90%) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index c34121dcae..9cbc29984f 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -76,6 +76,7 @@ 1DFAB51E2A8982A600A0F7F6 /* SetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFAB51C2A8982A600A0F7F6 /* SetExtension.swift */; }; 1DFAB5222A8983DE00A0F7F6 /* SetExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFAB51F2A89830D00A0F7F6 /* SetExtensionTests.swift */; }; 1DFAB5232A8983E100A0F7F6 /* SetExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFAB51F2A89830D00A0F7F6 /* SetExtensionTests.swift */; }; + 1E0068AD2B1673BB00BBF43B /* SubscriptionUI in Frameworks */ = {isa = PBXBuildFile; productRef = 1E0068AC2B1673BB00BBF43B /* SubscriptionUI */; }; 1E0C72062ABC63BD00802009 /* SubscriptionPagesUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E0C72052ABC63BD00802009 /* SubscriptionPagesUserScript.swift */; }; 1E0C72072ABC63BD00802009 /* SubscriptionPagesUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E0C72052ABC63BD00802009 /* SubscriptionPagesUserScript.swift */; }; 1E0C72082ABC63BD00802009 /* SubscriptionPagesUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E0C72052ABC63BD00802009 /* SubscriptionPagesUserScript.swift */; }; @@ -2611,7 +2612,6 @@ 4B957BDD2AC7AE700062CA31 /* UserScript in Frameworks */ = {isa = PBXBuildFile; productRef = 4B9579302AC7AE700062CA31 /* UserScript */; }; 4B957BDE2AC7AE700062CA31 /* Configuration in Frameworks */ = {isa = PBXBuildFile; productRef = 4B9579322AC7AE700062CA31 /* Configuration */; }; 4B957BE02AC7AE700062CA31 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 4B95793A2AC7AE700062CA31 /* Lottie */; }; - 4B957BE12AC7AE700062CA31 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = 4B9579402AC7AE700062CA31 /* Subscription */; }; 4B957BE22AC7AE700062CA31 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 4B9579292AC7AE700062CA31 /* Sparkle */; }; 4B957BE32AC7AE700062CA31 /* Navigation in Frameworks */ = {isa = PBXBuildFile; productRef = 4B9579332AC7AE700062CA31 /* Navigation */; }; 4B957BE42AC7AE700062CA31 /* DDGSync in Frameworks */ = {isa = PBXBuildFile; productRef = 4B9579352AC7AE700062CA31 /* DDGSync */; }; @@ -3837,7 +3837,7 @@ 1E7E2E9129029F9B00C01B54 /* WebsiteBreakageReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteBreakageReporter.swift; sourceTree = ""; }; 1E7E2E932902AC0E00C01B54 /* PrivacyDashboardPermissionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardPermissionHandler.swift; sourceTree = ""; }; 1E862A852A9FBD7000F84D4B /* Account */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Account; sourceTree = ""; }; - 1E862A882A9FC01200F84D4B /* Subscription */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Subscription; sourceTree = ""; }; + 1E862A882A9FC01200F84D4B /* SubscriptionUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = SubscriptionUI; sourceTree = ""; }; 3106AD75287F000600159FE5 /* CookieConsentUserPermissionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieConsentUserPermissionViewController.swift; sourceTree = ""; }; 310E79BE294A19A8007C49E8 /* FireproofingReferenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FireproofingReferenceTests.swift; sourceTree = ""; }; 311B262628E73E0A00FD181A /* TabShadowConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabShadowConfig.swift; sourceTree = ""; }; @@ -5030,7 +5030,6 @@ 4B957BDD2AC7AE700062CA31 /* UserScript in Frameworks */, 4B957BDE2AC7AE700062CA31 /* Configuration in Frameworks */, 4B957BE02AC7AE700062CA31 /* Lottie in Frameworks */, - 4B957BE12AC7AE700062CA31 /* Subscription in Frameworks */, 4B957BE22AC7AE700062CA31 /* Sparkle in Frameworks */, 4B957BE32AC7AE700062CA31 /* Navigation in Frameworks */, 4B957BE42AC7AE700062CA31 /* DDGSync in Frameworks */, @@ -5041,6 +5040,7 @@ 4B957BE82AC7AE700062CA31 /* SyncUI in Frameworks */, 4B957BE92AC7AE700062CA31 /* NetworkProtectionUI in Frameworks */, 4B957BEA2AC7AE700062CA31 /* Common in Frameworks */, + 1E0068AD2B1673BB00BBF43B /* SubscriptionUI in Frameworks */, 4B957BEB2AC7AE700062CA31 /* Persistence in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -5463,7 +5463,7 @@ 4BE15DB12A0B0DD500898243 /* PixelKit */, 378F44E229B4B7B600899924 /* SwiftUIExtensions */, 37BA812B29B3CB8A0053F1A3 /* SyncUI */, - 1E862A882A9FC01200F84D4B /* Subscription */, + 1E862A882A9FC01200F84D4B /* SubscriptionUI */, 7BC185EA2AD6CB4900F9D9DC /* XPC */, ); path = LocalPackages; @@ -8809,9 +8809,9 @@ 4B95793D2AC7AE700062CA31 /* NetworkProtectionUI */, 4B95793E2AC7AE700062CA31 /* NetworkProtection */, 4B95793F2AC7AE700062CA31 /* LoginItems */, - 4B9579402AC7AE700062CA31 /* Subscription */, 4B9579412AC7AE700062CA31 /* Account */, 7BF770642AD6CA14001C9182 /* PixelKit */, + 1E0068AC2B1673BB00BBF43B /* SubscriptionUI */, ); productName = DuckDuckGo; productReference = 4B957C412AC7AE700062CA31 /* DuckDuckGo Privacy Pro.app */; @@ -14230,6 +14230,10 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 1E0068AC2B1673BB00BBF43B /* SubscriptionUI */ = { + isa = XCSwiftPackageProductDependency; + productName = SubscriptionUI; + }; 1E25269B28F8741A00E44DFA /* Common */ = { isa = XCSwiftPackageProductDependency; package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; @@ -14589,10 +14593,6 @@ isa = XCSwiftPackageProductDependency; productName = LoginItems; }; - 4B9579402AC7AE700062CA31 /* Subscription */ = { - isa = XCSwiftPackageProductDependency; - productName = Subscription; - }; 4B9579412AC7AE700062CA31 /* Account */ = { isa = XCSwiftPackageProductDependency; productName = Account; diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index 5cc7405f9e..f209498e77 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -28,7 +28,7 @@ import NetworkProtection #endif #if SUBSCRIPTION -import Subscription +import SubscriptionUI #endif @MainActor diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index d5666a99fd..2623e9c5d5 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -27,7 +27,7 @@ import NetworkProtectionUI #endif #if SUBSCRIPTION -import Subscription +import SubscriptionUI #endif // swiftlint:disable:next type_body_length diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index 20ba6bdd87..c54062a094 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -22,7 +22,7 @@ import SyncUI #if SUBSCRIPTION import Account -import Subscription +import SubscriptionUI #endif fileprivate extension Preferences.Const { @@ -131,7 +131,7 @@ extension Preferences { }) let model = PreferencesSubscriptionModel(actionHandler: actionHandler, sheetActionHandler: sheetActionHandler) - return Subscription.PreferencesSubscriptionView(model: model) + return SubscriptionUI.PreferencesSubscriptionView(model: model) } @MainActor diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 0a8d327edb..408b1854ba 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -26,7 +26,7 @@ import Navigation import WebKit import UserScript import Account -import Subscription +import SubscriptionUI public extension Notification.Name { static let subscriptionPageCloseAndOpenPreferences = Notification.Name("com.duckduckgo.subscriptionPage.CloseAndOpenPreferences") diff --git a/LocalPackages/Subscription/.gitignore b/LocalPackages/SubscriptionUI/.gitignore similarity index 100% rename from LocalPackages/Subscription/.gitignore rename to LocalPackages/SubscriptionUI/.gitignore diff --git a/LocalPackages/Subscription/Package.swift b/LocalPackages/SubscriptionUI/Package.swift similarity index 75% rename from LocalPackages/Subscription/Package.swift rename to LocalPackages/SubscriptionUI/Package.swift index 263aa023fe..8571181475 100644 --- a/LocalPackages/Subscription/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -4,12 +4,12 @@ import PackageDescription let package = Package( - name: "Subscription", + name: "SubscriptionUI", platforms: [ .macOS(.v11) ], products: [ .library( - name: "Subscription", - targets: ["Subscription"]), + name: "SubscriptionUI", + targets: ["SubscriptionUI"]), ], dependencies: [ .package(path: "../Account"), @@ -17,7 +17,7 @@ let package = Package( ], targets: [ .target( - name: "Subscription", + name: "SubscriptionUI", dependencies: [ .product(name: "Account", package: "Account"), .product(name: "SwiftUIExtensions", package: "SwiftUIExtensions") @@ -26,7 +26,7 @@ let package = Package( .process("Resources") ]), .testTarget( - name: "SubscriptionTests", - dependencies: ["Subscription"]), + name: "SubscriptionUITests", + dependencies: ["SubscriptionUI"]), ] ) diff --git a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseModel.swift similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseModel.swift rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseModel.swift diff --git a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseView.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseView.swift similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseView.swift rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseView.swift diff --git a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseViewController.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseViewController.swift similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/DebugMenu/DebugPurchaseViewController.swift rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseViewController.swift diff --git a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/PurchaseInProgressViewController.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/PurchaseInProgressViewController.swift similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/DebugMenu/PurchaseInProgressViewController.swift rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/PurchaseInProgressViewController.swift diff --git a/LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/DebugMenu/SubscriptionDebugMenu.swift rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift diff --git a/LocalPackages/Subscription/Sources/Subscription/Extensions/RoundedBorder.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Extensions/RoundedBorder.swift similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/Extensions/RoundedBorder.swift rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Extensions/RoundedBorder.swift diff --git a/LocalPackages/Subscription/Sources/Subscription/NSAlert+Subscription.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/NSAlert+Subscription.swift similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/NSAlert+Subscription.swift rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/NSAlert+Subscription.swift diff --git a/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionModel.swift rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift diff --git a/LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionView.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/Preferences/PreferencesSubscriptionView.swift rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStoreAccountManagementFlow.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows/AppStoreAccountManagementFlow.swift similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStoreAccountManagementFlow.swift rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows/AppStoreAccountManagementFlow.swift diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows/AppStorePurchaseFlow.swift similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStorePurchaseFlow.swift rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows/AppStorePurchaseFlow.swift diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStoreRestoreFlow.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows/AppStoreRestoreFlow.swift similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/AppStoreRestoreFlow.swift rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows/AppStoreRestoreFlow.swift diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/PurchaseFlow.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows/PurchaseFlow.swift similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/PurchaseFlow.swift rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows/PurchaseFlow.swift diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/StripePurchaseFlow.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows/StripePurchaseFlow.swift similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/PurchaseFlows/StripePurchaseFlow.swift rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows/StripePurchaseFlow.swift diff --git a/LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Colors/BadgeBackground.colorset/Contents.json b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Colors/BadgeBackground.colorset/Contents.json similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Colors/BadgeBackground.colorset/Contents.json rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Colors/BadgeBackground.colorset/Contents.json diff --git a/LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Colors/Contents.json b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Colors/Contents.json similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Colors/Contents.json rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Colors/Contents.json diff --git a/LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Colors/TextPrimary.colorset/Contents.json b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Colors/TextPrimary.colorset/Contents.json similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Colors/TextPrimary.colorset/Contents.json rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Colors/TextPrimary.colorset/Contents.json diff --git a/LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Colors/TextSecondary.colorset/Contents.json b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Colors/TextSecondary.colorset/Contents.json similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Colors/TextSecondary.colorset/Contents.json rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Colors/TextSecondary.colorset/Contents.json diff --git a/LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Contents.json b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Contents.json similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Contents.json rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Contents.json diff --git a/LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Icons/Contents.json b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/Contents.json similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Icons/Contents.json rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/Contents.json diff --git a/LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Icons/apple-id-icon.imageset/Contents.json b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/apple-id-icon.imageset/Contents.json similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Icons/apple-id-icon.imageset/Contents.json rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/apple-id-icon.imageset/Contents.json diff --git a/LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Icons/apple-id-icon.imageset/apple-id-icon.pdf b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/apple-id-icon.imageset/apple-id-icon.pdf similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Icons/apple-id-icon.imageset/apple-id-icon.pdf rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/apple-id-icon.imageset/apple-id-icon.pdf diff --git a/LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Icons/email-icon.imageset/Contents.json b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/email-icon.imageset/Contents.json similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Icons/email-icon.imageset/Contents.json rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/email-icon.imageset/Contents.json diff --git a/LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Icons/email-icon.imageset/email-icon.pdf b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/email-icon.imageset/email-icon.pdf similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Icons/email-icon.imageset/email-icon.pdf rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/email-icon.imageset/email-icon.pdf diff --git a/LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Icons/itr-service-icon.imageset/Contents.json b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/itr-service-icon.imageset/Contents.json similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Icons/itr-service-icon.imageset/Contents.json rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/itr-service-icon.imageset/Contents.json diff --git a/LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Icons/itr-service-icon.imageset/itr-service-icon.pdf b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/itr-service-icon.imageset/itr-service-icon.pdf similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Icons/itr-service-icon.imageset/itr-service-icon.pdf rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/itr-service-icon.imageset/itr-service-icon.pdf diff --git a/LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Icons/pir-service-icon.imageset/Contents.json b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/pir-service-icon.imageset/Contents.json similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Icons/pir-service-icon.imageset/Contents.json rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/pir-service-icon.imageset/Contents.json diff --git a/LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Icons/pir-service-icon.imageset/pir-service-icon.pdf b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/pir-service-icon.imageset/pir-service-icon.pdf similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Icons/pir-service-icon.imageset/pir-service-icon.pdf rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/pir-service-icon.imageset/pir-service-icon.pdf diff --git a/LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Icons/subscription-active-icon.imageset/Contents.json b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-active-icon.imageset/Contents.json similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Icons/subscription-active-icon.imageset/Contents.json rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-active-icon.imageset/Contents.json diff --git a/LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Icons/subscription-active-icon.imageset/subscription-active-icon.pdf b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-active-icon.imageset/subscription-active-icon.pdf similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Icons/subscription-active-icon.imageset/subscription-active-icon.pdf rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-active-icon.imageset/subscription-active-icon.pdf diff --git a/LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Icons/subscription-inactive-icon.imageset/Contents.json b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-inactive-icon.imageset/Contents.json similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Icons/subscription-inactive-icon.imageset/Contents.json rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-inactive-icon.imageset/Contents.json diff --git a/LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Icons/subscription-inactive-icon.imageset/subscription-inactive-icon.pdf b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-inactive-icon.imageset/subscription-inactive-icon.pdf similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Icons/subscription-inactive-icon.imageset/subscription-inactive-icon.pdf rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-inactive-icon.imageset/subscription-inactive-icon.pdf diff --git a/LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Icons/sync-icon.imageset/Contents.json b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/sync-icon.imageset/Contents.json similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Icons/sync-icon.imageset/Contents.json rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/sync-icon.imageset/Contents.json diff --git a/LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Icons/sync-icon.imageset/sync-icon.pdf b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/sync-icon.imageset/sync-icon.pdf similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Icons/sync-icon.imageset/sync-icon.pdf rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/sync-icon.imageset/sync-icon.pdf diff --git a/LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Icons/vpn-service-icon.imageset/Contents.json b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/vpn-service-icon.imageset/Contents.json similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Icons/vpn-service-icon.imageset/Contents.json rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/vpn-service-icon.imageset/Contents.json diff --git a/LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Icons/vpn-service-icon.imageset/vpn-service-icon.pdf b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/vpn-service-icon.imageset/vpn-service-icon.pdf similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Icons/vpn-service-icon.imageset/vpn-service-icon.pdf rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/vpn-service-icon.imageset/vpn-service-icon.pdf diff --git a/LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Images/Contents.json b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Images/Contents.json similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Images/Contents.json rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Images/Contents.json diff --git a/LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Images/Placeholder-96x64.imageset/Contents.json b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Images/Placeholder-96x64.imageset/Contents.json similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Images/Placeholder-96x64.imageset/Contents.json rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Images/Placeholder-96x64.imageset/Contents.json diff --git a/LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Images/Placeholder-96x64.imageset/Placeholder-96x64.pdf b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Images/Placeholder-96x64.imageset/Placeholder-96x64.pdf similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/Resources/Subscription.xcassets/Images/Placeholder-96x64.imageset/Placeholder-96x64.pdf rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Images/Placeholder-96x64.imageset/Placeholder-96x64.pdf diff --git a/LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/Model/AccessChannel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/AccessChannel.swift similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/Model/AccessChannel.swift rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/AccessChannel.swift diff --git a/LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/Model/ActivateSubscriptionAccessModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ActivateSubscriptionAccessModel.swift similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/Model/ActivateSubscriptionAccessModel.swift rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ActivateSubscriptionAccessModel.swift diff --git a/LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/Model/ShareSubscriptionAccessModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ShareSubscriptionAccessModel.swift similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/Model/ShareSubscriptionAccessModel.swift rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ShareSubscriptionAccessModel.swift diff --git a/LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/Model/SubscriptionAccessModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/SubscriptionAccessModel.swift similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/Model/SubscriptionAccessModel.swift rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/SubscriptionAccessModel.swift diff --git a/LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/SubscriptionAccessRow.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/SubscriptionAccessRow.swift similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/SubscriptionAccessRow.swift rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/SubscriptionAccessRow.swift diff --git a/LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/SubscriptionAccessView.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/SubscriptionAccessView.swift similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/SubscriptionAccessView.swift rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/SubscriptionAccessView.swift diff --git a/LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/SubscriptionAccessViewController.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/SubscriptionAccessViewController.swift similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/SubscriptionAccessView/SubscriptionAccessViewController.swift rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/SubscriptionAccessViewController.swift diff --git a/LocalPackages/Subscription/Sources/Subscription/URL+Subscription.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/URL+Subscription.swift similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/URL+Subscription.swift rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/URL+Subscription.swift diff --git a/LocalPackages/Subscription/Sources/Subscription/UserText.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/UserText.swift rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift diff --git a/LocalPackages/Subscription/Tests/SubscriptionTests/SubscriptionTests.swift b/LocalPackages/SubscriptionUI/Tests/SubscriptionUITests/SubscriptionTests.swift similarity index 90% rename from LocalPackages/Subscription/Tests/SubscriptionTests/SubscriptionTests.swift rename to LocalPackages/SubscriptionUI/Tests/SubscriptionUITests/SubscriptionTests.swift index 16c144c04f..014c2b2c34 100644 --- a/LocalPackages/Subscription/Tests/SubscriptionTests/SubscriptionTests.swift +++ b/LocalPackages/SubscriptionUI/Tests/SubscriptionUITests/SubscriptionTests.swift @@ -17,13 +17,13 @@ // import XCTest -@testable import Subscription +@testable import SubscriptionUI final class SubscriptionTests: XCTestCase { func testExample() throws { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct // results. - XCTAssertEqual(Subscription().text, "Hello, World!") + XCTAssertEqual(SubscriptionUI().text, "Hello, World!") } } From cbb8ea10f3b7bf6477c9113a82eb691914272976 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Tue, 28 Nov 2023 22:53:57 +0100 Subject: [PATCH 50/96] Rename Account to Subscrtiption package --- DuckDuckGo.xcodeproj/project.pbxproj | 18 ++++++------ DuckDuckGo/AppDelegate/AppDelegate.swift | 2 +- .../NavigationBar/View/MoreOptionsMenu.swift | 2 +- .../View/PreferencesRootView.swift | 2 +- .../SubscriptionPagesUserScript.swift | 2 +- .../Tests/AccountTests/AccountsTests.swift | 29 ------------------- .../{Account => Subscription}/.gitignore | 3 +- .../{Account => Subscription}/Package.swift | 14 ++++----- .../Subscription}/AccountManager.swift | 0 .../AccountKeychainStorage.swift | 0 .../AccountStorage/AccountStorage.swift | 0 .../Sources/Subscription}/Logging.swift | 0 .../Subscription}/PurchaseManager.swift | 0 .../Subscription}/Services/APIService.swift | 0 .../Subscription}/Services/AuthService.swift | 0 .../Services/SubscriptionService.swift | 0 .../SubscriptionTests/SubscriptionTests.swift | 12 ++++++++ LocalPackages/SubscriptionUI/Package.swift | 4 +-- .../DebugMenu/DebugPurchaseModel.swift | 2 +- .../DebugPurchaseViewController.swift | 2 +- .../DebugMenu/SubscriptionDebugMenu.swift | 2 +- .../PreferencesSubscriptionModel.swift | 2 +- .../AppStoreAccountManagementFlow.swift | 2 +- .../PurchaseFlows/AppStorePurchaseFlow.swift | 2 +- .../PurchaseFlows/AppStoreRestoreFlow.swift | 2 +- .../PurchaseFlows/StripePurchaseFlow.swift | 2 +- .../SubscriptionAccessViewController.swift | 2 +- 27 files changed, 44 insertions(+), 62 deletions(-) delete mode 100644 LocalPackages/Account/Tests/AccountTests/AccountsTests.swift rename LocalPackages/{Account => Subscription}/.gitignore (72%) rename LocalPackages/{Account => Subscription}/Package.swift (68%) rename LocalPackages/{Account/Sources/Account => Subscription/Sources/Subscription}/AccountManager.swift (100%) rename LocalPackages/{Account/Sources/Account => Subscription/Sources/Subscription}/AccountStorage/AccountKeychainStorage.swift (100%) rename LocalPackages/{Account/Sources/Account => Subscription/Sources/Subscription}/AccountStorage/AccountStorage.swift (100%) rename LocalPackages/{Account/Sources/Account => Subscription/Sources/Subscription}/Logging.swift (100%) rename LocalPackages/{Account/Sources/Account => Subscription/Sources/Subscription}/PurchaseManager.swift (100%) rename LocalPackages/{Account/Sources/Account => Subscription/Sources/Subscription}/Services/APIService.swift (100%) rename LocalPackages/{Account/Sources/Account => Subscription/Sources/Subscription}/Services/AuthService.swift (100%) rename LocalPackages/{Account/Sources/Account => Subscription/Sources/Subscription}/Services/SubscriptionService.swift (100%) create mode 100644 LocalPackages/Subscription/Tests/SubscriptionTests/SubscriptionTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 9cbc29984f..9f0eefe335 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -90,6 +90,7 @@ 1E7E2E9029029A2A00C01B54 /* ContentBlockingRulesUpdateObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7E2E8F29029A2A00C01B54 /* ContentBlockingRulesUpdateObserver.swift */; }; 1E7E2E9229029F9B00C01B54 /* WebsiteBreakageReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7E2E9129029F9B00C01B54 /* WebsiteBreakageReporter.swift */; }; 1E7E2E942902AC0E00C01B54 /* PrivacyDashboardPermissionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7E2E932902AC0E00C01B54 /* PrivacyDashboardPermissionHandler.swift */; }; + 1E934E2B2B167CA80084722B /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = 1E934E2A2B167CA80084722B /* Subscription */; }; 1E950E3F2912A10D0051A99B /* ContentBlocking in Frameworks */ = {isa = PBXBuildFile; productRef = 1E950E3E2912A10D0051A99B /* ContentBlocking */; }; 1E950E412912A10D0051A99B /* PrivacyDashboard in Frameworks */ = {isa = PBXBuildFile; productRef = 1E950E402912A10D0051A99B /* PrivacyDashboard */; }; 1E950E432912A10D0051A99B /* UserScript in Frameworks */ = {isa = PBXBuildFile; productRef = 1E950E422912A10D0051A99B /* UserScript */; }; @@ -2605,7 +2606,6 @@ 4B957BD62AC7AE700062CA31 /* LoginItems in Frameworks */ = {isa = PBXBuildFile; productRef = 4B95793F2AC7AE700062CA31 /* LoginItems */; }; 4B957BD72AC7AE700062CA31 /* NetworkProtection in Frameworks */ = {isa = PBXBuildFile; productRef = 4B95793E2AC7AE700062CA31 /* NetworkProtection */; }; 4B957BD82AC7AE700062CA31 /* BrowserServicesKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4B95792B2AC7AE700062CA31 /* BrowserServicesKit */; }; - 4B957BD92AC7AE700062CA31 /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 4B9579412AC7AE700062CA31 /* Account */; }; 4B957BDA2AC7AE700062CA31 /* Bookmarks in Frameworks */ = {isa = PBXBuildFile; productRef = 4B9579342AC7AE700062CA31 /* Bookmarks */; }; 4B957BDB2AC7AE700062CA31 /* ContentBlocking in Frameworks */ = {isa = PBXBuildFile; productRef = 4B95792E2AC7AE700062CA31 /* ContentBlocking */; }; 4B957BDC2AC7AE700062CA31 /* SwiftUIExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = 4B9579372AC7AE700062CA31 /* SwiftUIExtensions */; }; @@ -3836,8 +3836,8 @@ 1E7E2E8F29029A2A00C01B54 /* ContentBlockingRulesUpdateObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentBlockingRulesUpdateObserver.swift; sourceTree = ""; }; 1E7E2E9129029F9B00C01B54 /* WebsiteBreakageReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteBreakageReporter.swift; sourceTree = ""; }; 1E7E2E932902AC0E00C01B54 /* PrivacyDashboardPermissionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardPermissionHandler.swift; sourceTree = ""; }; - 1E862A852A9FBD7000F84D4B /* Account */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Account; sourceTree = ""; }; 1E862A882A9FC01200F84D4B /* SubscriptionUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = SubscriptionUI; sourceTree = ""; }; + 1ECCFCCF2B16837200E81916 /* Subscription */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Subscription; sourceTree = ""; }; 3106AD75287F000600159FE5 /* CookieConsentUserPermissionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieConsentUserPermissionViewController.swift; sourceTree = ""; }; 310E79BE294A19A8007C49E8 /* FireproofingReferenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FireproofingReferenceTests.swift; sourceTree = ""; }; 311B262628E73E0A00FD181A /* TabShadowConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabShadowConfig.swift; sourceTree = ""; }; @@ -5020,10 +5020,10 @@ buildActionMask = 2147483647; files = ( 4B957BD52AC7AE700062CA31 /* QuickLookUI.framework in Frameworks */, + 1E934E2B2B167CA80084722B /* Subscription in Frameworks */, 4B957BD62AC7AE700062CA31 /* LoginItems in Frameworks */, 4B957BD72AC7AE700062CA31 /* NetworkProtection in Frameworks */, 4B957BD82AC7AE700062CA31 /* BrowserServicesKit in Frameworks */, - 4B957BD92AC7AE700062CA31 /* Account in Frameworks */, 4B957BDA2AC7AE700062CA31 /* Bookmarks in Frameworks */, 4B957BDB2AC7AE700062CA31 /* ContentBlocking in Frameworks */, 4B957BDC2AC7AE700062CA31 /* SwiftUIExtensions in Frameworks */, @@ -5455,7 +5455,7 @@ 378E279C2970217400FCADA2 /* LocalPackages */ = { isa = PBXGroup; children = ( - 1E862A852A9FBD7000F84D4B /* Account */, + 1ECCFCCF2B16837200E81916 /* Subscription */, 378E279D2970217400FCADA2 /* BuildToolPlugins */, 3192A2702A4C4E330084EA89 /* DataBrokerProtection */, 9DB6E7222AA0DA7A00A17F3C /* LoginItems */, @@ -8809,9 +8809,9 @@ 4B95793D2AC7AE700062CA31 /* NetworkProtectionUI */, 4B95793E2AC7AE700062CA31 /* NetworkProtection */, 4B95793F2AC7AE700062CA31 /* LoginItems */, - 4B9579412AC7AE700062CA31 /* Account */, 7BF770642AD6CA14001C9182 /* PixelKit */, 1E0068AC2B1673BB00BBF43B /* SubscriptionUI */, + 1E934E2A2B167CA80084722B /* Subscription */, ); productName = DuckDuckGo; productReference = 4B957C412AC7AE700062CA31 /* DuckDuckGo Privacy Pro.app */; @@ -14239,6 +14239,10 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = Common; }; + 1E934E2A2B167CA80084722B /* Subscription */ = { + isa = XCSwiftPackageProductDependency; + productName = Subscription; + }; 1E950E3E2912A10D0051A99B /* ContentBlocking */ = { isa = XCSwiftPackageProductDependency; package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; @@ -14593,10 +14597,6 @@ isa = XCSwiftPackageProductDependency; productName = LoginItems; }; - 4B9579412AC7AE700062CA31 /* Account */ = { - isa = XCSwiftPackageProductDependency; - productName = Account; - }; 7BF7705E2AD6C999001C9182 /* PixelKit */ = { isa = XCSwiftPackageProductDependency; productName = PixelKit; diff --git a/DuckDuckGo/AppDelegate/AppDelegate.swift b/DuckDuckGo/AppDelegate/AppDelegate.swift index 3e7a379c6b..7b94ddf26b 100644 --- a/DuckDuckGo/AppDelegate/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate/AppDelegate.swift @@ -34,7 +34,7 @@ import NetworkProtection #endif #if SUBSCRIPTION -import Account +import Subscription #endif @MainActor diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index bbea90d0a1..173403a0c8 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -26,7 +26,7 @@ import NetworkProtection #endif #if SUBSCRIPTION -import Account +import Subscription #endif protocol OptionsButtonMenuDelegate: AnyObject { diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index c54062a094..cab66ffbfd 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -21,7 +21,7 @@ import SwiftUIExtensions import SyncUI #if SUBSCRIPTION -import Account +import Subscription import SubscriptionUI #endif diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 408b1854ba..81bb45232c 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -25,7 +25,7 @@ import Foundation import Navigation import WebKit import UserScript -import Account +import Subscription import SubscriptionUI public extension Notification.Name { diff --git a/LocalPackages/Account/Tests/AccountTests/AccountsTests.swift b/LocalPackages/Account/Tests/AccountTests/AccountsTests.swift deleted file mode 100644 index 09f797525f..0000000000 --- a/LocalPackages/Account/Tests/AccountTests/AccountsTests.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// AccountTests.swift -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import XCTest -@testable import Account - -final class AccountTests: XCTestCase { - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. - XCTAssertEqual(Account().text, "Hello, World!") - } -} diff --git a/LocalPackages/Account/.gitignore b/LocalPackages/Subscription/.gitignore similarity index 72% rename from LocalPackages/Account/.gitignore rename to LocalPackages/Subscription/.gitignore index 3b29812086..0023a53406 100644 --- a/LocalPackages/Account/.gitignore +++ b/LocalPackages/Subscription/.gitignore @@ -1,9 +1,8 @@ .DS_Store /.build /Packages -/*.xcodeproj xcuserdata/ DerivedData/ -.swiftpm/config/registries.json +.swiftpm/configuration/registries.json .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata .netrc diff --git a/LocalPackages/Account/Package.swift b/LocalPackages/Subscription/Package.swift similarity index 68% rename from LocalPackages/Account/Package.swift rename to LocalPackages/Subscription/Package.swift index 3da49fc4d3..79d0515484 100644 --- a/LocalPackages/Account/Package.swift +++ b/LocalPackages/Subscription/Package.swift @@ -1,27 +1,27 @@ -// swift-tools-version: 5.8 +// swift-tools-version: 5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( - name: "Account", + name: "Subscription", platforms: [ .macOS(.v11) ], products: [ .library( - name: "Account", - targets: ["Account"]), + name: "Subscription", + targets: ["Subscription"]), ], dependencies: [ .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "81.4.0"), ], targets: [ .target( - name: "Account", + name: "Subscription", dependencies: [ .product(name: "BrowserServicesKit", package: "BrowserServicesKit"), ]), .testTarget( - name: "AccountTests", - dependencies: ["Account"]), + name: "SubscriptionTests", + dependencies: ["Subscription"]), ] ) diff --git a/LocalPackages/Account/Sources/Account/AccountManager.swift b/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift similarity index 100% rename from LocalPackages/Account/Sources/Account/AccountManager.swift rename to LocalPackages/Subscription/Sources/Subscription/AccountManager.swift diff --git a/LocalPackages/Account/Sources/Account/AccountStorage/AccountKeychainStorage.swift b/LocalPackages/Subscription/Sources/Subscription/AccountStorage/AccountKeychainStorage.swift similarity index 100% rename from LocalPackages/Account/Sources/Account/AccountStorage/AccountKeychainStorage.swift rename to LocalPackages/Subscription/Sources/Subscription/AccountStorage/AccountKeychainStorage.swift diff --git a/LocalPackages/Account/Sources/Account/AccountStorage/AccountStorage.swift b/LocalPackages/Subscription/Sources/Subscription/AccountStorage/AccountStorage.swift similarity index 100% rename from LocalPackages/Account/Sources/Account/AccountStorage/AccountStorage.swift rename to LocalPackages/Subscription/Sources/Subscription/AccountStorage/AccountStorage.swift diff --git a/LocalPackages/Account/Sources/Account/Logging.swift b/LocalPackages/Subscription/Sources/Subscription/Logging.swift similarity index 100% rename from LocalPackages/Account/Sources/Account/Logging.swift rename to LocalPackages/Subscription/Sources/Subscription/Logging.swift diff --git a/LocalPackages/Account/Sources/Account/PurchaseManager.swift b/LocalPackages/Subscription/Sources/Subscription/PurchaseManager.swift similarity index 100% rename from LocalPackages/Account/Sources/Account/PurchaseManager.swift rename to LocalPackages/Subscription/Sources/Subscription/PurchaseManager.swift diff --git a/LocalPackages/Account/Sources/Account/Services/APIService.swift b/LocalPackages/Subscription/Sources/Subscription/Services/APIService.swift similarity index 100% rename from LocalPackages/Account/Sources/Account/Services/APIService.swift rename to LocalPackages/Subscription/Sources/Subscription/Services/APIService.swift diff --git a/LocalPackages/Account/Sources/Account/Services/AuthService.swift b/LocalPackages/Subscription/Sources/Subscription/Services/AuthService.swift similarity index 100% rename from LocalPackages/Account/Sources/Account/Services/AuthService.swift rename to LocalPackages/Subscription/Sources/Subscription/Services/AuthService.swift diff --git a/LocalPackages/Account/Sources/Account/Services/SubscriptionService.swift b/LocalPackages/Subscription/Sources/Subscription/Services/SubscriptionService.swift similarity index 100% rename from LocalPackages/Account/Sources/Account/Services/SubscriptionService.swift rename to LocalPackages/Subscription/Sources/Subscription/Services/SubscriptionService.swift diff --git a/LocalPackages/Subscription/Tests/SubscriptionTests/SubscriptionTests.swift b/LocalPackages/Subscription/Tests/SubscriptionTests/SubscriptionTests.swift new file mode 100644 index 0000000000..32417fc73a --- /dev/null +++ b/LocalPackages/Subscription/Tests/SubscriptionTests/SubscriptionTests.swift @@ -0,0 +1,12 @@ +import XCTest +@testable import Subscription + +final class SubscriptionTests: XCTestCase { + func testExample() throws { + // XCTest Documentation + // https://developer.apple.com/documentation/xctest + + // Defining Test Cases and Test Methods + // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods + } +} diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 8571181475..ea045d373c 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,14 +12,14 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(path: "../Account"), + .package(path: "../Subscription"), .package(path: "../SwiftUIExtensions") ], targets: [ .target( name: "SubscriptionUI", dependencies: [ - .product(name: "Account", package: "Account"), + .product(name: "Subscription", package: "Subscription"), .product(name: "SwiftUIExtensions", package: "SwiftUIExtensions") ], resources: [ diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseModel.swift index b23195b5a0..273a043618 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseModel.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseModel.swift @@ -18,7 +18,7 @@ import Foundation import StoreKit -import Account +import Subscription @available(macOS 12.0, *) public final class DebugPurchaseModel: ObservableObject { diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseViewController.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseViewController.swift index bfc5af3d9f..0d9c34bcbb 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseViewController.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseViewController.swift @@ -20,7 +20,7 @@ import AppKit import SwiftUI import Combine import StoreKit -import Account +import Subscription @available(macOS 12.0, *) public final class DebugPurchaseViewController: NSViewController { diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift index ce6029faa2..886b15d9e1 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift @@ -17,7 +17,7 @@ // import AppKit -import Account +import Subscription public final class SubscriptionDebugMenu: NSMenuItem { diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift index 6d860ff757..41490d77b7 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift @@ -17,7 +17,7 @@ // import Foundation -import Account +import Subscription public final class PreferencesSubscriptionModel: ObservableObject { diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows/AppStoreAccountManagementFlow.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows/AppStoreAccountManagementFlow.swift index 4eee62b914..90f4adf0f8 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows/AppStoreAccountManagementFlow.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows/AppStoreAccountManagementFlow.swift @@ -18,7 +18,7 @@ import Foundation import StoreKit -import Account +import Subscription public final class AppStoreAccountManagementFlow { diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows/AppStorePurchaseFlow.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows/AppStorePurchaseFlow.swift index 6fda6ec4ce..016f1fd5a4 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows/AppStorePurchaseFlow.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows/AppStorePurchaseFlow.swift @@ -18,7 +18,7 @@ import Foundation import StoreKit -import Account +import Subscription @available(macOS 12.0, *) public final class AppStorePurchaseFlow { diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows/AppStoreRestoreFlow.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows/AppStoreRestoreFlow.swift index d9c61ea02b..5a005cd847 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows/AppStoreRestoreFlow.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows/AppStoreRestoreFlow.swift @@ -18,7 +18,7 @@ import Foundation import StoreKit -import Account +import Subscription @available(macOS 12.0, *) public final class AppStoreRestoreFlow { diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows/StripePurchaseFlow.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows/StripePurchaseFlow.swift index b82123c7c5..2337ddb883 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows/StripePurchaseFlow.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows/StripePurchaseFlow.swift @@ -18,7 +18,7 @@ import Foundation import StoreKit -import Account +import Subscription public final class StripePurchaseFlow { diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/SubscriptionAccessViewController.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/SubscriptionAccessViewController.swift index 4b96a8764e..103f9c927e 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/SubscriptionAccessViewController.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/SubscriptionAccessViewController.swift @@ -17,7 +17,7 @@ // import AppKit -import Account +import Subscription import SwiftUI public final class SubscriptionAccessViewController: NSViewController { From aaad27d2933d1acd1b4292913ed084e06dc424aa Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Tue, 28 Nov 2023 23:55:43 +0100 Subject: [PATCH 51/96] Enable Stripe flow --- Configuration/App/DuckDuckGoPrivacyPro.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/App/DuckDuckGoPrivacyPro.xcconfig b/Configuration/App/DuckDuckGoPrivacyPro.xcconfig index e49e19d954..291c9d31c9 100644 --- a/Configuration/App/DuckDuckGoPrivacyPro.xcconfig +++ b/Configuration/App/DuckDuckGoPrivacyPro.xcconfig @@ -21,5 +21,5 @@ #include "DuckDuckGo.xcconfig" -FEATURE_FLAGS = FEEDBACK SPARKLE SUBSCRIPTION NOSTRIPE +FEATURE_FLAGS = FEEDBACK SPARKLE SUBSCRIPTION STRIPE PRODUCT_NAME = $(PRODUCT_NAME_PREFIX) Privacy Pro From 57d7a948a7274ca256162623b7560e086a184368 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Thu, 7 Dec 2023 12:44:02 +0100 Subject: [PATCH 52/96] Change the subscription build config to Review --- scripts/archive.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/archive.sh b/scripts/archive.sh index fe16f89283..03f91a70ab 100755 --- a/scripts/archive.sh +++ b/scripts/archive.sh @@ -67,7 +67,7 @@ read_command_line_arguments() { subscription) app_name="DuckDuckGo Privacy Pro" scheme="DuckDuckGo Privacy Pro" - configuration="Release" + configuration="Review" ;; clear-keychain) clear_keychain From 05e82d2966cec67fd56d9925739d4996ff0708ae Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Thu, 7 Dec 2023 16:31:02 +0100 Subject: [PATCH 53/96] Move files around --- Configuration/App/DuckDuckGoPrivacyPro.xcconfig | 2 +- DuckDuckGo.xcodeproj/project.pbxproj | 4 ++-- .../Subscription/Flows}/AppStoreAccountManagementFlow.swift | 0 .../Sources/Subscription/Flows}/AppStorePurchaseFlow.swift | 1 - .../Sources/Subscription/Flows}/AppStoreRestoreFlow.swift | 0 .../Sources/Subscription/Flows}/PurchaseFlow.swift | 0 .../Sources/Subscription/Flows}/StripePurchaseFlow.swift | 0 .../Sources/Subscription}/URL+Subscription.swift | 0 .../Model/ShareSubscriptionAccessModel.swift | 1 + 9 files changed, 4 insertions(+), 4 deletions(-) rename LocalPackages/{SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows => Subscription/Sources/Subscription/Flows}/AppStoreAccountManagementFlow.swift (100%) rename LocalPackages/{SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows => Subscription/Sources/Subscription/Flows}/AppStorePurchaseFlow.swift (99%) rename LocalPackages/{SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows => Subscription/Sources/Subscription/Flows}/AppStoreRestoreFlow.swift (100%) rename LocalPackages/{SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows => Subscription/Sources/Subscription/Flows}/PurchaseFlow.swift (100%) rename LocalPackages/{SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows => Subscription/Sources/Subscription/Flows}/StripePurchaseFlow.swift (100%) rename LocalPackages/{SubscriptionUI/Sources/SubscriptionUI => Subscription/Sources/Subscription}/URL+Subscription.swift (100%) diff --git a/Configuration/App/DuckDuckGoPrivacyPro.xcconfig b/Configuration/App/DuckDuckGoPrivacyPro.xcconfig index 291c9d31c9..e49e19d954 100644 --- a/Configuration/App/DuckDuckGoPrivacyPro.xcconfig +++ b/Configuration/App/DuckDuckGoPrivacyPro.xcconfig @@ -21,5 +21,5 @@ #include "DuckDuckGo.xcconfig" -FEATURE_FLAGS = FEEDBACK SPARKLE SUBSCRIPTION STRIPE +FEATURE_FLAGS = FEEDBACK SPARKLE SUBSCRIPTION NOSTRIPE PRODUCT_NAME = $(PRODUCT_NAME_PREFIX) Privacy Pro diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 9f0eefe335..f46945bfd0 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -3837,7 +3837,7 @@ 1E7E2E9129029F9B00C01B54 /* WebsiteBreakageReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteBreakageReporter.swift; sourceTree = ""; }; 1E7E2E932902AC0E00C01B54 /* PrivacyDashboardPermissionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardPermissionHandler.swift; sourceTree = ""; }; 1E862A882A9FC01200F84D4B /* SubscriptionUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = SubscriptionUI; sourceTree = ""; }; - 1ECCFCCF2B16837200E81916 /* Subscription */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Subscription; sourceTree = ""; }; + 1E8F997E2B221B3600AC5D34 /* Subscription */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Subscription; sourceTree = ""; }; 3106AD75287F000600159FE5 /* CookieConsentUserPermissionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieConsentUserPermissionViewController.swift; sourceTree = ""; }; 310E79BE294A19A8007C49E8 /* FireproofingReferenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FireproofingReferenceTests.swift; sourceTree = ""; }; 311B262628E73E0A00FD181A /* TabShadowConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabShadowConfig.swift; sourceTree = ""; }; @@ -5455,7 +5455,6 @@ 378E279C2970217400FCADA2 /* LocalPackages */ = { isa = PBXGroup; children = ( - 1ECCFCCF2B16837200E81916 /* Subscription */, 378E279D2970217400FCADA2 /* BuildToolPlugins */, 3192A2702A4C4E330084EA89 /* DataBrokerProtection */, 9DB6E7222AA0DA7A00A17F3C /* LoginItems */, @@ -5463,6 +5462,7 @@ 4BE15DB12A0B0DD500898243 /* PixelKit */, 378F44E229B4B7B600899924 /* SwiftUIExtensions */, 37BA812B29B3CB8A0053F1A3 /* SyncUI */, + 1E8F997E2B221B3600AC5D34 /* Subscription */, 1E862A882A9FC01200F84D4B /* SubscriptionUI */, 7BC185EA2AD6CB4900F9D9DC /* XPC */, ); diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows/AppStoreAccountManagementFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStoreAccountManagementFlow.swift similarity index 100% rename from LocalPackages/SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows/AppStoreAccountManagementFlow.swift rename to LocalPackages/Subscription/Sources/Subscription/Flows/AppStoreAccountManagementFlow.swift diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows/AppStorePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStorePurchaseFlow.swift similarity index 99% rename from LocalPackages/SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows/AppStorePurchaseFlow.swift rename to LocalPackages/Subscription/Sources/Subscription/Flows/AppStorePurchaseFlow.swift index 016f1fd5a4..a7983a1d71 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows/AppStorePurchaseFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStorePurchaseFlow.swift @@ -18,7 +18,6 @@ import Foundation import StoreKit -import Subscription @available(macOS 12.0, *) public final class AppStorePurchaseFlow { diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows/AppStoreRestoreFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStoreRestoreFlow.swift similarity index 100% rename from LocalPackages/SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows/AppStoreRestoreFlow.swift rename to LocalPackages/Subscription/Sources/Subscription/Flows/AppStoreRestoreFlow.swift diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows/PurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/PurchaseFlow.swift similarity index 100% rename from LocalPackages/SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows/PurchaseFlow.swift rename to LocalPackages/Subscription/Sources/Subscription/Flows/PurchaseFlow.swift diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows/StripePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/StripePurchaseFlow.swift similarity index 100% rename from LocalPackages/SubscriptionUI/Sources/SubscriptionUI/PurchaseFlows/StripePurchaseFlow.swift rename to LocalPackages/Subscription/Sources/Subscription/Flows/StripePurchaseFlow.swift diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/URL+Subscription.swift b/LocalPackages/Subscription/Sources/Subscription/URL+Subscription.swift similarity index 100% rename from LocalPackages/SubscriptionUI/Sources/SubscriptionUI/URL+Subscription.swift rename to LocalPackages/Subscription/Sources/Subscription/URL+Subscription.swift diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ShareSubscriptionAccessModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ShareSubscriptionAccessModel.swift index 8ece29fe02..f3623e722f 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ShareSubscriptionAccessModel.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ShareSubscriptionAccessModel.swift @@ -17,6 +17,7 @@ // import Foundation +import Subscription public final class ShareSubscriptionAccessModel: SubscriptionAccessModel { public var title = UserText.shareModalTitle From 423c89819e10aa9c41ac4eb6165e15864e3fae54 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Thu, 7 Dec 2023 19:32:21 +0100 Subject: [PATCH 54/96] Clean up --- DuckDuckGo/Preferences/View/PreferencesRootView.swift | 4 ++-- .../Tab/UserScripts/SubscriptionPagesUserScript.swift | 4 ++-- .../Subscription/Flows/AppStoreAccountManagementFlow.swift | 1 - .../Sources/Subscription/Flows/AppStorePurchaseFlow.swift | 6 +++--- .../Sources/Subscription/Flows/AppStoreRestoreFlow.swift | 5 ++--- .../Sources/Subscription/Flows/StripePurchaseFlow.swift | 1 - 6 files changed, 9 insertions(+), 12 deletions(-) diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index cab66ffbfd..f7f1c730a8 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -109,8 +109,8 @@ extension Preferences { guard case .success = await PurchaseManager.shared.syncAppleIDAccount() else { return } switch await AppStoreRestoreFlow.restoreAccountFromPastPurchase() { - case .success(let success): - if !success.isActive { + case .success(let subscription): + if !subscription.isActive { AccountManager().signOut() self.showSubscriptionInactiveAlert() } diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 81bb45232c..38dc6f5f3a 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -281,8 +281,8 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { guard case .success = await PurchaseManager.shared.syncAppleIDAccount() else { return } switch await AppStoreRestoreFlow.restoreAccountFromPastPurchase() { - case .success(let success): - if !success.isActive { + case .success(let subscription): + if !subscription.isActive { self.showSubscriptionInactiveAlert() } case .failure: diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStoreAccountManagementFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStoreAccountManagementFlow.swift index 90f4adf0f8..5d42ea7130 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStoreAccountManagementFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStoreAccountManagementFlow.swift @@ -18,7 +18,6 @@ import Foundation import StoreKit -import Subscription public final class AppStoreAccountManagementFlow { diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStorePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStorePurchaseFlow.swift index a7983a1d71..bb86443182 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStorePurchaseFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStorePurchaseFlow.swift @@ -59,9 +59,9 @@ public final class AppStorePurchaseFlow { // Check for past transactions most recent switch await AppStoreRestoreFlow.restoreAccountFromPastPurchase() { - case .success(let success): - guard !success.isActive else { return .failure(.activeSubscriptionAlreadyPresent)} - externalID = success.externalID + case .success(let subscription): + guard !subscription.isActive else { return .failure(.activeSubscriptionAlreadyPresent)} + externalID = subscription.externalID case .failure(let error): switch error { case .missingAccountOrTransactions: diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStoreRestoreFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStoreRestoreFlow.swift index 5a005cd847..35a6854147 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStoreRestoreFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStoreRestoreFlow.swift @@ -18,12 +18,11 @@ import Foundation import StoreKit -import Subscription @available(macOS 12.0, *) public final class AppStoreRestoreFlow { - public typealias Success = (externalID: String, isActive: Bool) + public typealias Subscription = (externalID: String, isActive: Bool) public enum Error: Swift.Error { case missingAccountOrTransactions @@ -33,7 +32,7 @@ public final class AppStoreRestoreFlow { case somethingWentWrong } - public static func restoreAccountFromPastPurchase() async -> Result { + public static func restoreAccountFromPastPurchase() async -> Result { guard let lastTransactionJWSRepresentation = await PurchaseManager.mostRecentTransaction() else { return .failure(.missingAccountOrTransactions) } // Do the store login to get short-lived token diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/StripePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/StripePurchaseFlow.swift index 2337ddb883..4a95963131 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/StripePurchaseFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Flows/StripePurchaseFlow.swift @@ -18,7 +18,6 @@ import Foundation import StoreKit -import Subscription public final class StripePurchaseFlow { From 3b9f696cbbb8be90b680ec46bf4117c5a6940764 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 8 Dec 2023 10:50:15 +0100 Subject: [PATCH 55/96] Improve separation of concerns in the API --- .../View/PreferencesRootView.swift | 19 ++++--- .../SubscriptionPagesUserScript.swift | 26 ++++++++-- .../Sources/Subscription/AccountManager.swift | 33 +++--------- .../AppStoreAccountManagementFlow.swift | 0 .../{ => AppStore}/AppStorePurchaseFlow.swift | 13 +++-- .../{ => AppStore}/AppStoreRestoreFlow.swift | 51 ++++++++++++------- .../{ => Stripe}/StripePurchaseFlow.swift | 7 ++- .../Services/SubscriptionService.swift | 4 +- .../DebugMenu/SubscriptionDebugMenu.swift | 6 +-- .../PreferencesSubscriptionModel.swift | 2 +- 10 files changed, 97 insertions(+), 64 deletions(-) rename LocalPackages/Subscription/Sources/Subscription/Flows/{ => AppStore}/AppStoreAccountManagementFlow.swift (100%) rename LocalPackages/Subscription/Sources/Subscription/Flows/{ => AppStore}/AppStorePurchaseFlow.swift (86%) rename LocalPackages/Subscription/Sources/Subscription/Flows/{ => AppStore}/AppStoreRestoreFlow.swift (50%) rename LocalPackages/Subscription/Sources/Subscription/Flows/{ => Stripe}/StripePurchaseFlow.swift (87%) diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index f7f1c730a8..640028c349 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -109,17 +109,16 @@ extension Preferences { guard case .success = await PurchaseManager.shared.syncAppleIDAccount() else { return } switch await AppStoreRestoreFlow.restoreAccountFromPastPurchase() { - case .success(let subscription): - if !subscription.isActive { - AccountManager().signOut() - self.showSubscriptionInactiveAlert() - } + case .success: + break case .failure(let error): switch error { case .missingAccountOrTransactions: self.showSubscriptionNotFoundAlert() + case .subscriptionExpired: + self.showSubscriptionInactiveAlert() default: - break + self.showSomethingWentWrongAlert() } } } @@ -134,6 +133,14 @@ extension Preferences { return SubscriptionUI.PreferencesSubscriptionView(model: model) } + @MainActor + private func showSomethingWentWrongAlert() { + guard let window = WindowControllersManager.shared.lastKeyMainWindowController?.window else { return } + + let alert = NSAlert.somethingWentWrongAlert() + alert.beginSheetModal(for: window) + } + @MainActor private func showSubscriptionNotFoundAlert() { guard let window = WindowControllersManager.shared.lastKeyMainWindowController?.window else { return } diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 38dc6f5f3a..4c63fb95a1 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -129,12 +129,23 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { return nil } - await AccountManager().exchangeAndStoreTokens(with: subscriptionValues.token) + let authToken = subscriptionValues.token + let accountManager = AccountManager() + if case let .success(accessToken) = await accountManager.exchangeAuthTokenToAccessToken(authToken), + case let .success(accountDetails) = await accountManager.fetchAccountDetails(with: accessToken) { + accountManager.storeAuthToken(token: authToken) + accountManager.storeAccount(token: accessToken, email: accountDetails.email, externalID: accountDetails.externalID) + } + return nil } func backToSettings(params: Any, original: WKScriptMessage) async throws -> Encodable? { - await AccountManager().refreshAccountData() + let accountManager = AccountManager() + if let accessToken = accountManager.accessToken, + case let .success(accountDetails) = await accountManager.fetchAccountDetails(with: accessToken) { + accountManager.storeAccount(token: accessToken, email: accountDetails.email, externalID: accountDetails.externalID) + } DispatchQueue.main.async { NotificationCenter.default.post(name: .subscriptionPageCloseAndOpenPreferences, object: self) @@ -282,11 +293,16 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { switch await AppStoreRestoreFlow.restoreAccountFromPastPurchase() { case .success(let subscription): - if !subscription.isActive { + break + case .failure(let error): + switch error { + case .missingAccountOrTransactions: + self.showSubscriptionNotFoundAlert() + case .subscriptionExpired: self.showSubscriptionInactiveAlert() + default: + self.showSomethingWentWrongAlert() } - case .failure: - self.showSubscriptionNotFoundAlert() } message.webView?.reload() diff --git a/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift b/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift index fb8261ca46..63f5af6e61 100644 --- a/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift +++ b/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift @@ -176,44 +176,25 @@ public class AccountManager { } } - @discardableResult - public func exchangeAndStoreTokens(with authToken: String) async -> Result { - // Exchange short-lived auth token to a long-lived access token - let accessToken: String + public func exchangeAuthTokenToAccessToken(_ authToken: String) async -> Result { switch await AuthService.getAccessToken(token: authToken) { case .success(let response): - accessToken = response.accessToken + return .success(response.accessToken) case .failure(let error): os_log("AccountManager error: %{public}@", log: .error, error.localizedDescription) return .failure(error) } + } + + public typealias AccountDetails = (email: String?, externalID: String) - // Fetch entitlements and account details and store the data + public func fetchAccountDetails(with accessToken: String) async -> Result { switch await AuthService.validateToken(accessToken: accessToken) { case .success(let response): - self.storeAuthToken(token: authToken) - self.storeAccount(token: accessToken, - email: response.account.email, - externalID: response.account.externalID) - - return .success(response.account.externalID) - + return .success(AccountDetails(email: response.account.email, externalID: response.account.externalID)) case .failure(let error): os_log("AccountManager error: %{public}@", log: .error, error.localizedDescription) return .failure(error) } } - - public func refreshAccountData() async { - guard let accessToken else { return } - - switch await AuthService.validateToken(accessToken: accessToken) { - case .success(let response): - self.storeAccount(token: accessToken, - email: response.account.email, - externalID: response.account.externalID) - case .failure: - break - } - } } diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStoreAccountManagementFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreAccountManagementFlow.swift similarity index 100% rename from LocalPackages/Subscription/Sources/Subscription/Flows/AppStoreAccountManagementFlow.swift rename to LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreAccountManagementFlow.swift diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStorePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift similarity index 86% rename from LocalPackages/Subscription/Sources/Subscription/Flows/AppStorePurchaseFlow.swift rename to LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift index bb86443182..934ac70360 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStorePurchaseFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift @@ -55,21 +55,28 @@ public final class AppStorePurchaseFlow { } public static func purchaseSubscription(with subscriptionIdentifier: String, emailAccessToken: String?) async -> Result { + let accountManager = AccountManager() let externalID: String // Check for past transactions most recent switch await AppStoreRestoreFlow.restoreAccountFromPastPurchase() { case .success(let subscription): - guard !subscription.isActive else { return .failure(.activeSubscriptionAlreadyPresent)} - externalID = subscription.externalID + return .failure(.activeSubscriptionAlreadyPresent) case .failure(let error): switch error { + case .subscriptionExpired(let expiredExternalID): + externalID = expiredExternalID case .missingAccountOrTransactions: // No history, create new account switch await AuthService.createAccount(emailAccessToken: emailAccessToken) { case .success(let response): externalID = response.externalID - await AccountManager().exchangeAndStoreTokens(with: response.authToken) + + if case let .success(accessToken) = await accountManager.exchangeAuthTokenToAccessToken(response.authToken), + case let .success(accountDetails) = await accountManager.fetchAccountDetails(with: accessToken) { + accountManager.storeAuthToken(token: response.authToken) + accountManager.storeAccount(token: accessToken, email: accountDetails.email, externalID: accountDetails.externalID) + } case .failure: return .failure(.accountCreationFailed) } diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStoreRestoreFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift similarity index 50% rename from LocalPackages/Subscription/Sources/Subscription/Flows/AppStoreRestoreFlow.swift rename to LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift index 35a6854147..a8ef80a734 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStoreRestoreFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift @@ -22,18 +22,20 @@ import StoreKit @available(macOS 12.0, *) public final class AppStoreRestoreFlow { - public typealias Subscription = (externalID: String, isActive: Bool) - public enum Error: Swift.Error { case missingAccountOrTransactions - case pastTransactionAuthenticationFailure - case accessTokenObtainingError -// case subscriptionExpired + case pastTransactionAuthenticationError + case failedToObtainAccessToken + case failedToFetchAccountDetails + case failedToFetchSubscriptionDetails + case subscriptionExpired(externalID: String) case somethingWentWrong } - public static func restoreAccountFromPastPurchase() async -> Result { + public static func restoreAccountFromPastPurchase() async -> Result { guard let lastTransactionJWSRepresentation = await PurchaseManager.mostRecentTransaction() else { return .failure(.missingAccountOrTransactions) } + + let accountManager = AccountManager() // Do the store login to get short-lived token let authToken: String @@ -42,29 +44,44 @@ public final class AppStoreRestoreFlow { case .success(let response): authToken = response.authToken case .failure: - return .failure(.pastTransactionAuthenticationFailure) + return .failure(.pastTransactionAuthenticationError) } + let accessToken: String + let email: String? let externalID: String - switch await AccountManager().exchangeAndStoreTokens(with: authToken) { - case .success(let existingExternalID): - externalID = existingExternalID + switch await accountManager.exchangeAuthTokenToAccessToken(authToken) { + case .success(let exchangedAccessToken): + accessToken = exchangedAccessToken case .failure: - return .failure(.accessTokenObtainingError) + return .failure(.failedToObtainAccessToken) } - let accessToken = AccountManager().accessToken ?? "" - var isActive = false + switch await accountManager.fetchAccountDetails(with: accessToken) { + case .success(let accountDetails): + email = accountDetails.email + externalID = accountDetails.externalID + case .failure: + return .failure(.failedToFetchAccountDetails) + } - switch await SubscriptionService.getSubscriptionInfo(token: accessToken) { + // + var isSubscriptionActive = false + + switch await SubscriptionService.getSubscriptionDetails(token: accessToken) { case .success(let response): - isActive = response.status != "Expired" && response.status != "Inactive" + isSubscriptionActive = response.status != "Expired" && response.status != "Inactive" case .failure: return .failure(.somethingWentWrong) } - // TOOD: Fix this by probably splitting/changing result of exchangeAndStoreTokens - return .success((externalID: externalID, isActive: isActive)) + if isSubscriptionActive { + accountManager.storeAuthToken(token: authToken) + accountManager.storeAccount(token: accessToken, email: email, externalID: externalID) + return .success(()) + } else { + return .failure(.subscriptionExpired(externalID: externalID)) + } } } diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/StripePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/Stripe/StripePurchaseFlow.swift similarity index 87% rename from LocalPackages/Subscription/Sources/Subscription/Flows/StripePurchaseFlow.swift rename to LocalPackages/Subscription/Sources/Subscription/Flows/Stripe/StripePurchaseFlow.swift index 4a95963131..b7e9b0c195 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/StripePurchaseFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Flows/Stripe/StripePurchaseFlow.swift @@ -76,7 +76,12 @@ public final class StripePurchaseFlow { if let authToken = accountManager.authToken { print("Exchanging token") - await accountManager.exchangeAndStoreTokens(with: authToken) + + if case let .success(accessToken) = await accountManager.exchangeAuthTokenToAccessToken(authToken), + case let .success(accountDetails) = await accountManager.fetchAccountDetails(with: accessToken) { + accountManager.storeAuthToken(token: authToken) + accountManager.storeAccount(token: accessToken, email: accountDetails.email, externalID: accountDetails.externalID) + } } if #available(macOS 12.0, *) { diff --git a/LocalPackages/Subscription/Sources/Subscription/Services/SubscriptionService.swift b/LocalPackages/Subscription/Sources/Subscription/Services/SubscriptionService.swift index 877a45f245..6a37cfb2a2 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Services/SubscriptionService.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Services/SubscriptionService.swift @@ -30,11 +30,11 @@ public struct SubscriptionService: APIService { // MARK: - - public static func getSubscriptionInfo(token: String) async -> Result { + public static func getSubscriptionDetails(token: String) async -> Result { await executeAPICall(method: "GET", endpoint: "subscription", headers: makeAuthorizationHeader(for: token)) } - public struct GetSubscriptionInfoResponse: Decodable { + public struct GetSubscriptionDetailsResponse: Decodable { public let productId: String public let startedAt: Date public let expiresOrRenewsAt: Date diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift index 886b15d9e1..e8a08a50f6 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift @@ -53,7 +53,7 @@ public final class SubscriptionDebugMenu: NSMenuItem { menu.addItem(.separator()) menu.addItem(NSMenuItem(title: "Validate Token", action: #selector(validateToken), target: self)) menu.addItem(NSMenuItem(title: "Check Entitlements", action: #selector(checkEntitlements), target: self)) - menu.addItem(NSMenuItem(title: "Get Subscription Info", action: #selector(getSubscriptionInfo), target: self)) + menu.addItem(NSMenuItem(title: "Get Subscription Info", action: #selector(getSubscriptionDetails), target: self)) if #available(macOS 12.0, *) { menu.addItem(NSMenuItem(title: "Check Purchase Products Availability", action: #selector(checkProductsAvailability), target: self)) } @@ -118,10 +118,10 @@ public final class SubscriptionDebugMenu: NSMenuItem { } @objc - func getSubscriptionInfo() { + func getSubscriptionDetails() { Task { guard let token = accountManager.accessToken else { return } - switch await SubscriptionService.getSubscriptionInfo(token: token) { + switch await SubscriptionService.getSubscriptionDetails(token: token) { case .success(let response): showAlert(title: "Subscription info", message: "\(response)") case .failure(let error): diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift index 41490d77b7..62557ea237 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift @@ -103,7 +103,7 @@ public final class PreferencesSubscriptionModel: ObservableObject { guard let token = accountManager.accessToken else { return } - if case .success(let response) = await SubscriptionService.getSubscriptionInfo(token: token) { + if case .success(let response) = await SubscriptionService.getSubscriptionDetails(token: token) { if response.expiresOrRenewsAt < Date() { AccountManager().signOut() return From 5b9af1f4589e1311e153a41371b452816dd6d116 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 8 Dec 2023 13:14:16 +0100 Subject: [PATCH 56/96] Show progress view on restoring subscription --- .../View/PreferencesRootView.swift | 17 +++++++++++++ .../SubscriptionPagesUserScript.swift | 25 ++++++++++--------- ...ler.swift => ProgressViewController.swift} | 22 ++++++++-------- 3 files changed, 41 insertions(+), 23 deletions(-) rename LocalPackages/SubscriptionUI/Sources/SubscriptionUI/{DebugMenu/PurchaseInProgressViewController.swift => ProgressViewController.swift} (70%) diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index 640028c349..9fadf5868c 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -90,6 +90,19 @@ extension Preferences { } #if SUBSCRIPTION + + @MainActor + private func showProgress(with title: String) -> ProgressViewController { + let progressVC = ProgressViewController(title: title) + WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.presentAsSheet(progressVC) + return progressVC + } + + @MainActor + private func hideProgress(_ progressVC: ProgressViewController) { + WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.dismiss(progressVC) + } + private func makeSubscriptionView() -> some View { let actionHandler = PreferencesSubscriptionActionHandlers(openURL: { url in WindowControllersManager.shared.show(url: url, newTab: true) @@ -106,6 +119,10 @@ extension Preferences { let sheetActionHandler = SubscriptionAccessActionHandlers(restorePurchases: { if #available(macOS 12.0, *) { Task { + let progressViewController = self.showProgress(with: "Restoring subscription...") + + defer { self.hideProgress(progressViewController) } + guard case .success = await PurchaseManager.shared.syncAppleIDAccount() else { return } switch await AppStoreRestoreFlow.restoreAccountFromPastPurchase() { diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 4c63fb95a1..974a8090ad 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -256,27 +256,27 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { return nil } - private weak var purchaseInProgressViewController: PurchaseInProgressViewController? + private weak var progressViewController: ProgressViewController? @MainActor private func showProgress(with title: String) { - guard purchaseInProgressViewController == nil else { return } - let progressVC = PurchaseInProgressViewController(title: title) + guard progressViewController == nil else { return } + let progressVC = ProgressViewController(title: title) WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.presentAsSheet(progressVC) - purchaseInProgressViewController = progressVC + progressViewController = progressVC } @MainActor private func updateProgressTitle(_ title: String) { - guard let purchaseInProgressViewController else { return } - purchaseInProgressViewController.updateTitleText(title) + guard let progressViewController else { return } + progressViewController.updateTitleText(title) } @MainActor private func hideProgress() { - guard let purchaseInProgressViewController else { return } - WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.dismiss(purchaseInProgressViewController) - self.purchaseInProgressViewController = nil + guard let progressViewController else { return } + WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.dismiss(progressViewController) + self.progressViewController = nil } func activateSubscription(params: Any, original: WKScriptMessage) async throws -> Encodable? { @@ -289,11 +289,14 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { restorePurchases: { if #available(macOS 12.0, *) { Task { + defer { self.hideProgress() } + self.showProgress(with: "Restoring subscription...") + guard case .success = await PurchaseManager.shared.syncAppleIDAccount() else { return } switch await AppStoreRestoreFlow.restoreAccountFromPastPurchase() { case .success(let subscription): - break + message.webView?.reload() case .failure(let error): switch error { case .missingAccountOrTransactions: @@ -304,8 +307,6 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { self.showSomethingWentWrongAlert() } } - - message.webView?.reload() } } }, diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/PurchaseInProgressViewController.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/ProgressViewController.swift similarity index 70% rename from LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/PurchaseInProgressViewController.swift rename to LocalPackages/SubscriptionUI/Sources/SubscriptionUI/ProgressViewController.swift index 5a3babbf5b..9f25cf85f2 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/PurchaseInProgressViewController.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/ProgressViewController.swift @@ -1,5 +1,5 @@ // -// PurchaseInProgressViewController.swift +// ProgressViewController.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -19,26 +19,26 @@ import AppKit import SwiftUI -public final class PurchaseInProgressViewController: NSViewController { +public final class ProgressViewController: NSViewController { - private var purchaseInProgressView: PurchaseInProgressView? - private var viewModel: PurchaseInProgressViewModel + private var progressView: ProgressView? + private var viewModel: ProgressViewModel public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } public init(title: String) { - self.viewModel = PurchaseInProgressViewModel(title: title) + self.viewModel = ProgressViewModel(title: title) super.init(nibName: nil, bundle: nil) } public override func loadView() { - let purchaseInProgressView = PurchaseInProgressView(viewModel: viewModel) - let hostingView = NSHostingView(rootView: purchaseInProgressView) + let progressView = ProgressView(viewModel: viewModel) + let hostingView = NSHostingView(rootView: progressView) - self.purchaseInProgressView = purchaseInProgressView + self.progressView = progressView view = NSView(frame: NSRect(x: 0, y: 0, width: 360, height: 160)) hostingView.frame = view.bounds @@ -53,7 +53,7 @@ public final class PurchaseInProgressViewController: NSViewController { } } -final class PurchaseInProgressViewModel: ObservableObject { +final class ProgressViewModel: ObservableObject { @Published var title: String init(title: String) { @@ -61,9 +61,9 @@ final class PurchaseInProgressViewModel: ObservableObject { } } -struct PurchaseInProgressView: View { +struct ProgressView: View { - @ObservedObject var viewModel: PurchaseInProgressViewModel + @ObservedObject var viewModel: ProgressViewModel public var body: some View { VStack { From a2022d331b6812f1ccf0d121e12298e045f198b9 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 8 Dec 2023 14:43:15 +0100 Subject: [PATCH 57/96] Purchase dependant environment setup --- .../App/DuckDuckGoPrivacyPro.xcconfig | 2 +- DuckDuckGo/AppDelegate/AppDelegate.swift | 5 ++++ .../View/PreferencesRootView.swift | 8 +++++- .../SubscriptionPurchaseEnvironment.swift | 28 +++++++++++++++++++ .../Model/SubscriptionAccessModel.swift | 3 +- .../SubscriptionAccessView.swift | 3 +- 6 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 LocalPackages/Subscription/Sources/Subscription/SubscriptionPurchaseEnvironment.swift diff --git a/Configuration/App/DuckDuckGoPrivacyPro.xcconfig b/Configuration/App/DuckDuckGoPrivacyPro.xcconfig index e49e19d954..291c9d31c9 100644 --- a/Configuration/App/DuckDuckGoPrivacyPro.xcconfig +++ b/Configuration/App/DuckDuckGoPrivacyPro.xcconfig @@ -21,5 +21,5 @@ #include "DuckDuckGo.xcconfig" -FEATURE_FLAGS = FEEDBACK SPARKLE SUBSCRIPTION NOSTRIPE +FEATURE_FLAGS = FEEDBACK SPARKLE SUBSCRIPTION STRIPE PRODUCT_NAME = $(PRODUCT_NAME_PREFIX) Privacy Pro diff --git a/DuckDuckGo/AppDelegate/AppDelegate.swift b/DuckDuckGo/AppDelegate/AppDelegate.swift index 7b94ddf26b..3a8a1cd435 100644 --- a/DuckDuckGo/AppDelegate/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate/AppDelegate.swift @@ -231,7 +231,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, FileDownloadManagerDel #if SUBSCRIPTION if #available(macOS 12.0, *) { Task { + #if STRIPE + SubscriptionPurchaseEnvironment.current = .stripe + #else + SubscriptionPurchaseEnvironment.current = .appStore await PurchaseManager.shared.updateAvailableProducts() + #endif } } #endif diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index 9fadf5868c..e573e2e6a3 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -107,7 +107,13 @@ extension Preferences { let actionHandler = PreferencesSubscriptionActionHandlers(openURL: { url in WindowControllersManager.shared.show(url: url, newTab: true) }, manageSubscriptionInAppStore: { - NSWorkspace.shared.open(.manageSubscriptionsInAppStoreAppURL) + switch SubscriptionPurchaseEnvironment.current { + case .appStore: + NSWorkspace.shared.open(.manageSubscriptionsInAppStoreAppURL) + case .stripe: + // fetch the management url and open in new tab + break + } }, openVPN: { print("openVPN") }, openPersonalInformationRemoval: { diff --git a/LocalPackages/Subscription/Sources/Subscription/SubscriptionPurchaseEnvironment.swift b/LocalPackages/Subscription/Sources/Subscription/SubscriptionPurchaseEnvironment.swift new file mode 100644 index 0000000000..c2e7f5ab5a --- /dev/null +++ b/LocalPackages/Subscription/Sources/Subscription/SubscriptionPurchaseEnvironment.swift @@ -0,0 +1,28 @@ +// +// SubscriptionPurchaseEnvironment.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +public final class SubscriptionPurchaseEnvironment { + + public enum Environment { + case appStore, stripe + } + + public static var current: Environment = .appStore +} diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/SubscriptionAccessModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/SubscriptionAccessModel.swift index aee70c96a9..bbc2922b8f 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/SubscriptionAccessModel.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/SubscriptionAccessModel.swift @@ -17,6 +17,7 @@ // import Foundation +import Subscription public protocol SubscriptionAccessModel { var items: [AccessChannel] { get } @@ -31,7 +32,7 @@ public protocol SubscriptionAccessModel { } extension SubscriptionAccessModel { - public var items: [AccessChannel] { AccessChannel.allCases } + public var items: [AccessChannel] { SubscriptionPurchaseEnvironment.current == .appStore ? [.appleID, .email] : [.email] } public func descriptionHeader(for channel: AccessChannel) -> String? { nil } } diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/SubscriptionAccessView.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/SubscriptionAccessView.swift index e25354b7e1..bfc0fc386f 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/SubscriptionAccessView.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/SubscriptionAccessView.swift @@ -26,12 +26,13 @@ public struct SubscriptionAccessView: View { private let dismissAction: (() -> Void)? - @State private var selection: AccessChannel? = .appleID + @State private var selection: AccessChannel? @State var fullHeight: CGFloat = 0.0 public init(model: SubscriptionAccessModel, dismiss: (() -> Void)? = nil) { self.model = model self.dismissAction = dismiss + _selection = State(initialValue: model.items.first) } public var body: some View { From ba50dd622dc0372df550f0810d8c62512c8c57ef Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 8 Dec 2023 15:16:31 +0100 Subject: [PATCH 58/96] Navigate to Stripe customer portal to manage the subscription --- .../Preferences/View/PreferencesRootView.swift | 11 ++++++++--- .../Subscription/Services/SubscriptionService.swift | 12 ++++++++++++ .../Preferences/PreferencesSubscriptionModel.swift | 8 ++++---- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index e573e2e6a3..2e64fcc9fc 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -106,13 +106,18 @@ extension Preferences { private func makeSubscriptionView() -> some View { let actionHandler = PreferencesSubscriptionActionHandlers(openURL: { url in WindowControllersManager.shared.show(url: url, newTab: true) - }, manageSubscriptionInAppStore: { + }, changePlanOrBilling: { switch SubscriptionPurchaseEnvironment.current { case .appStore: NSWorkspace.shared.open(.manageSubscriptionsInAppStoreAppURL) case .stripe: - // fetch the management url and open in new tab - break + Task { + guard let accessToken = AccountManager().accessToken, let externalID = AccountManager().externalID, + case let .success(response) = await SubscriptionService.getCustomerPortalURL(accessToken: accessToken, externalID: externalID) else { return } + guard let customerPortalURL = URL(string: response.customerPortalUrl) else { return } + + WindowControllersManager.shared.show(url: customerPortalURL, newTab: true) + } } }, openVPN: { print("openVPN") diff --git a/LocalPackages/Subscription/Sources/Subscription/Services/SubscriptionService.swift b/LocalPackages/Subscription/Sources/Subscription/Services/SubscriptionService.swift index 6a37cfb2a2..1a77a0b0a2 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Services/SubscriptionService.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Services/SubscriptionService.swift @@ -58,4 +58,16 @@ public struct SubscriptionService: APIService { public let currency: String } + // MARK: - + + public static func getCustomerPortalURL(accessToken: String, externalID: String) async -> Result { + var headers = makeAuthorizationHeader(for: accessToken) + headers["externalAccountId"] = externalID + return await executeAPICall(method: "GET", endpoint: "checkout/portal", headers: headers) + } + + public struct GetCustomerPortalURLResponse: Decodable { + public let customerPortalUrl: String + } + } diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift index 62557ea237..8ec8e450b8 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift @@ -67,7 +67,7 @@ public final class PreferencesSubscriptionModel: ObservableObject { @MainActor func changePlanOrBillingAction() { - actionHandler.manageSubscriptionInAppStore() + actionHandler.changePlanOrBilling() } @MainActor @@ -123,14 +123,14 @@ public final class PreferencesSubscriptionModel: ObservableObject { public final class PreferencesSubscriptionActionHandlers { var openURL: (URL) -> Void - var manageSubscriptionInAppStore: () -> Void + var changePlanOrBilling: () -> Void var openVPN: () -> Void var openPersonalInformationRemoval: () -> Void var openIdentityTheftRestoration: () -> Void - public init(openURL: @escaping (URL) -> Void, manageSubscriptionInAppStore: @escaping () -> Void, openVPN: @escaping () -> Void, openPersonalInformationRemoval: @escaping () -> Void, openIdentityTheftRestoration: @escaping () -> Void) { + public init(openURL: @escaping (URL) -> Void, changePlanOrBilling: @escaping () -> Void, openVPN: @escaping () -> Void, openPersonalInformationRemoval: @escaping () -> Void, openIdentityTheftRestoration: @escaping () -> Void) { self.openURL = openURL - self.manageSubscriptionInAppStore = manageSubscriptionInAppStore + self.changePlanOrBilling = changePlanOrBilling self.openVPN = openVPN self.openPersonalInformationRemoval = openPersonalInformationRemoval self.openIdentityTheftRestoration = openIdentityTheftRestoration From 7ddfaf1d0510204fbef78095d9d41d56ebe3007d Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 11 Dec 2023 13:03:34 +0100 Subject: [PATCH 59/96] Update progress usage during purchase --- .../App/DuckDuckGoPrivacyPro.xcconfig | 2 +- .../View/PreferencesRootView.swift | 24 +++----- .../SubscriptionPagesUserScript.swift | 59 ++++++------------- .../Flows/AppStore/AppStorePurchaseFlow.swift | 8 ++- .../Flows/AppStore/AppStoreRestoreFlow.swift | 7 ++- 5 files changed, 38 insertions(+), 62 deletions(-) diff --git a/Configuration/App/DuckDuckGoPrivacyPro.xcconfig b/Configuration/App/DuckDuckGoPrivacyPro.xcconfig index 291c9d31c9..e49e19d954 100644 --- a/Configuration/App/DuckDuckGoPrivacyPro.xcconfig +++ b/Configuration/App/DuckDuckGoPrivacyPro.xcconfig @@ -21,5 +21,5 @@ #include "DuckDuckGo.xcconfig" -FEATURE_FLAGS = FEEDBACK SPARKLE SUBSCRIPTION STRIPE +FEATURE_FLAGS = FEEDBACK SPARKLE SUBSCRIPTION NOSTRIPE PRODUCT_NAME = $(PRODUCT_NAME_PREFIX) Privacy Pro diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index 2e64fcc9fc..108ae2a2af 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -90,19 +90,8 @@ extension Preferences { } #if SUBSCRIPTION - - @MainActor - private func showProgress(with title: String) -> ProgressViewController { - let progressVC = ProgressViewController(title: title) - WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.presentAsSheet(progressVC) - return progressVC - } - - @MainActor - private func hideProgress(_ progressVC: ProgressViewController) { - WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.dismiss(progressVC) - } - + // swiftlint:disable:next cyclomatic_complexity + // swiftlint:disable:next function_body_length private func makeSubscriptionView() -> some View { let actionHandler = PreferencesSubscriptionActionHandlers(openURL: { url in WindowControllersManager.shared.show(url: url, newTab: true) @@ -130,9 +119,12 @@ extension Preferences { let sheetActionHandler = SubscriptionAccessActionHandlers(restorePurchases: { if #available(macOS 12.0, *) { Task { - let progressViewController = self.showProgress(with: "Restoring subscription...") + let mainViewController = WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController + let progressViewController = ProgressViewController(title: "Restoring subscription...") - defer { self.hideProgress(progressViewController) } + defer { mainViewController?.dismiss(progressViewController) } + + mainViewController?.presentAsSheet(progressViewController) guard case .success = await PurchaseManager.shared.syncAppleIDAccount() else { return } @@ -168,7 +160,7 @@ extension Preferences { let alert = NSAlert.somethingWentWrongAlert() alert.beginSheetModal(for: window) } - + @MainActor private func showSubscriptionNotFoundAlert() { guard let window = WindowControllersManager.shared.lastKeyMainWindowController?.window else { return } diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 974a8090ad..7cf88181bc 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -194,9 +194,12 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { } #else if #available(macOS 12.0, *) { + let mainViewController = await WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController + let progressViewController = await ProgressViewController(title: "Purchase in progress...") + defer { Task { - await hideProgress() + await mainViewController?.dismiss(progressViewController) } } @@ -207,7 +210,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { print("Selected: \(subscriptionSelection.id)") - await showProgress(with: "Purchase in progress...") + await mainViewController?.presentAsSheet(progressViewController) // Trigger sign in pop-up switch await PurchaseManager.shared.syncAppleIDAccount() { @@ -224,14 +227,6 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { return nil } - // Hide it after some time in case nothing happens - /* - DispatchQueue.main.asyncAfter(deadline: .now() + 60) { - print("hiding it since nothing happened!") - self.hideProgress() - } - */ - let emailAccessToken = try? EmailManager().getToken() switch await AppStorePurchaseFlow.purchaseSubscription(with: subscriptionSelection.id, emailAccessToken: emailAccessToken) { @@ -241,14 +236,14 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { return nil } - await updateProgressTitle("Completing purchase...") + await progressViewController.updateTitleText("Completing purchase...") switch await AppStorePurchaseFlow.completeSubscriptionPurchase() { case .success(let purchaseUpdate): await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: purchaseUpdate) case .failure: // TODO: handle errors - missing entitlements on post purchase check - return nil + await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: PurchaseUpdate(type: "completed")) } } #endif @@ -256,29 +251,6 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { return nil } - private weak var progressViewController: ProgressViewController? - - @MainActor - private func showProgress(with title: String) { - guard progressViewController == nil else { return } - let progressVC = ProgressViewController(title: title) - WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.presentAsSheet(progressVC) - progressViewController = progressVC - } - - @MainActor - private func updateProgressTitle(_ title: String) { - guard let progressViewController else { return } - progressViewController.updateTitleText(title) - } - - @MainActor - private func hideProgress() { - guard let progressViewController else { return } - WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.dismiss(progressViewController) - self.progressViewController = nil - } - func activateSubscription(params: Any, original: WKScriptMessage) async throws -> Encodable? { print(">>> Selected to activate a subscription -- show the activation settings screen") @@ -289,13 +261,17 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { restorePurchases: { if #available(macOS 12.0, *) { Task { - defer { self.hideProgress() } - self.showProgress(with: "Restoring subscription...") + let mainViewController = WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController + let progressViewController = ProgressViewController(title: "Restoring subscription...") + + defer { mainViewController?.dismiss(progressViewController) } + + mainViewController?.presentAsSheet(progressViewController) guard case .success = await PurchaseManager.shared.syncAppleIDAccount() else { return } switch await AppStoreRestoreFlow.restoreAccountFromPastPurchase() { - case .success(let subscription): + case .success: message.webView?.reload() case .failure(let error): switch error { @@ -340,9 +316,12 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { func completeStripePayment(params: Any, original: WKScriptMessage) async throws -> Encodable? { print(">>> completeStripePayment") - await showProgress(with: "Completing purchase...") + let mainViewController = await WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController + let progressViewController = await ProgressViewController(title: "Completing purchase...") + + await mainViewController?.presentAsSheet(progressViewController) await StripePurchaseFlow.completeSubscriptionPurchase() - await hideProgress() + await mainViewController?.dismiss(progressViewController) return [String: String]() // cannot be nil } diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift index 934ac70360..5e0d5b150e 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift @@ -64,8 +64,10 @@ public final class AppStorePurchaseFlow { return .failure(.activeSubscriptionAlreadyPresent) case .failure(let error): switch error { - case .subscriptionExpired(let expiredExternalID): - externalID = expiredExternalID + case .subscriptionExpired(let expiredAccountDetails): + externalID = expiredAccountDetails.externalID + accountManager.storeAuthToken(token: expiredAccountDetails.authToken) + accountManager.storeAccount(token: expiredAccountDetails.accessToken, email: expiredAccountDetails.email, externalID: expiredAccountDetails.externalID) case .missingAccountOrTransactions: // No history, create new account switch await AuthService.createAccount(emailAccessToken: emailAccessToken) { @@ -99,7 +101,7 @@ public final class AppStorePurchaseFlow { @discardableResult public static func completeSubscriptionPurchase() async -> Result { - let result = await checkForEntitlements(wait: 2.0, retry: 15) + let result = await checkForEntitlements(wait: 2.0, retry: 5) return result ? .success(PurchaseUpdate(type: "completed")) : .failure(.missingEntitlements) } diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift index a8ef80a734..8ff38f1edd 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift @@ -22,13 +22,15 @@ import StoreKit @available(macOS 12.0, *) public final class AppStoreRestoreFlow { + public typealias RestoredAccountDetails = (authToken: String, accessToken: String, externalID: String, email: String?) + public enum Error: Swift.Error { case missingAccountOrTransactions case pastTransactionAuthenticationError case failedToObtainAccessToken case failedToFetchAccountDetails case failedToFetchSubscriptionDetails - case subscriptionExpired(externalID: String) + case subscriptionExpired(accountDetails: RestoredAccountDetails) case somethingWentWrong } @@ -81,7 +83,8 @@ public final class AppStoreRestoreFlow { accountManager.storeAccount(token: accessToken, email: email, externalID: externalID) return .success(()) } else { - return .failure(.subscriptionExpired(externalID: externalID)) + let details = RestoredAccountDetails(authToken: authToken, accessToken: accessToken, externalID: externalID, email: email) + return .failure(.subscriptionExpired(accountDetails: details)) } } } From f63b05071a159ccff7d7c05b29200eee22f5a180 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 11 Dec 2023 14:24:08 +0100 Subject: [PATCH 60/96] Pass email token during Stripe account creation --- .../Tab/UserScripts/SubscriptionPagesUserScript.swift | 4 +++- .../Subscription/Flows/Stripe/StripePurchaseFlow.swift | 10 +++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 7cf88181bc..03af94a9d1 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -185,7 +185,9 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { let message = original #if STRIPE - switch await StripePurchaseFlow.prepareSubscriptionPurchase(emailAccessToken: "passemailaccesstokenhere") { + let emailAccessToken = try? EmailManager().getToken() + + switch await StripePurchaseFlow.prepareSubscriptionPurchase(emailAccessToken: emailAccessToken) { case .success(let purchaseUpdate): await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: purchaseUpdate) case .failure: diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/Stripe/StripePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/Stripe/StripePurchaseFlow.swift index b7e9b0c195..34fd2de672 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/Stripe/StripePurchaseFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Flows/Stripe/StripePurchaseFlow.swift @@ -58,17 +58,17 @@ public final class StripePurchaseFlow { public static func prepareSubscriptionPurchase(emailAccessToken: String?) async -> Result { - var token: String = "" + var authToken: String = "" - switch await AuthService.createAccount(emailAccessToken: nil) { + switch await AuthService.createAccount(emailAccessToken: emailAccessToken) { case .success(let response): - token = response.authToken - AccountManager().storeAuthToken(token: response.authToken) + authToken = response.authToken + AccountManager().storeAuthToken(token: authToken) case .failure: return .failure(.accountCreationFailed) } - return .success(PurchaseUpdate(type: "redirect", token: token)) + return .success(PurchaseUpdate(type: "redirect", token: authToken)) } public static func completeSubscriptionPurchase() async { From 9730905e4f9070666f6650914fb22ea7ad693d2e Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 11 Dec 2023 18:18:42 +0100 Subject: [PATCH 61/96] Cache subscription end date --- .../Services/SubscriptionService.swift | 13 ++++++- .../PreferencesSubscriptionModel.swift | 39 ++++++++++++------- .../PreferencesSubscriptionView.swift | 14 +++---- .../Sources/SubscriptionUI/UserText.swift | 5 ++- 4 files changed, 46 insertions(+), 25 deletions(-) diff --git a/LocalPackages/Subscription/Sources/Subscription/Services/SubscriptionService.swift b/LocalPackages/Subscription/Sources/Subscription/Services/SubscriptionService.swift index 1a77a0b0a2..a300858030 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Services/SubscriptionService.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Services/SubscriptionService.swift @@ -31,7 +31,16 @@ public struct SubscriptionService: APIService { // MARK: - public static func getSubscriptionDetails(token: String) async -> Result { - await executeAPICall(method: "GET", endpoint: "subscription", headers: makeAuthorizationHeader(for: token)) + let result: Result = await executeAPICall(method: "GET", endpoint: "subscription", headers: makeAuthorizationHeader(for: token)) + + switch result { + case .success(let response): + cachedSubscriptionDetailsResponse = response + case .failure: + cachedSubscriptionDetailsResponse = nil + } + + return result } public struct GetSubscriptionDetailsResponse: Decodable { @@ -42,6 +51,8 @@ public struct SubscriptionService: APIService { public let status: String } + public static var cachedSubscriptionDetailsResponse: GetSubscriptionDetailsResponse? + // MARK: - public static func getProducts() async -> Result { diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift index 8ec8e450b8..c3392a65e6 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift @@ -22,7 +22,6 @@ import Subscription public final class PreferencesSubscriptionModel: ObservableObject { @Published var isUserAuthenticated: Bool = false - @Published var hasEntitlements: Bool = false @Published var subscriptionDetails: String? lazy var sheetModel: SubscriptionAccessModel = makeSubscriptionAccessModel() @@ -35,8 +34,13 @@ public final class PreferencesSubscriptionModel: ObservableObject { self.actionHandler = actionHandler self.sheetActionHandler = sheetActionHandler - let isUserAuthenticated = accountManager.isUserAuthenticated - self.isUserAuthenticated = isUserAuthenticated + self.isUserAuthenticated = accountManager.isUserAuthenticated + + if let cachedDate = SubscriptionService.cachedSubscriptionDetailsResponse?.expiresOrRenewsAt { + print(" -- got cached \(cachedDate)") + + updateDescription(for: cachedDate) + } NotificationCenter.default.addObserver(forName: .accountDidSignIn, object: nil, queue: .main) { _ in self.updateUserAuthenticatedState(true) @@ -96,29 +100,36 @@ public final class PreferencesSubscriptionModel: ObservableObject { } @MainActor - func fetchEntitlements() { - print("Entitlements!") + func fetchAndUpdateSubscriptionDetails() { Task { - self.hasEntitlements = await AccountManager().hasEntitlement(for: "dummy1") - guard let token = accountManager.accessToken else { return } + if let cachedDate = SubscriptionService.cachedSubscriptionDetailsResponse?.expiresOrRenewsAt { + updateDescription(for: cachedDate) + } + if case .success(let response) = await SubscriptionService.getSubscriptionDetails(token: token) { if response.expiresOrRenewsAt < Date() { AccountManager().signOut() return } - let dateFormatter = DateFormatter() - dateFormatter.dateStyle = .medium - dateFormatter.timeStyle = .short - - let stringDate = dateFormatter.string(from: response.expiresOrRenewsAt) - - self.subscriptionDetails = "Your monthly Privacy Pro subscription renews on \(stringDate)." + updateDescription(for: response.expiresOrRenewsAt) } } } + + private func updateDescription(for date: Date) { + self.subscriptionDetails = UserText.preferencesSubscriptionActiveCaption(formattedDate: dateFormatter.string(from: date)) + } + + private var dateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .short + + return dateFormatter + }() } public final class PreferencesSubscriptionActionHandlers { diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift index 40333dbafc..76b373cb6f 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift @@ -70,8 +70,7 @@ public struct PreferencesSubscriptionView: View { .padding(4) } content: { TextMenuItemHeader(text: UserText.preferencesSubscriptionActiveHeader) - TextMenuItemCaption(text: model.subscriptionDetails ?? UserText.preferencesSubscriptionActiveCaption) -// TextMenuItemCaption(text: UserText.preferencesSubscriptionActiveCaption) + TextMenuItemCaption(text: model.subscriptionDetails ?? "") } buttons: { Button(UserText.addToAnotherDeviceButton) { showingSheet.toggle() } @@ -86,7 +85,7 @@ public struct PreferencesSubscriptionView: View { .fixedSize() } .onAppear { - model.fetchEntitlements() + model.fetchAndUpdateSubscriptionDetails() } } else { @@ -113,8 +112,7 @@ public struct PreferencesSubscriptionView: View { title: UserText.vpnServiceTitle, description: UserText.vpnServiceDescription, buttonName: model.isUserAuthenticated ? "Manage" : nil, - buttonAction: { model.openVPN() }, - enabled: model.hasEntitlements) + buttonAction: { model.openVPN() }) Divider() .foregroundColor(Color.secondary) @@ -123,8 +121,7 @@ public struct PreferencesSubscriptionView: View { title: UserText.personalInformationRemovalServiceTitle, description: UserText.personalInformationRemovalServiceDescription, buttonName: model.isUserAuthenticated ? "View" : nil, - buttonAction: { model.openPersonalInformationRemoval() }, - enabled: model.hasEntitlements) + buttonAction: { model.openPersonalInformationRemoval() }) Divider() .foregroundColor(Color.secondary) @@ -133,8 +130,7 @@ public struct PreferencesSubscriptionView: View { title: UserText.identityTheftRestorationServiceTitle, description: UserText.identityTheftRestorationServiceDescription, buttonName: model.isUserAuthenticated ? "View" : nil, - buttonAction: { model.openIdentityTheftRestoration() }, - enabled: model.hasEntitlements) + buttonAction: { model.openIdentityTheftRestoration() }) } .padding(10) .roundedBorder() diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift index 2c836f1103..ce8beb209e 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift @@ -39,7 +39,10 @@ enum UserText { // MARK: Preferences when subscription is active static let preferencesSubscriptionActiveHeader = NSLocalizedString("subscription.preferences.subscription.active.header", value: "Privacy Pro is active on this device", comment: "Header for the subscription preferences pane when the subscription is active") - static let preferencesSubscriptionActiveCaption = NSLocalizedString("subscription.preferences.subscription.active.caption", value: "Your monthly Privacy Pro subscription renews on April 20, 2027.", comment: "Caption for the subscription preferences pane when the subscription is active") + static func preferencesSubscriptionActiveCaption(formattedDate: String) -> String { + let localized = NSLocalizedString("subscription.preferences.subscription.active.caption", value: "Your monthly Privacy Pro subscription renews on %@", comment: "Caption for the subscription preferences pane when the subscription is active") + return String(format: localized, formattedDate) + } static let addToAnotherDeviceButton = NSLocalizedString("subscription.preferences.add.to.another.device.button", value: "Add to Another Device…", comment: "Button to add subscription to another device") static let manageSubscriptionButton = NSLocalizedString("subscription.preferences.manage.subscription.button", value: "Manage Subscription", comment: "Button to manage subscription") From d78fc8e7c827bd2ad34449d50254ea1995946741 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 11 Dec 2023 20:51:17 +0100 Subject: [PATCH 62/96] Check for expiration --- .../Tab/UserScripts/SubscriptionPagesUserScript.swift | 1 + .../Subscription/Flows/AppStore/AppStoreRestoreFlow.swift | 3 +-- .../Sources/Subscription/Services/SubscriptionService.swift | 6 +++++- .../Preferences/PreferencesSubscriptionModel.swift | 4 +--- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 03af94a9d1..ec4974607a 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -377,6 +377,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { if case .alertFirstButtonReturn = response { WindowControllersManager.shared.show(url: .purchaseSubscription, newTab: true) AccountManager().signOut() + // TODO: Check if it is required } }) } diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift index 8ff38f1edd..4c8cfc593f 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift @@ -68,12 +68,11 @@ public final class AppStoreRestoreFlow { return .failure(.failedToFetchAccountDetails) } - // var isSubscriptionActive = false switch await SubscriptionService.getSubscriptionDetails(token: accessToken) { case .success(let response): - isSubscriptionActive = response.status != "Expired" && response.status != "Inactive" + isSubscriptionActive = response.isSubscriptionActive case .failure: return .failure(.somethingWentWrong) } diff --git a/LocalPackages/Subscription/Sources/Subscription/Services/SubscriptionService.swift b/LocalPackages/Subscription/Sources/Subscription/Services/SubscriptionService.swift index a300858030..6c3ed27db1 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Services/SubscriptionService.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Services/SubscriptionService.swift @@ -49,6 +49,10 @@ public struct SubscriptionService: APIService { public let expiresOrRenewsAt: Date public let platform: String public let status: String + + public var isSubscriptionActive: Bool { + status.lowercased() != "expired" && status.lowercased() != "inactive" + } } public static var cachedSubscriptionDetailsResponse: GetSubscriptionDetailsResponse? @@ -58,7 +62,7 @@ public struct SubscriptionService: APIService { public static func getProducts() async -> Result { await executeAPICall(method: "GET", endpoint: "products") } - + public typealias GetProductsResponse = [GetProductsItem] public struct GetProductsItem: Decodable { diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift index c3392a65e6..fb9f83ce57 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift @@ -37,8 +37,6 @@ public final class PreferencesSubscriptionModel: ObservableObject { self.isUserAuthenticated = accountManager.isUserAuthenticated if let cachedDate = SubscriptionService.cachedSubscriptionDetailsResponse?.expiresOrRenewsAt { - print(" -- got cached \(cachedDate)") - updateDescription(for: cachedDate) } @@ -109,7 +107,7 @@ public final class PreferencesSubscriptionModel: ObservableObject { } if case .success(let response) = await SubscriptionService.getSubscriptionDetails(token: token) { - if response.expiresOrRenewsAt < Date() { + if !response.isSubscriptionActive { AccountManager().signOut() return } From 6d9c30a28b5e26afbb74a6c010f5926cbc2013a9 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 11 Dec 2023 21:40:02 +0100 Subject: [PATCH 63/96] Refactor alerts --- .../View/PreferencesRootView.swift | 38 +---------- .../SubscriptionPagesUserScript.swift | 65 ++++++++----------- .../Flows/AppStore/AppStorePurchaseFlow.swift | 4 +- .../SubscriptionUI/NSAlert+Subscription.swift | 11 ++++ 4 files changed, 44 insertions(+), 74 deletions(-) diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index 108ae2a2af..2ddb5336a8 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -134,11 +134,11 @@ extension Preferences { case .failure(let error): switch error { case .missingAccountOrTransactions: - self.showSubscriptionNotFoundAlert() + WindowControllersManager.shared.lastKeyMainWindowController?.showSubscriptionNotFoundAlert() case .subscriptionExpired: - self.showSubscriptionInactiveAlert() + WindowControllersManager.shared.lastKeyMainWindowController?.showSubscriptionInactiveAlert() default: - self.showSomethingWentWrongAlert() + WindowControllersManager.shared.lastKeyMainWindowController?.showSomethingWentWrongAlert() } } } @@ -152,38 +152,6 @@ extension Preferences { let model = PreferencesSubscriptionModel(actionHandler: actionHandler, sheetActionHandler: sheetActionHandler) return SubscriptionUI.PreferencesSubscriptionView(model: model) } - - @MainActor - private func showSomethingWentWrongAlert() { - guard let window = WindowControllersManager.shared.lastKeyMainWindowController?.window else { return } - - let alert = NSAlert.somethingWentWrongAlert() - alert.beginSheetModal(for: window) - } - - @MainActor - private func showSubscriptionNotFoundAlert() { - guard let window = WindowControllersManager.shared.lastKeyMainWindowController?.window else { return } - - let alert = NSAlert.subscriptionNotFoundAlert() - alert.beginSheetModal(for: window, completionHandler: { response in - if case .alertFirstButtonReturn = response { - WindowControllersManager.shared.show(url: .purchaseSubscription, newTab: true) - } - }) - } - - @MainActor - private func showSubscriptionInactiveAlert() { - guard let window = WindowControllersManager.shared.lastKeyMainWindowController?.window else { return } - - let alert = NSAlert.subscriptionInactiveAlert() - alert.beginSheetModal(for: window, completionHandler: { response in - if case .alertFirstButtonReturn = response { - WindowControllersManager.shared.show(url: .purchaseSubscription, newTab: true) - } - }) - } #endif } } diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index ec4974607a..9ca38e3dec 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -225,7 +225,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { // Check for active subscriptions if await PurchaseManager.hasActiveSubscription() { print("hasActiveSubscription: TRUE") - await showSubscriptionFoundAlert(originalMessage: message) + await WindowControllersManager.shared.lastKeyMainWindowController?.showSubscriptionFoundAlert(originalMessage: message) return nil } @@ -278,11 +278,11 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { case .failure(let error): switch error { case .missingAccountOrTransactions: - self.showSubscriptionNotFoundAlert() + WindowControllersManager.shared.lastKeyMainWindowController?.showSubscriptionNotFoundAlert() case .subscriptionExpired: - self.showSubscriptionInactiveAlert() + WindowControllersManager.shared.lastKeyMainWindowController?.showSubscriptionInactiveAlert() default: - self.showSomethingWentWrongAlert() + WindowControllersManager.shared.lastKeyMainWindowController?.showSomethingWentWrongAlert() } } } @@ -345,55 +345,46 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { print(">>> Pushing into WebView:", method.rawValue, String(describing: params)) broker.push(method: method.rawValue, params: params, for: self, into: webView) } +} - // MARK: Alerts +extension MainWindowController { @MainActor - private func showSomethingWentWrongAlert() { - guard let window = WindowControllersManager.shared.lastKeyMainWindowController?.window else { return } + func showSomethingWentWrongAlert() { + guard let window else { return } - let alert = NSAlert.somethingWentWrongAlert() - alert.beginSheetModal(for: window) + window.show(.somethingWentWrongAlert()) } @MainActor - private func showSubscriptionNotFoundAlert() { - guard let window = WindowControllersManager.shared.lastKeyMainWindowController?.window else { return } + func showSubscriptionNotFoundAlert() { + guard let window else { return } - let alert = NSAlert.subscriptionNotFoundAlert() - alert.beginSheetModal(for: window, completionHandler: { response in - if case .alertFirstButtonReturn = response { - WindowControllersManager.shared.show(url: .purchaseSubscription, newTab: true) - } + window.show(.subscriptionNotFoundAlert(), firstButtonAction: { + WindowControllersManager.shared.show(url: .purchaseSubscription, newTab: true) }) } @MainActor - private func showSubscriptionInactiveAlert() { - guard let window = WindowControllersManager.shared.lastKeyMainWindowController?.window else { return } - - let alert = NSAlert.subscriptionInactiveAlert() - alert.beginSheetModal(for: window, completionHandler: { response in - if case .alertFirstButtonReturn = response { - WindowControllersManager.shared.show(url: .purchaseSubscription, newTab: true) - AccountManager().signOut() - // TODO: Check if it is required - } + func showSubscriptionInactiveAlert() { + guard let window else { return } + + window.show(.subscriptionInactiveAlert(), firstButtonAction: { + WindowControllersManager.shared.show(url: .purchaseSubscription, newTab: true) +// AccountManager().signOut() + // TODO: Check if it is required }) } @MainActor - private func showSubscriptionFoundAlert(originalMessage: WKScriptMessage) { - guard let window = WindowControllersManager.shared.lastKeyMainWindowController?.window else { return } - - let alert = NSAlert.subscriptionFoundAlert() - alert.beginSheetModal(for: window, completionHandler: { response in - if case .alertFirstButtonReturn = response { - if #available(macOS 12.0, *) { - Task { - _ = await AppStoreRestoreFlow.restoreAccountFromPastPurchase() - originalMessage.webView?.reload() - } + func showSubscriptionFoundAlert(originalMessage: WKScriptMessage) { + guard let window else { return } + + window.show(.subscriptionFoundAlert(), firstButtonAction: { + if #available(macOS 12.0, *) { + Task { + _ = await AppStoreRestoreFlow.restoreAccountFromPastPurchase() + originalMessage.webView?.reload() } } }) diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift index 5e0d5b150e..b65df679c6 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift @@ -60,7 +60,7 @@ public final class AppStorePurchaseFlow { // Check for past transactions most recent switch await AppStoreRestoreFlow.restoreAccountFromPastPurchase() { - case .success(let subscription): + case .success: return .failure(.activeSubscriptionAlreadyPresent) case .failure(let error): switch error { @@ -101,7 +101,7 @@ public final class AppStorePurchaseFlow { @discardableResult public static func completeSubscriptionPurchase() async -> Result { - let result = await checkForEntitlements(wait: 2.0, retry: 5) + let result = await checkForEntitlements(wait: 2.0, retry: 10) return result ? .success(PurchaseUpdate(type: "completed")) : .failure(.missingEntitlements) } diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/NSAlert+Subscription.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/NSAlert+Subscription.swift index 2d6ba34a76..675b371019 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/NSAlert+Subscription.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/NSAlert+Subscription.swift @@ -57,3 +57,14 @@ public extension NSAlert { return alert } } + +public extension NSWindow { + + func show(_ alert: NSAlert, firstButtonAction: (() -> Void)? = nil) { + alert.beginSheetModal(for: self, completionHandler: { response in + if case .alertFirstButtonReturn = response { + firstButtonAction?() + } + }) + } +} From 5658059499d4d347b344e9bc11c87f58b89b6ef9 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Tue, 12 Dec 2023 09:27:50 +0100 Subject: [PATCH 64/96] Restore inactive buttons state and disable automatic sign out on expired subscripton in settings --- .../Preferences/PreferencesSubscriptionModel.swift | 14 ++++++++++---- .../Preferences/PreferencesSubscriptionView.swift | 9 ++++++--- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift index fb9f83ce57..73ac4f2d6f 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift @@ -22,6 +22,7 @@ import Subscription public final class PreferencesSubscriptionModel: ObservableObject { @Published var isUserAuthenticated: Bool = false + @Published var hasEntitlements: Bool = false @Published var subscriptionDetails: String? lazy var sheetModel: SubscriptionAccessModel = makeSubscriptionAccessModel() @@ -35,6 +36,7 @@ public final class PreferencesSubscriptionModel: ObservableObject { self.sheetActionHandler = sheetActionHandler self.isUserAuthenticated = accountManager.isUserAuthenticated + self.hasEntitlements = self.isUserAuthenticated if let cachedDate = SubscriptionService.cachedSubscriptionDetailsResponse?.expiresOrRenewsAt { updateDescription(for: cachedDate) @@ -104,16 +106,20 @@ public final class PreferencesSubscriptionModel: ObservableObject { if let cachedDate = SubscriptionService.cachedSubscriptionDetailsResponse?.expiresOrRenewsAt { updateDescription(for: cachedDate) + self.hasEntitlements = cachedDate.timeIntervalSinceNow > 0 } if case .success(let response) = await SubscriptionService.getSubscriptionDetails(token: token) { - if !response.isSubscriptionActive { - AccountManager().signOut() - return - } + // TODO: Disabling auto-sign out on expired subscription +// if !response.isSubscriptionActive { +// AccountManager().signOut() +// return +// } updateDescription(for: response.expiresOrRenewsAt) } + + self.hasEntitlements = await AccountManager().hasEntitlement(for: "dummy1") } } diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift index 76b373cb6f..2f20bee389 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift @@ -112,7 +112,8 @@ public struct PreferencesSubscriptionView: View { title: UserText.vpnServiceTitle, description: UserText.vpnServiceDescription, buttonName: model.isUserAuthenticated ? "Manage" : nil, - buttonAction: { model.openVPN() }) + buttonAction: { model.openVPN() }, + enabled: model.hasEntitlements) Divider() .foregroundColor(Color.secondary) @@ -121,7 +122,8 @@ public struct PreferencesSubscriptionView: View { title: UserText.personalInformationRemovalServiceTitle, description: UserText.personalInformationRemovalServiceDescription, buttonName: model.isUserAuthenticated ? "View" : nil, - buttonAction: { model.openPersonalInformationRemoval() }) + buttonAction: { model.openPersonalInformationRemoval() }, + enabled: model.hasEntitlements) Divider() .foregroundColor(Color.secondary) @@ -130,7 +132,8 @@ public struct PreferencesSubscriptionView: View { title: UserText.identityTheftRestorationServiceTitle, description: UserText.identityTheftRestorationServiceDescription, buttonName: model.isUserAuthenticated ? "View" : nil, - buttonAction: { model.openIdentityTheftRestoration() }) + buttonAction: { model.openIdentityTheftRestoration() }, + enabled: model.hasEntitlements) } .padding(10) .roundedBorder() From defe0627a330ede43086cfeae4c8af7f8992f9bf Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Tue, 12 Dec 2023 10:22:49 +0100 Subject: [PATCH 65/96] When setting purchase environment fetch available products to determine if purchase is available --- DuckDuckGo/AppDelegate/AppDelegate.swift | 9 ++---- .../Sources/Subscription/AccountManager.swift | 10 ++++++ .../Subscription/PurchaseManager.swift | 15 --------- .../SubscriptionPurchaseEnvironment.swift | 32 ++++++++++++++++++- 4 files changed, 44 insertions(+), 22 deletions(-) diff --git a/DuckDuckGo/AppDelegate/AppDelegate.swift b/DuckDuckGo/AppDelegate/AppDelegate.swift index 3a8a1cd435..70ee1c8c7b 100644 --- a/DuckDuckGo/AppDelegate/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate/AppDelegate.swift @@ -229,15 +229,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, FileDownloadManagerDel #endif #if SUBSCRIPTION - if #available(macOS 12.0, *) { - Task { + Task { #if STRIPE - SubscriptionPurchaseEnvironment.current = .stripe + SubscriptionPurchaseEnvironment.current = .stripe #else - SubscriptionPurchaseEnvironment.current = .appStore - await PurchaseManager.shared.updateAvailableProducts() + SubscriptionPurchaseEnvironment.current = .appStore #endif - } } #endif } diff --git a/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift b/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift index 63f5af6e61..0510434d7e 100644 --- a/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift +++ b/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift @@ -197,4 +197,14 @@ public class AccountManager { return .failure(error) } } + + public func checkSubscriptionState() async { + guard let token = accessToken else { return } + + if case .success(let response) = await SubscriptionService.getSubscriptionDetails(token: token) { + if response.isSubscriptionActive { + signOut() + } + } + } } diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseManager.swift b/LocalPackages/Subscription/Sources/Subscription/PurchaseManager.swift index a219dc4d04..e75e096d76 100644 --- a/LocalPackages/Subscription/Sources/Subscription/PurchaseManager.swift +++ b/LocalPackages/Subscription/Sources/Subscription/PurchaseManager.swift @@ -115,21 +115,6 @@ public final class PurchaseManager: ObservableObject { } } - @MainActor - func fetchAvailableProducts() async -> [Product] { - print(" -- [PurchaseManager] fetchAvailableProducts()") - - do { - let availableProducts = try await Product.products(for: Self.productIdentifiers) - print(" -- [PurchaseManager] fetchAvailableProducts(): fetched \(availableProducts.count) products") - - return availableProducts - } catch { - print("Error fetching available products: \(error)") - return [] - } - } - @MainActor public func updatePurchasedProducts() async { print(" -- [PurchaseManager] updatePurchasedProducts()") diff --git a/LocalPackages/Subscription/Sources/Subscription/SubscriptionPurchaseEnvironment.swift b/LocalPackages/Subscription/Sources/Subscription/SubscriptionPurchaseEnvironment.swift index c2e7f5ab5a..0e1c93968e 100644 --- a/LocalPackages/Subscription/Sources/Subscription/SubscriptionPurchaseEnvironment.swift +++ b/LocalPackages/Subscription/Sources/Subscription/SubscriptionPurchaseEnvironment.swift @@ -24,5 +24,35 @@ public final class SubscriptionPurchaseEnvironment { case appStore, stripe } - public static var current: Environment = .appStore + public static var current: Environment = .appStore { + didSet { + canPurchase = false + + switch current { + case .appStore: + setupForAppStore() + case .stripe: + setupForStripe() + } + } + } + + public static var canPurchase: Bool = false + + private static func setupForAppStore() { + if #available(macOS 12.0, *) { + Task { + await PurchaseManager.shared.updateAvailableProducts() + canPurchase = !PurchaseManager.shared.availableProducts.isEmpty + } + } + } + + private static func setupForStripe() { + Task { + if case let .success(products) = await SubscriptionService.getProducts() { + canPurchase = !products.isEmpty + } + } + } } From 5150caa9007a7754278c8d56e2440ff706a4b70a Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Tue, 12 Dec 2023 14:37:47 +0100 Subject: [PATCH 66/96] Hide Subscription related entrypoints if not authentcated and no able to purchase --- DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift | 6 +++++- DuckDuckGo/Preferences/Model/PreferencesSection.swift | 7 +++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index 173403a0c8..1d15c26066 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -294,7 +294,11 @@ final class MoreOptionsMenu: NSMenu { var items: [NSMenuItem] = [] #if SUBSCRIPTION - items.append(contentsOf: AccountManager().isUserAuthenticated ? makeActiveSubscriptionItems() : makeInactiveSubscriptionItems()) + if AccountManager().isUserAuthenticated { + items.append(contentsOf: makeActiveSubscriptionItems()) + } else if SubscriptionPurchaseEnvironment.canPurchase { + items.append(contentsOf: makeInactiveSubscriptionItems()) + } #else items.append(contentsOf: makeActiveSubscriptionItems()) // this only adds NETP and DBP (if enabled) #endif diff --git a/DuckDuckGo/Preferences/Model/PreferencesSection.swift b/DuckDuckGo/Preferences/Model/PreferencesSection.swift index 15a9494b1a..35a6f684fc 100644 --- a/DuckDuckGo/Preferences/Model/PreferencesSection.swift +++ b/DuckDuckGo/Preferences/Model/PreferencesSection.swift @@ -18,6 +18,7 @@ import Foundation import SwiftUI +import Subscription struct PreferencesSection: Hashable, Identifiable { let id: PreferencesSectionIdentifier @@ -34,6 +35,12 @@ struct PreferencesSection: Hashable, Identifiable { panes.insert(.sync, at: generalIndex + 1) } } + + if !AccountManager().isUserAuthenticated || !SubscriptionPurchaseEnvironment.canPurchase { + if let subscriptionIndex = panes.firstIndex(of: .subscription) { + panes.remove(at: subscriptionIndex) + } + } #else var panes: [PreferencePaneIdentifier] = [.general, .appearance, .privacy, .autofill, .downloads] From 1887c63c2a23ab4c46e6ed883e89791009958063 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Tue, 12 Dec 2023 14:44:03 +0100 Subject: [PATCH 67/96] Sign out on expired subscription --- DuckDuckGo/AppDelegate/AppDelegate.swift | 1 + .../Sources/Subscription/AccountManager.swift | 2 +- .../Preferences/PreferencesSubscriptionModel.swift | 9 ++++----- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo/AppDelegate/AppDelegate.swift b/DuckDuckGo/AppDelegate/AppDelegate.swift index 70ee1c8c7b..27ea28b9b5 100644 --- a/DuckDuckGo/AppDelegate/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate/AppDelegate.swift @@ -235,6 +235,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, FileDownloadManagerDel #else SubscriptionPurchaseEnvironment.current = .appStore #endif + await AccountManager().checkSubscriptionState() } #endif } diff --git a/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift b/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift index 0510434d7e..655742298d 100644 --- a/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift +++ b/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift @@ -202,7 +202,7 @@ public class AccountManager { guard let token = accessToken else { return } if case .success(let response) = await SubscriptionService.getSubscriptionDetails(token: token) { - if response.isSubscriptionActive { + if !response.isSubscriptionActive { signOut() } } diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift index 73ac4f2d6f..be6d174845 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift @@ -110,11 +110,10 @@ public final class PreferencesSubscriptionModel: ObservableObject { } if case .success(let response) = await SubscriptionService.getSubscriptionDetails(token: token) { - // TODO: Disabling auto-sign out on expired subscription -// if !response.isSubscriptionActive { -// AccountManager().signOut() -// return -// } + if !response.isSubscriptionActive { + AccountManager().signOut() + return + } updateDescription(for: response.expiresOrRenewsAt) } From d0e952e92a9058f79bf89d581f7317f1f013a662 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Wed, 13 Dec 2023 11:29:06 +0100 Subject: [PATCH 68/96] Switch BSK to a branch --- DuckDuckGo.xcodeproj/project.pbxproj | 5 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../Sources/Subscription/Logging.swift | 56 ------------------- 3 files changed, 4 insertions(+), 61 deletions(-) delete mode 100644 LocalPackages/Subscription/Sources/Subscription/Logging.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index f29986d25c..f1671b9048 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -8133,7 +8133,6 @@ 4B95793F2AC7AE700062CA31 /* LoginItems */, 7B31FD8F2AD1257B0086AA24 /* NetworkProtectionIPC */, 3143C8782B0D1F3D00382627 /* DataBrokerProtection */, - 7BF770642AD6CA14001C9182 /* PixelKit */, 1E0068AC2B1673BB00BBF43B /* SubscriptionUI */, 1E934E2A2B167CA80084722B /* Subscription */, ); @@ -12742,8 +12741,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { - kind = exactVersion; - version = 94.0.0; + branch = michal/logging; + kind = branch; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9aaa137dac..91ef332646 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "e4f4ae624174c1398d345cfc387db38f8f69986d", - "version" : "94.0.0" + "branch" : "michal/logging", + "revision" : "d6c1746f096cdf72b3eb03e7f1c2aeb2c6768106" } }, { diff --git a/LocalPackages/Subscription/Sources/Subscription/Logging.swift b/LocalPackages/Subscription/Sources/Subscription/Logging.swift deleted file mode 100644 index b15ec8e1fa..0000000000 --- a/LocalPackages/Subscription/Sources/Subscription/Logging.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// Logging.swift -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import Common - -struct Logging { - - static let subsystem = "com.duckduckgo.macos.browser.account" - - fileprivate static let accountLoggingEnabled = true - fileprivate static let account: OSLog = OSLog(subsystem: subsystem, category: "Account") - - fileprivate static let authServiceLoggingEnabled = true - fileprivate static let authService: OSLog = OSLog(subsystem: subsystem, category: "Account : AuthService") - - fileprivate static let subscriptionServiceLoggingEnabled = true - fileprivate static let subscriptionService: OSLog = OSLog(subsystem: subsystem, category: "Account : SubscriptionService") - - fileprivate static let errorsLoggingEnabled = true - fileprivate static let error: OSLog = OSLog(subsystem: subsystem, category: "Account : Errors") -} - -extension OSLog { - - public static var account: OSLog { - Logging.accountLoggingEnabled ? Logging.account : .disabled - } - - public static var authService: OSLog { - Logging.authServiceLoggingEnabled ? Logging.authService : .disabled - } - - public static var subscriptionService: OSLog { - Logging.subscriptionServiceLoggingEnabled ? Logging.subscriptionService : .disabled - } - - public static var error: OSLog { - Logging.errorsLoggingEnabled ? Logging.error : .disabled - } -} From a170b5c0ad3c596557743d915f7ba98b1d24e32e Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Wed, 13 Dec 2023 11:29:27 +0100 Subject: [PATCH 69/96] Update subscription logs --- .../Subscription/Sources/Subscription/AccountManager.swift | 6 +++--- .../Sources/Subscription/Services/APIService.swift | 2 +- .../Sources/Subscription/Services/AuthService.swift | 2 +- .../Sources/Subscription/Services/SubscriptionService.swift | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift b/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift index 655742298d..b711b25bc2 100644 --- a/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift +++ b/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift @@ -171,7 +171,7 @@ public class AccountManager { return entitlements.map { $0.name } case .failure(let error): - os_log("AccountManager error: %{public}@", log: .error, error.localizedDescription) + os_log(.error, log: .subscription, "AccountManager error: %{public}@", error.localizedDescription) return [] } } @@ -181,7 +181,7 @@ public class AccountManager { case .success(let response): return .success(response.accessToken) case .failure(let error): - os_log("AccountManager error: %{public}@", log: .error, error.localizedDescription) + os_log(.error, log: .subscription, "AccountManager error: %{public}@", error.localizedDescription) return .failure(error) } } @@ -193,7 +193,7 @@ public class AccountManager { case .success(let response): return .success(AccountDetails(email: response.account.email, externalID: response.account.externalID)) case .failure(let error): - os_log("AccountManager error: %{public}@", log: .error, error.localizedDescription) + os_log(.error, log: .subscription, "AccountManager error: %{public}@", error.localizedDescription) return .failure(error) } } diff --git a/LocalPackages/Subscription/Sources/Subscription/Services/APIService.swift b/LocalPackages/Subscription/Sources/Subscription/Services/APIService.swift index 5a7d8f0d1b..84181c377f 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Services/APIService.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Services/APIService.swift @@ -63,7 +63,7 @@ public extension APIService { } } } catch { - os_log("Service error: %{public}@", log: .error, error.localizedDescription) + os_log(.error, log: .subscription, "Service error: %{public}@", error.localizedDescription) return .failure(.connectionError) } } diff --git a/LocalPackages/Subscription/Sources/Subscription/Services/AuthService.swift b/LocalPackages/Subscription/Sources/Subscription/Services/AuthService.swift index 33a18262ad..7a9cd0be7d 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Services/AuthService.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Services/AuthService.swift @@ -21,7 +21,7 @@ import Common public struct AuthService: APIService { - public static let logger: OSLog = .authService + public static let logger: OSLog = .subscription public static let session = { let configuration = URLSessionConfiguration.ephemeral return URLSession(configuration: configuration) diff --git a/LocalPackages/Subscription/Sources/Subscription/Services/SubscriptionService.swift b/LocalPackages/Subscription/Sources/Subscription/Services/SubscriptionService.swift index 6c3ed27db1..376dca4061 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Services/SubscriptionService.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Services/SubscriptionService.swift @@ -21,7 +21,7 @@ import Common public struct SubscriptionService: APIService { - public static let logger: OSLog = .subscriptionService + public static let logger: OSLog = .subscription public static let session = { let configuration = URLSessionConfiguration.ephemeral return URLSession(configuration: configuration) From 49949370c6d7a448e1d451c908180671aee2bace Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Wed, 13 Dec 2023 15:27:18 +0100 Subject: [PATCH 70/96] Update logging --- .../Subscription/PurchaseManager.swift | 65 +++++++------------ .../Subscription/Services/APIService.swift | 4 +- .../Subscription/Services/AuthService.swift | 1 - .../Services/SubscriptionService.swift | 3 +- .../SubscriptionPurchaseEnvironment.swift | 11 +++- .../DebugMenu/SubscriptionDebugMenu.swift | 14 ---- 6 files changed, 34 insertions(+), 64 deletions(-) diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseManager.swift b/LocalPackages/Subscription/Sources/Subscription/PurchaseManager.swift index e75e096d76..1cca00c6e4 100644 --- a/LocalPackages/Subscription/Sources/Subscription/PurchaseManager.swift +++ b/LocalPackages/Subscription/Sources/Subscription/PurchaseManager.swift @@ -18,6 +18,7 @@ import Foundation import StoreKit +import Common @available(macOS 12.0, *) typealias Transaction = StoreKit.Transaction @available(macOS 12.0, *) typealias RenewalInfo = StoreKit.Product.SubscriptionInfo.RenewalInfo @@ -64,60 +65,47 @@ public final class PurchaseManager: ObservableObject { storefrontChanges?.cancel() } - @MainActor - public func hasProductsAvailable() async -> Bool { - do { - let availableProducts = try await Product.products(for: Self.productIdentifiers) - print(" -- [PurchaseManager] updateAvailableProducts(): fetched \(availableProducts.count)") - return !availableProducts.isEmpty - } catch { - print("Error fetching available products: \(error)") - return false - } - } - @MainActor @discardableResult public func syncAppleIDAccount() async -> Result { do { purchaseQueue.removeAll() - print("Before AppStore.sync()") + os_log(.info, log: .subscription, "[PurchaseManager] Before AppStore.sync()") try await AppStore.sync() - print("After AppStore.sync()") + os_log(.info, log: .subscription, "[PurchaseManager] After AppStore.sync()") await updatePurchasedProducts() await updateAvailableProducts() return .success(()) } catch { - print("AppStore.sync error: \(error)") + os_log(.error, log: .subscription, "[PurchaseManager] Error: %{public}s", String(reflecting: error)) return .failure(error) } } @MainActor public func updateAvailableProducts() async { - print(" -- [PurchaseManager] updateAvailableProducts()") + os_log(.info, log: .subscription, "[PurchaseManager] updateAvailableProducts") do { let availableProducts = try await Product.products(for: Self.productIdentifiers) - print(" -- [PurchaseManager] updateAvailableProducts(): fetched \(availableProducts.count) products") + os_log(.info, log: .subscription, "[PurchaseManager] updateAvailableProducts fetched %d products", availableProducts.count) if self.availableProducts != availableProducts { - print("availableProducts changed!") self.availableProducts = availableProducts } } catch { - print("Error updating available products: \(error)") + os_log(.error, log: .subscription, "[PurchaseManager] Error: %{public}s", String(reflecting: error)) } } @MainActor public func updatePurchasedProducts() async { - print(" -- [PurchaseManager] updatePurchasedProducts()") + os_log(.info, log: .subscription, "[PurchaseManager] updatePurchasedProducts") var purchasedSubscriptions: [String] = [] @@ -130,20 +118,15 @@ public final class PurchaseManager: ObservableObject { if let expirationDate = transaction.expirationDate, expirationDate > .now { purchasedSubscriptions.append(transaction.productID) - - if let token = transaction.appAccountToken { - print(" -- [PurchaseManager] updatePurchasedProducts(): \(transaction.productID) -- custom UUID: \(token)" ) - } } } } catch { - print("Error updating purchased products: \(error)") + os_log(.error, log: .subscription, "[PurchaseManager] Error: %{public}s", String(reflecting: error)) } - print(" -- [PurchaseManager] updatePurchasedProducts(): have \(purchasedSubscriptions.count) active subscriptions") + os_log(.info, log: .subscription, "[PurchaseManager] updatePurchasedProducts fetched %d active subscriptions", purchasedSubscriptions.count) if self.purchasedProductIDs != purchasedSubscriptions { - print("purchasedSubscriptions changed!") self.purchasedProductIDs = purchasedSubscriptions } @@ -152,22 +135,22 @@ public final class PurchaseManager: ObservableObject { @MainActor public static func mostRecentTransaction() async -> String? { - print(" -- [PurchaseManager] mostRecentTransaction()") + os_log(.info, log: .subscription, "[PurchaseManager] mostRecentTransaction") var transactions: [VerificationResult] = [] for await result in Transaction.all { transactions.append(result) } - - print(" -- [PurchaseManager] mostRecentTransaction(): fetched \(transactions.count) transactions") + + os_log(.info, log: .subscription, "[PurchaseManager] mostRecentTransaction fetched %d transactions", transactions.count) return transactions.first?.jwsRepresentation } @MainActor public static func hasActiveSubscription() async -> Bool { - print(" -- [PurchaseManager] hasActiveSubscription()") + os_log(.info, log: .subscription, "[PurchaseManager] hasActiveSubscription") var transactions: [VerificationResult] = [] @@ -175,29 +158,26 @@ public final class PurchaseManager: ObservableObject { transactions.append(result) } - print(" -- [PurchaseManager] hasActiveSubscription(): fetched \(transactions.count) transactions") + os_log(.info, log: .subscription, "[PurchaseManager] hasActiveSubscription fetched %d transactions", transactions.count) return !transactions.isEmpty } @MainActor public func purchaseSubscription(with identifier: String, externalID: String) async -> Result { - + guard let product = availableProducts.first(where: { $0.id == identifier }) else { return .failure(PurchaseManagerError.productNotFound) } - print(" -- [PurchaseManager] buy: \(product.displayName) (customUUID: \(externalID))") + os_log(.info, log: .subscription, "[PurchaseManager] purchaseSubscription %s (customUUID: %s)", product.displayName, externalID) - print("purchaseQueue append!") purchaseQueue.append(product.id) - print(" -- [PurchaseManager] starting purchase") - var options: Set = Set() if let token = UUID(uuidString: externalID) { options.insert(.appAccountToken(token)) } else { - print("Wrong UUID") + os_log(.error, log: .subscription, "[PurchaseManager] Error: Failed to create UUID") return .failure(PurchaseManagerError.externalIDisNotAValidUUID) } @@ -205,14 +185,13 @@ public final class PurchaseManager: ObservableObject { do { result = try await product.purchase(options: options) } catch { - print("error \(error)") + os_log(.error, log: .subscription, "[PurchaseManager] Error: %{public}s", String(reflecting: error)) return .failure(PurchaseManagerError.purchaseFailed) } - print(" -- [PurchaseManager] purchase complete") + os_log(.info, log: .subscription, "[PurchaseManager] purchaseSubscription complete") purchaseQueue.removeAll() - print("purchaseQueue removeAll!") switch result { case let .success(.verified(transaction)): @@ -252,7 +231,7 @@ public final class PurchaseManager: ObservableObject { Task.detached { [unowned self] in for await result in Transaction.updates { - print(" -- [PurchaseManager] observeTransactionUpdates()") + os_log(.info, log: .subscription, "[PurchaseManager] observeTransactionUpdates") if case .verified(let transaction) = result { await transaction.finish() @@ -267,7 +246,7 @@ public final class PurchaseManager: ObservableObject { Task.detached { [unowned self] in for await result in Storefront.updates { - print(" -- [PurchaseManager] observeStorefrontChanges(): \(result.countryCode)") + os_log(.info, log: .subscription, "[PurchaseManager] observeStorefrontChanges: %s", result.countryCode) await updatePurchasedProducts() await updateAvailableProducts() } diff --git a/LocalPackages/Subscription/Sources/Subscription/Services/APIService.swift b/LocalPackages/Subscription/Sources/Subscription/Services/APIService.swift index 84181c377f..e3d361e29d 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Services/APIService.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Services/APIService.swift @@ -32,7 +32,6 @@ struct ErrorResponse: Decodable { } public protocol APIService { - static var logger: OSLog { get } static var baseURL: URL { get } static var session: URLSession { get } static func executeAPICall(method: String, endpoint: String, headers: [String: String]?, body: Data?) async -> Result where T: Decodable @@ -93,7 +92,8 @@ public extension APIService { private static func printDebugInfo(method: String, endpoint: String, data: Data, response: URLResponse) { let statusCode = (response as? HTTPURLResponse)!.statusCode let stringData = String(data: data, encoding: .utf8) ?? "" - os_log("[%d] %{public}@ /%{public}@ :: %{public}@", log: logger, statusCode, method, endpoint, stringData) + + os_log(.info, log: .subscription, "[API] %d %{public}s /%{public}s :: %{private}s", statusCode, method, endpoint, stringData) } static func makeAuthorizationHeader(for token: String) -> [String: String] { diff --git a/LocalPackages/Subscription/Sources/Subscription/Services/AuthService.swift b/LocalPackages/Subscription/Sources/Subscription/Services/AuthService.swift index 7a9cd0be7d..4e4755ad62 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Services/AuthService.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Services/AuthService.swift @@ -21,7 +21,6 @@ import Common public struct AuthService: APIService { - public static let logger: OSLog = .subscription public static let session = { let configuration = URLSessionConfiguration.ephemeral return URLSession(configuration: configuration) diff --git a/LocalPackages/Subscription/Sources/Subscription/Services/SubscriptionService.swift b/LocalPackages/Subscription/Sources/Subscription/Services/SubscriptionService.swift index 376dca4061..8155e5c5b9 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Services/SubscriptionService.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Services/SubscriptionService.swift @@ -20,8 +20,7 @@ import Foundation import Common public struct SubscriptionService: APIService { - - public static let logger: OSLog = .subscription + public static let session = { let configuration = URLSessionConfiguration.ephemeral return URLSession(configuration: configuration) diff --git a/LocalPackages/Subscription/Sources/Subscription/SubscriptionPurchaseEnvironment.swift b/LocalPackages/Subscription/Sources/Subscription/SubscriptionPurchaseEnvironment.swift index 0e1c93968e..36ad6b9063 100644 --- a/LocalPackages/Subscription/Sources/Subscription/SubscriptionPurchaseEnvironment.swift +++ b/LocalPackages/Subscription/Sources/Subscription/SubscriptionPurchaseEnvironment.swift @@ -17,15 +17,18 @@ // import Foundation +import Common public final class SubscriptionPurchaseEnvironment { - public enum Environment { + public enum Environment: String { case appStore, stripe } public static var current: Environment = .appStore { didSet { + os_log(.info, log: .subscription, "[SubscriptionPurchaseEnvironment] Setting to %@", current.rawValue) + canPurchase = false switch current { @@ -37,7 +40,11 @@ public final class SubscriptionPurchaseEnvironment { } } - public static var canPurchase: Bool = false + public static var canPurchase: Bool = false { + didSet { + os_log(.info, log: .subscription, "[SubscriptionPurchaseEnvironment] canPurchase %@", (canPurchase ? "true" : "false")) + } + } private static func setupForAppStore() { if #available(macOS 12.0, *) { diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift index e8a08a50f6..2e00069fdb 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift @@ -54,9 +54,6 @@ public final class SubscriptionDebugMenu: NSMenuItem { menu.addItem(NSMenuItem(title: "Validate Token", action: #selector(validateToken), target: self)) menu.addItem(NSMenuItem(title: "Check Entitlements", action: #selector(checkEntitlements), target: self)) menu.addItem(NSMenuItem(title: "Get Subscription Info", action: #selector(getSubscriptionDetails), target: self)) - if #available(macOS 12.0, *) { - menu.addItem(NSMenuItem(title: "Check Purchase Products Availability", action: #selector(checkProductsAvailability), target: self)) - } menu.addItem(NSMenuItem(title: "Restore Subscription from App Store transaction", action: #selector(restorePurchases), target: self)) menu.addItem(.separator()) if #available(macOS 12.0, *) { @@ -138,17 +135,6 @@ public final class SubscriptionDebugMenu: NSMenuItem { } } - @available(macOS 12.0, *) - @objc - func checkProductsAvailability() { - Task { - - let result = await purchaseManager.hasProductsAvailable() - showAlert(title: "Check App Store Product Availability", - message: "Can purchase: \(result ? "YES" : "NO")") - } - } - @objc func restorePurchases(_ sender: Any?) { if #available(macOS 12.0, *) { From 1f638ebd402f9ea90caee49edc9cc5c5a27567ed Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Wed, 13 Dec 2023 15:28:20 +0100 Subject: [PATCH 71/96] =?UTF-8?q?Add=20conditionals=20for=20iOS=2015=20plu?= =?UTF-8?q?s=20iOS=20product=20identifiers=20and=20linting=20=E2=80=A6=20(?= =?UTF-8?q?#1963)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add conditionals for iOS 15 plus iOS product identifiers and linting fixes Co-authored-by: Michal Smaga --- .../AppStore/AppStoreAccountManagementFlow.swift | 2 +- .../Flows/AppStore/AppStorePurchaseFlow.swift | 2 +- .../Flows/AppStore/AppStoreRestoreFlow.swift | 3 ++- .../Flows/Stripe/StripePurchaseFlow.swift | 2 +- .../Sources/Subscription/PurchaseManager.swift | 11 ++++++----- .../SubscriptionPurchaseEnvironment.swift | 4 ++-- 6 files changed, 13 insertions(+), 11 deletions(-) diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreAccountManagementFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreAccountManagementFlow.swift index 5d42ea7130..a3a8b29add 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreAccountManagementFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreAccountManagementFlow.swift @@ -34,7 +34,7 @@ public final class AppStoreAccountManagementFlow { if case let .failure(error) = await AuthService.validateToken(accessToken: authToken) { print(error) - if #available(macOS 12.0, *) { + if #available(macOS 12.0, iOS 15.0, *) { // In case of invalid token attempt store based authentication to obtain a new one guard let lastTransactionJWSRepresentation = await PurchaseManager.mostRecentTransaction() else { return .failure(.noPastTransaction) } diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift index b65df679c6..2d4497cb00 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift @@ -19,7 +19,7 @@ import Foundation import StoreKit -@available(macOS 12.0, *) +@available(macOS 12.0, iOS 15.0, *) public final class AppStorePurchaseFlow { public enum Error: Swift.Error { diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift index 4c8cfc593f..a23a3255f7 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift @@ -19,9 +19,10 @@ import Foundation import StoreKit -@available(macOS 12.0, *) +@available(macOS 12.0, iOS 15.0, *) public final class AppStoreRestoreFlow { + // swiftlint:disable:next large_tuple public typealias RestoredAccountDetails = (authToken: String, accessToken: String, externalID: String, email: String?) public enum Error: Swift.Error { diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/Stripe/StripePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/Stripe/StripePurchaseFlow.swift index 34fd2de672..67070f383b 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/Stripe/StripePurchaseFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Flows/Stripe/StripePurchaseFlow.swift @@ -84,7 +84,7 @@ public final class StripePurchaseFlow { } } - if #available(macOS 12.0, *) { + if #available(macOS 12.0, iOS 15.0, *) { await AppStorePurchaseFlow.checkForEntitlements(wait: 2.0, retry: 5) } } diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseManager.swift b/LocalPackages/Subscription/Sources/Subscription/PurchaseManager.swift index 1cca00c6e4..07fe3e789e 100644 --- a/LocalPackages/Subscription/Sources/Subscription/PurchaseManager.swift +++ b/LocalPackages/Subscription/Sources/Subscription/PurchaseManager.swift @@ -20,9 +20,9 @@ import Foundation import StoreKit import Common -@available(macOS 12.0, *) typealias Transaction = StoreKit.Transaction -@available(macOS 12.0, *) typealias RenewalInfo = StoreKit.Product.SubscriptionInfo.RenewalInfo -@available(macOS 12.0, *) typealias RenewalState = StoreKit.Product.SubscriptionInfo.RenewalState +@available(macOS 12.0, iOS 15.0, *) typealias Transaction = StoreKit.Transaction +@available(macOS 12.0, iOS 15.0, *) typealias RenewalInfo = StoreKit.Product.SubscriptionInfo.RenewalInfo +@available(macOS 12.0, iOS 15.0, *) typealias RenewalState = StoreKit.Product.SubscriptionInfo.RenewalState public enum StoreError: Error { case failedVerification @@ -38,10 +38,11 @@ enum PurchaseManagerError: Error { case unknownError } -@available(macOS 12.0, *) +@available(macOS 12.0, iOS 15.0, *) public final class PurchaseManager: ObservableObject { - static let productIdentifiers = ["subscription.1week", "subscription.1month", "subscription.1year", + static let productIdentifiers = ["ios.subscription.1month", "ios.subscription.1year", + "subscription.1week", "subscription.1month", "subscription.1year", "review.subscription.1week", "review.subscription.1month", "review.subscription.1year"] public static let shared = PurchaseManager() diff --git a/LocalPackages/Subscription/Sources/Subscription/SubscriptionPurchaseEnvironment.swift b/LocalPackages/Subscription/Sources/Subscription/SubscriptionPurchaseEnvironment.swift index 36ad6b9063..8ab9aa219d 100644 --- a/LocalPackages/Subscription/Sources/Subscription/SubscriptionPurchaseEnvironment.swift +++ b/LocalPackages/Subscription/Sources/Subscription/SubscriptionPurchaseEnvironment.swift @@ -24,7 +24,7 @@ public final class SubscriptionPurchaseEnvironment { public enum Environment: String { case appStore, stripe } - + public static var current: Environment = .appStore { didSet { os_log(.info, log: .subscription, "[SubscriptionPurchaseEnvironment] Setting to %@", current.rawValue) @@ -47,7 +47,7 @@ public final class SubscriptionPurchaseEnvironment { } private static func setupForAppStore() { - if #available(macOS 12.0, *) { + if #available(macOS 12.0, iOS 15.0, *) { Task { await PurchaseManager.shared.updateAvailableProducts() canPurchase = !PurchaseManager.shared.availableProducts.isEmpty From b330c99a1bb4dc5ce042e7e4a81a4d60b199a69e Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Wed, 13 Dec 2023 16:00:03 +0100 Subject: [PATCH 72/96] Update logs --- .../SubscriptionPagesUserScript.swift | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 58c9d82de3..5b1fe01d7d 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -210,7 +210,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { return nil } - print("Selected: \(subscriptionSelection.id)") + os_log(.info, log: .subscription, "[Purchase] Starting purchase for: %s", subscriptionSelection.id) await mainViewController?.presentAsSheet(progressViewController) @@ -224,29 +224,35 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { // Check for active subscriptions if await PurchaseManager.hasActiveSubscription() { - print("hasActiveSubscription: TRUE") + os_log(.info, log: .subscription, "[Purchase] Found active subscription during purchase") await WindowControllersManager.shared.lastKeyMainWindowController?.showSubscriptionFoundAlert(originalMessage: message) return nil } let emailAccessToken = try? EmailManager().getToken() + os_log(.info, log: .subscription, "[Purchase] Purchasing") switch await AppStorePurchaseFlow.purchaseSubscription(with: subscriptionSelection.id, emailAccessToken: emailAccessToken) { case .success: break - case .failure: + case .failure(let error): + os_log(.error, log: .subscription, "[Purchase] Error: %{public}s", String(reflecting: error)) return nil } await progressViewController.updateTitleText("Completing purchase...") + os_log(.info, log: .subscription, "[Purchase] Completing purchase") + switch await AppStorePurchaseFlow.completeSubscriptionPurchase() { case .success(let purchaseUpdate): await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: purchaseUpdate) - case .failure: - // TODO: handle errors - missing entitlements on post purchase check + case .failure(let error): + os_log(.error, log: .subscription, "[Purchase] Error: %{public}s", String(reflecting: error)) await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: PurchaseUpdate(type: "completed")) } + + os_log(.info, log: .subscription, "[Purchase] Purchase complete") } #endif From 9b1e950a170ec5af1b382d644d077850be3a812e Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Wed, 13 Dec 2023 16:03:24 +0100 Subject: [PATCH 73/96] Fix missed handling of an error case --- DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift | 1 + .../Subscription/Flows/AppStore/AppStorePurchaseFlow.swift | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 5b1fe01d7d..8499df7a38 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -237,6 +237,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { break case .failure(let error): os_log(.error, log: .subscription, "[Purchase] Error: %{public}s", String(reflecting: error)) + await WindowControllersManager.shared.lastKeyMainWindowController?.showSomethingWentWrongAlert() return nil } diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift index 2d4497cb00..b4efad52bb 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift @@ -68,7 +68,7 @@ public final class AppStorePurchaseFlow { externalID = expiredAccountDetails.externalID accountManager.storeAuthToken(token: expiredAccountDetails.authToken) accountManager.storeAccount(token: expiredAccountDetails.accessToken, email: expiredAccountDetails.email, externalID: expiredAccountDetails.externalID) - case .missingAccountOrTransactions: + case .missingAccountOrTransactions, .pastTransactionAuthenticationError: // No history, create new account switch await AuthService.createAccount(emailAccessToken: emailAccessToken) { case .success(let response): From 7bb6312b78bc61d26c02a70daab2058cb146af12 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Wed, 13 Dec 2023 20:07:50 +0100 Subject: [PATCH 74/96] Tweak logs --- DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift | 2 +- .../Subscription/Sources/Subscription/PurchaseManager.swift | 2 +- .../Sources/Subscription/Services/APIService.swift | 2 +- .../Subscription/SubscriptionPurchaseEnvironment.swift | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 8499df7a38..d4330c2f96 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -210,7 +210,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { return nil } - os_log(.info, log: .subscription, "[Purchase] Starting purchase for: %s", subscriptionSelection.id) + os_log(.info, log: .subscription, "[Purchase] Starting purchase for: %{public}%s", subscriptionSelection.id) await mainViewController?.presentAsSheet(progressViewController) diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseManager.swift b/LocalPackages/Subscription/Sources/Subscription/PurchaseManager.swift index 07fe3e789e..ed4d93047c 100644 --- a/LocalPackages/Subscription/Sources/Subscription/PurchaseManager.swift +++ b/LocalPackages/Subscription/Sources/Subscription/PurchaseManager.swift @@ -169,7 +169,7 @@ public final class PurchaseManager: ObservableObject { guard let product = availableProducts.first(where: { $0.id == identifier }) else { return .failure(PurchaseManagerError.productNotFound) } - os_log(.info, log: .subscription, "[PurchaseManager] purchaseSubscription %s (customUUID: %s)", product.displayName, externalID) + os_log(.info, log: .subscription, "[PurchaseManager] purchaseSubscription %{public}s (%{public}s)", product.displayName, externalID) purchaseQueue.append(product.id) diff --git a/LocalPackages/Subscription/Sources/Subscription/Services/APIService.swift b/LocalPackages/Subscription/Sources/Subscription/Services/APIService.swift index e3d361e29d..503ed1b18b 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Services/APIService.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Services/APIService.swift @@ -93,7 +93,7 @@ public extension APIService { let statusCode = (response as? HTTPURLResponse)!.statusCode let stringData = String(data: data, encoding: .utf8) ?? "" - os_log(.info, log: .subscription, "[API] %d %{public}s /%{public}s :: %{private}s", statusCode, method, endpoint, stringData) + os_log(.info, log: .subscription, "[API] %d %{public}s /%{public}s :: %{public}s", statusCode, method, endpoint, stringData) } static func makeAuthorizationHeader(for token: String) -> [String: String] { diff --git a/LocalPackages/Subscription/Sources/Subscription/SubscriptionPurchaseEnvironment.swift b/LocalPackages/Subscription/Sources/Subscription/SubscriptionPurchaseEnvironment.swift index 8ab9aa219d..95c9094161 100644 --- a/LocalPackages/Subscription/Sources/Subscription/SubscriptionPurchaseEnvironment.swift +++ b/LocalPackages/Subscription/Sources/Subscription/SubscriptionPurchaseEnvironment.swift @@ -27,7 +27,7 @@ public final class SubscriptionPurchaseEnvironment { public static var current: Environment = .appStore { didSet { - os_log(.info, log: .subscription, "[SubscriptionPurchaseEnvironment] Setting to %@", current.rawValue) + os_log(.info, log: .subscription, "[SubscriptionPurchaseEnvironment] Setting to %{public}%@", current.rawValue) canPurchase = false @@ -42,7 +42,7 @@ public final class SubscriptionPurchaseEnvironment { public static var canPurchase: Bool = false { didSet { - os_log(.info, log: .subscription, "[SubscriptionPurchaseEnvironment] canPurchase %@", (canPurchase ? "true" : "false")) + os_log(.info, log: .subscription, "[SubscriptionPurchaseEnvironment] canPurchase %{public}%@", (canPurchase ? "true" : "false")) } } From 67e385b76b48d46afcef60de5e1856bfd9e37032 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Wed, 13 Dec 2023 21:09:03 +0100 Subject: [PATCH 75/96] Further update to logs --- .../Tab/UserScripts/SubscriptionPagesUserScript.swift | 2 +- .../Sources/Subscription/AccountManager.swift | 10 +++++++++- .../Flows/AppStore/AppStorePurchaseFlow.swift | 11 +++++++---- .../Flows/AppStore/AppStoreRestoreFlow.swift | 2 +- .../SubscriptionPurchaseEnvironment.swift | 4 ++-- 5 files changed, 20 insertions(+), 9 deletions(-) diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index d4330c2f96..17b7bf9315 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -210,7 +210,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { return nil } - os_log(.info, log: .subscription, "[Purchase] Starting purchase for: %{public}%s", subscriptionSelection.id) + os_log(.info, log: .subscription, "[Purchase] Starting purchase for: %{public}s", subscriptionSelection.id) await mainViewController?.presentAsSheet(progressViewController) diff --git a/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift b/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift index b711b25bc2..de33226f8e 100644 --- a/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift +++ b/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift @@ -96,8 +96,10 @@ public class AccountManager { return nil } } - + public func storeAuthToken(token: String) { + os_log(.info, log: .subscription, "[AccountManager] storeAuthToken") + do { try storage.store(authToken: token) } catch { @@ -110,6 +112,8 @@ public class AccountManager { } public func storeAccount(token: String, email: String?, externalID: String?) { + os_log(.info, log: .subscription, "[AccountManager] storeAccount") + do { try storage.store(accessToken: token) } catch { @@ -143,6 +147,8 @@ public class AccountManager { } public func signOut() { + os_log(.info, log: .subscription, "[AccountManager] signOut") + do { try storage.clearAuthenticationState() } catch { @@ -199,6 +205,8 @@ public class AccountManager { } public func checkSubscriptionState() async { + os_log(.info, log: .subscription, "[AccountManager] checkSubscriptionState") + guard let token = accessToken else { return } if case .success(let response) = await SubscriptionService.getSubscriptionDetails(token: token) { diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift index b4efad52bb..e26ca93510 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift @@ -18,6 +18,7 @@ import Foundation import StoreKit +import Common @available(macOS 12.0, iOS 15.0, *) public final class AppStorePurchaseFlow { @@ -55,20 +56,24 @@ public final class AppStorePurchaseFlow { } public static func purchaseSubscription(with subscriptionIdentifier: String, emailAccessToken: String?) async -> Result { + os_log(.info, log: .subscription, "[AppStorePurchaseFlow] purchaseSubscription") + let accountManager = AccountManager() let externalID: String // Check for past transactions most recent switch await AppStoreRestoreFlow.restoreAccountFromPastPurchase() { case .success: + os_log(.info, log: .subscription, "[AppStorePurchaseFlow] purchaseSubscription -> AppStoreRestoreFlow.restoreAccountFromPastPurchase: activeSubscriptionAlreadyPresent") return .failure(.activeSubscriptionAlreadyPresent) case .failure(let error): + os_log(.info, log: .subscription, "[AppStorePurchaseFlow] purchaseSubscription -> AppStoreRestoreFlow.restoreAccountFromPastPurchase: %{public}s", String(reflecting: error)) switch error { case .subscriptionExpired(let expiredAccountDetails): externalID = expiredAccountDetails.externalID accountManager.storeAuthToken(token: expiredAccountDetails.authToken) accountManager.storeAccount(token: expiredAccountDetails.accessToken, email: expiredAccountDetails.email, externalID: expiredAccountDetails.externalID) - case .missingAccountOrTransactions, .pastTransactionAuthenticationError: + default: // No history, create new account switch await AuthService.createAccount(emailAccessToken: emailAccessToken) { case .success(let response): @@ -82,8 +87,6 @@ public final class AppStorePurchaseFlow { case .failure: return .failure(.accountCreationFailed) } - default: - return .failure(.authenticatingWithTransactionFailed) } } @@ -92,7 +95,7 @@ public final class AppStorePurchaseFlow { case .success: return .success(()) case .failure(let error): - print("Something went wrong, reason: \(error)") + os_log(.error, log: .subscription, "[AppStorePurchaseFlow] Error: %{public}s", String(reflecting: error)) AccountManager().signOut() return .failure(.purchaseFailed) } diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift index a23a3255f7..40c80d34b5 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift @@ -75,7 +75,7 @@ public final class AppStoreRestoreFlow { case .success(let response): isSubscriptionActive = response.isSubscriptionActive case .failure: - return .failure(.somethingWentWrong) + return .failure(.failedToFetchSubscriptionDetails) } if isSubscriptionActive { diff --git a/LocalPackages/Subscription/Sources/Subscription/SubscriptionPurchaseEnvironment.swift b/LocalPackages/Subscription/Sources/Subscription/SubscriptionPurchaseEnvironment.swift index 95c9094161..58034b1c82 100644 --- a/LocalPackages/Subscription/Sources/Subscription/SubscriptionPurchaseEnvironment.swift +++ b/LocalPackages/Subscription/Sources/Subscription/SubscriptionPurchaseEnvironment.swift @@ -27,7 +27,7 @@ public final class SubscriptionPurchaseEnvironment { public static var current: Environment = .appStore { didSet { - os_log(.info, log: .subscription, "[SubscriptionPurchaseEnvironment] Setting to %{public}%@", current.rawValue) + os_log(.info, log: .subscription, "[SubscriptionPurchaseEnvironment] Setting to %{public}s", current.rawValue) canPurchase = false @@ -42,7 +42,7 @@ public final class SubscriptionPurchaseEnvironment { public static var canPurchase: Bool = false { didSet { - os_log(.info, log: .subscription, "[SubscriptionPurchaseEnvironment] canPurchase %{public}%@", (canPurchase ? "true" : "false")) + os_log(.info, log: .subscription, "[SubscriptionPurchaseEnvironment] canPurchase %{public}s", (canPurchase ? "true" : "false")) } } From a0c09d9221b524c3166e12b777e065ac1da18446 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Thu, 14 Dec 2023 09:26:33 +0100 Subject: [PATCH 76/96] Additional logs --- .../Subscription/Sources/Subscription/PurchaseManager.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseManager.swift b/LocalPackages/Subscription/Sources/Subscription/PurchaseManager.swift index ed4d93047c..86b4889c34 100644 --- a/LocalPackages/Subscription/Sources/Subscription/PurchaseManager.swift +++ b/LocalPackages/Subscription/Sources/Subscription/PurchaseManager.swift @@ -196,22 +196,26 @@ public final class PurchaseManager: ObservableObject { switch result { case let .success(.verified(transaction)): + os_log(.info, log: .subscription, "[PurchaseManager] purchaseSubscription result: success") // Successful purchase await transaction.finish() await self.updatePurchasedProducts() return .success(()) case let .success(.unverified(_, error)): + os_log(.info, log: .subscription, "[PurchaseManager] purchaseSubscription result: success /unverified/ - %{public}s", String(reflecting: error)) // Successful purchase but transaction/receipt can't be verified // Could be a jailbroken phone - print("Error: \(error.localizedDescription)") return .failure(PurchaseManagerError.transactionCannotBeVerified) case .pending: + os_log(.info, log: .subscription, "[PurchaseManager] purchaseSubscription result: pending") // Transaction waiting on SCA (Strong Customer Authentication) or // approval from Ask to Buy return .failure(PurchaseManagerError.transactionPendingAuthentication) case .userCancelled: + os_log(.info, log: .subscription, "[PurchaseManager] purchaseSubscription result: user cancelled") return .failure(PurchaseManagerError.purchaseCancelledByUser) @unknown default: + os_log(.info, log: .subscription, "[PurchaseManager] purchaseSubscription result: unknown") return .failure(PurchaseManagerError.unknownError) } } From f994e0b62325eec729a8b320bc1478b9e53205a8 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 15 Dec 2023 11:18:33 +0100 Subject: [PATCH 77/96] Fix whitespace --- .../Subscription/Sources/Subscription/AccountManager.swift | 2 +- .../Subscription/Flows/AppStore/AppStoreRestoreFlow.swift | 2 +- .../Subscription/Sources/Subscription/PurchaseManager.swift | 2 +- .../Sources/Subscription/Services/SubscriptionService.swift | 2 +- .../Sources/Subscription/SubscriptionPurchaseEnvironment.swift | 2 +- .../Sources/SubscriptionUI/NSAlert+Subscription.swift | 1 - 6 files changed, 5 insertions(+), 6 deletions(-) diff --git a/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift b/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift index de33226f8e..d3d3ca9182 100644 --- a/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift +++ b/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift @@ -92,7 +92,7 @@ public class AccountManager { } else { assertionFailure("Expected AccountKeychainAccessError") } - + return nil } } diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift index 40c80d34b5..0623f0452e 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift @@ -37,7 +37,7 @@ public final class AppStoreRestoreFlow { public static func restoreAccountFromPastPurchase() async -> Result { guard let lastTransactionJWSRepresentation = await PurchaseManager.mostRecentTransaction() else { return .failure(.missingAccountOrTransactions) } - + let accountManager = AccountManager() // Do the store login to get short-lived token diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseManager.swift b/LocalPackages/Subscription/Sources/Subscription/PurchaseManager.swift index 86b4889c34..5b04b7f5cf 100644 --- a/LocalPackages/Subscription/Sources/Subscription/PurchaseManager.swift +++ b/LocalPackages/Subscription/Sources/Subscription/PurchaseManager.swift @@ -143,7 +143,7 @@ public final class PurchaseManager: ObservableObject { for await result in Transaction.all { transactions.append(result) } - + os_log(.info, log: .subscription, "[PurchaseManager] mostRecentTransaction fetched %d transactions", transactions.count) return transactions.first?.jwsRepresentation diff --git a/LocalPackages/Subscription/Sources/Subscription/Services/SubscriptionService.swift b/LocalPackages/Subscription/Sources/Subscription/Services/SubscriptionService.swift index 8155e5c5b9..96952d01be 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Services/SubscriptionService.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Services/SubscriptionService.swift @@ -20,7 +20,7 @@ import Foundation import Common public struct SubscriptionService: APIService { - + public static let session = { let configuration = URLSessionConfiguration.ephemeral return URLSession(configuration: configuration) diff --git a/LocalPackages/Subscription/Sources/Subscription/SubscriptionPurchaseEnvironment.swift b/LocalPackages/Subscription/Sources/Subscription/SubscriptionPurchaseEnvironment.swift index 58034b1c82..4414fd9650 100644 --- a/LocalPackages/Subscription/Sources/Subscription/SubscriptionPurchaseEnvironment.swift +++ b/LocalPackages/Subscription/Sources/Subscription/SubscriptionPurchaseEnvironment.swift @@ -24,7 +24,7 @@ public final class SubscriptionPurchaseEnvironment { public enum Environment: String { case appStore, stripe } - + public static var current: Environment = .appStore { didSet { os_log(.info, log: .subscription, "[SubscriptionPurchaseEnvironment] Setting to %{public}s", current.rawValue) diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/NSAlert+Subscription.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/NSAlert+Subscription.swift index 675b371019..be47768dab 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/NSAlert+Subscription.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/NSAlert+Subscription.swift @@ -47,7 +47,6 @@ public extension NSAlert { return alert } - static func subscriptionFoundAlert() -> NSAlert { let alert = NSAlert() alert.messageText = "Subscription Found" From b9a539e3b13891dfc87747ec5d294ca7c3c3966e Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 15 Dec 2023 11:34:08 +0100 Subject: [PATCH 78/96] Fix further linting errors --- DuckDuckGo/Application/AppDelegate.swift | 1 + .../Preferences/View/PreferencesRootView.swift | 3 +-- .../SubscriptionPagesUserScript.swift | 3 +-- .../Flows/AppStore/AppStoreRestoreFlow.swift | 1 - .../SubscriptionTests/SubscriptionTests.swift | 18 ++++++++++++++++++ ...onTests.swift => SubscriptionUITests.swift} | 4 ++-- 6 files changed, 23 insertions(+), 7 deletions(-) rename LocalPackages/SubscriptionUI/Tests/SubscriptionUITests/{SubscriptionTests.swift => SubscriptionUITests.swift} (90%) diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index d0534c759a..24542e8f9f 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -183,6 +183,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, FileDownloadManagerDel appIconChanger = AppIconChanger(internalUserDecider: internalUserDecider) } + // swiftlint:disable:next function_body_length func applicationDidFinishLaunching(_ notification: Notification) { guard NSApp.runType.requiresEnvironment else { return } defer { diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index 2274f3528a..850192ffb2 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -97,8 +97,7 @@ extension Preferences { } #if SUBSCRIPTION - // swiftlint:disable:next cyclomatic_complexity - // swiftlint:disable:next function_body_length + // swiftlint:disable:next cyclomatic_complexity function_body_length private func makeSubscriptionView() -> some View { let actionHandler = PreferencesSubscriptionActionHandlers(openURL: { url in WindowControllersManager.shared.show(url: url, source: .ui, newTab: true) diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 17b7bf9315..4975f9f79f 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -177,6 +177,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { #endif } + // swiftlint:disable:next cyclomatic_complexity function_body_length func subscriptionSelected(params: Any, original: WKScriptMessage) async throws -> Encodable? { struct SubscriptionSelection: Decodable { let id: String @@ -378,8 +379,6 @@ extension MainWindowController { window.show(.subscriptionInactiveAlert(), firstButtonAction: { WindowControllersManager.shared.show(url: .purchaseSubscription, source: .ui, newTab: true) -// AccountManager().signOut() - // TODO: Check if it is required }) } diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift index 0623f0452e..61fa8ea641 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift @@ -22,7 +22,6 @@ import StoreKit @available(macOS 12.0, iOS 15.0, *) public final class AppStoreRestoreFlow { - // swiftlint:disable:next large_tuple public typealias RestoredAccountDetails = (authToken: String, accessToken: String, externalID: String, email: String?) public enum Error: Swift.Error { diff --git a/LocalPackages/Subscription/Tests/SubscriptionTests/SubscriptionTests.swift b/LocalPackages/Subscription/Tests/SubscriptionTests/SubscriptionTests.swift index 32417fc73a..43aed49cab 100644 --- a/LocalPackages/Subscription/Tests/SubscriptionTests/SubscriptionTests.swift +++ b/LocalPackages/Subscription/Tests/SubscriptionTests/SubscriptionTests.swift @@ -1,3 +1,21 @@ +// +// SubscriptionTests.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + import XCTest @testable import Subscription diff --git a/LocalPackages/SubscriptionUI/Tests/SubscriptionUITests/SubscriptionTests.swift b/LocalPackages/SubscriptionUI/Tests/SubscriptionUITests/SubscriptionUITests.swift similarity index 90% rename from LocalPackages/SubscriptionUI/Tests/SubscriptionUITests/SubscriptionTests.swift rename to LocalPackages/SubscriptionUI/Tests/SubscriptionUITests/SubscriptionUITests.swift index 014c2b2c34..6adc3d6a82 100644 --- a/LocalPackages/SubscriptionUI/Tests/SubscriptionUITests/SubscriptionTests.swift +++ b/LocalPackages/SubscriptionUI/Tests/SubscriptionUITests/SubscriptionUITests.swift @@ -1,5 +1,5 @@ // -// SubscriptionTests.swift +// SubscriptionUITests.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -24,6 +24,6 @@ final class SubscriptionTests: XCTestCase { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct // results. - XCTAssertEqual(SubscriptionUI().text, "Hello, World!") +// XCTAssertEqual(SubscriptionUI().text, "Hello, World!") } } From 82cbbee3ffe296a50534a942d7d516f66401027a Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 15 Dec 2023 13:05:27 +0100 Subject: [PATCH 79/96] Further logging enhancements --- .../SubscriptionPagesUserScript.swift | 9 +++------ .../Sources/Subscription/AccountManager.swift | 6 +++--- .../AppStore/AppStoreAccountManagementFlow.swift | 10 +++++++--- .../Flows/AppStore/AppStorePurchaseFlow.swift | 16 +++++++++++----- .../Flows/AppStore/AppStoreRestoreFlow.swift | 13 ++++++++++++- .../Subscription/Flows/PurchaseFlow.swift | 4 ---- .../Flows/Stripe/StripePurchaseFlow.swift | 13 ++++++++++--- 7 files changed, 46 insertions(+), 25 deletions(-) diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 4975f9f79f..01f3684cbb 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -262,8 +262,6 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { } func activateSubscription(params: Any, original: WKScriptMessage) async throws -> Encodable? { - print(">>> Selected to activate a subscription -- show the activation settings screen") - let message = original Task { @MainActor in @@ -320,12 +318,13 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { } print(">>> Selected a feature -- show the corresponding UI", featureSelection) + + // TODO: implement selection + return nil } func completeStripePayment(params: Any, original: WKScriptMessage) async throws -> Encodable? { - print(">>> completeStripePayment") - let mainViewController = await WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController let progressViewController = await ProgressViewController(title: "Completing purchase...") @@ -349,8 +348,6 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { func pushAction(method: SubscribeActionName, webView: WKWebView, params: Encodable) { let broker = UserScriptMessageBroker(context: SubscriptionPagesUserScript.context, requiresRunInPageContentWorld: true ) - - print(">>> Pushing into WebView:", method.rawValue, String(describing: params)) broker.push(method: method.rawValue, params: params, for: self, into: webView) } } diff --git a/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift b/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift index d3d3ca9182..2bfbe2baf3 100644 --- a/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift +++ b/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift @@ -177,7 +177,7 @@ public class AccountManager { return entitlements.map { $0.name } case .failure(let error): - os_log(.error, log: .subscription, "AccountManager error: %{public}@", error.localizedDescription) + os_log(.error, log: .subscription, "[AccountManager] fetchEntitlements error: %{public}@", error.localizedDescription) return [] } } @@ -187,7 +187,7 @@ public class AccountManager { case .success(let response): return .success(response.accessToken) case .failure(let error): - os_log(.error, log: .subscription, "AccountManager error: %{public}@", error.localizedDescription) + os_log(.error, log: .subscription, "[AccountManager] exchangeAuthTokenToAccessToken error: %{public}@", error.localizedDescription) return .failure(error) } } @@ -199,7 +199,7 @@ public class AccountManager { case .success(let response): return .success(AccountDetails(email: response.account.email, externalID: response.account.externalID)) case .failure(let error): - os_log(.error, log: .subscription, "AccountManager error: %{public}@", error.localizedDescription) + os_log(.error, log: .subscription, "[AccountManager] fetchAccountDetails error: %{public}@", error.localizedDescription) return .failure(error) } } diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreAccountManagementFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreAccountManagementFlow.swift index a3a8b29add..755e62f3c8 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreAccountManagementFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreAccountManagementFlow.swift @@ -18,6 +18,7 @@ import Foundation import StoreKit +import Common public final class AppStoreAccountManagementFlow { @@ -28,11 +29,13 @@ public final class AppStoreAccountManagementFlow { @discardableResult public static func refreshAuthTokenIfNeeded() async -> Result { + os_log(.info, log: .subscription, "[AppStoreAccountManagementFlow] refreshAuthTokenIfNeeded") + var authToken = AccountManager().authToken ?? "" // Check if auth token if still valid - if case let .failure(error) = await AuthService.validateToken(accessToken: authToken) { - print(error) + if case let .failure(validateTokenError) = await AuthService.validateToken(accessToken: authToken) { + os_log(.error, log: .subscription, "[AppStoreAccountManagementFlow] validateToken error: %{public}s", String(reflecting: validateTokenError)) if #available(macOS 12.0, iOS 15.0, *) { // In case of invalid token attempt store based authentication to obtain a new one @@ -44,7 +47,8 @@ public final class AppStoreAccountManagementFlow { authToken = response.authToken AccountManager().storeAuthToken(token: authToken) } - case .failure: + case .failure(let storeLoginError): + os_log(.error, log: .subscription, "[AppStoreAccountManagementFlow] storeLogin error: %{public}s", String(reflecting: storeLoginError)) return .failure(.authenticatingWithTransactionFailed) } } diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift index e26ca93510..002232a618 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift @@ -37,13 +37,17 @@ public final class AppStorePurchaseFlow { } public static func subscriptionOptions() async -> Result { + os_log(.info, log: .subscription, "[AppStorePurchaseFlow] subscriptionOptions") let products = PurchaseManager.shared.availableProducts let monthly = products.first(where: { $0.id.contains("1month") }) let yearly = products.first(where: { $0.id.contains("1year") }) - guard let monthly, let yearly else { return .failure(.noProductsFound) } + guard let monthly, let yearly else { + os_log(.error, log: .subscription, "[AppStorePurchaseFlow] Error: noProductsFound") + return .failure(.noProductsFound) + } let options = [SubscriptionOption(id: monthly.id, cost: .init(displayPrice: monthly.displayPrice, recurrence: "monthly")), SubscriptionOption(id: yearly.id, cost: .init(displayPrice: yearly.displayPrice, recurrence: "yearly"))] @@ -64,10 +68,10 @@ public final class AppStorePurchaseFlow { // Check for past transactions most recent switch await AppStoreRestoreFlow.restoreAccountFromPastPurchase() { case .success: - os_log(.info, log: .subscription, "[AppStorePurchaseFlow] purchaseSubscription -> AppStoreRestoreFlow.restoreAccountFromPastPurchase: activeSubscriptionAlreadyPresent") + os_log(.info, log: .subscription, "[AppStorePurchaseFlow] purchaseSubscription -> restoreAccountFromPastPurchase: activeSubscriptionAlreadyPresent") return .failure(.activeSubscriptionAlreadyPresent) case .failure(let error): - os_log(.info, log: .subscription, "[AppStorePurchaseFlow] purchaseSubscription -> AppStoreRestoreFlow.restoreAccountFromPastPurchase: %{public}s", String(reflecting: error)) + os_log(.info, log: .subscription, "[AppStorePurchaseFlow] purchaseSubscription -> restoreAccountFromPastPurchase: %{public}s", String(reflecting: error)) switch error { case .subscriptionExpired(let expiredAccountDetails): externalID = expiredAccountDetails.externalID @@ -84,7 +88,8 @@ public final class AppStorePurchaseFlow { accountManager.storeAuthToken(token: response.authToken) accountManager.storeAccount(token: accessToken, email: accountDetails.email, externalID: accountDetails.externalID) } - case .failure: + case .failure(let error): + os_log(.error, log: .subscription, "[AppStorePurchaseFlow] createAccount error: %{public}s", String(reflecting: error)) return .failure(.accountCreationFailed) } } @@ -95,7 +100,7 @@ public final class AppStorePurchaseFlow { case .success: return .success(()) case .failure(let error): - os_log(.error, log: .subscription, "[AppStorePurchaseFlow] Error: %{public}s", String(reflecting: error)) + os_log(.error, log: .subscription, "[AppStorePurchaseFlow] purchaseSubscription error: %{public}s", String(reflecting: error)) AccountManager().signOut() return .failure(.purchaseFailed) } @@ -103,6 +108,7 @@ public final class AppStorePurchaseFlow { @discardableResult public static func completeSubscriptionPurchase() async -> Result { + os_log(.info, log: .subscription, "[AppStorePurchaseFlow] completeSubscriptionPurchase") let result = await checkForEntitlements(wait: 2.0, retry: 10) diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift index 61fa8ea641..ace6757aa5 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift @@ -18,6 +18,7 @@ import Foundation import StoreKit +import Common @available(macOS 12.0, iOS 15.0, *) public final class AppStoreRestoreFlow { @@ -35,7 +36,12 @@ public final class AppStoreRestoreFlow { } public static func restoreAccountFromPastPurchase() async -> Result { - guard let lastTransactionJWSRepresentation = await PurchaseManager.mostRecentTransaction() else { return .failure(.missingAccountOrTransactions) } + os_log(.info, log: .subscription, "[AppStoreRestoreFlow] restoreAccountFromPastPurchase") + + guard let lastTransactionJWSRepresentation = await PurchaseManager.mostRecentTransaction() else { + os_log(.error, log: .subscription, "[AppStoreRestoreFlow] Error: missingAccountOrTransactions") + return .failure(.missingAccountOrTransactions) + } let accountManager = AccountManager() @@ -46,6 +52,7 @@ public final class AppStoreRestoreFlow { case .success(let response): authToken = response.authToken case .failure: + os_log(.error, log: .subscription, "[AppStoreRestoreFlow] Error: pastTransactionAuthenticationError") return .failure(.pastTransactionAuthenticationError) } @@ -57,6 +64,7 @@ public final class AppStoreRestoreFlow { case .success(let exchangedAccessToken): accessToken = exchangedAccessToken case .failure: + os_log(.error, log: .subscription, "[AppStoreRestoreFlow] Error: failedToObtainAccessToken") return .failure(.failedToObtainAccessToken) } @@ -65,6 +73,7 @@ public final class AppStoreRestoreFlow { email = accountDetails.email externalID = accountDetails.externalID case .failure: + os_log(.error, log: .subscription, "[AppStoreRestoreFlow] Error: failedToFetchAccountDetails") return .failure(.failedToFetchAccountDetails) } @@ -74,6 +83,7 @@ public final class AppStoreRestoreFlow { case .success(let response): isSubscriptionActive = response.isSubscriptionActive case .failure: + os_log(.error, log: .subscription, "[AppStoreRestoreFlow] Error: failedToFetchSubscriptionDetails") return .failure(.failedToFetchSubscriptionDetails) } @@ -83,6 +93,7 @@ public final class AppStoreRestoreFlow { return .success(()) } else { let details = RestoredAccountDetails(authToken: authToken, accessToken: accessToken, externalID: externalID, email: email) + os_log(.error, log: .subscription, "[AppStoreRestoreFlow] Error: subscriptionExpired") return .failure(.subscriptionExpired(accountDetails: details)) } } diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/PurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/PurchaseFlow.swift index d1a666bde2..d6f21609e2 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/PurchaseFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Flows/PurchaseFlow.swift @@ -18,10 +18,6 @@ import Foundation -protocol PurchaseFlow { - -} - public struct SubscriptionOptions: Encodable { let platform: String let options: [SubscriptionOption] diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/Stripe/StripePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/Stripe/StripePurchaseFlow.swift index 67070f383b..98a083c4a7 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/Stripe/StripePurchaseFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Flows/Stripe/StripePurchaseFlow.swift @@ -18,6 +18,7 @@ import Foundation import StoreKit +import Common public final class StripePurchaseFlow { @@ -27,8 +28,12 @@ public final class StripePurchaseFlow { } public static func subscriptionOptions() async -> Result { + os_log(.info, log: .subscription, "[StripePurchaseFlow] subscriptionOptions") - guard case let .success(products) = await SubscriptionService.getProducts(), !products.isEmpty else { return .failure(.noProductsFound) } + guard case let .success(products) = await SubscriptionService.getProducts(), !products.isEmpty else { + os_log(.error, log: .subscription, "[StripePurchaseFlow] Error: noProductsFound") + return .failure(.noProductsFound) + } let currency = products.first?.currency ?? "USD" @@ -57,6 +62,7 @@ public final class StripePurchaseFlow { } public static func prepareSubscriptionPurchase(emailAccessToken: String?) async -> Result { + os_log(.info, log: .subscription, "[StripePurchaseFlow] prepareSubscriptionPurchase") var authToken: String = "" @@ -65,6 +71,7 @@ public final class StripePurchaseFlow { authToken = response.authToken AccountManager().storeAuthToken(token: authToken) case .failure: + os_log(.error, log: .subscription, "[StripePurchaseFlow] Error: accountCreationFailed") return .failure(.accountCreationFailed) } @@ -72,11 +79,11 @@ public final class StripePurchaseFlow { } public static func completeSubscriptionPurchase() async { + os_log(.info, log: .subscription, "[StripePurchaseFlow] completeSubscriptionPurchase") + let accountManager = AccountManager() if let authToken = accountManager.authToken { - print("Exchanging token") - if case let .success(accessToken) = await accountManager.exchangeAuthTokenToAccessToken(authToken), case let .success(accountDetails) = await accountManager.fetchAccountDetails(with: accessToken) { accountManager.storeAuthToken(token: authToken) From 8fc9ce1f6c3be6fa6b612a0594284351148a8f26 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 15 Dec 2023 13:05:47 +0100 Subject: [PATCH 80/96] Disable forced sign in on purchase --- DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 01f3684cbb..f34454ab03 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -216,12 +216,15 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { await mainViewController?.presentAsSheet(progressViewController) // Trigger sign in pop-up + + /* TODO: Disabling for now switch await PurchaseManager.shared.syncAppleIDAccount() { case .success: break case .failure: return nil } + */ // Check for active subscriptions if await PurchaseManager.hasActiveSubscription() { From b69fc8f929ab47fde3185b4008ce841dce42ff6f Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 15 Dec 2023 13:06:08 +0100 Subject: [PATCH 81/96] Bump retries on entitlements polling --- .../Subscription/Flows/AppStore/AppStorePurchaseFlow.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift index 002232a618..62654af593 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift @@ -110,7 +110,7 @@ public final class AppStorePurchaseFlow { public static func completeSubscriptionPurchase() async -> Result { os_log(.info, log: .subscription, "[AppStorePurchaseFlow] completeSubscriptionPurchase") - let result = await checkForEntitlements(wait: 2.0, retry: 10) + let result = await checkForEntitlements(wait: 2.0, retry: 20) return result ? .success(PurchaseUpdate(type: "completed")) : .failure(.missingEntitlements) } From 5ad4d1a5e7f24233632be864985c195dafc9434e Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 15 Dec 2023 14:44:58 +0100 Subject: [PATCH 82/96] Only pass authToken when accessToken is also present --- .../Tab/UserScripts/SubscriptionPagesUserScript.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index f34454ab03..39889d917d 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -119,8 +119,11 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { } func getSubscription(params: Any, original: WKScriptMessage) async throws -> Encodable? { - let authToken = AccountManager().authToken ?? "" - return Subscription(token: authToken) + if let authToken = AccountManager().authToken, let accessToken = AccountManager().accessToken { + return Subscription(token: authToken) + } else { + return Subscription(token: "") + } } func setSubscription(params: Any, original: WKScriptMessage) async throws -> Encodable? { From b7ba2c68d35caaa8190c1bf908fe2fca3064c178 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 18 Dec 2023 11:42:02 +0100 Subject: [PATCH 83/96] In unauthenticated state make the sections not greyed out --- .../Preferences/PreferencesSubscriptionView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift index 2f20bee389..904fd141bc 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift @@ -113,7 +113,7 @@ public struct PreferencesSubscriptionView: View { description: UserText.vpnServiceDescription, buttonName: model.isUserAuthenticated ? "Manage" : nil, buttonAction: { model.openVPN() }, - enabled: model.hasEntitlements) + enabled: !model.isUserAuthenticated || model.hasEntitlements) Divider() .foregroundColor(Color.secondary) @@ -123,7 +123,7 @@ public struct PreferencesSubscriptionView: View { description: UserText.personalInformationRemovalServiceDescription, buttonName: model.isUserAuthenticated ? "View" : nil, buttonAction: { model.openPersonalInformationRemoval() }, - enabled: model.hasEntitlements) + enabled: !model.isUserAuthenticated || model.hasEntitlements) Divider() .foregroundColor(Color.secondary) @@ -133,7 +133,7 @@ public struct PreferencesSubscriptionView: View { description: UserText.identityTheftRestorationServiceDescription, buttonName: model.isUserAuthenticated ? "View" : nil, buttonAction: { model.openIdentityTheftRestoration() }, - enabled: model.hasEntitlements) + enabled: !model.isUserAuthenticated || model.hasEntitlements) } .padding(10) .roundedBorder() From 4546733c3bb6ec54853fee995de463b03dd5f546 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 18 Dec 2023 11:52:07 +0100 Subject: [PATCH 84/96] Extract logic to separate functions --- .../View/PreferencesRootView.swift | 85 ++++++++++--------- 1 file changed, 46 insertions(+), 39 deletions(-) diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index 850192ffb2..3221271521 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -97,23 +97,11 @@ extension Preferences { } #if SUBSCRIPTION - // swiftlint:disable:next cyclomatic_complexity function_body_length private func makeSubscriptionView() -> some View { let actionHandler = PreferencesSubscriptionActionHandlers(openURL: { url in WindowControllersManager.shared.show(url: url, source: .ui, newTab: true) }, changePlanOrBilling: { - switch SubscriptionPurchaseEnvironment.current { - case .appStore: - NSWorkspace.shared.open(.manageSubscriptionsInAppStoreAppURL) - case .stripe: - Task { - guard let accessToken = AccountManager().accessToken, let externalID = AccountManager().externalID, - case let .success(response) = await SubscriptionService.getCustomerPortalURL(accessToken: accessToken, externalID: externalID) else { return } - guard let customerPortalURL = URL(string: response.customerPortalUrl) else { return } - - WindowControllersManager.shared.show(url: customerPortalURL, source: .ui, newTab: true) - } - } + self.changePlanOrBilling() }, openVPN: { print("openVPN") }, openPersonalInformationRemoval: { @@ -123,32 +111,7 @@ extension Preferences { }) let sheetActionHandler = SubscriptionAccessActionHandlers(restorePurchases: { - if #available(macOS 12.0, *) { - Task { - let mainViewController = WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController - let progressViewController = ProgressViewController(title: "Restoring subscription...") - - defer { mainViewController?.dismiss(progressViewController) } - - mainViewController?.presentAsSheet(progressViewController) - - guard case .success = await PurchaseManager.shared.syncAppleIDAccount() else { return } - - switch await AppStoreRestoreFlow.restoreAccountFromPastPurchase() { - case .success: - break - case .failure(let error): - switch error { - case .missingAccountOrTransactions: - WindowControllersManager.shared.lastKeyMainWindowController?.showSubscriptionNotFoundAlert() - case .subscriptionExpired: - WindowControllersManager.shared.lastKeyMainWindowController?.showSubscriptionInactiveAlert() - default: - WindowControllersManager.shared.lastKeyMainWindowController?.showSomethingWentWrongAlert() - } - } - } - } + self.restorePurchases() }, openURLHandler: { url in WindowControllersManager.shared.show(url: url, source: .ui, newTab: true) }, goToSyncPreferences: { @@ -158,6 +121,50 @@ extension Preferences { let model = PreferencesSubscriptionModel(actionHandler: actionHandler, sheetActionHandler: sheetActionHandler) return SubscriptionUI.PreferencesSubscriptionView(model: model) } + + private func changePlanOrBilling() { + switch SubscriptionPurchaseEnvironment.current { + case .appStore: + NSWorkspace.shared.open(.manageSubscriptionsInAppStoreAppURL) + case .stripe: + Task { + guard let accessToken = AccountManager().accessToken, let externalID = AccountManager().externalID, + case let .success(response) = await SubscriptionService.getCustomerPortalURL(accessToken: accessToken, externalID: externalID) else { return } + guard let customerPortalURL = URL(string: response.customerPortalUrl) else { return } + + WindowControllersManager.shared.show(url: customerPortalURL, source: .ui, newTab: true) + } + } + } + + private func restorePurchases() { + if #available(macOS 12.0, *) { + Task { + let mainViewController = WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController + let progressViewController = ProgressViewController(title: "Restoring subscription...") + + defer { mainViewController?.dismiss(progressViewController) } + + mainViewController?.presentAsSheet(progressViewController) + + guard case .success = await PurchaseManager.shared.syncAppleIDAccount() else { return } + + switch await AppStoreRestoreFlow.restoreAccountFromPastPurchase() { + case .success: + break + case .failure(let error): + switch error { + case .missingAccountOrTransactions: + WindowControllersManager.shared.lastKeyMainWindowController?.showSubscriptionNotFoundAlert() + case .subscriptionExpired: + WindowControllersManager.shared.lastKeyMainWindowController?.showSubscriptionInactiveAlert() + default: + WindowControllersManager.shared.lastKeyMainWindowController?.showSomethingWentWrongAlert() + } + } + } + } + } #endif } } From 40b262fc87ec44224168c588c062eec141e7dc65 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 18 Dec 2023 14:39:07 +0100 Subject: [PATCH 85/96] Clean AppStoreAccountManagementFlow.refreshAuthTokenIfNeeded --- .../AppStoreAccountManagementFlow.swift | 25 +++++++++---------- .../Model/ShareSubscriptionAccessModel.swift | 6 ++++- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreAccountManagementFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreAccountManagementFlow.swift index 755e62f3c8..7b502d2147 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreAccountManagementFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreAccountManagementFlow.swift @@ -20,6 +20,7 @@ import Foundation import StoreKit import Common +@available(macOS 12.0, iOS 15.0, *) public final class AppStoreAccountManagementFlow { public enum Error: Swift.Error { @@ -37,20 +38,18 @@ public final class AppStoreAccountManagementFlow { if case let .failure(validateTokenError) = await AuthService.validateToken(accessToken: authToken) { os_log(.error, log: .subscription, "[AppStoreAccountManagementFlow] validateToken error: %{public}s", String(reflecting: validateTokenError)) - if #available(macOS 12.0, iOS 15.0, *) { - // In case of invalid token attempt store based authentication to obtain a new one - guard let lastTransactionJWSRepresentation = await PurchaseManager.mostRecentTransaction() else { return .failure(.noPastTransaction) } - - switch await AuthService.storeLogin(signature: lastTransactionJWSRepresentation) { - case .success(let response): - if response.externalID == AccountManager().externalID { - authToken = response.authToken - AccountManager().storeAuthToken(token: authToken) - } - case .failure(let storeLoginError): - os_log(.error, log: .subscription, "[AppStoreAccountManagementFlow] storeLogin error: %{public}s", String(reflecting: storeLoginError)) - return .failure(.authenticatingWithTransactionFailed) + // In case of invalid token attempt store based authentication to obtain a new one + guard let lastTransactionJWSRepresentation = await PurchaseManager.mostRecentTransaction() else { return .failure(.noPastTransaction) } + + switch await AuthService.storeLogin(signature: lastTransactionJWSRepresentation) { + case .success(let response): + if response.externalID == AccountManager().externalID { + authToken = response.authToken + AccountManager().storeAuthToken(token: authToken) } + case .failure(let storeLoginError): + os_log(.error, log: .subscription, "[AppStoreAccountManagementFlow] storeLogin error: %{public}s", String(reflecting: storeLoginError)) + return .failure(.authenticatingWithTransactionFailed) } } diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ShareSubscriptionAccessModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ShareSubscriptionAccessModel.swift index f3623e722f..22c7822c7f 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ShareSubscriptionAccessModel.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ShareSubscriptionAccessModel.swift @@ -67,7 +67,11 @@ public final class ShareSubscriptionAccessModel: SubscriptionAccessModel { let url: URL = hasEmail ? .manageSubscriptionEmail : .addEmailToSubscription Task { - await AppStoreAccountManagementFlow.refreshAuthTokenIfNeeded() + if SubscriptionPurchaseEnvironment.current == .appStore { + if #available(macOS 12.0, iOS 15.0, *) { + await AppStoreAccountManagementFlow.refreshAuthTokenIfNeeded() + } + } DispatchQueue.main.async { self.actionHandlers.openURLHandler(url) From b9289a141989adca535b44a84598ea381c1c4dec Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 18 Dec 2023 14:42:37 +0100 Subject: [PATCH 86/96] Clean up errors --- .../Subscription/Flows/AppStore/AppStorePurchaseFlow.swift | 4 ---- .../Subscription/Flows/AppStore/AppStoreRestoreFlow.swift | 1 - 2 files changed, 5 deletions(-) diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift index 62654af593..ab7e18fbc0 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift @@ -25,15 +25,11 @@ public final class AppStorePurchaseFlow { public enum Error: Swift.Error { case noProductsFound - case activeSubscriptionAlreadyPresent case authenticatingWithTransactionFailed case accountCreationFailed case purchaseFailed - case missingEntitlements - - case somethingWentWrong } public static func subscriptionOptions() async -> Result { diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift index ace6757aa5..c03b505539 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift @@ -32,7 +32,6 @@ public final class AppStoreRestoreFlow { case failedToFetchAccountDetails case failedToFetchSubscriptionDetails case subscriptionExpired(accountDetails: RestoredAccountDetails) - case somethingWentWrong } public static func restoreAccountFromPastPurchase() async -> Result { From 5361ff7e16ae466ff1ed501d0434db3782580e4a Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 18 Dec 2023 14:50:27 +0100 Subject: [PATCH 87/96] Move checkForEntitlements --- .../Sources/Subscription/AccountManager.swift | 26 +++++++++++++++++ .../Flows/AppStore/AppStorePurchaseFlow.swift | 28 +------------------ .../Flows/Stripe/StripePurchaseFlow.swift | 4 +-- 3 files changed, 28 insertions(+), 30 deletions(-) diff --git a/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift b/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift index 2bfbe2baf3..744c0dcd61 100644 --- a/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift +++ b/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift @@ -215,4 +215,30 @@ public class AccountManager { } } } + + @discardableResult + public static func checkForEntitlements(wait waitTime: Double, retry retryCount: Int) async -> Bool { + var count = 0 + var hasEntitlements = false + + repeat { + hasEntitlements = await !AccountManager().fetchEntitlements().isEmpty + + if hasEntitlements { + break + } else { + count += 1 + try? await Task.sleep(seconds: waitTime) + } + } while !hasEntitlements && count < retryCount + + return hasEntitlements + } +} + +extension Task where Success == Never, Failure == Never { + static func sleep(seconds: Double) async throws { + let duration = UInt64(seconds * 1_000_000_000) + try await Task.sleep(nanoseconds: duration) + } } diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift index ab7e18fbc0..ef822341b0 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift @@ -106,34 +106,8 @@ public final class AppStorePurchaseFlow { public static func completeSubscriptionPurchase() async -> Result { os_log(.info, log: .subscription, "[AppStorePurchaseFlow] completeSubscriptionPurchase") - let result = await checkForEntitlements(wait: 2.0, retry: 20) + let result = await AccountManager.checkForEntitlements(wait: 2.0, retry: 20) return result ? .success(PurchaseUpdate(type: "completed")) : .failure(.missingEntitlements) } - - @discardableResult - public static func checkForEntitlements(wait waitTime: Double, retry retryCount: Int) async -> Bool { - var count = 0 - var hasEntitlements = false - - repeat { - hasEntitlements = await !AccountManager().fetchEntitlements().isEmpty - - if hasEntitlements { - break - } else { - count += 1 - try? await Task.sleep(seconds: waitTime) - } - } while !hasEntitlements && count < retryCount - - return hasEntitlements - } -} - -extension Task where Success == Never, Failure == Never { - static func sleep(seconds: Double) async throws { - let duration = UInt64(seconds * 1_000_000_000) - try await Task.sleep(nanoseconds: duration) - } } diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/Stripe/StripePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/Stripe/StripePurchaseFlow.swift index 98a083c4a7..d4aaf0aa13 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/Stripe/StripePurchaseFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Flows/Stripe/StripePurchaseFlow.swift @@ -91,8 +91,6 @@ public final class StripePurchaseFlow { } } - if #available(macOS 12.0, iOS 15.0, *) { - await AppStorePurchaseFlow.checkForEntitlements(wait: 2.0, retry: 5) - } + await AccountManager.checkForEntitlements(wait: 2.0, retry: 5) } } From c7bc825fda729aa0679e8e1c4159145757e490c5 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 18 Dec 2023 15:47:39 +0100 Subject: [PATCH 88/96] Extract alert titles and descriptions --- .../App/DuckDuckGoPrivacyPro.xcconfig | 2 +- .../SubscriptionUI/NSAlert+Subscription.swift | 30 +++++++++---------- .../Sources/SubscriptionUI/UserText.swift | 18 +++++++++++ 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/Configuration/App/DuckDuckGoPrivacyPro.xcconfig b/Configuration/App/DuckDuckGoPrivacyPro.xcconfig index 3d6af0cc31..6eb6a4edf2 100644 --- a/Configuration/App/DuckDuckGoPrivacyPro.xcconfig +++ b/Configuration/App/DuckDuckGoPrivacyPro.xcconfig @@ -21,5 +21,5 @@ #include "DuckDuckGo.xcconfig" -FEATURE_FLAGS = FEEDBACK NETWORK_PROTECTION SPARKLE SUBSCRIPTION DBP NOSTRIPE +FEATURE_FLAGS = FEEDBACK NETWORK_PROTECTION SPARKLE SUBSCRIPTION DBP STRIPE PRODUCT_NAME = $(PRODUCT_NAME_PREFIX) Privacy Pro diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/NSAlert+Subscription.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/NSAlert+Subscription.swift index be47768dab..7080cdd43f 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/NSAlert+Subscription.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/NSAlert+Subscription.swift @@ -23,36 +23,36 @@ public extension NSAlert { static func somethingWentWrongAlert() -> NSAlert { let alert = NSAlert() - alert.messageText = "Something Went Wrong" - alert.informativeText = "The App Store was not able to process your purchase. Please try again later." - alert.addButton(withTitle: "OK") + alert.messageText = UserText.somethingWentWrongAlertTitle + alert.informativeText = UserText.somethingWentWrongAlertDescription + alert.addButton(withTitle: UserText.okButtonTitle) return alert } static func subscriptionNotFoundAlert() -> NSAlert { let alert = NSAlert() - alert.messageText = "Subscription Not Found" - alert.informativeText = "We couldn’t find a subscription associated with this Apple ID." - alert.addButton(withTitle: "View Plans") - alert.addButton(withTitle: "Cancel") + alert.messageText = UserText.subscriptionNotFoundAlertTitle + alert.informativeText = UserText.subscriptionNotFoundAlertDescription + alert.addButton(withTitle: UserText.viewPlansButtonTitle) + alert.addButton(withTitle: UserText.cancelButtonTitle) return alert } static func subscriptionInactiveAlert() -> NSAlert { let alert = NSAlert() - alert.messageText = "Subscription Not Found" - alert.informativeText = "The subscription associated with this Apple ID is no longer active." - alert.addButton(withTitle: "View Plans") - alert.addButton(withTitle: "Cancel") + alert.messageText = UserText.subscriptionInactiveAlertTitle + alert.informativeText = UserText.subscriptionInactiveAlertDescription + alert.addButton(withTitle: UserText.viewPlansButtonTitle) + alert.addButton(withTitle: UserText.cancelButtonTitle) return alert } static func subscriptionFoundAlert() -> NSAlert { let alert = NSAlert() - alert.messageText = "Subscription Found" - alert.informativeText = "We found a subscription associated with this Apple ID." - alert.addButton(withTitle: "Restore") - alert.addButton(withTitle: "Cancel") + alert.messageText = UserText.subscriptionFoundAlertTitle + alert.informativeText = UserText.subscriptionFoundAlertDescription + alert.addButton(withTitle: UserText.restoreButtonTitle) + alert.addButton(withTitle: UserText.cancelButtonTitle) return alert } } diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift index c5bd295470..578f7e971a 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift @@ -89,4 +89,22 @@ enum UserText { static let manageEmailButton = NSLocalizedString("subscription.modal.manage.email.button", value: "Manage", comment: "Button for opening manage email address page") static let enterEmailButton = NSLocalizedString("subscription.modal.enter.email.button", value: "Enter Email", comment: "Button for opening page to enter email address") static let goToSyncSettingsButton = NSLocalizedString("subscription.modal.sync.settings.button", value: "Go to Sync Settings", comment: "Button to open sync settings") + + // MARK: - Alerts + static let okButtonTitle = NSLocalizedString("subscription.alert.button.ok", value: "OK", comment: "Alert button for confirming it") + static let cancelButtonTitle = NSLocalizedString("subscription.alert.button.cancel", value: "Cancel", comment: "Alert button for dismissing it") + static let viewPlansButtonTitle = NSLocalizedString("subscription.alert.button.view.plans", value: "View Plans", comment: "Alert button for viewing subscription plans") + static let restoreButtonTitle = NSLocalizedString("subscription.alert.button.restore", value: "Restore", comment: "Alert button for restoring past subscription purchases") + + static let somethingWentWrongAlertTitle = NSLocalizedString("subscription.alert.something.went.wrong.title", value: "Something Went Wrong", comment: "Alert title when unknown error has occurred") + static let somethingWentWrongAlertDescription = NSLocalizedString("subscription.alert.something.went.wrong.description", value: "The App Store was not able to process your purchase. Please try again later.", comment: "Alert message when unknown error has occurred") + + static let subscriptionNotFoundAlertTitle = NSLocalizedString("subscription.alert.subscription.not.found.title", value: "Subscription Not Found", comment: "Alert title when subscription was not found") + static let subscriptionNotFoundAlertDescription = NSLocalizedString("subscription.alert.subscription.not.found.description", value: "We couldn’t find a subscription associated with this Apple ID.", comment: "Alert message when subscription was not found") + + static let subscriptionInactiveAlertTitle = NSLocalizedString("subscription.alert.subscription.inactive.title", value: "Subscription Not Found", comment: "Alert title when subscription was inactive") + static let subscriptionInactiveAlertDescription = NSLocalizedString("subscription.alert.subscription.inactive.description", value: "The subscription associated with this Apple ID is no longer active.", comment: "Alert message when subscription was inactive") + + static let subscriptionFoundAlertTitle = NSLocalizedString("subscription.alert.subscription.found.title", value: "Subscription Found", comment: "Alert title when subscription was found") + static let subscriptionFoundAlertDescription = NSLocalizedString("subscription.alert.subscription.found.description", value: "We found a subscription associated with this Apple ID.", comment: "Alert message when subscription was found") } From 035b9237e341db39d875d6438466bb4425a35f10 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 18 Dec 2023 16:09:43 +0100 Subject: [PATCH 89/96] Extract progress view titles --- DuckDuckGo/Common/Localizables/UserText.swift | 4 ++++ .../Tab/UserScripts/SubscriptionPagesUserScript.swift | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 4f2e03e1ba..e45f488f1e 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -1035,5 +1035,9 @@ struct UserText { #if SUBSCRIPTION static let subscriptionOptionsMenuItem = NSLocalizedString("subscription.menu.item", value: "Privacy Pro", comment: "Title for Subscription item in the options menu") static let subscription = NSLocalizedString("preferences.subscription", value: "Privacy Pro", comment: "Show subscription preferences") + + static let purchasingSubscriptionTitle = NSLocalizedString("subscription.progress.view.purchasing.subscription", value: "Purchase in progress...", comment: "Progress view title when starting the purchase") + static let restoringSubscriptionTitle = NSLocalizedString("subscription.progress.view.restoring.subscription", value: "Restoring subscription...", comment: "Progress view title when restoring past subscription purchase") + static let completingPurchaseTitle = NSLocalizedString("subscription.progress.view.completing.purchase", value: "Completing purchase...", comment: "Progress view title when completing the purchase") #endif } diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 39889d917d..a933e2f166 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -201,7 +201,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { #else if #available(macOS 12.0, *) { let mainViewController = await WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController - let progressViewController = await ProgressViewController(title: "Purchase in progress...") + let progressViewController = await ProgressViewController(title: UserText.purchasingSubscriptionTitle) defer { Task { @@ -248,7 +248,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { return nil } - await progressViewController.updateTitleText("Completing purchase...") + await progressViewController.updateTitleText(UserText.completingPurchaseTitle) os_log(.info, log: .subscription, "[Purchase] Completing purchase") @@ -276,7 +276,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { if #available(macOS 12.0, *) { Task { let mainViewController = WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController - let progressViewController = ProgressViewController(title: "Restoring subscription...") + let progressViewController = ProgressViewController(title: UserText.restoringSubscriptionTitle) defer { mainViewController?.dismiss(progressViewController) } @@ -332,7 +332,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { func completeStripePayment(params: Any, original: WKScriptMessage) async throws -> Encodable? { let mainViewController = await WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController - let progressViewController = await ProgressViewController(title: "Completing purchase...") + let progressViewController = await ProgressViewController(title: UserText.completingPurchaseTitle) await mainViewController?.presentAsSheet(progressViewController) await StripePurchaseFlow.completeSubscriptionPurchase() From 439c2752e4f2fea73cfdb2745f2a0ba75cd1a981 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 18 Dec 2023 16:48:13 +0100 Subject: [PATCH 90/96] Clean up todos --- .../UserScripts/SubscriptionPagesUserScript.swift | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index a933e2f166..878e3dc71b 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -163,7 +163,6 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { case .success(let subscriptionOptions): return subscriptionOptions case .failure: - // TODO: handle errors - no products found return nil } #else @@ -172,7 +171,6 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { case .success(let subscriptionOptions): return subscriptionOptions case .failure: - // TODO: handle errors - no products found return nil } } @@ -218,17 +216,6 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { await mainViewController?.presentAsSheet(progressViewController) - // Trigger sign in pop-up - - /* TODO: Disabling for now - switch await PurchaseManager.shared.syncAppleIDAccount() { - case .success: - break - case .failure: - return nil - } - */ - // Check for active subscriptions if await PurchaseManager.hasActiveSubscription() { os_log(.info, log: .subscription, "[Purchase] Found active subscription during purchase") From 4b6f0fffcb32c15130391447de68808399eedfcb Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 18 Dec 2023 17:45:10 +0100 Subject: [PATCH 91/96] Handle request for navigating to subscription feature via notification --- .../App/DuckDuckGoPrivacyPro.xcconfig | 2 +- .../View/PreferencesRootView.swift | 6 ++-- .../SubscriptionPagesUserScript.swift | 22 ++++++++++++-- .../Sources/Subscription/AccountManager.swift | 4 +-- .../Subscription/Flows/PurchaseFlow.swift | 2 +- .../NSNotificationName+Subscription.swift | 30 +++++++++++++++++++ 6 files changed, 57 insertions(+), 9 deletions(-) create mode 100644 LocalPackages/Subscription/Sources/Subscription/NSNotificationName+Subscription.swift diff --git a/Configuration/App/DuckDuckGoPrivacyPro.xcconfig b/Configuration/App/DuckDuckGoPrivacyPro.xcconfig index 6eb6a4edf2..3d6af0cc31 100644 --- a/Configuration/App/DuckDuckGoPrivacyPro.xcconfig +++ b/Configuration/App/DuckDuckGoPrivacyPro.xcconfig @@ -21,5 +21,5 @@ #include "DuckDuckGo.xcconfig" -FEATURE_FLAGS = FEEDBACK NETWORK_PROTECTION SPARKLE SUBSCRIPTION DBP STRIPE +FEATURE_FLAGS = FEEDBACK NETWORK_PROTECTION SPARKLE SUBSCRIPTION DBP NOSTRIPE PRODUCT_NAME = $(PRODUCT_NAME_PREFIX) Privacy Pro diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index 3221271521..ae5a806cc3 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -103,11 +103,11 @@ extension Preferences { }, changePlanOrBilling: { self.changePlanOrBilling() }, openVPN: { - print("openVPN") + NotificationCenter.default.post(name: .openVPN, object: self, userInfo: nil) }, openPersonalInformationRemoval: { - print("openPersonalInformationRemoval") + NotificationCenter.default.post(name: .openPersonalInformationRemoval, object: self, userInfo: nil) }, openIdentityTheftRestoration: { - print("openIdentityTheftRestoration") + NotificationCenter.default.post(name: .openIdentityTheftRestoration, object: self, userInfo: nil) }) let sheetActionHandler = SubscriptionAccessActionHandlers(restorePurchases: { diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 878e3dc71b..1122df48d3 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -310,9 +310,27 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { return nil } - print(">>> Selected a feature -- show the corresponding UI", featureSelection) + guard let subscriptionFeatureName = SubscriptionFeatureName(rawValue: featureSelection.feature) else { + assertionFailure("SubscriptionPagesUserScript: feature name does not matches mapping") + return nil + } - // TODO: implement selection + switch subscriptionFeatureName { + case .privateBrowsing: + NotificationCenter.default.post(name: .openPrivateBrowsing, object: self, userInfo: nil) + case .privateSearch: + NotificationCenter.default.post(name: .openPrivateSearch, object: self, userInfo: nil) + case .emailProtection: + NotificationCenter.default.post(name: .openEmailProtection, object: self, userInfo: nil) + case .appTrackingProtection: + NotificationCenter.default.post(name: .openAppTrackingProtection, object: self, userInfo: nil) + case .vpn: + NotificationCenter.default.post(name: .openVPN, object: self, userInfo: nil) + case .personalInformationRemoval: + NotificationCenter.default.post(name: .openPersonalInformationRemoval, object: self, userInfo: nil) + case .identityTheftRestoration: + NotificationCenter.default.post(name: .openIdentityTheftRestoration, object: self, userInfo: nil) + } return nil } diff --git a/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift b/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift index 744c0dcd61..0923f4f8d4 100644 --- a/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift +++ b/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift @@ -20,8 +20,8 @@ import Foundation import Common public extension Notification.Name { - static let accountDidSignIn = Notification.Name("com.duckduckgo.browserServicesKit.AccountDidSignIn") - static let accountDidSignOut = Notification.Name("com.duckduckgo.browserServicesKit.AccountDidSignOut") + static let accountDidSignIn = Notification.Name("com.duckduckgo.subscription.AccountDidSignIn") + static let accountDidSignOut = Notification.Name("com.duckduckgo.subscription.AccountDidSignOut") } public protocol AccountManagerKeychainAccessDelegate: AnyObject { diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/PurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/PurchaseFlow.swift index d6f21609e2..f32765ff96 100644 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/PurchaseFlow.swift +++ b/LocalPackages/Subscription/Sources/Subscription/Flows/PurchaseFlow.swift @@ -1,5 +1,5 @@ // -// AppStorePurchaseFlow.swift +// PurchaseFlow.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/LocalPackages/Subscription/Sources/Subscription/NSNotificationName+Subscription.swift b/LocalPackages/Subscription/Sources/Subscription/NSNotificationName+Subscription.swift new file mode 100644 index 0000000000..20fc2f7b17 --- /dev/null +++ b/LocalPackages/Subscription/Sources/Subscription/NSNotificationName+Subscription.swift @@ -0,0 +1,30 @@ +// +// NSNotificationName+Subscription.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +public extension NSNotification.Name { + + static let openPrivateBrowsing = Notification.Name("com.duckduckgo.subscription.open.private-browsing") + static let openPrivateSearch = Notification.Name("com.duckduckgo.subscription.open.private-search") + static let openEmailProtection = Notification.Name("com.duckduckgo.subscription.open.email-protection") + static let openAppTrackingProtection = Notification.Name("com.duckduckgo.subscription.open.app-tracking-protection") + static let openVPN = Notification.Name("com.duckduckgo.subscription.open.vpn") + static let openPersonalInformationRemoval = Notification.Name("com.duckduckgo.subscription.open.personal-information-removal") + static let openIdentityTheftRestoration = Notification.Name("com.duckduckgo.subscription.open.identity-theft-restoration") +} From ebde2a49d6de3c8f9bcec37cdc041ca95c852b75 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 18 Dec 2023 17:45:46 +0100 Subject: [PATCH 92/96] Update comment --- DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 1122df48d3..8c7d731353 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -193,7 +193,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { case .success(let purchaseUpdate): await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: purchaseUpdate) case .failure: - // TODO: handle errors - failed prepare purchae + // TODO: handle error with alert? return nil } #else From ab18525daa5a5899d2c6937049efdaaaded1c683 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 22 Dec 2023 14:23:15 +0100 Subject: [PATCH 93/96] Remove dead code --- .../Tests/SubscriptionTests/SubscriptionTests.swift | 7 ------- .../Tests/SubscriptionUITests/SubscriptionUITests.swift | 8 +------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/LocalPackages/Subscription/Tests/SubscriptionTests/SubscriptionTests.swift b/LocalPackages/Subscription/Tests/SubscriptionTests/SubscriptionTests.swift index 43aed49cab..e209d149d3 100644 --- a/LocalPackages/Subscription/Tests/SubscriptionTests/SubscriptionTests.swift +++ b/LocalPackages/Subscription/Tests/SubscriptionTests/SubscriptionTests.swift @@ -20,11 +20,4 @@ import XCTest @testable import Subscription final class SubscriptionTests: XCTestCase { - func testExample() throws { - // XCTest Documentation - // https://developer.apple.com/documentation/xctest - - // Defining Test Cases and Test Methods - // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods - } } diff --git a/LocalPackages/SubscriptionUI/Tests/SubscriptionUITests/SubscriptionUITests.swift b/LocalPackages/SubscriptionUI/Tests/SubscriptionUITests/SubscriptionUITests.swift index 6adc3d6a82..2e2cf8dcee 100644 --- a/LocalPackages/SubscriptionUI/Tests/SubscriptionUITests/SubscriptionUITests.swift +++ b/LocalPackages/SubscriptionUI/Tests/SubscriptionUITests/SubscriptionUITests.swift @@ -19,11 +19,5 @@ import XCTest @testable import SubscriptionUI -final class SubscriptionTests: XCTestCase { - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. -// XCTAssertEqual(SubscriptionUI().text, "Hello, World!") - } +final class SubscriptionUITests: XCTestCase { } From b6aa09829ee9c2481cc767b81889f01727af9d68 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 22 Dec 2023 14:56:56 +0100 Subject: [PATCH 94/96] Add Stripe version of something went wrong alert --- .../UserScripts/SubscriptionPagesUserScript.swift | 12 +++++++++--- .../SubscriptionUI/NSAlert+Subscription.swift | 8 ++++++++ .../Sources/SubscriptionUI/UserText.swift | 1 + 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 8c7d731353..09ae0e33ec 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -193,7 +193,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { case .success(let purchaseUpdate): await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: purchaseUpdate) case .failure: - // TODO: handle error with alert? + await WindowControllersManager.shared.lastKeyMainWindowController?.showSomethingWentWrongAlert() return nil } #else @@ -366,10 +366,16 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { extension MainWindowController { @MainActor - func showSomethingWentWrongAlert() { + func showSomethingWentWrongAlert(environment: SubscriptionPurchaseEnvironment.Environment = SubscriptionPurchaseEnvironment.current) { guard let window else { return } - window.show(.somethingWentWrongAlert()) + switch environment { + case .appStore: + window.show(.somethingWentWrongAlert()) + case .stripe: + window.show(.somethingWentWrongStripeAlert()) + } + } @MainActor diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/NSAlert+Subscription.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/NSAlert+Subscription.swift index 7080cdd43f..d79042d247 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/NSAlert+Subscription.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/NSAlert+Subscription.swift @@ -29,6 +29,14 @@ public extension NSAlert { return alert } + static func somethingWentWrongStripeAlert() -> NSAlert { + let alert = NSAlert() + alert.messageText = UserText.somethingWentWrongAlertTitle + alert.informativeText = UserText.somethingWentWrongStripeAlertDescription + alert.addButton(withTitle: UserText.okButtonTitle) + return alert + } + static func subscriptionNotFoundAlert() -> NSAlert { let alert = NSAlert() alert.messageText = UserText.subscriptionNotFoundAlertTitle diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift index 578f7e971a..c3c9d61d14 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift @@ -98,6 +98,7 @@ enum UserText { static let somethingWentWrongAlertTitle = NSLocalizedString("subscription.alert.something.went.wrong.title", value: "Something Went Wrong", comment: "Alert title when unknown error has occurred") static let somethingWentWrongAlertDescription = NSLocalizedString("subscription.alert.something.went.wrong.description", value: "The App Store was not able to process your purchase. Please try again later.", comment: "Alert message when unknown error has occurred") + static let somethingWentWrongStripeAlertDescription = NSLocalizedString("subscription.alert.something.went.wrong.stripe.description", value: "We were not able to start your purchase process. Please try again later.", comment: "Alert message when unknown error has occurred") static let subscriptionNotFoundAlertTitle = NSLocalizedString("subscription.alert.subscription.not.found.title", value: "Subscription Not Found", comment: "Alert title when subscription was not found") static let subscriptionNotFoundAlertDescription = NSLocalizedString("subscription.alert.subscription.not.found.description", value: "We couldn’t find a subscription associated with this Apple ID.", comment: "Alert message when subscription was not found") From c2fbcedf08dc3de45491534d8717d3e41fa1c51f Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 22 Dec 2023 18:29:13 +0100 Subject: [PATCH 95/96] Fix linting errors --- DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift | 2 +- .../SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 09ae0e33ec..60d2b5ad1f 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -178,7 +178,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { #endif } - // swiftlint:disable:next cyclomatic_complexity function_body_length + // swiftlint:disable:next function_body_length func subscriptionSelected(params: Any, original: WKScriptMessage) async throws -> Encodable? { struct SubscriptionSelection: Decodable { let id: String diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift index 9a4f1e563a..7c4dbf2ed8 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift @@ -147,7 +147,7 @@ public final class SubscriptionDebugMenu: NSMenuItem { @objc func testError1(_ sender: Any?) { Task { @MainActor in - let alert = NSAlert.init() + let alert = NSAlert() alert.messageText = "Something Went Wrong" alert.informativeText = "The App Store was not able to process your purchase. Please try again later." alert.addButton(withTitle: "OK") @@ -158,7 +158,7 @@ public final class SubscriptionDebugMenu: NSMenuItem { @objc func testError2(_ sender: Any?) { Task { @MainActor in - let alert = NSAlert.init() + let alert = NSAlert() alert.messageText = "Subscription Not Found" alert.informativeText = "The subscription associated with this Apple ID is no longer active." alert.addButton(withTitle: "View Plans") From 71595d96d443bf801d7517dfde505a79ef7d9859 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 22 Dec 2023 18:40:24 +0100 Subject: [PATCH 96/96] Add check for the Subscription import --- DuckDuckGo/Preferences/Model/PreferencesSection.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/DuckDuckGo/Preferences/Model/PreferencesSection.swift b/DuckDuckGo/Preferences/Model/PreferencesSection.swift index cde05bd3f0..7200dd805d 100644 --- a/DuckDuckGo/Preferences/Model/PreferencesSection.swift +++ b/DuckDuckGo/Preferences/Model/PreferencesSection.swift @@ -18,7 +18,10 @@ import Foundation import SwiftUI + +#if SUBSCRIPTION import Subscription +#endif struct PreferencesSection: Hashable, Identifiable { let id: PreferencesSectionIdentifier