Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Subscription refactoring #815

Merged
merged 45 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
047ddbd
indentation
federicocappelli May 1, 2024
0d59a74
Merge branch 'main' into fcappelli/subscription_refactoring
federicocappelli May 1, 2024
690063b
AccountManager injected
federicocappelli May 2, 2024
bc0492f
AccountManaging protocol
federicocappelli May 2, 2024
e87b6e4
Merge branch 'main' of https://github.com/duckduckgo/BrowserServicesK…
federicocappelli May 2, 2024
9e32e19
swiftlint
federicocappelli May 3, 2024
a99a83c
Merge branch 'main' into fcappelli/subscription_refactoring
federicocappelli May 3, 2024
cd3441d
subscription service DI
federicocappelli May 6, 2024
09fb68d
Merge branch 'main' into fcappelli/subscription_refactoring
federicocappelli May 6, 2024
6d7fab5
Merge branch 'fcappelli/subscription_refactoring' into fcappelli/subs…
federicocappelli May 6, 2024
196a6de
more dismantling and rebuilding
federicocappelli May 7, 2024
6455cff
Subscription tests and test utilities
federicocappelli May 7, 2024
2ef57d3
Merge branch 'main' into fcappelli/subscription_refactoring_2
federicocappelli May 8, 2024
ef4d1f2
AccountManagerMock improvements
federicocappelli May 8, 2024
8502c4c
Merge branch 'main' into fcappelli/subscription_refactoring_2
federicocappelli May 8, 2024
5c22b40
SubscriptionManaging
federicocappelli May 8, 2024
e51b448
Merge branch 'main' into fcappelli/subscription_refactoring_2
federicocappelli May 9, 2024
59fc9f7
load and save subscriptionenvironment
federicocappelli May 9, 2024
3e26d5b
Merge branch 'main' into fcappelli/subscription_refactoring_2
federicocappelli May 9, 2024
96a05d8
ios subscription env
federicocappelli May 9, 2024
6f3651c
iOS compatibility #1
federicocappelli May 9, 2024
889aff3
minor improvements for iOS compatibility
federicocappelli May 10, 2024
5936635
Merge branch 'main' into fcappelli/subscription_refactoring_2
federicocappelli May 10, 2024
ee49b91
Merge branch 'main' into fcappelli/subscription_refactoring_2
federicocappelli May 13, 2024
da1742a
lint
federicocappelli May 13, 2024
5e59e5b
subscription manager mock
federicocappelli May 13, 2024
c8e1e98
lint
federicocappelli May 13, 2024
2689c06
discardable result added
federicocappelli May 13, 2024
2fb70be
can purchase fixed
federicocappelli May 14, 2024
c6ec025
Merge branch 'main' into fcappelli/subscription_refactoring_2
federicocappelli May 15, 2024
823c356
SubscriptionEnvironment Default moved to main apps
federicocappelli May 15, 2024
6f8a1da
SubscriptionEnvNames renamed SubscriptionEnvironmentNames
federicocappelli May 15, 2024
a3a0614
Platform renamed PurchasePlatform
federicocappelli May 15, 2024
91191b5
Merge branch 'main' into fcappelli/subscription_refactoring_2
federicocappelli May 15, 2024
7967d93
Merge branch 'main' into fcappelli/subscription_refactoring_2
federicocappelli May 16, 2024
46ef62f
Merge branch 'main' into fcappelli/subscription_refactoring_2
federicocappelli May 16, 2024
dae75b9
access control cleanup and renaming
federicocappelli May 16, 2024
845353b
PR comment addressed
federicocappelli May 16, 2024
0335dcb
various improvements suggested in the PR
federicocappelli May 16, 2024
3819577
mock class improved
federicocappelli May 16, 2024
73ee685
Merge branch 'main' into fcappelli/subscription_refactoring_2
federicocappelli May 17, 2024
dc996ee
Merge branch 'main' into fcappelli/subscription_refactoring_2
federicocappelli May 17, 2024
b2d8981
Merge branch 'main' into fcappelli/subscription_refactoring_2
federicocappelli May 21, 2024
e826efd
renamings
federicocappelli May 21, 2024
874ae42
lint
federicocappelli May 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ let package = Package(
.library(name: "NetworkProtectionTestUtils", targets: ["NetworkProtectionTestUtils"]),
.library(name: "SecureStorage", targets: ["SecureStorage"]),
.library(name: "Subscription", targets: ["Subscription"]),
.library(name: "SubscriptionTestingUtilities", targets: ["SubscriptionTestingUtilities"]),
.library(name: "History", targets: ["History"]),
.library(name: "Suggestions", targets: ["Suggestions"]),
.library(name: "PixelKit", targets: ["PixelKit"]),
Expand Down Expand Up @@ -331,6 +332,12 @@ let package = Package(
.define("DEBUG", .when(configuration: .debug))
]
),
.target(
name: "SubscriptionTestingUtilities",
dependencies: [
"Subscription"
]
),
.target(
name: "PixelKit",
swiftSettings: [
Expand Down Expand Up @@ -505,6 +512,13 @@ let package = Package(
"TestUtils",
]
),
.testTarget(
name: "SubscriptionTests",
dependencies: [
"Subscription",
"SubscriptionTestingUtilities",
]
),
.testTarget(
name: "PixelKitTests",
dependencies: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
// limitations under the License.
//

import Foundation
import Subscription

public protocol SubscriptionFeatureAvailability {
Expand All @@ -26,9 +27,10 @@ public protocol SubscriptionFeatureAvailability {
public final class DefaultSubscriptionFeatureAvailability: SubscriptionFeatureAvailability {

private let privacyConfigurationManager: PrivacyConfigurationManaging
private let purchasePlatform: SubscriptionPurchaseEnvironment.Environment
private let purchasePlatform: SubscriptionEnvironment.PurchasePlatform

public init(privacyConfigurationManager: PrivacyConfigurationManaging, purchasePlatform: SubscriptionPurchaseEnvironment.Environment) {
public init(privacyConfigurationManager: PrivacyConfigurationManaging,
purchasePlatform: SubscriptionEnvironment.PurchasePlatform) {
self.privacyConfigurationManager = privacyConfigurationManager
self.purchasePlatform = purchasePlatform
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ public final class NetworkProtectionKeychainTokenStore: NetworkProtectionTokenSt
private let keychainStore: NetworkProtectionKeychainStore
private let errorEvents: EventMapping<NetworkProtectionError>?
private let isSubscriptionEnabled: Bool
private let accessTokenProvider: () -> String?
public typealias AccessTokenProvider = () -> String?
private let accessTokenProvider: AccessTokenProvider

public static var authTokenPrefix: String { "ddg:" }

Expand All @@ -57,7 +58,7 @@ public final class NetworkProtectionKeychainTokenStore: NetworkProtectionTokenSt
serviceName: String = Defaults.tokenStoreService,
errorEvents: EventMapping<NetworkProtectionError>?,
isSubscriptionEnabled: Bool,
accessTokenProvider: @escaping () -> String?) {
accessTokenProvider: @escaping AccessTokenProvider) {
keychainStore = NetworkProtectionKeychainStore(label: Defaults.tokenStoreEntryLabel,
serviceName: serviceName,
keychainType: keychainType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ final public class NetworkProtectionLocationListCompositeRepository: NetworkProt
}

@MainActor
@discardableResult
public func fetchLocationList() async throws -> [NetworkProtectionLocation] {
guard !canUseCache else {
return Self.locationList
Expand Down
41 changes: 20 additions & 21 deletions Sources/Subscription/AccountStorage/AccountKeychainStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,57 +48,56 @@ public enum AccountKeychainAccessError: Error, Equatable {
}
}

public class AccountKeychainStorage: AccountStorage {
public class AccountKeychainStorage: AccountStoring {

public init() {}

public func getAuthToken() throws -> String? {
try Self.getString(forField: .authToken)
try getString(forField: .authToken)
}

public func store(authToken: String) throws {
try Self.set(string: authToken, forField: .authToken)
try set(string: authToken, forField: .authToken)
}

public func getAccessToken() throws -> String? {
try Self.getString(forField: .accessToken)
try getString(forField: .accessToken)
}

public func store(accessToken: String) throws {
try Self.set(string: accessToken, forField: .accessToken)
try set(string: accessToken, forField: .accessToken)
}

public func getEmail() throws -> String? {
try Self.getString(forField: .email)
try getString(forField: .email)
}

public func getExternalID() throws -> String? {
try Self.getString(forField: .externalID)
try getString(forField: .externalID)
}

public func store(externalID: String?) throws {
if let externalID = externalID, !externalID.isEmpty {
try Self.set(string: externalID, forField: .externalID)
try set(string: externalID, forField: .externalID)
} else {
try Self.deleteItem(forField: .externalID)
try deleteItem(forField: .externalID)
}
}

public func store(email: String?) throws {
if let email = email, !email.isEmpty {
try Self.set(string: email, forField: .email)
try set(string: email, forField: .email)
} else {
try Self.deleteItem(forField: .email)
try deleteItem(forField: .email)
}
}

public func clearAuthenticationState() throws {
try Self.deleteItem(forField: .authToken)
try Self.deleteItem(forField: .accessToken)
try Self.deleteItem(forField: .email)
try Self.deleteItem(forField: .externalID)
try deleteItem(forField: .authToken)
try deleteItem(forField: .accessToken)
try deleteItem(forField: .email)
try deleteItem(forField: .externalID)
}

}

private extension AccountKeychainStorage {
Expand All @@ -118,7 +117,7 @@ private extension AccountKeychainStorage {
}
}

static func getString(forField field: AccountKeychainField) throws -> String? {
func getString(forField field: AccountKeychainField) throws -> String? {
guard let data = try retrieveData(forField: field) else {
return nil
}
Expand All @@ -130,7 +129,7 @@ private extension AccountKeychainStorage {
}
}

static func retrieveData(forField field: AccountKeychainField, useDataProtectionKeychain: Bool = true) throws -> Data? {
func retrieveData(forField field: AccountKeychainField, useDataProtectionKeychain: Bool = true) throws -> Data? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecMatchLimit as String: kSecMatchLimitOne,
Expand All @@ -155,7 +154,7 @@ private extension AccountKeychainStorage {
}
}

static func set(string: String, forField field: AccountKeychainField) throws {
func set(string: String, forField field: AccountKeychainField) throws {
guard let stringData = string.data(using: .utf8) else {
return
}
Expand All @@ -164,7 +163,7 @@ private extension AccountKeychainStorage {
try store(data: stringData, forField: field)
}

static func store(data: Data, forField field: AccountKeychainField, useDataProtectionKeychain: Bool = true) throws {
func store(data: Data, forField field: AccountKeychainField, useDataProtectionKeychain: Bool = true) throws {
let query = [
kSecClass: kSecClassGenericPassword,
kSecAttrSynchronizable: false,
Expand All @@ -180,7 +179,7 @@ private extension AccountKeychainStorage {
}
}

static func deleteItem(forField field: AccountKeychainField, useDataProtectionKeychain: Bool = true) throws {
func deleteItem(forField field: AccountKeychainField, useDataProtectionKeychain: Bool = true) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: field.keyValue,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// AccountStorage.swift
// AccountStoring.swift
//
// Copyright © 2023 DuckDuckGo. All rights reserved.
//
Expand All @@ -18,7 +18,7 @@

import Foundation

public protocol AccountStorage: AnyObject {
public protocol AccountStoring: AnyObject {
func getAuthToken() throws -> String?
func store(authToken: String) throws
func getAccessToken() throws -> String?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

import Foundation

public class SubscriptionTokenKeychainStorage: SubscriptionTokenStorage {
public class SubscriptionTokenKeychainStorage: SubscriptionTokenStoring {

private let keychainType: KeychainType

Expand Down Expand Up @@ -65,6 +65,7 @@ private extension SubscriptionTokenKeychainStorage {
throw AccountKeychainAccessError.failedToDecodeKeychainDataAsString
}
}

func retrieveData(forField field: AccountKeychainField) throws -> Data? {
var query = defaultAttributes()
query[kSecAttrService] = field.keyValue
Expand Down Expand Up @@ -153,11 +154,8 @@ private extension SubscriptionTokenKeychainStorage {

public enum KeychainType {
case dataProtection(_ accessGroup: AccessGroup)

/// Uses the system keychain.
///
case system

case fileBased

public enum AccessGroup {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// SubscriptionTokenStorage.swift
// SubscriptionTokenStoring.swift
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
Expand All @@ -18,7 +18,7 @@

import Foundation

public protocol SubscriptionTokenStorage: AnyObject {
public protocol SubscriptionTokenStoring: AnyObject {
func getAccessToken() throws -> String?
func store(accessToken: String) throws
func removeAccessToken() throws
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,26 +23,33 @@ import Common
@available(macOS 12.0, iOS 15.0, *)
public final class AppStoreAccountManagementFlow {

public enum Error: Swift.Error {
case noPastTransaction
case authenticatingWithTransactionFailed
}
private let subscriptionManager: SubscriptionManaging
private var accountManager: AccountManaging {
subscriptionManager.accountManager
}

public init(subscriptionManager: SubscriptionManaging) {
self.subscriptionManager = subscriptionManager
}

public enum Error: Swift.Error {
case noPastTransaction
case authenticatingWithTransactionFailed
}

@discardableResult
public static func refreshAuthTokenIfNeeded(subscriptionAppGroup: String) async -> Result<String, AppStoreAccountManagementFlow.Error> {
public func refreshAuthTokenIfNeeded() async -> Result<String, AppStoreAccountManagementFlow.Error> {
os_log(.info, log: .subscription, "[AppStoreAccountManagementFlow] refreshAuthTokenIfNeeded")
let accountManager = AccountManager(subscriptionAppGroup: subscriptionAppGroup)

var authToken = accountManager.authToken ?? ""

// Check if auth token if still valid
if case let .failure(validateTokenError) = await AuthService.validateToken(accessToken: authToken) {
if case let .failure(validateTokenError) = await subscriptionManager.authService.validateToken(accessToken: authToken) {
os_log(.error, log: .subscription, "[AppStoreAccountManagementFlow] validateToken error: %{public}s", String(reflecting: validateTokenError))

// In case of invalid token attempt store based authentication to obtain a new one
guard let lastTransactionJWSRepresentation = await PurchaseManager.mostRecentTransaction() else { return .failure(.noPastTransaction) }
guard let lastTransactionJWSRepresentation = await subscriptionManager.storePurchaseManager().mostRecentTransaction() else { return .failure(.noPastTransaction) }

switch await AuthService.storeLogin(signature: lastTransactionJWSRepresentation) {
switch await subscriptionManager.authService.storeLogin(signature: lastTransactionJWSRepresentation) {
case .success(let response):
if response.externalID == accountManager.externalID {
authToken = response.authToken
Expand Down
Loading
Loading