diff --git a/Package.swift b/Package.swift index 3812fde62..8de20a026 100644 --- a/Package.swift +++ b/Package.swift @@ -35,6 +35,7 @@ let package = Package( .library(name: "SecureStorage", targets: ["SecureStorage"]), .library(name: "Subscription", targets: ["Subscription"]), .library(name: "History", targets: ["History"]), + .library(name: "Suggestions", targets: ["Suggestions"]), ], dependencies: [ .package(url: "https://github.com/duckduckgo/duckduckgo-autofill.git", exact: "10.1.0"), @@ -112,6 +113,16 @@ let package = Package( ], plugins: [swiftlintPlugin] ), + .target( + name: "Suggestions", + dependencies: [ + "Common" + ], + swiftSettings: [ + .define("DEBUG", .when(configuration: .debug)) + ], + plugins: [swiftlintPlugin] + ), .executableTarget( name: "BookmarksTestDBBuilder", dependencies: [ @@ -325,6 +336,7 @@ let package = Package( .target( name: "Subscription", dependencies: [ + "BrowserServicesKit", "Common", ], swiftSettings: [ @@ -341,6 +353,13 @@ let package = Package( ], plugins: [swiftlintPlugin] ), + .testTarget( + name: "SuggestionsTests", + dependencies: [ + "Suggestions", + ], + plugins: [swiftlintPlugin] + ), .testTarget( name: "BookmarksTests", dependencies: [ @@ -490,6 +509,13 @@ let package = Package( ], plugins: [swiftlintPlugin] ), + .testTarget( + name: "SubscriptionTests", + dependencies: [ + "Subscription", + ], + plugins: [swiftlintPlugin] + ), ], cxxLanguageStandard: .cxx11 ) diff --git a/Sources/BrowserServicesKit/PrivacyConfig/Features/PrivacyFeature.swift b/Sources/BrowserServicesKit/PrivacyConfig/Features/PrivacyFeature.swift index 15a5e4332..51c796956 100644 --- a/Sources/BrowserServicesKit/PrivacyConfig/Features/PrivacyFeature.swift +++ b/Sources/BrowserServicesKit/PrivacyConfig/Features/PrivacyFeature.swift @@ -44,6 +44,7 @@ public enum PrivacyFeature: String { case sync case privacyDashboard case history + case privacyPro } /// An abstraction to be implemented by any "subfeature" of a given `PrivacyConfiguration` feature. @@ -112,3 +113,14 @@ public enum AutoconsentSubfeature: String, PrivacySubfeature { case onByDefault } + +public enum PrivacyProSubfeature: String, Equatable, PrivacySubfeature { + public var parent: PrivacyFeature { .privacyPro } + + case isLaunched + case isLaunchedStripe + case allowPurchase + case allowPurchaseStripe + case isLaunchedOverride + case isLaunchedOverrideStripe +} diff --git a/Sources/NetworkProtection/Diagnostics/NetworkProtectionEntitlementMonitor.swift b/Sources/NetworkProtection/Diagnostics/NetworkProtectionEntitlementMonitor.swift index 1f2b43f97..7d9bd6967 100644 --- a/Sources/NetworkProtection/Diagnostics/NetworkProtectionEntitlementMonitor.swift +++ b/Sources/NetworkProtection/Diagnostics/NetworkProtectionEntitlementMonitor.swift @@ -40,7 +40,7 @@ public actor NetworkProtectionEntitlementMonitor { // MARK: - Init & deinit - init() { + public init() { os_log("[+] %{public}@", log: .networkProtectionMemoryLog, type: .debug, String(describing: self)) } diff --git a/Sources/NetworkProtection/KeyManagement/NetworkProtectionTokenStore.swift b/Sources/NetworkProtection/KeyManagement/NetworkProtectionTokenStore.swift index 91f5f6b05..57c8fb270 100644 --- a/Sources/NetworkProtection/KeyManagement/NetworkProtectionTokenStore.swift +++ b/Sources/NetworkProtection/KeyManagement/NetworkProtectionTokenStore.swift @@ -43,7 +43,7 @@ public final class NetworkProtectionKeychainTokenStore: NetworkProtectionTokenSt private let isSubscriptionEnabled: Bool private let accessTokenProvider: () -> String? - private static var authTokenPrefix: String { "ddg:" } + public static var authTokenPrefix: String { "ddg:" } public struct Defaults { static let tokenStoreEntryLabel = "DuckDuckGo Network Protection Auth Token" @@ -51,6 +51,8 @@ public final class NetworkProtectionKeychainTokenStore: NetworkProtectionTokenSt static let tokenStoreName = "com.duckduckgo.networkprotection.token" } + /// - isSubscriptionEnabled: Controls whether the subscription access token is used to authenticate with the NetP backend + /// - accessTokenProvider: Defines how to actually retrieve the subscription access token public init(keychainType: KeychainType, serviceName: String = Defaults.tokenStoreService, errorEvents: EventMapping?, @@ -74,13 +76,13 @@ public final class NetworkProtectionKeychainTokenStore: NetworkProtectionTokenSt } } - public func makeToken(from subscriptionAccessToken: String) -> String { + private func makeToken(from subscriptionAccessToken: String) -> String { Self.authTokenPrefix + subscriptionAccessToken } public func fetchToken() throws -> String? { - if isSubscriptionEnabled, let authToken = accessTokenProvider() { - return makeToken(from: authToken) + if isSubscriptionEnabled { + return accessTokenProvider().map { makeToken(from: $0) } } do { @@ -114,6 +116,3 @@ public final class NetworkProtectionKeychainTokenStore: NetworkProtectionTokenSt errorEvents?.fire(error.networkProtectionError) } } - -extension NetworkProtectionTokenStore { -} diff --git a/Sources/NetworkProtection/Networking/NetworkProtectionClient.swift b/Sources/NetworkProtection/Networking/NetworkProtectionClient.swift index 37ce3a174..bb1d33eb8 100644 --- a/Sources/NetworkProtection/Networking/NetworkProtectionClient.swift +++ b/Sources/NetworkProtection/Networking/NetworkProtectionClient.swift @@ -119,7 +119,7 @@ final class NetworkProtectionBackendClient: NetworkProtectionClient { enum Constants { static let productionEndpoint = URL(string: "https://controller.netp.duckduckgo.com")! - static let stagingEndpoint = URL(string: "https://staging.netp.duckduckgo.com")! + static let stagingEndpoint = URL(string: "https://staging1.netp.duckduckgo.com")! } private enum DecoderError: Error { @@ -170,13 +170,7 @@ final class NetworkProtectionBackendClient: NetworkProtectionClient { init(environment: VPNSettings.SelectedEnvironment = .default, isSubscriptionEnabled: Bool) { self.isSubscriptionEnabled = isSubscriptionEnabled - - // todo - https://app.asana.com/0/0/1206470585910129/f - if isSubscriptionEnabled { - self.endpointURL = URL(string: "https://staging1.netp.duckduckgo.com")! - } else { - self.endpointURL = environment.endpointURL - } + self.endpointURL = environment.endpointURL } func getLocations(authToken: String) async -> Result<[NetworkProtectionLocation], NetworkProtectionClientError> { diff --git a/Sources/NetworkProtection/Notifications/NetworkProtectionNotification.swift b/Sources/NetworkProtection/Notifications/NetworkProtectionNotification.swift index 359c318d5..829296ff5 100644 --- a/Sources/NetworkProtection/Notifications/NetworkProtectionNotification.swift +++ b/Sources/NetworkProtection/Notifications/NetworkProtectionNotification.swift @@ -107,6 +107,7 @@ public enum NetworkProtectionNotification: String { case showConnectedNotification case showIssuesNotResolvedNotification case showVPNSupersededNotification + case showExpiredEntitlementNotification case showTestNotification // Server Selection diff --git a/Sources/NetworkProtection/Settings/Extensions/UserDefaults+showEntitlementMessaging.swift b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+showMessaging.swift similarity index 82% rename from Sources/NetworkProtection/Settings/Extensions/UserDefaults+showEntitlementMessaging.swift rename to Sources/NetworkProtection/Settings/Extensions/UserDefaults+showMessaging.swift index cea0a17d9..d905ccde2 100644 --- a/Sources/NetworkProtection/Settings/Extensions/UserDefaults+showEntitlementMessaging.swift +++ b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+showMessaging.swift @@ -1,5 +1,5 @@ // -// UserDefaults+showEntitlementMessaging.swift +// UserDefaults+showMessaging.swift // // Copyright © 2024 DuckDuckGo. All rights reserved. // @@ -85,3 +85,24 @@ extension UserDefaults { public extension Notification.Name { static let vpnEntitlementMessagingDidChange = Notification.Name("com.duckduckgo.network-protection.entitlement-messaging-changed") } + +extension UserDefaults { + private var vpnEarlyAccessOverAlertAlreadyShownKey: String { + "vpnEarlyAccessOverAlertAlreadyShown" + } + + @objc + public dynamic var vpnEarlyAccessOverAlertAlreadyShown: Bool { + get { + value(forKey: vpnEarlyAccessOverAlertAlreadyShownKey) as? Bool ?? false + } + + set { + set(newValue, forKey: vpnEarlyAccessOverAlertAlreadyShownKey) + } + } + + public func resetThankYouMessaging() { + removeObject(forKey: vpnEarlyAccessOverAlertAlreadyShownKey) + } +} diff --git a/Sources/NetworkProtection/Settings/Extensions/UserDefaults+subscriptionOverrideEnabled.swift b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+subscriptionOverrideEnabled.swift new file mode 100644 index 000000000..0e2d86ffc --- /dev/null +++ b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+subscriptionOverrideEnabled.swift @@ -0,0 +1,40 @@ +// +// UserDefaults+subscriptionOverrideEnabled.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 Combine +import Foundation + +extension UserDefaults { + private var subscriptionOverrideEnabledKey: String { + "subscriptionOverrideEnabled" + } + + public var subscriptionOverrideEnabled: Bool? { + get { + value(forKey: subscriptionOverrideEnabledKey) as? Bool + } + + set { + set(newValue, forKey: subscriptionOverrideEnabledKey) + } + } + + public func resetsubscriptionOverrideEnabled() { + removeObject(forKey: subscriptionOverrideEnabledKey) + } +} diff --git a/Sources/Subscription/Flows/PurchaseFlow.swift b/Sources/Subscription/Flows/PurchaseFlow.swift index f32765ff9..58c244b69 100644 --- a/Sources/Subscription/Flows/PurchaseFlow.swift +++ b/Sources/Subscription/Flows/PurchaseFlow.swift @@ -22,6 +22,10 @@ public struct SubscriptionOptions: Encodable { let platform: String let options: [SubscriptionOption] let features: [SubscriptionFeature] + public static var empty: SubscriptionOptions { + let features = SubscriptionFeatureName.allCases.map { SubscriptionFeature(name: $0.rawValue) } + return SubscriptionOptions(platform: "macos", options: [], features: features) + } } public struct SubscriptionOption: Encodable { diff --git a/Sources/Subscription/SubscriptionFeatureAvailability.swift b/Sources/Subscription/SubscriptionFeatureAvailability.swift new file mode 100644 index 000000000..fa3213e63 --- /dev/null +++ b/Sources/Subscription/SubscriptionFeatureAvailability.swift @@ -0,0 +1,76 @@ +// +// SubscriptionFeatureAvailability.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 BrowserServicesKit + +public protocol SubscriptionFeatureAvailability { + var isFeatureAvailable: Bool { get } + var isSubscriptionPurchaseAllowed: Bool { get } +} + +public final class DefaultSubscriptionFeatureAvailability: SubscriptionFeatureAvailability { + + private let privacyConfigurationManager: PrivacyConfigurationManaging + private let purchasePlatform: SubscriptionPurchaseEnvironment.Environment + + public init(privacyConfigurationManager: PrivacyConfigurationManaging, purchasePlatform: SubscriptionPurchaseEnvironment.Environment) { + self.privacyConfigurationManager = privacyConfigurationManager + self.purchasePlatform = purchasePlatform + } + + public var isFeatureAvailable: Bool { + isInternalUser || isSubscriptionLaunched || isSubscriptionLaunchedOverride + } + + public var isSubscriptionPurchaseAllowed: Bool { + let isPurchaseAllowed: Bool + + switch purchasePlatform { + case .appStore: + isPurchaseAllowed = privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.allowPurchase) + case .stripe: + isPurchaseAllowed = privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.allowPurchaseStripe) + } + + return isPurchaseAllowed || isInternalUser + } + +// MARK: - Conditions + + private var isInternalUser: Bool { + privacyConfigurationManager.internalUserDecider.isInternalUser + } + + private var isSubscriptionLaunched: Bool { + switch purchasePlatform { + case .appStore: + privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.isLaunched) + case .stripe: + privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.isLaunchedStripe) + } + } + + private var isSubscriptionLaunchedOverride: Bool { + switch purchasePlatform { + case .appStore: + privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.isLaunchedOverride) + case .stripe: + privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.isLaunchedOverrideStripe) + } + } +} diff --git a/Sources/BrowserServicesKit/Suggestions/APIResult.swift b/Sources/Suggestions/APIResult.swift similarity index 100% rename from Sources/BrowserServicesKit/Suggestions/APIResult.swift rename to Sources/Suggestions/APIResult.swift diff --git a/Sources/BrowserServicesKit/Suggestions/Bookmark.swift b/Sources/Suggestions/Bookmark.swift similarity index 100% rename from Sources/BrowserServicesKit/Suggestions/Bookmark.swift rename to Sources/Suggestions/Bookmark.swift diff --git a/Sources/BrowserServicesKit/Suggestions/HistorySuggestion.swift b/Sources/Suggestions/HistorySuggestion.swift similarity index 100% rename from Sources/BrowserServicesKit/Suggestions/HistorySuggestion.swift rename to Sources/Suggestions/HistorySuggestion.swift diff --git a/Sources/BrowserServicesKit/Suggestions/Query.swift b/Sources/Suggestions/Query.swift similarity index 100% rename from Sources/BrowserServicesKit/Suggestions/Query.swift rename to Sources/Suggestions/Query.swift diff --git a/Sources/BrowserServicesKit/Suggestions/Score.swift b/Sources/Suggestions/Score.swift similarity index 100% rename from Sources/BrowserServicesKit/Suggestions/Score.swift rename to Sources/Suggestions/Score.swift diff --git a/Sources/BrowserServicesKit/Suggestions/Suggestion.swift b/Sources/Suggestions/Suggestion.swift similarity index 100% rename from Sources/BrowserServicesKit/Suggestions/Suggestion.swift rename to Sources/Suggestions/Suggestion.swift diff --git a/Sources/BrowserServicesKit/Suggestions/SuggestionLoading.swift b/Sources/Suggestions/SuggestionLoading.swift similarity index 100% rename from Sources/BrowserServicesKit/Suggestions/SuggestionLoading.swift rename to Sources/Suggestions/SuggestionLoading.swift diff --git a/Sources/BrowserServicesKit/Suggestions/SuggestionProcessing.swift b/Sources/Suggestions/SuggestionProcessing.swift similarity index 100% rename from Sources/BrowserServicesKit/Suggestions/SuggestionProcessing.swift rename to Sources/Suggestions/SuggestionProcessing.swift diff --git a/Sources/BrowserServicesKit/Suggestions/SuggestionResult.swift b/Sources/Suggestions/SuggestionResult.swift similarity index 100% rename from Sources/BrowserServicesKit/Suggestions/SuggestionResult.swift rename to Sources/Suggestions/SuggestionResult.swift diff --git a/Tests/SubscriptionTests/SubscriptionFeatureAvailabilityTests.swift b/Tests/SubscriptionTests/SubscriptionFeatureAvailabilityTests.swift new file mode 100644 index 000000000..ffac7a8a5 --- /dev/null +++ b/Tests/SubscriptionTests/SubscriptionFeatureAvailabilityTests.swift @@ -0,0 +1,290 @@ +// +// SubscriptionFeatureAvailabilityTests.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 BrowserServicesKit +import Common +import Combine +@testable import Subscription + +final class SubscriptionFeatureAvailabilityTests: XCTestCase { + + var internalUserDeciderStore: MockInternalUserStoring! + var privacyConfig: MockPrivacyConfiguration! + var privacyConfigurationManager: MockPrivacyConfigurationManager! + + override func setUp() { + super.setUp() + internalUserDeciderStore = MockInternalUserStoring() + privacyConfig = MockPrivacyConfiguration() + + privacyConfigurationManager = MockPrivacyConfigurationManager(privacyConfig: privacyConfig, + internalUserDecider: DefaultInternalUserDecider(store: internalUserDeciderStore)) + } + + override func tearDown() { + internalUserDeciderStore = nil + privacyConfig = nil + + privacyConfigurationManager = nil + super.tearDown() + } + + // MARK: - Tests for App Store + + func testSubscriptionFeatureNotAvailableWhenAllFlagsDisabledAndNotInternalUser() { + let purchasePlatform = SubscriptionPurchaseEnvironment.Environment.appStore + + internalUserDeciderStore.isInternalUser = false + XCTAssertFalse(internalUserDeciderStore.isInternalUser) + + XCTAssertFalse(privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.isLaunched)) + XCTAssertFalse(privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.isLaunchedOverride)) + XCTAssertFalse(privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.allowPurchase)) + + let subscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability(privacyConfigurationManager: privacyConfigurationManager, + purchasePlatform: purchasePlatform) + XCTAssertFalse(subscriptionFeatureAvailability.isFeatureAvailable) + XCTAssertFalse(subscriptionFeatureAvailability.isSubscriptionPurchaseAllowed) + } + + func testSubscriptionFeatureAvailableWhenIsLaunchedFlagEnabled() { + let purchasePlatform = SubscriptionPurchaseEnvironment.Environment.appStore + + internalUserDeciderStore.isInternalUser = false + XCTAssertFalse(internalUserDeciderStore.isInternalUser) + + privacyConfig.isSubfeatureEnabledCheck = makeSubfeatureEnabledCheck(for: [.isLaunched, .allowPurchase]) + + XCTAssertTrue(privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.isLaunched)) + XCTAssertFalse(privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.isLaunchedOverride)) + XCTAssertTrue(privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.allowPurchase)) + + let subscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability(privacyConfigurationManager: privacyConfigurationManager, + purchasePlatform: purchasePlatform) + XCTAssertTrue(subscriptionFeatureAvailability.isFeatureAvailable) + XCTAssertTrue(subscriptionFeatureAvailability.isSubscriptionPurchaseAllowed) + } + + func testSubscriptionFeatureAvailableWhenIsLaunchedOverrideFlagEnabled() { + let purchasePlatform = SubscriptionPurchaseEnvironment.Environment.appStore + + internalUserDeciderStore.isInternalUser = false + XCTAssertFalse(internalUserDeciderStore.isInternalUser) + + privacyConfig.isSubfeatureEnabledCheck = makeSubfeatureEnabledCheck(for: [.isLaunchedOverride, .allowPurchase]) + + XCTAssertFalse(privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.isLaunched)) + XCTAssertTrue(privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.isLaunchedOverride)) + XCTAssertTrue(privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.allowPurchase)) + + let subscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability(privacyConfigurationManager: privacyConfigurationManager, + purchasePlatform: purchasePlatform) + XCTAssertTrue(subscriptionFeatureAvailability.isFeatureAvailable) + XCTAssertTrue(subscriptionFeatureAvailability.isSubscriptionPurchaseAllowed) + } + + func testSubscriptionFeatureAvailableAndPurchaseNotAllowed() { + let purchasePlatform = SubscriptionPurchaseEnvironment.Environment.appStore + + internalUserDeciderStore.isInternalUser = false + XCTAssertFalse(internalUserDeciderStore.isInternalUser) + + privacyConfig.isSubfeatureEnabledCheck = makeSubfeatureEnabledCheck(for: [.isLaunched]) + + XCTAssertTrue(privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.isLaunched)) + XCTAssertFalse(privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.isLaunchedOverride)) + XCTAssertFalse(privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.allowPurchase)) + + let subscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability(privacyConfigurationManager: privacyConfigurationManager, + purchasePlatform: purchasePlatform) + XCTAssertTrue(subscriptionFeatureAvailability.isFeatureAvailable) + XCTAssertFalse(subscriptionFeatureAvailability.isSubscriptionPurchaseAllowed) + } + + func testSubscriptionFeatureAvailableWhenAllFlagsDisabledAndInternalUser() { + let purchasePlatform = SubscriptionPurchaseEnvironment.Environment.appStore + + internalUserDeciderStore.isInternalUser = true + XCTAssertTrue(internalUserDeciderStore.isInternalUser) + + XCTAssertFalse(privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.isLaunched)) + XCTAssertFalse(privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.isLaunchedOverride)) + XCTAssertFalse(privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.allowPurchase)) + + let subscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability(privacyConfigurationManager: privacyConfigurationManager, + purchasePlatform: purchasePlatform) + XCTAssertTrue(subscriptionFeatureAvailability.isFeatureAvailable) + XCTAssertTrue(subscriptionFeatureAvailability.isSubscriptionPurchaseAllowed) + } + + // MARK: - Tests for Stripe + + func testStripeSubscriptionFeatureNotAvailableWhenAllFlagsDisabledAndNotInternalUser() { + let purchasePlatform = SubscriptionPurchaseEnvironment.Environment.stripe + + internalUserDeciderStore.isInternalUser = false + XCTAssertFalse(internalUserDeciderStore.isInternalUser) + + XCTAssertFalse(privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.isLaunchedStripe)) + XCTAssertFalse(privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.isLaunchedOverrideStripe)) + XCTAssertFalse(privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.allowPurchaseStripe)) + + let subscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability(privacyConfigurationManager: privacyConfigurationManager, + purchasePlatform: purchasePlatform) + XCTAssertFalse(subscriptionFeatureAvailability.isFeatureAvailable) + XCTAssertFalse(subscriptionFeatureAvailability.isSubscriptionPurchaseAllowed) + } + + func testStripeSubscriptionFeatureAvailableWhenIsLaunchedFlagEnabled() { + let purchasePlatform = SubscriptionPurchaseEnvironment.Environment.stripe + + internalUserDeciderStore.isInternalUser = false + XCTAssertFalse(internalUserDeciderStore.isInternalUser) + + privacyConfig.isSubfeatureEnabledCheck = makeSubfeatureEnabledCheck(for: [.isLaunchedStripe, .allowPurchaseStripe]) + + XCTAssertTrue(privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.isLaunchedStripe)) + XCTAssertFalse(privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.isLaunchedOverrideStripe)) + XCTAssertTrue(privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.allowPurchaseStripe)) + + let subscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability(privacyConfigurationManager: privacyConfigurationManager, + purchasePlatform: purchasePlatform) + XCTAssertTrue(subscriptionFeatureAvailability.isFeatureAvailable) + XCTAssertTrue(subscriptionFeatureAvailability.isSubscriptionPurchaseAllowed) + } + + func testStripeSubscriptionFeatureAvailableWhenIsLaunchedOverrideFlagEnabled() { + let purchasePlatform = SubscriptionPurchaseEnvironment.Environment.stripe + + internalUserDeciderStore.isInternalUser = false + XCTAssertFalse(internalUserDeciderStore.isInternalUser) + + privacyConfig.isSubfeatureEnabledCheck = makeSubfeatureEnabledCheck(for: [.isLaunchedOverrideStripe, .allowPurchaseStripe]) + + XCTAssertFalse(privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.isLaunchedStripe)) + XCTAssertTrue(privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.isLaunchedOverrideStripe)) + XCTAssertTrue(privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.allowPurchaseStripe)) + + let subscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability(privacyConfigurationManager: privacyConfigurationManager, + purchasePlatform: purchasePlatform) + XCTAssertTrue(subscriptionFeatureAvailability.isFeatureAvailable) + XCTAssertTrue(subscriptionFeatureAvailability.isSubscriptionPurchaseAllowed) + } + + func testStripeSubscriptionFeatureAvailableAndPurchaseNotAllowed() { + let purchasePlatform = SubscriptionPurchaseEnvironment.Environment.stripe + + internalUserDeciderStore.isInternalUser = false + XCTAssertFalse(internalUserDeciderStore.isInternalUser) + + privacyConfig.isSubfeatureEnabledCheck = makeSubfeatureEnabledCheck(for: [.isLaunchedStripe]) + + XCTAssertTrue(privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.isLaunchedStripe)) + XCTAssertFalse(privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.isLaunchedOverrideStripe)) + XCTAssertFalse(privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.allowPurchaseStripe)) + + let subscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability(privacyConfigurationManager: privacyConfigurationManager, + purchasePlatform: purchasePlatform) + XCTAssertTrue(subscriptionFeatureAvailability.isFeatureAvailable) + XCTAssertFalse(subscriptionFeatureAvailability.isSubscriptionPurchaseAllowed) + } + + func testStripeSubscriptionFeatureAvailableWhenAllFlagsDisabledAndInternalUser() { + let purchasePlatform = SubscriptionPurchaseEnvironment.Environment.stripe + + internalUserDeciderStore.isInternalUser = true + XCTAssertTrue(internalUserDeciderStore.isInternalUser) + + XCTAssertFalse(privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.isLaunchedStripe)) + XCTAssertFalse(privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.isLaunchedOverrideStripe)) + XCTAssertFalse(privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.allowPurchaseStripe)) + + let subscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability(privacyConfigurationManager: privacyConfigurationManager, + purchasePlatform: purchasePlatform) + XCTAssertTrue(subscriptionFeatureAvailability.isFeatureAvailable) + XCTAssertTrue(subscriptionFeatureAvailability.isSubscriptionPurchaseAllowed) + } + + // MARK: - Helper + + private func makeSubfeatureEnabledCheck(for enabledSubfeatures: [PrivacyProSubfeature]) -> (any PrivacySubfeature) -> Bool { + return { + guard let subfeature = $0 as? PrivacyProSubfeature else { return false } + return enabledSubfeatures.contains(subfeature) + } + } +} + +class MockInternalUserStoring: InternalUserStoring { + var isInternalUser: Bool = false +} + +class MockPrivacyConfiguration: PrivacyConfiguration { + + func isEnabled(featureKey: PrivacyFeature, versionProvider: AppVersionProvider) -> Bool { true } + + func stateFor(featureKey: BrowserServicesKit.PrivacyFeature, versionProvider: BrowserServicesKit.AppVersionProvider) -> BrowserServicesKit.PrivacyConfigurationFeatureState { + return .enabled + } + + var isSubfeatureEnabledCheck: ((any PrivacySubfeature) -> Bool)? + + func isSubfeatureEnabled(_ subfeature: any PrivacySubfeature, versionProvider: AppVersionProvider, randomizer: (Range) -> Double) -> Bool { + isSubfeatureEnabledCheck?(subfeature) ?? false + } + + func stateFor(_ subfeature: any PrivacySubfeature, versionProvider: AppVersionProvider, randomizer: (Range) -> Double) -> PrivacyConfigurationFeatureState { + if isSubfeatureEnabledCheck?(subfeature) == true { + return .enabled + } + return .disabled(.disabledInConfig) + } + + var identifier: String = "abcd" + var userUnprotectedDomains: [String] = [] + var tempUnprotectedDomains: [String] = [] + var trackerAllowlist: PrivacyConfigurationData.TrackerAllowlist = .init(json: ["state": "disabled"])! + func exceptionsList(forFeature featureKey: PrivacyFeature) -> [String] { [] } + func isFeature(_ feature: PrivacyFeature, enabledForDomain: String?) -> Bool { true } + func isProtected(domain: String?) -> Bool { false } + func isUserUnprotected(domain: String?) -> Bool { false } + func isTempUnprotected(domain: String?) -> Bool { false } + func isInExceptionList(domain: String?, forFeature featureKey: PrivacyFeature) -> Bool { false } + func settings(for feature: PrivacyFeature) -> PrivacyConfigurationData.PrivacyFeature.FeatureSettings { .init() } + func userEnabledProtection(forDomain: String) {} + func userDisabledProtection(forDomain: String) {} +} + +class MockPrivacyConfigurationManager: PrivacyConfigurationManaging { + var currentConfig: Data = .init() + var updatesSubject = PassthroughSubject() + let updatesPublisher: AnyPublisher + var privacyConfig: PrivacyConfiguration + let internalUserDecider: InternalUserDecider + var toggleProtectionsCounter = ToggleProtectionsCounter(eventReporting: EventMapping { _, _, _, _ in }) + func reload(etag: String?, data: Data?) -> PrivacyConfigurationManager.ReloadResult { + .downloaded + } + + init(privacyConfig: PrivacyConfiguration, internalUserDecider: InternalUserDecider) { + self.updatesPublisher = updatesSubject.eraseToAnyPublisher() + self.privacyConfig = privacyConfig + self.internalUserDecider = internalUserDecider + } +} diff --git a/Tests/BrowserServicesKitTests/Suggestions/APIResultTests.swift b/Tests/SuggestionsTests/APIResultTests.swift similarity index 98% rename from Tests/BrowserServicesKitTests/Suggestions/APIResultTests.swift rename to Tests/SuggestionsTests/APIResultTests.swift index e9b8b5e13..2bd57ea53 100644 --- a/Tests/BrowserServicesKitTests/Suggestions/APIResultTests.swift +++ b/Tests/SuggestionsTests/APIResultTests.swift @@ -17,7 +17,7 @@ // import XCTest -@testable import BrowserServicesKit +@testable import Suggestions final class APIResultTests: XCTestCase { diff --git a/Tests/BrowserServicesKitTests/Suggestions/BookmarkMock.swift b/Tests/SuggestionsTests/BookmarkMock.swift similarity index 95% rename from Tests/BrowserServicesKitTests/Suggestions/BookmarkMock.swift rename to Tests/SuggestionsTests/BookmarkMock.swift index 8ff102aa3..4e45133a8 100644 --- a/Tests/BrowserServicesKitTests/Suggestions/BookmarkMock.swift +++ b/Tests/SuggestionsTests/BookmarkMock.swift @@ -17,7 +17,7 @@ // import Foundation -@testable import BrowserServicesKit +@testable import Suggestions struct BookmarkMock: Bookmark { diff --git a/Tests/BrowserServicesKitTests/Suggestions/HistoryEntryMock.swift b/Tests/SuggestionsTests/HistoryEntryMock.swift similarity index 96% rename from Tests/BrowserServicesKitTests/Suggestions/HistoryEntryMock.swift rename to Tests/SuggestionsTests/HistoryEntryMock.swift index 80b8ef405..9ee160608 100644 --- a/Tests/BrowserServicesKitTests/Suggestions/HistoryEntryMock.swift +++ b/Tests/SuggestionsTests/HistoryEntryMock.swift @@ -18,7 +18,7 @@ import Foundation -@testable import BrowserServicesKit +@testable import Suggestions struct HistoryEntryMock: HistorySuggestion { diff --git a/Tests/BrowserServicesKitTests/Suggestions/ScoreTests.swift b/Tests/SuggestionsTests/ScoreTests.swift similarity index 98% rename from Tests/BrowserServicesKitTests/Suggestions/ScoreTests.swift rename to Tests/SuggestionsTests/ScoreTests.swift index 1d3094406..744a24e35 100644 --- a/Tests/BrowserServicesKitTests/Suggestions/ScoreTests.swift +++ b/Tests/SuggestionsTests/ScoreTests.swift @@ -18,7 +18,7 @@ import XCTest -@testable import BrowserServicesKit +@testable import Suggestions final class ScoreTests: XCTestCase { diff --git a/Tests/BrowserServicesKitTests/Suggestions/SuggestionLoadingTests.swift b/Tests/SuggestionsTests/SuggestionLoadingTests.swift similarity index 99% rename from Tests/BrowserServicesKitTests/Suggestions/SuggestionLoadingTests.swift rename to Tests/SuggestionsTests/SuggestionLoadingTests.swift index eee9fef40..4a4f74b28 100644 --- a/Tests/BrowserServicesKitTests/Suggestions/SuggestionLoadingTests.swift +++ b/Tests/SuggestionsTests/SuggestionLoadingTests.swift @@ -17,7 +17,7 @@ // import XCTest -@testable import BrowserServicesKit +@testable import Suggestions final class SuggestionLoadingTests: XCTestCase { diff --git a/Tests/BrowserServicesKitTests/Suggestions/SuggestionProcessingTests.swift b/Tests/SuggestionsTests/SuggestionProcessingTests.swift similarity index 98% rename from Tests/BrowserServicesKitTests/Suggestions/SuggestionProcessingTests.swift rename to Tests/SuggestionsTests/SuggestionProcessingTests.swift index bfe7e3f32..b75f8aa65 100644 --- a/Tests/BrowserServicesKitTests/Suggestions/SuggestionProcessingTests.swift +++ b/Tests/SuggestionsTests/SuggestionProcessingTests.swift @@ -18,7 +18,7 @@ import XCTest -@testable import BrowserServicesKit +@testable import Suggestions final class SuggestionProcessingTests: XCTestCase { diff --git a/Tests/BrowserServicesKitTests/Suggestions/SuggestionResultTests.swift b/Tests/SuggestionsTests/SuggestionResultTests.swift similarity index 96% rename from Tests/BrowserServicesKitTests/Suggestions/SuggestionResultTests.swift rename to Tests/SuggestionsTests/SuggestionResultTests.swift index 2ad800591..9dabab776 100644 --- a/Tests/BrowserServicesKitTests/Suggestions/SuggestionResultTests.swift +++ b/Tests/SuggestionsTests/SuggestionResultTests.swift @@ -17,7 +17,7 @@ // import XCTest -@testable import BrowserServicesKit +@testable import Suggestions final class SuggestionResultTests: XCTestCase { diff --git a/Tests/BrowserServicesKitTests/Suggestions/SuggestionTests.swift b/Tests/SuggestionsTests/SuggestionTests.swift similarity index 99% rename from Tests/BrowserServicesKitTests/Suggestions/SuggestionTests.swift rename to Tests/SuggestionsTests/SuggestionTests.swift index 6101849e9..148304251 100644 --- a/Tests/BrowserServicesKitTests/Suggestions/SuggestionTests.swift +++ b/Tests/SuggestionsTests/SuggestionTests.swift @@ -18,7 +18,7 @@ import XCTest -@testable import BrowserServicesKit +@testable import Suggestions final class SuggestionTests: XCTestCase {