Skip to content
This repository was archived by the owner on Feb 24, 2025. It is now read-only.

Commit 7b78b46

Browse files
authored
Update to subscription cookie (#1053)
<!-- Note: This checklist is a reminder of our shared engineering expectations. --> Please review the release process for BrowserServicesKit [here](https://app.asana.com/0/1200194497630846/1200837094583426). **Required**: Task/Issue URL: https://app.asana.com/0/1108686900785972/1208264562025859/f iOS PR: duckduckgo/iOS#3512 macOS PR: duckduckgo/macos-browser#3489 What kind of version bump will this require?: Patch **Description**: Update to how the subscription cookie operates: - constraint the cookie to `subscriptions.duckduckgo.com` - on sign out do not fully remove the cookie - just clear the value - gate the feature behind the `setAccessTokenCookieForSubscriptionDomains` privacy config feature flag **Steps to test this PR**: See client PRs <!-- Before submitting a PR, please ensure you have tested the combinations you expect the reviewer to test, then delete configurations you *know* do not need explicit testing. Using a simulator where a physical device is unavailable is acceptable. --> **OS Testing**: * [ ] iOS 14 * [ ] iOS 15 * [ ] iOS 16 * [ ] macOS 10.15 * [ ] macOS 11 * [ ] macOS 12 --- ###### Internal references: [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943)
1 parent d39d04c commit 7b78b46

File tree

5 files changed

+71
-40
lines changed

5 files changed

+71
-40
lines changed

Sources/BrowserServicesKit/PrivacyConfig/Features/PrivacyFeature.swift

+1
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ public enum PrivacyProSubfeature: String, Equatable, PrivacySubfeature {
152152
case isLaunchedOverride
153153
case isLaunchedOverrideStripe
154154
case useUnifiedFeedback
155+
case setAccessTokenCookieForSubscriptionDomains
155156
}
156157

157158
public enum SslCertificatesSubfeature: String, PrivacySubfeature {

Sources/Subscription/SubscriptionCookie/SubscriptionCookieManager.swift

+51-32
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ import os.log
2222

2323
public protocol SubscriptionCookieManaging {
2424
init(subscriptionManager: SubscriptionManager, currentCookieStore: @MainActor @escaping () -> HTTPCookieStore?, eventMapping: EventMapping<SubscriptionCookieManagerEvent>)
25+
func enableSettingSubscriptionCookie()
26+
func disableSettingSubscriptionCookie() async
27+
2528
func refreshSubscriptionCookie() async
2629
func resetLastRefreshDate()
2730

@@ -30,7 +33,7 @@ public protocol SubscriptionCookieManaging {
3033

3134
public final class SubscriptionCookieManager: SubscriptionCookieManaging {
3235

33-
public static let cookieDomain = ".duckduckgo.com"
36+
public static let cookieDomain = "subscriptions.duckduckgo.com"
3437
public static let cookieName = "privacy_pro_access_token"
3538

3639
private static let defaultRefreshTimeInterval: TimeInterval = .hours(4)
@@ -41,6 +44,7 @@ public final class SubscriptionCookieManager: SubscriptionCookieManaging {
4144

4245
public private(set) var lastRefreshDate: Date?
4346
private let refreshTimeInterval: TimeInterval
47+
private var isSettingSubscriptionCookieEnabled: Bool = false
4448

4549
convenience nonisolated public required init(subscriptionManager: SubscriptionManager,
4650
currentCookieStore: @MainActor @escaping () -> HTTPCookieStore?,
@@ -60,17 +64,28 @@ public final class SubscriptionCookieManager: SubscriptionCookieManaging {
6064
self.eventMapping = eventMapping
6165
self.refreshTimeInterval = refreshTimeInterval
6266

63-
registerForSubscriptionAccountManagerEvents()
64-
}
65-
66-
private func registerForSubscriptionAccountManagerEvents() {
6767
NotificationCenter.default.addObserver(self, selector: #selector(handleAccountDidSignIn), name: .accountDidSignIn, object: nil)
6868
NotificationCenter.default.addObserver(self, selector: #selector(handleAccountDidSignOut), name: .accountDidSignOut, object: nil)
6969
}
7070

71+
public func enableSettingSubscriptionCookie() {
72+
isSettingSubscriptionCookieEnabled = true
73+
}
74+
75+
public func disableSettingSubscriptionCookie() async {
76+
isSettingSubscriptionCookieEnabled = false
77+
if let cookieStore = await currentCookieStore(),
78+
let cookie = await cookieStore.fetchCurrentSubscriptionCookie() {
79+
await cookieStore.deleteCookie(cookie)
80+
}
81+
}
82+
7183
@objc private func handleAccountDidSignIn() {
7284
Task {
73-
guard let cookieStore = await currentCookieStore() else { return }
85+
guard isSettingSubscriptionCookieEnabled,
86+
let cookieStore = await currentCookieStore()
87+
else { return }
88+
7489
guard let accessToken = subscriptionManager.accountManager.accessToken else {
7590
Logger.subscription.error("[SubscriptionCookieManager] Handle .accountDidSignIn - can't set the cookie, token is missing")
7691
eventMapping.fire(.errorHandlingAccountDidSignInTokenIsMissing)
@@ -89,47 +104,47 @@ public final class SubscriptionCookieManager: SubscriptionCookieManaging {
89104

90105
@objc private func handleAccountDidSignOut() {
91106
Task {
92-
guard let cookieStore = await currentCookieStore() else { return }
93-
guard let subscriptionCookie = await cookieStore.fetchCurrentSubscriptionCookie() else {
94-
Logger.subscription.error("[SubscriptionCookieManager] Handle .accountDidSignOut - can't delete the cookie, cookie is missing")
95-
eventMapping.fire(.errorHandlingAccountDidSignOutCookieIsMissing)
96-
return
97-
}
107+
guard isSettingSubscriptionCookieEnabled,
108+
let cookieStore = await currentCookieStore()
109+
else { return }
98110
Logger.subscription.info("[SubscriptionCookieManager] Handle .accountDidSignOut - deleting cookie")
99-
await cookieStore.deleteCookie(subscriptionCookie)
100-
updateLastRefreshDateToNow()
111+
112+
do {
113+
try await cookieStore.setEmptySubscriptionCookie()
114+
updateLastRefreshDateToNow()
115+
} catch {
116+
eventMapping.fire(.failedToSetSubscriptionCookie)
117+
}
101118
}
102119
}
103120

104121
public func refreshSubscriptionCookie() async {
105-
guard shouldRefreshSubscriptionCookie() else { return }
106-
guard let cookieStore = await currentCookieStore() else { return }
122+
guard isSettingSubscriptionCookieEnabled,
123+
shouldRefreshSubscriptionCookie(),
124+
let cookieStore = await currentCookieStore() else { return }
107125

108126
Logger.subscription.info("[SubscriptionCookieManager] Refresh subscription cookie")
109127
updateLastRefreshDateToNow()
110128

111129
let accessToken: String? = subscriptionManager.accountManager.accessToken
112130
let subscriptionCookie = await cookieStore.fetchCurrentSubscriptionCookie()
113131

114-
if let accessToken {
115-
if subscriptionCookie == nil || subscriptionCookie?.value != accessToken {
116-
Logger.subscription.info("[SubscriptionCookieManager] Refresh: No cookie or one with different value")
117-
do {
132+
let noCookieOrWithUnexpectedValue = (accessToken ?? "") != subscriptionCookie?.value
133+
134+
do {
135+
if noCookieOrWithUnexpectedValue {
136+
Logger.subscription.info("[SubscriptionCookieManager] Refresh: No cookie or one with unexpected value")
137+
138+
if let accessToken {
118139
try await cookieStore.setSubscriptionCookie(for: accessToken)
119-
eventMapping.fire(.subscriptionCookieRefreshedWithUpdate)
120-
} catch {
121-
eventMapping.fire(.failedToSetSubscriptionCookie)
140+
eventMapping.fire(.subscriptionCookieRefreshedWithAccessToken)
141+
} else {
142+
try await cookieStore.setEmptySubscriptionCookie()
143+
eventMapping.fire(.subscriptionCookieRefreshedWithEmptyValue)
122144
}
123-
} else {
124-
Logger.subscription.info("[SubscriptionCookieManager] Refresh: Cookie exists and is up to date")
125-
return
126-
}
127-
} else {
128-
if let subscriptionCookie {
129-
Logger.subscription.info("[SubscriptionCookieManager] Refresh: No access token but old cookie exists, deleting it")
130-
await cookieStore.deleteCookie(subscriptionCookie)
131-
eventMapping.fire(.subscriptionCookieRefreshedWithDelete)
132145
}
146+
} catch {
147+
eventMapping.fire(.failedToSetSubscriptionCookie)
133148
}
134149
}
135150

@@ -161,6 +176,10 @@ private extension HTTPCookieStore {
161176
await allCookies().first { $0.domain == SubscriptionCookieManager.cookieDomain && $0.name == SubscriptionCookieManager.cookieName }
162177
}
163178

179+
func setEmptySubscriptionCookie() async throws {
180+
try await setSubscriptionCookie(for: "")
181+
}
182+
164183
func setSubscriptionCookie(for token: String) async throws {
165184
guard let cookie = HTTPCookie(properties: [
166185
.domain: SubscriptionCookieManager.cookieDomain,

Sources/Subscription/SubscriptionCookie/SubscriptionCookieManagerEvent.swift

+2-3
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,9 @@ import Foundation
2020

2121
public enum SubscriptionCookieManagerEvent {
2222
case errorHandlingAccountDidSignInTokenIsMissing
23-
case errorHandlingAccountDidSignOutCookieIsMissing
2423

25-
case subscriptionCookieRefreshedWithUpdate
26-
case subscriptionCookieRefreshedWithDelete
24+
case subscriptionCookieRefreshedWithAccessToken
25+
case subscriptionCookieRefreshedWithEmptyValue
2726

2827
case failedToSetSubscriptionCookie
2928
}

Sources/SubscriptionTestingUtilities/SubscriptionCookie/SubscriptionCookieManagerMock.swift

+2
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ public final class SubscriptionCookieManagerMock: SubscriptionCookieManaging {
4848

4949
}
5050

51+
public func enableSettingSubscriptionCookie() { }
52+
public func disableSettingSubscriptionCookie() async { }
5153
public func refreshSubscriptionCookie() async { }
5254
public func resetLastRefreshDate() { }
5355
}

Tests/SubscriptionTests/SubscriptionCookie/SubscriptionCookieManagerTests.swift

+15-5
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ final class SubscriptionCookieManagerTests: XCTestCase {
7676
accountManager.accessToken = Constants.accessToken
7777

7878
// When
79+
subscriptionCookieManager.enableSettingSubscriptionCookie()
7980
NotificationCenter.default.post(name: .accountDidSignIn, object: self, userInfo: nil)
8081
try await Task.sleep(seconds: 0.1)
8182

@@ -88,11 +89,12 @@ final class SubscriptionCookieManagerTests: XCTestCase {
8889
await ensureSubscriptionCookieIsInTheCookieStore()
8990

9091
// When
92+
subscriptionCookieManager.enableSettingSubscriptionCookie()
9193
NotificationCenter.default.post(name: .accountDidSignOut, object: self, userInfo: nil)
9294
try await Task.sleep(seconds: 0.1)
9395

9496
// Then
95-
await checkSubscriptionCookieIsNotPresent()
97+
await checkSubscriptionCookieIsHasEmptyValue()
9698
}
9799

98100
func testRefreshWhenSignedInButCookieIsMissing() async throws {
@@ -101,6 +103,7 @@ final class SubscriptionCookieManagerTests: XCTestCase {
101103
await ensureNoSubscriptionCookieInTheCookieStore()
102104

103105
// When
106+
subscriptionCookieManager.enableSettingSubscriptionCookie()
104107
await subscriptionCookieManager.refreshSubscriptionCookie()
105108
try await Task.sleep(seconds: 0.1)
106109

@@ -114,11 +117,12 @@ final class SubscriptionCookieManagerTests: XCTestCase {
114117
await ensureSubscriptionCookieIsInTheCookieStore()
115118

116119
// When
120+
subscriptionCookieManager.enableSettingSubscriptionCookie()
117121
await subscriptionCookieManager.refreshSubscriptionCookie()
118122
try await Task.sleep(seconds: 0.1)
119123

120124
// Then
121-
await checkSubscriptionCookieIsNotPresent()
125+
await checkSubscriptionCookieIsHasEmptyValue()
122126
}
123127

124128
func testRefreshNotTriggeredTwiceWithinSetRefreshInterval() async throws {
@@ -127,6 +131,7 @@ final class SubscriptionCookieManagerTests: XCTestCase {
127131
let secondRefreshDate: Date?
128132

129133
// When
134+
subscriptionCookieManager.enableSettingSubscriptionCookie()
130135
await subscriptionCookieManager.refreshSubscriptionCookie()
131136
firstRefreshDate = subscriptionCookieManager.lastRefreshDate
132137

@@ -145,6 +150,7 @@ final class SubscriptionCookieManagerTests: XCTestCase {
145150
let secondRefreshDate: Date?
146151

147152
// When
153+
subscriptionCookieManager.enableSettingSubscriptionCookie()
148154
await subscriptionCookieManager.refreshSubscriptionCookie()
149155
firstRefreshDate = subscriptionCookieManager.lastRefreshDate
150156

@@ -186,9 +192,12 @@ final class SubscriptionCookieManagerTests: XCTestCase {
186192
XCTAssertEqual(subscriptionCookie.value, Constants.accessToken)
187193
}
188194

189-
private func checkSubscriptionCookieIsNotPresent() async {
190-
let cookie = await cookieStore.fetchSubscriptionCookie()
191-
XCTAssertNil(cookie)
195+
private func checkSubscriptionCookieIsHasEmptyValue() async {
196+
guard let subscriptionCookie = await cookieStore.fetchSubscriptionCookie() else {
197+
XCTFail("No subscription cookie in the store")
198+
return
199+
}
200+
XCTAssertEqual(subscriptionCookie.value, "")
192201
}
193202

194203
}
@@ -213,6 +222,7 @@ class MockHTTPCookieStore: HTTPCookieStore {
213222
}
214223

215224
func setCookie(_ cookie: HTTPCookie) async {
225+
cookies.removeAll { $0.domain == cookie.domain }
216226
cookies.append(cookie)
217227
}
218228

0 commit comments

Comments
 (0)