From 1aea67c45443a8b7e8ca6d1cffa69f43019aafe0 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Tue, 22 Oct 2024 12:43:14 +0200 Subject: [PATCH 01/12] Add SubscriptionCookieManager --- .../SubscriptionCookieManager.swift | 197 ++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift diff --git a/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift b/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift new file mode 100644 index 000000000..6e2342855 --- /dev/null +++ b/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift @@ -0,0 +1,197 @@ +// +// SubscriptionCookieManager.swift +// +// Copyright © 2024 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 WebKit + +public protocol HTTPCookieStore { + func allCookies() async -> [HTTPCookie] + func setCookie(_ cookie: HTTPCookie) async + func deleteCookie(_ cookie: HTTPCookie) async +} + +extension WKHTTPCookieStore: HTTPCookieStore {} + +public protocol SubscriptionCookieManaging { + init(subscriptionManager: SubscriptionManager, currentCookieStore: @MainActor @escaping () -> HTTPCookieStore) async + func refreshSubscriptionCookie() async + func testCookies() async +} + +public final class SubscriptionCookieManager: SubscriptionCookieManaging { + + public static let cookieDomain = "duckduckgo.com" + public static let cookieName = "privacy_pro_access_token" + + private static let defaultRefreshTimeInterval: TimeInterval = .seconds(10) // TODO: change the default to e.g. 4h + + private let subscriptionManager: SubscriptionManager + private let currentCookieStore: @MainActor () -> HTTPCookieStore + + private var lastRefreshDate: Date? + private let refreshTimeInterval: TimeInterval + + convenience nonisolated public required init(subscriptionManager: SubscriptionManager, + currentCookieStore: @MainActor @escaping () -> HTTPCookieStore) { + self.init(subscriptionManager: subscriptionManager, + currentCookieStore: currentCookieStore, + refreshTimeInterval: SubscriptionCookieManager.defaultRefreshTimeInterval) + } + + nonisolated public required init(subscriptionManager: SubscriptionManager, + currentCookieStore: @MainActor @escaping () -> HTTPCookieStore, + refreshTimeInterval: TimeInterval) { + self.subscriptionManager = subscriptionManager + self.currentCookieStore = currentCookieStore + self.refreshTimeInterval = refreshTimeInterval + + registerForSubscriptionAccountManagerEvents() + } + + private func registerForSubscriptionAccountManagerEvents() { + NotificationCenter.default.addObserver(self, selector: #selector(handleAccountDidSignIn), name: .accountDidSignIn, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(handleAccountDidSignOut), name: .accountDidSignOut, object: nil) + } + + @objc private func handleAccountDidSignIn() { + Task { + guard let accessToken = subscriptionManager.accountManager.accessToken else { + // TODO: Add error handling ".accountDidSignIn event but access token is missing" + return + } + + await setSubscriptionCookie(for: accessToken) + print("[🍪 Cookie] Subscription sign in - setting cookie") + } + } + + @objc private func handleAccountDidSignOut() { + Task { + guard let subscriptionCookie = await fetchCurrentSubscriptionCookie() else { + // TODO: Add error handling ".accountDidSignOut event but cookie is missing" + return + } + + await currentCookieStore().deleteCookie(subscriptionCookie) + await deleteSubscriptionCookie() + print("[🍪 Cookie] Subscription sign out - removing cookie") + } + } + + public func refreshSubscriptionCookie() async { + print("[🍪 Cookie] Refresh subscription cookie (last refresh date since now: \(lastRefreshDate?.timeIntervalSinceNow ?? 0.0)") + guard shouldRefreshSubscriptionCookie() else { return } + +// let token = "Test(\(String(describing: Date()))" +// let token = "TEST2" + let accessToken: String? = subscriptionManager.accountManager.accessToken + + print("[🍪 Cookie] Token: \(accessToken ?? "")") + + lastRefreshDate = Date() + print("[🍪 Cookie] New last refresh date") + + if let accessToken { + if let cookie = await fetchCurrentSubscriptionCookie(), cookie.value == accessToken { + print("[🍪 Cookie] Current up to date") + // Cookie present with proper value + return + } else { + // Cookie not present or with different value + print("[🍪 Cookie] Cookie not present or with different value") + await setSubscriptionCookie(for: accessToken) + + // TODO: Pixel that refresh actually was required - fixed by updating the token + } + } else { + // remove cookie + await deleteSubscriptionCookie() + + // TODO: Pixel that refresh actually was required - fixed by deleting the token + } + } + + private func shouldRefreshSubscriptionCookie() -> Bool { + switch lastRefreshDate { + case .none: + return true + case .some(let previousLastRefreshDate): + return previousLastRefreshDate.timeIntervalSinceNow < -refreshTimeInterval + } + } + + private func fetchCurrentSubscriptionCookie() async -> HTTPCookie? { + let cookieStore = await currentCookieStore() + var currentCookie: HTTPCookie? + + for cookie in await cookieStore.allCookies() { + if cookie.domain == Self.cookieDomain && cookie.name == Self.cookieName { + currentCookie = cookie + break + } + } + + return currentCookie + } + + private func setSubscriptionCookie(for token: String) async { + guard let subscriptionCookie = makeSubscriptionCookie(for: token) else { + // TODO: Add error handling "Failed to make a subscription cookie" + return + } + + print("[🍪 Cookie] Updating cookie") + await currentCookieStore().setCookie(subscriptionCookie) + } + + private func deleteSubscriptionCookie() async { + guard let cookie = await fetchCurrentSubscriptionCookie() else { + // Not really an error + // Add error handling "Tried to delete subscription cookie but it was not present" + return + } + + await currentCookieStore().deleteCookie(cookie) + print("[🍪 Cookie] Deleting cookie") + } + + private func makeSubscriptionCookie(for token: String) -> HTTPCookie? { + HTTPCookie(properties: [ + .domain: Self.cookieDomain, + .path: "/", + .expires: Date().addingTimeInterval(.days(365)), + .name: Self.cookieName, + .value: token, + .secure: true, + .init(rawValue: "HttpOnly"): true + ]) + } + + public func testCookies() async { + print("[🍪 testCookie] Test cookies ================= ") + let cookieStore = await currentCookieStore() + + for cookie in await cookieStore.allCookies() { + print(" [🍪 testCookie] Cookie: \(cookie.domain) \(cookie.name)") + if cookie.domain == Self.cookieDomain { + print(" \(cookie.debugDescription.replacingOccurrences(of: "\n", with: "; "))") + } + } + print("[🍪 testCookie] ============================== ") + } +} From 2458b9fd5505af4f756541c55bdc2971731c1ae7 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Wed, 23 Oct 2024 10:33:42 +0200 Subject: [PATCH 02/12] Make currentCookieStore closure return value optional --- .../SubscriptionCookieManager.swift | 111 +++++++++--------- 1 file changed, 53 insertions(+), 58 deletions(-) diff --git a/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift b/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift index 6e2342855..604a7f600 100644 --- a/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift +++ b/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift @@ -28,7 +28,7 @@ public protocol HTTPCookieStore { extension WKHTTPCookieStore: HTTPCookieStore {} public protocol SubscriptionCookieManaging { - init(subscriptionManager: SubscriptionManager, currentCookieStore: @MainActor @escaping () -> HTTPCookieStore) async + init(subscriptionManager: SubscriptionManager, currentCookieStore: @MainActor @escaping () -> HTTPCookieStore?) async func refreshSubscriptionCookie() async func testCookies() async } @@ -41,20 +41,20 @@ public final class SubscriptionCookieManager: SubscriptionCookieManaging { private static let defaultRefreshTimeInterval: TimeInterval = .seconds(10) // TODO: change the default to e.g. 4h private let subscriptionManager: SubscriptionManager - private let currentCookieStore: @MainActor () -> HTTPCookieStore + private let currentCookieStore: @MainActor () -> HTTPCookieStore? private var lastRefreshDate: Date? private let refreshTimeInterval: TimeInterval convenience nonisolated public required init(subscriptionManager: SubscriptionManager, - currentCookieStore: @MainActor @escaping () -> HTTPCookieStore) { - self.init(subscriptionManager: subscriptionManager, + currentCookieStore: @MainActor @escaping () -> HTTPCookieStore?) { + self.init(subscriptionManager: subscriptionManager, currentCookieStore: currentCookieStore, refreshTimeInterval: SubscriptionCookieManager.defaultRefreshTimeInterval) } nonisolated public required init(subscriptionManager: SubscriptionManager, - currentCookieStore: @MainActor @escaping () -> HTTPCookieStore, + currentCookieStore: @MainActor @escaping () -> HTTPCookieStore?, refreshTimeInterval: TimeInterval) { self.subscriptionManager = subscriptionManager self.currentCookieStore = currentCookieStore @@ -70,57 +70,61 @@ public final class SubscriptionCookieManager: SubscriptionCookieManaging { @objc private func handleAccountDidSignIn() { Task { + guard let cookieStore = await currentCookieStore() else { return } guard let accessToken = subscriptionManager.accountManager.accessToken else { // TODO: Add error handling ".accountDidSignIn event but access token is missing" return } - await setSubscriptionCookie(for: accessToken) - print("[🍪 Cookie] Subscription sign in - setting cookie") + await cookieStore.setSubscriptionCookie(for: accessToken) + updateLastRefreshDateToNow() + print("[🍪 Cookie] == Subscription sign in - setting cookie (token: \(accessToken))") } } @objc private func handleAccountDidSignOut() { Task { - guard let subscriptionCookie = await fetchCurrentSubscriptionCookie() else { + guard let cookieStore = await currentCookieStore() else { return } + guard let subscriptionCookie = await cookieStore.fetchCurrentSubscriptionCookie() else { // TODO: Add error handling ".accountDidSignOut event but cookie is missing" return } - await currentCookieStore().deleteCookie(subscriptionCookie) - await deleteSubscriptionCookie() - print("[🍪 Cookie] Subscription sign out - removing cookie") + await cookieStore.deleteCookie(subscriptionCookie) + updateLastRefreshDateToNow() + print("[🍪 Cookie] == Subscription sign out - removing cookie") } } public func refreshSubscriptionCookie() async { + guard let cookieStore = await currentCookieStore() else { return } + print("[🍪 Cookie] Refresh subscription cookie (last refresh date since now: \(lastRefreshDate?.timeIntervalSinceNow ?? 0.0)") guard shouldRefreshSubscriptionCookie() else { return } -// let token = "Test(\(String(describing: Date()))" -// let token = "TEST2" let accessToken: String? = subscriptionManager.accountManager.accessToken print("[🍪 Cookie] Token: \(accessToken ?? "")") + updateLastRefreshDateToNow() - lastRefreshDate = Date() - print("[🍪 Cookie] New last refresh date") if let accessToken { - if let cookie = await fetchCurrentSubscriptionCookie(), cookie.value == accessToken { + if let subscriptionCookie = await cookieStore.fetchCurrentSubscriptionCookie(), subscriptionCookie.value == accessToken { print("[🍪 Cookie] Current up to date") // Cookie present with proper value return } else { // Cookie not present or with different value print("[🍪 Cookie] Cookie not present or with different value") - await setSubscriptionCookie(for: accessToken) + await cookieStore.setSubscriptionCookie(for: accessToken) // TODO: Pixel that refresh actually was required - fixed by updating the token } } else { // remove cookie - await deleteSubscriptionCookie() + if let subscriptionCookie = await cookieStore.fetchCurrentSubscriptionCookie() { + await cookieStore.deleteCookie(subscriptionCookie) + } // TODO: Pixel that refresh actually was required - fixed by deleting the token } @@ -135,63 +139,54 @@ public final class SubscriptionCookieManager: SubscriptionCookieManaging { } } - private func fetchCurrentSubscriptionCookie() async -> HTTPCookie? { - let cookieStore = await currentCookieStore() - var currentCookie: HTTPCookie? + private func updateLastRefreshDateToNow() { + lastRefreshDate = Date() + } + + public func testCookies() async { + print("[🍪 testCookie] Test cookies ================= ") + guard let cookieStore = await currentCookieStore() else { return } for cookie in await cookieStore.allCookies() { - if cookie.domain == Self.cookieDomain && cookie.name == Self.cookieName { - currentCookie = cookie - break + if cookie.domain == Self.cookieDomain { + print(" [🍪 testCookie] Cookie: \(cookie.domain) \(cookie.name)") + print(" \(cookie.debugDescription.replacingOccurrences(of: "\n", with: "; "))") } } - - return currentCookie + print("[🍪 testCookie] ============================== ") } +} - private func setSubscriptionCookie(for token: String) async { - guard let subscriptionCookie = makeSubscriptionCookie(for: token) else { - // TODO: Add error handling "Failed to make a subscription cookie" - return - } +private extension HTTPCookieStore { - print("[🍪 Cookie] Updating cookie") - await currentCookieStore().setCookie(subscriptionCookie) - } + func fetchCurrentSubscriptionCookie() async -> HTTPCookie? { + var currentCookie: HTTPCookie? - private func deleteSubscriptionCookie() async { - guard let cookie = await fetchCurrentSubscriptionCookie() else { - // Not really an error - // Add error handling "Tried to delete subscription cookie but it was not present" - return + for cookie in await allCookies() { + if cookie.domain == SubscriptionCookieManager.cookieDomain && cookie.name == SubscriptionCookieManager.cookieName { + currentCookie = cookie + break + } } - await currentCookieStore().deleteCookie(cookie) - print("[🍪 Cookie] Deleting cookie") + return currentCookie } - private func makeSubscriptionCookie(for token: String) -> HTTPCookie? { - HTTPCookie(properties: [ - .domain: Self.cookieDomain, + func setSubscriptionCookie(for token: String) async { + guard let cookie = HTTPCookie(properties: [ + .domain: SubscriptionCookieManager.cookieDomain, .path: "/", .expires: Date().addingTimeInterval(.days(365)), - .name: Self.cookieName, + .name: SubscriptionCookieManager.cookieName, .value: token, .secure: true, .init(rawValue: "HttpOnly"): true - ]) - } - - public func testCookies() async { - print("[🍪 testCookie] Test cookies ================= ") - let cookieStore = await currentCookieStore() - - for cookie in await cookieStore.allCookies() { - print(" [🍪 testCookie] Cookie: \(cookie.domain) \(cookie.name)") - if cookie.domain == Self.cookieDomain { - print(" \(cookie.debugDescription.replacingOccurrences(of: "\n", with: "; "))") - } + ]) else { + // TODO: Add error handling "Failed to make a subscription cookie" + return } - print("[🍪 testCookie] ============================== ") + + print("[🍪 Cookie] Updating cookie") + await setCookie(cookie) } } From f46f8a5cbd21386a22b4d85a12212eefee57d9c5 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Wed, 23 Oct 2024 15:16:55 +0200 Subject: [PATCH 03/12] Clean up and updated logging --- .../SubscriptionCookieManager.swift | 67 ++++++------------- 1 file changed, 22 insertions(+), 45 deletions(-) diff --git a/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift b/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift index 604a7f600..27f6e5d92 100644 --- a/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift +++ b/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift @@ -18,6 +18,7 @@ import Foundation import WebKit +import os.log public protocol HTTPCookieStore { func allCookies() async -> [HTTPCookie] @@ -30,7 +31,7 @@ extension WKHTTPCookieStore: HTTPCookieStore {} public protocol SubscriptionCookieManaging { init(subscriptionManager: SubscriptionManager, currentCookieStore: @MainActor @escaping () -> HTTPCookieStore?) async func refreshSubscriptionCookie() async - func testCookies() async + func resetLastRefreshDate() } public final class SubscriptionCookieManager: SubscriptionCookieManaging { @@ -75,10 +76,9 @@ public final class SubscriptionCookieManager: SubscriptionCookieManaging { // TODO: Add error handling ".accountDidSignIn event but access token is missing" return } - + Logger.subscription.info("[SubscriptionCookieManager] Handle .accountDidSignIn - setting cookie") await cookieStore.setSubscriptionCookie(for: accessToken) updateLastRefreshDateToNow() - print("[🍪 Cookie] == Subscription sign in - setting cookie (token: \(accessToken))") } } @@ -89,44 +89,37 @@ public final class SubscriptionCookieManager: SubscriptionCookieManaging { // TODO: Add error handling ".accountDidSignOut event but cookie is missing" return } - + Logger.subscription.info("[SubscriptionCookieManager] Handle .accountDidSignOut - deleting cookie") await cookieStore.deleteCookie(subscriptionCookie) updateLastRefreshDateToNow() - print("[🍪 Cookie] == Subscription sign out - removing cookie") } } public func refreshSubscriptionCookie() async { - guard let cookieStore = await currentCookieStore() else { return } - - print("[🍪 Cookie] Refresh subscription cookie (last refresh date since now: \(lastRefreshDate?.timeIntervalSinceNow ?? 0.0)") guard shouldRefreshSubscriptionCookie() else { return } + guard let cookieStore = await currentCookieStore() else { return } - let accessToken: String? = subscriptionManager.accountManager.accessToken - - print("[🍪 Cookie] Token: \(accessToken ?? "")") + Logger.subscription.info("[SubscriptionCookieManager] Refresh subscription cookie") updateLastRefreshDateToNow() + let accessToken: String? = subscriptionManager.accountManager.accessToken + let subscriptionCookie = await cookieStore.fetchCurrentSubscriptionCookie() if let accessToken { - if let subscriptionCookie = await cookieStore.fetchCurrentSubscriptionCookie(), subscriptionCookie.value == accessToken { - print("[🍪 Cookie] Current up to date") - // Cookie present with proper value - return - } else { - // Cookie not present or with different value - print("[🍪 Cookie] Cookie not present or with different value") + if subscriptionCookie == nil || subscriptionCookie?.value != accessToken { + Logger.subscription.info("[SubscriptionCookieManager] Refresh: No cookie or one with different value") await cookieStore.setSubscriptionCookie(for: accessToken) - // TODO: Pixel that refresh actually was required - fixed by updating the token + } else { + Logger.subscription.info("[SubscriptionCookieManager] Refresh: Cookie exists and is up to date") + return } } else { - // remove cookie - if let subscriptionCookie = await cookieStore.fetchCurrentSubscriptionCookie() { + if let subscriptionCookie { + Logger.subscription.info("[SubscriptionCookieManager] Refresh: No access token but old cookie exists, deleting it") await cookieStore.deleteCookie(subscriptionCookie) + // TODO: Pixel that refresh actually was required - fixed by deleting the token } - - // TODO: Pixel that refresh actually was required - fixed by deleting the token } } @@ -143,33 +136,15 @@ public final class SubscriptionCookieManager: SubscriptionCookieManaging { lastRefreshDate = Date() } - public func testCookies() async { - print("[🍪 testCookie] Test cookies ================= ") - guard let cookieStore = await currentCookieStore() else { return } - - for cookie in await cookieStore.allCookies() { - if cookie.domain == Self.cookieDomain { - print(" [🍪 testCookie] Cookie: \(cookie.domain) \(cookie.name)") - print(" \(cookie.debugDescription.replacingOccurrences(of: "\n", with: "; "))") - } - } - print("[🍪 testCookie] ============================== ") + public func resetLastRefreshDate() { + lastRefreshDate = nil } } private extension HTTPCookieStore { func fetchCurrentSubscriptionCookie() async -> HTTPCookie? { - var currentCookie: HTTPCookie? - - for cookie in await allCookies() { - if cookie.domain == SubscriptionCookieManager.cookieDomain && cookie.name == SubscriptionCookieManager.cookieName { - currentCookie = cookie - break - } - } - - return currentCookie + await allCookies().first { $0.domain == SubscriptionCookieManager.cookieDomain && $0.name == SubscriptionCookieManager.cookieName } } func setSubscriptionCookie(for token: String) async { @@ -183,10 +158,12 @@ private extension HTTPCookieStore { .init(rawValue: "HttpOnly"): true ]) else { // TODO: Add error handling "Failed to make a subscription cookie" + Logger.subscription.error("[HTTPCookieStore] Subscription cookie could not be created") + assertionFailure("Subscription cookie could not be created") return } - print("[🍪 Cookie] Updating cookie") + Logger.subscription.info("[HTTPCookieStore] Setting subscription cookie") await setCookie(cookie) } } From 757fad256d6d35a007933858570e60063f01719e Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Wed, 23 Oct 2024 16:41:40 +0200 Subject: [PATCH 04/12] Clean up and error handling --- .../SubscriptionCookie/HTTPCookieStore.swift | 28 ++++++++++ .../SubscriptionCookieManager.swift | 54 +++++++++++-------- .../SubscriptionCookieManagerEvent.swift | 29 ++++++++++ 3 files changed, 90 insertions(+), 21 deletions(-) create mode 100644 Sources/Subscription/SubscriptionCookie/HTTPCookieStore.swift create mode 100644 Sources/Subscription/SubscriptionCookie/SubscriptionCookieManagerEvent.swift diff --git a/Sources/Subscription/SubscriptionCookie/HTTPCookieStore.swift b/Sources/Subscription/SubscriptionCookie/HTTPCookieStore.swift new file mode 100644 index 000000000..74fec5568 --- /dev/null +++ b/Sources/Subscription/SubscriptionCookie/HTTPCookieStore.swift @@ -0,0 +1,28 @@ +// +// HTTPCookieStore.swift +// +// Copyright © 2024 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 WebKit + +public protocol HTTPCookieStore { + func allCookies() async -> [HTTPCookie] + func setCookie(_ cookie: HTTPCookie) async + func deleteCookie(_ cookie: HTTPCookie) async +} + +extension WKHTTPCookieStore: HTTPCookieStore {} diff --git a/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift b/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift index 27f6e5d92..c5f7dd39d 100644 --- a/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift +++ b/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift @@ -17,19 +17,11 @@ // import Foundation -import WebKit +import Common import os.log -public protocol HTTPCookieStore { - func allCookies() async -> [HTTPCookie] - func setCookie(_ cookie: HTTPCookie) async - func deleteCookie(_ cookie: HTTPCookie) async -} - -extension WKHTTPCookieStore: HTTPCookieStore {} - public protocol SubscriptionCookieManaging { - init(subscriptionManager: SubscriptionManager, currentCookieStore: @MainActor @escaping () -> HTTPCookieStore?) async + init(subscriptionManager: SubscriptionManager, currentCookieStore: @MainActor @escaping () -> HTTPCookieStore?, eventMapping: EventMapping) async func refreshSubscriptionCookie() async func resetLastRefreshDate() } @@ -43,22 +35,27 @@ public final class SubscriptionCookieManager: SubscriptionCookieManaging { private let subscriptionManager: SubscriptionManager private let currentCookieStore: @MainActor () -> HTTPCookieStore? + private let eventMapping: EventMapping private var lastRefreshDate: Date? private let refreshTimeInterval: TimeInterval convenience nonisolated public required init(subscriptionManager: SubscriptionManager, - currentCookieStore: @MainActor @escaping () -> HTTPCookieStore?) { + currentCookieStore: @MainActor @escaping () -> HTTPCookieStore?, + eventMapping: EventMapping) { self.init(subscriptionManager: subscriptionManager, currentCookieStore: currentCookieStore, + eventMapping: eventMapping, refreshTimeInterval: SubscriptionCookieManager.defaultRefreshTimeInterval) } nonisolated public required init(subscriptionManager: SubscriptionManager, currentCookieStore: @MainActor @escaping () -> HTTPCookieStore?, + eventMapping: EventMapping, refreshTimeInterval: TimeInterval) { self.subscriptionManager = subscriptionManager self.currentCookieStore = currentCookieStore + self.eventMapping = eventMapping self.refreshTimeInterval = refreshTimeInterval registerForSubscriptionAccountManagerEvents() @@ -73,12 +70,18 @@ public final class SubscriptionCookieManager: SubscriptionCookieManaging { Task { guard let cookieStore = await currentCookieStore() else { return } guard let accessToken = subscriptionManager.accountManager.accessToken else { - // TODO: Add error handling ".accountDidSignIn event but access token is missing" + Logger.subscription.error("[SubscriptionCookieManager] Handle .accountDidSignIn - can't set the cookie, token is missing") + eventMapping.fire(.errorHandlingAccountDidSignInTokenIsMissing) return } Logger.subscription.info("[SubscriptionCookieManager] Handle .accountDidSignIn - setting cookie") - await cookieStore.setSubscriptionCookie(for: accessToken) - updateLastRefreshDateToNow() + + do { + try await cookieStore.setSubscriptionCookie(for: accessToken) + updateLastRefreshDateToNow() + } catch { + eventMapping.fire(.errorFailedToSetSubscriptionCookie) + } } } @@ -86,7 +89,8 @@ public final class SubscriptionCookieManager: SubscriptionCookieManaging { Task { guard let cookieStore = await currentCookieStore() else { return } guard let subscriptionCookie = await cookieStore.fetchCurrentSubscriptionCookie() else { - // TODO: Add error handling ".accountDidSignOut event but cookie is missing" + Logger.subscription.error("[SubscriptionCookieManager] Handle .accountDidSignOut - can't delete the cookie, cookie is missing") + eventMapping.fire(.errorHandlingAccountDidSignOutCookieIsMissing) return } Logger.subscription.info("[SubscriptionCookieManager] Handle .accountDidSignOut - deleting cookie") @@ -108,8 +112,12 @@ public final class SubscriptionCookieManager: SubscriptionCookieManaging { if let accessToken { if subscriptionCookie == nil || subscriptionCookie?.value != accessToken { Logger.subscription.info("[SubscriptionCookieManager] Refresh: No cookie or one with different value") - await cookieStore.setSubscriptionCookie(for: accessToken) - // TODO: Pixel that refresh actually was required - fixed by updating the token + do { + try await cookieStore.setSubscriptionCookie(for: accessToken) + eventMapping.fire(.subscriptionCookieRefreshedWithUpdate) + } catch { + eventMapping.fire(.errorFailedToSetSubscriptionCookie) + } } else { Logger.subscription.info("[SubscriptionCookieManager] Refresh: Cookie exists and is up to date") return @@ -118,7 +126,7 @@ public final class SubscriptionCookieManager: SubscriptionCookieManaging { if let subscriptionCookie { Logger.subscription.info("[SubscriptionCookieManager] Refresh: No access token but old cookie exists, deleting it") await cookieStore.deleteCookie(subscriptionCookie) - // TODO: Pixel that refresh actually was required - fixed by deleting the token + eventMapping.fire(.subscriptionCookieRefreshedWithDelete) } } } @@ -141,13 +149,18 @@ public final class SubscriptionCookieManager: SubscriptionCookieManaging { } } +enum SubscriptionCookieManagerError: Error { + case failedToCreateSubscriptionCookie +} + + private extension HTTPCookieStore { func fetchCurrentSubscriptionCookie() async -> HTTPCookie? { await allCookies().first { $0.domain == SubscriptionCookieManager.cookieDomain && $0.name == SubscriptionCookieManager.cookieName } } - func setSubscriptionCookie(for token: String) async { + func setSubscriptionCookie(for token: String) async throws { guard let cookie = HTTPCookie(properties: [ .domain: SubscriptionCookieManager.cookieDomain, .path: "/", @@ -157,10 +170,9 @@ private extension HTTPCookieStore { .secure: true, .init(rawValue: "HttpOnly"): true ]) else { - // TODO: Add error handling "Failed to make a subscription cookie" Logger.subscription.error("[HTTPCookieStore] Subscription cookie could not be created") assertionFailure("Subscription cookie could not be created") - return + throw SubscriptionCookieManagerError.failedToCreateSubscriptionCookie } Logger.subscription.info("[HTTPCookieStore] Setting subscription cookie") diff --git a/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManagerEvent.swift b/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManagerEvent.swift new file mode 100644 index 000000000..d04b346ea --- /dev/null +++ b/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManagerEvent.swift @@ -0,0 +1,29 @@ +// +// SubscriptionCookieManagerEvent.swift +// +// Copyright © 2024 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 enum SubscriptionCookieManagerEvent { + case errorHandlingAccountDidSignInTokenIsMissing + case errorHandlingAccountDidSignOutCookieIsMissing + + case subscriptionCookieRefreshedWithUpdate + case subscriptionCookieRefreshedWithDelete + + case errorFailedToSetSubscriptionCookie +} From c945728f0cf163805e9e0b5962d6dcac44cf0f75 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Wed, 23 Oct 2024 16:42:07 +0200 Subject: [PATCH 05/12] Set defaultRefreshTimeInterval to 4h --- .../SubscriptionCookie/SubscriptionCookieManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift b/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift index c5f7dd39d..f710151bf 100644 --- a/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift +++ b/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift @@ -31,7 +31,7 @@ public final class SubscriptionCookieManager: SubscriptionCookieManaging { public static let cookieDomain = "duckduckgo.com" public static let cookieName = "privacy_pro_access_token" - private static let defaultRefreshTimeInterval: TimeInterval = .seconds(10) // TODO: change the default to e.g. 4h + private static let defaultRefreshTimeInterval: TimeInterval = .hours(4) private let subscriptionManager: SubscriptionManager private let currentCookieStore: @MainActor () -> HTTPCookieStore? From 22f61802e71f863b80110146336bf7a4db86fb1b Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Wed, 23 Oct 2024 16:52:35 +0200 Subject: [PATCH 06/12] Rename errorFailedToSetSubscriptionCookie -> failedToSetSubscriptionCookie --- .../SubscriptionCookie/SubscriptionCookieManager.swift | 4 ++-- .../SubscriptionCookie/SubscriptionCookieManagerEvent.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift b/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift index f710151bf..2627713f0 100644 --- a/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift +++ b/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift @@ -80,7 +80,7 @@ public final class SubscriptionCookieManager: SubscriptionCookieManaging { try await cookieStore.setSubscriptionCookie(for: accessToken) updateLastRefreshDateToNow() } catch { - eventMapping.fire(.errorFailedToSetSubscriptionCookie) + eventMapping.fire(.failedToSetSubscriptionCookie) } } } @@ -116,7 +116,7 @@ public final class SubscriptionCookieManager: SubscriptionCookieManaging { try await cookieStore.setSubscriptionCookie(for: accessToken) eventMapping.fire(.subscriptionCookieRefreshedWithUpdate) } catch { - eventMapping.fire(.errorFailedToSetSubscriptionCookie) + eventMapping.fire(.failedToSetSubscriptionCookie) } } else { Logger.subscription.info("[SubscriptionCookieManager] Refresh: Cookie exists and is up to date") diff --git a/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManagerEvent.swift b/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManagerEvent.swift index d04b346ea..86f604d3d 100644 --- a/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManagerEvent.swift +++ b/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManagerEvent.swift @@ -25,5 +25,5 @@ public enum SubscriptionCookieManagerEvent { case subscriptionCookieRefreshedWithUpdate case subscriptionCookieRefreshedWithDelete - case errorFailedToSetSubscriptionCookie + case failedToSetSubscriptionCookie } From 022b0c7be3016297d2e365d0cf7874389fef1bb9 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 25 Oct 2024 10:45:47 +0200 Subject: [PATCH 07/12] Prefix cookie domain with a dot --- .../SubscriptionCookie/SubscriptionCookieManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift b/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift index 2627713f0..3845d6d8b 100644 --- a/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift +++ b/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift @@ -28,7 +28,7 @@ public protocol SubscriptionCookieManaging { public final class SubscriptionCookieManager: SubscriptionCookieManaging { - public static let cookieDomain = "duckduckgo.com" + public static let cookieDomain = ".duckduckgo.com" public static let cookieName = "privacy_pro_access_token" private static let defaultRefreshTimeInterval: TimeInterval = .hours(4) From e9d1cb3186f41c9db5c8242d55ee33e4351cae2e Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 25 Oct 2024 15:39:45 +0200 Subject: [PATCH 08/12] Add SubscriptionCookieManagerTests --- .../SubscriptionCookieManager.swift | 4 +- .../Managers/AccountManagerTests.swift | 1 + .../SubscriptionCookieManagerTests.swift | 237 ++++++++++++++++++ 3 files changed, 241 insertions(+), 1 deletion(-) create mode 100644 Tests/SubscriptionTests/SubscriptionCookie/SubscriptionCookieManagerTests.swift diff --git a/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift b/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift index 3845d6d8b..72d7b378b 100644 --- a/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift +++ b/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift @@ -24,6 +24,8 @@ public protocol SubscriptionCookieManaging { init(subscriptionManager: SubscriptionManager, currentCookieStore: @MainActor @escaping () -> HTTPCookieStore?, eventMapping: EventMapping) async func refreshSubscriptionCookie() async func resetLastRefreshDate() + + var lastRefreshDate: Date? { get } } public final class SubscriptionCookieManager: SubscriptionCookieManaging { @@ -37,7 +39,7 @@ public final class SubscriptionCookieManager: SubscriptionCookieManaging { private let currentCookieStore: @MainActor () -> HTTPCookieStore? private let eventMapping: EventMapping - private var lastRefreshDate: Date? + public private(set) var lastRefreshDate: Date? private let refreshTimeInterval: TimeInterval convenience nonisolated public required init(subscriptionManager: SubscriptionManager, diff --git a/Tests/SubscriptionTests/Managers/AccountManagerTests.swift b/Tests/SubscriptionTests/Managers/AccountManagerTests.swift index ce823e6a9..40e633154 100644 --- a/Tests/SubscriptionTests/Managers/AccountManagerTests.swift +++ b/Tests/SubscriptionTests/Managers/AccountManagerTests.swift @@ -112,6 +112,7 @@ final class AccountManagerTests: XCTestCase { func testStoreAccount() async throws { // Given + let notificationExpectation = expectation(forNotification: .accountDidSignIn, object: accountManager, handler: nil) // When diff --git a/Tests/SubscriptionTests/SubscriptionCookie/SubscriptionCookieManagerTests.swift b/Tests/SubscriptionTests/SubscriptionCookie/SubscriptionCookieManagerTests.swift new file mode 100644 index 000000000..80970e616 --- /dev/null +++ b/Tests/SubscriptionTests/SubscriptionCookie/SubscriptionCookieManagerTests.swift @@ -0,0 +1,237 @@ +// +// SubscriptionCookieManagerTests.swift +// +// Copyright © 2024 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 +import Common +@testable import Subscription +import SubscriptionTestingUtilities + +final class SubscriptionCookieManagerTests: XCTestCase { + + private struct Constants { + static let authToken = UUID().uuidString + static let accessToken = UUID().uuidString + } + + var accountManager: AccountManagerMock! + var subscriptionService: SubscriptionEndpointServiceMock! + var authService: AuthEndpointServiceMock! + var storePurchaseManager: StorePurchaseManagerMock! + var subscriptionEnvironment: SubscriptionEnvironment! + var subscriptionManager: SubscriptionManagerMock! + + var cookieStore: HTTPCookieStore! + var subscriptionCookieManager: SubscriptionCookieManager! + + override func setUp() async throws { + accountManager = AccountManagerMock() + subscriptionService = SubscriptionEndpointServiceMock() + authService = AuthEndpointServiceMock() + storePurchaseManager = StorePurchaseManagerMock() + subscriptionEnvironment = SubscriptionEnvironment(serviceEnvironment: .production, + purchasePlatform: .appStore) + + subscriptionManager = SubscriptionManagerMock(accountManager: accountManager, + subscriptionEndpointService: subscriptionService, + authEndpointService: authService, + storePurchaseManager: storePurchaseManager, + currentEnvironment: subscriptionEnvironment, + canPurchase: true) + cookieStore = MockHTTPCookieStore() + + subscriptionCookieManager = SubscriptionCookieManager(subscriptionManager: subscriptionManager, + currentCookieStore: { self.cookieStore }, + eventMapping: MockSubscriptionCookieManageEventPixelMapping(), + refreshTimeInterval: .seconds(1)) + } + + override func tearDown() async throws { + accountManager = nil + subscriptionService = nil + authService = nil + storePurchaseManager = nil + subscriptionEnvironment = nil + + subscriptionManager = nil + } + + func testSubscriptionCookieIsAddedWhenSigningInToSubscription() async throws { + // Given + await ensureNoSubscriptionCookieInTheCookieStore() + accountManager.accessToken = Constants.accessToken + + // When + NotificationCenter.default.post(name: .accountDidSignIn, object: self, userInfo: nil) + try await Task.sleep(seconds: 0.1) + + // Then + await checkSubscriptionCookieIsPresent() + } + + func testSubscriptionCookieIsDeletedWhenSigningInToSubscription() async throws { + // Given + await ensureSubscriptionCookieIsInTheCookieStore() + + // When + NotificationCenter.default.post(name: .accountDidSignOut, object: self, userInfo: nil) + try await Task.sleep(seconds: 0.1) + + // Then + await checkSubscriptionCookieIsNotPresent() + } + + func testRefreshWhenSignedInButCookieIsMissing() async throws { + // Given + accountManager.accessToken = Constants.accessToken + await ensureNoSubscriptionCookieInTheCookieStore() + + // When + await subscriptionCookieManager.refreshSubscriptionCookie() + try await Task.sleep(seconds: 0.1) + + // Then + await checkSubscriptionCookieIsPresent() + } + + func testRefreshWhenSignedOutButCookieIsPresent() async throws { + // Given + accountManager.accessToken = nil + await ensureSubscriptionCookieIsInTheCookieStore() + + // When + await subscriptionCookieManager.refreshSubscriptionCookie() + try await Task.sleep(seconds: 0.1) + + // Then + await checkSubscriptionCookieIsNotPresent() + } + + func testRefreshNotTriggeredTwiceWithinSetRefreshInterval() async throws { + // Given + let firstRefreshDate: Date? + let secondRefreshDate: Date? + + + // When + await subscriptionCookieManager.refreshSubscriptionCookie() + firstRefreshDate = subscriptionCookieManager.lastRefreshDate + + try await Task.sleep(seconds: 0.5) + + await subscriptionCookieManager.refreshSubscriptionCookie() + secondRefreshDate = subscriptionCookieManager.lastRefreshDate + + // Then + XCTAssertEqual(firstRefreshDate!, secondRefreshDate!) + } + + func testRefreshNotTriggeredSecondTimeAfterSetRefreshInterval() async throws { + // Given + let firstRefreshDate: Date? + let secondRefreshDate: Date? + + // When + await subscriptionCookieManager.refreshSubscriptionCookie() + firstRefreshDate = subscriptionCookieManager.lastRefreshDate + + try await Task.sleep(seconds: 1.1) + + await subscriptionCookieManager.refreshSubscriptionCookie() + secondRefreshDate = subscriptionCookieManager.lastRefreshDate + + // Then + XCTAssertTrue(firstRefreshDate! < secondRefreshDate!) + } + + private func ensureSubscriptionCookieIsInTheCookieStore() async { + let subscriptionCookie = HTTPCookie(properties: [ + .domain: SubscriptionCookieManager.cookieDomain, + .path: "/", + .expires: Date().addingTimeInterval(.days(365)), + .name: SubscriptionCookieManager.cookieName, + .value: Constants.accessToken, + .secure: true, + .init(rawValue: "HttpOnly"): true + ])! + await cookieStore.setCookie(subscriptionCookie) + + let cookieStoreCookies = await cookieStore.allCookies() + XCTAssertEqual(cookieStoreCookies.count, 1) + } + + private func ensureNoSubscriptionCookieInTheCookieStore() async { + let cookieStoreCookies = await cookieStore.allCookies() + XCTAssertTrue(cookieStoreCookies.isEmpty) + } + + private func checkSubscriptionCookieIsPresent() async { + guard let subscriptionCookie = await cookieStore.fetchSubscriptionCookie() else { + XCTFail("No subscription cookie in the store") + return + } + XCTAssertEqual(subscriptionCookie.value, Constants.accessToken) + } + + private func checkSubscriptionCookieIsNotPresent() async { + let cookie = await cookieStore.fetchSubscriptionCookie() + XCTAssertNil(cookie) + } + +} + +private extension HTTPCookieStore { + + func fetchSubscriptionCookie() async -> HTTPCookie? { + await allCookies().first { $0.domain == SubscriptionCookieManager.cookieDomain && $0.name == SubscriptionCookieManager.cookieName } + } +} + +class MockHTTPCookieStore: HTTPCookieStore { + + var cookies: [HTTPCookie] + + init(cookies: [HTTPCookie] = []) { + self.cookies = cookies + } + + func allCookies() async -> [HTTPCookie] { + return cookies + } + + func setCookie(_ cookie: HTTPCookie) async { + cookies.append(cookie) + } + + func deleteCookie(_ cookie: HTTPCookie) async { + cookies.removeAll { $0.domain == cookie.domain } + } + +} + +class MockSubscriptionCookieManageEventPixelMapping: EventMapping { + + public init() { + super.init { event, _, _, _ in + + } + } + + override init(mapping: @escaping EventMapping.Mapping) { + fatalError("Use init()") + } +} From 57d4cd7548837ebf7adae8a85a8c950bf7662649 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 28 Oct 2024 14:20:34 +0100 Subject: [PATCH 09/12] Add SubscriptionCookieManagerMock --- .../SubscriptionCookieManagerMock.swift | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 Sources/SubscriptionTestingUtilities/SubscriptionCookie/SubscriptionCookieManagerMock.swift diff --git a/Sources/SubscriptionTestingUtilities/SubscriptionCookie/SubscriptionCookieManagerMock.swift b/Sources/SubscriptionTestingUtilities/SubscriptionCookie/SubscriptionCookieManagerMock.swift new file mode 100644 index 000000000..206bc3b9a --- /dev/null +++ b/Sources/SubscriptionTestingUtilities/SubscriptionCookie/SubscriptionCookieManagerMock.swift @@ -0,0 +1,66 @@ +// +// SubscriptionCookieManagerMock.swift +// +// Copyright © 2024 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 +import Subscription + +public final class SubscriptionCookieManagerMock: SubscriptionCookieManaging { + + public var lastRefreshDate: Date? + + public convenience init() { + let accountManager = AccountManagerMock() + let subscriptionService = DefaultSubscriptionEndpointService(currentServiceEnvironment: .production) + let authService = DefaultAuthEndpointService(currentServiceEnvironment: .production) + let storePurchaseManager = DefaultStorePurchaseManager() + let subscriptionManager = SubscriptionManagerMock(accountManager: accountManager, + subscriptionEndpointService: subscriptionService, + authEndpointService: authService, + storePurchaseManager: storePurchaseManager, + currentEnvironment: SubscriptionEnvironment(serviceEnvironment: .production, + purchasePlatform: .appStore), + canPurchase: true) + + self.init(subscriptionManager: subscriptionManager, + currentCookieStore: { return nil }, + eventMapping: MockSubscriptionCookieManagerEventPixelMapping()) + } + + public init(subscriptionManager: SubscriptionManager, + currentCookieStore: @MainActor @escaping () -> HTTPCookieStore?, + eventMapping: EventMapping) { + + } + + public func refreshSubscriptionCookie() async { } + public func resetLastRefreshDate() { } +} + + +public final class MockSubscriptionCookieManagerEventPixelMapping: EventMapping { + + public init() { + super.init { _, _, _, _ in + } + } + + override init(mapping: @escaping EventMapping.Mapping) { + fatalError("Use init()") + } +} From 5cdf6eff7bff8cd44223d01a179ebd9ccd2098ab Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 28 Oct 2024 14:20:49 +0100 Subject: [PATCH 10/12] Remove unnecessary async from init --- .../SubscriptionCookie/SubscriptionCookieManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift b/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift index 72d7b378b..a88b944d6 100644 --- a/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift +++ b/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift @@ -21,7 +21,7 @@ import Common import os.log public protocol SubscriptionCookieManaging { - init(subscriptionManager: SubscriptionManager, currentCookieStore: @MainActor @escaping () -> HTTPCookieStore?, eventMapping: EventMapping) async + init(subscriptionManager: SubscriptionManager, currentCookieStore: @MainActor @escaping () -> HTTPCookieStore?, eventMapping: EventMapping) func refreshSubscriptionCookie() async func resetLastRefreshDate() From c7f586739c852ca58697523c08c1dd567cad4638 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 28 Oct 2024 15:31:21 +0100 Subject: [PATCH 11/12] Swiftlint fixes --- .../SubscriptionCookie/SubscriptionCookieManager.swift | 3 +-- .../SubscriptionCookie/SubscriptionCookieManagerMock.swift | 1 - Tests/SubscriptionTests/Managers/AccountManagerTests.swift | 2 +- .../SubscriptionCookie/SubscriptionCookieManagerTests.swift | 5 ++--- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift b/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift index a88b944d6..7d7099d75 100644 --- a/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift +++ b/Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift @@ -77,7 +77,7 @@ public final class SubscriptionCookieManager: SubscriptionCookieManaging { return } Logger.subscription.info("[SubscriptionCookieManager] Handle .accountDidSignIn - setting cookie") - + do { try await cookieStore.setSubscriptionCookie(for: accessToken) updateLastRefreshDateToNow() @@ -155,7 +155,6 @@ enum SubscriptionCookieManagerError: Error { case failedToCreateSubscriptionCookie } - private extension HTTPCookieStore { func fetchCurrentSubscriptionCookie() async -> HTTPCookie? { diff --git a/Sources/SubscriptionTestingUtilities/SubscriptionCookie/SubscriptionCookieManagerMock.swift b/Sources/SubscriptionTestingUtilities/SubscriptionCookie/SubscriptionCookieManagerMock.swift index 206bc3b9a..6ec2e16c3 100644 --- a/Sources/SubscriptionTestingUtilities/SubscriptionCookie/SubscriptionCookieManagerMock.swift +++ b/Sources/SubscriptionTestingUtilities/SubscriptionCookie/SubscriptionCookieManagerMock.swift @@ -52,7 +52,6 @@ public final class SubscriptionCookieManagerMock: SubscriptionCookieManaging { public func resetLastRefreshDate() { } } - public final class MockSubscriptionCookieManagerEventPixelMapping: EventMapping { public init() { diff --git a/Tests/SubscriptionTests/Managers/AccountManagerTests.swift b/Tests/SubscriptionTests/Managers/AccountManagerTests.swift index 40e633154..0a04a4cde 100644 --- a/Tests/SubscriptionTests/Managers/AccountManagerTests.swift +++ b/Tests/SubscriptionTests/Managers/AccountManagerTests.swift @@ -112,7 +112,7 @@ final class AccountManagerTests: XCTestCase { func testStoreAccount() async throws { // Given - + let notificationExpectation = expectation(forNotification: .accountDidSignIn, object: accountManager, handler: nil) // When diff --git a/Tests/SubscriptionTests/SubscriptionCookie/SubscriptionCookieManagerTests.swift b/Tests/SubscriptionTests/SubscriptionCookie/SubscriptionCookieManagerTests.swift index 80970e616..04dfe7b8b 100644 --- a/Tests/SubscriptionTests/SubscriptionCookie/SubscriptionCookieManagerTests.swift +++ b/Tests/SubscriptionTests/SubscriptionCookie/SubscriptionCookieManagerTests.swift @@ -126,13 +126,12 @@ final class SubscriptionCookieManagerTests: XCTestCase { let firstRefreshDate: Date? let secondRefreshDate: Date? - // When await subscriptionCookieManager.refreshSubscriptionCookie() firstRefreshDate = subscriptionCookieManager.lastRefreshDate - + try await Task.sleep(seconds: 0.5) - + await subscriptionCookieManager.refreshSubscriptionCookie() secondRefreshDate = subscriptionCookieManager.lastRefreshDate From f0234c07c717ed7cd98e4c25a65f87973072427e Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 28 Oct 2024 15:39:38 +0100 Subject: [PATCH 12/12] Fix mock usage --- .../SubscriptionCookie/SubscriptionCookieManagerMock.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SubscriptionTestingUtilities/SubscriptionCookie/SubscriptionCookieManagerMock.swift b/Sources/SubscriptionTestingUtilities/SubscriptionCookie/SubscriptionCookieManagerMock.swift index 6ec2e16c3..56e970ebb 100644 --- a/Sources/SubscriptionTestingUtilities/SubscriptionCookie/SubscriptionCookieManagerMock.swift +++ b/Sources/SubscriptionTestingUtilities/SubscriptionCookie/SubscriptionCookieManagerMock.swift @@ -28,7 +28,7 @@ public final class SubscriptionCookieManagerMock: SubscriptionCookieManaging { let accountManager = AccountManagerMock() let subscriptionService = DefaultSubscriptionEndpointService(currentServiceEnvironment: .production) let authService = DefaultAuthEndpointService(currentServiceEnvironment: .production) - let storePurchaseManager = DefaultStorePurchaseManager() + let storePurchaseManager = StorePurchaseManagerMock() let subscriptionManager = SubscriptionManagerMock(accountManager: accountManager, subscriptionEndpointService: subscriptionService, authEndpointService: authService,