From 916cf823407c0bf697f5e2d52305b65bfef2da06 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Thu, 21 Nov 2024 18:15:04 +0100 Subject: [PATCH 01/20] Switch to local BSK --- DuckDuckGo.xcodeproj/project.pbxproj | 2 ++ .../xcshareddata/swiftpm/Package.resolved | 11 +---------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index c87afcfbce..64b05682a6 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1426,6 +1426,7 @@ 1E9529A02C4E748B006E80D4 /* UINavigationControllerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UINavigationControllerExtension.swift; sourceTree = ""; }; 1EA51375286596A000493C6A /* PrivacyIconLogic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyIconLogic.swift; sourceTree = ""; }; 1EA513772866039400493C6A /* TrackerAnimationLogic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackerAnimationLogic.swift; sourceTree = ""; }; + 1EBC7B462CEF898800DE53B9 /* BrowserServicesKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = BrowserServicesKit; path = ../BrowserServicesKit; sourceTree = SOURCE_ROOT; }; 1EC458452948932500CB2B13 /* UIHostingControllerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIHostingControllerExtension.swift; sourceTree = ""; }; 1EC51CD828D8C0DF00E9D05A /* UIImageExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImageExtension.swift; sourceTree = ""; }; 1EDE39D12705D4A100C99C72 /* FileSizeDebugViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileSizeDebugViewController.swift; sourceTree = ""; }; @@ -4183,6 +4184,7 @@ 84E341891E2F7EFB00BDBA6F = { isa = PBXGroup; children = ( + 1EBC7B462CEF898800DE53B9 /* BrowserServicesKit */, EE3B98EB2A963515002F63A0 /* WidgetsExtensionAlpha.entitlements */, 6FB030C7234331B400A10DB9 /* Configuration.xcconfig */, EEB8FDB92A990AEE00EBEDCF /* Configuration-Alpha.xcconfig */, diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index cdb32f0f44..c88e0ab07e 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -27,15 +27,6 @@ "version" : "3.0.0" } }, - { - "identity" : "browserserviceskit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", - "state" : { - "revision" : "7033b0d6f166ac8152cff602f1a1301641f4da60", - "version" : "211.0.0" - } - }, { "identity" : "content-scope-scripts", "kind" : "remoteSourceControl", @@ -138,7 +129,7 @@ { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser", + "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { "revision" : "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b", "version" : "1.4.0" From 8559360ab4adb31cc65a5ce5a49fd42c16d59eec Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Thu, 21 Nov 2024 18:16:06 +0100 Subject: [PATCH 02/20] Fix init --- DuckDuckGo/AppDependencyProvider.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/AppDependencyProvider.swift b/DuckDuckGo/AppDependencyProvider.swift index 245901da44..08b976a4a6 100644 --- a/DuckDuckGo/AppDependencyProvider.swift +++ b/DuckDuckGo/AppDependencyProvider.swift @@ -109,15 +109,19 @@ final class AppDependencyProvider: DependencyProvider { let accessTokenStorage = SubscriptionTokenKeychainStorage(keychainType: .dataProtection(.named(subscriptionAppGroup))) let subscriptionService = DefaultSubscriptionEndpointService(currentServiceEnvironment: subscriptionEnvironment.serviceEnvironment) let authService = DefaultAuthEndpointService(currentServiceEnvironment: subscriptionEnvironment.serviceEnvironment) + let subscriptionFeatureMappingCache = DefaultSubscriptionFeatureMappingCache(subscriptionEndpointService: subscriptionService, + userDefaults: subscriptionUserDefaults) + let accountManager = DefaultAccountManager(accessTokenStorage: accessTokenStorage, entitlementsCache: entitlementsCache, subscriptionEndpointService: subscriptionService, authEndpointService: authService) - let subscriptionManager = DefaultSubscriptionManager(storePurchaseManager: DefaultStorePurchaseManager(), + let subscriptionManager = DefaultSubscriptionManager(storePurchaseManager: DefaultStorePurchaseManager(subscriptionFeatureMappingCache: subscriptionFeatureMappingCache), accountManager: accountManager, subscriptionEndpointService: subscriptionService, authEndpointService: authService, + subscriptionFeatureMappingCache: subscriptionFeatureMappingCache, subscriptionEnvironment: subscriptionEnvironment) accountManager.delegate = subscriptionManager From 7f4a9c05110b151fb1130f4cecb884ce86f5b315 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Thu, 21 Nov 2024 18:16:28 +0100 Subject: [PATCH 03/20] Set proper purchase section descriptions --- DuckDuckGo/SettingsSubscriptionView.swift | 14 +++++++++++++- DuckDuckGo/UserText.swift | 2 ++ DuckDuckGo/en.lproj/Localizable.strings | 6 ++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/SettingsSubscriptionView.swift b/DuckDuckGo/SettingsSubscriptionView.swift index b2414c2855..6ecb8c431a 100644 --- a/DuckDuckGo/SettingsSubscriptionView.swift +++ b/DuckDuckGo/SettingsSubscriptionView.swift @@ -64,8 +64,20 @@ struct SettingsSubscriptionView: View { @ViewBuilder private var purchaseSubscriptionView: some View { Group { + let subtitleText = { + // TODO: no feature flag (old text) + // UserText.settingsPProDescription + + switch subscriptionManager.storePurchaseManager().currentStorefrontRegion { + case .usa: + UserText.settingsPProPurchaseUSDescription + case .restOfWorld: + UserText.settingsPProPurchaseROWDescription + } + }() + SettingsCellView(label: UserText.settingsPProSubscribe, - subtitle: UserText.settingsPProDescription, + subtitle: subtitleText, image: Image("SettingsPrivacyPro")) // Get privacy pro diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index 43e48a8762..7f8396a6a4 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -1084,6 +1084,8 @@ But if you *do* want a peek under the hood, you can find more information about public static let settingsPProSectionFooter = NSLocalizedString("settings.ppro.footer", value: "Privacy Policy and Terms of Service", comment: "Title for Link in the Footer of Privacy Pro section") public static let settingsPProSubscribe = NSLocalizedString("settings.subscription.subscribe", value: "Protect your connection and identity with Privacy Pro", comment: "Call to action title for Privacy Pro settings") public static let settingsPProDescription = NSLocalizedString("settings.subscription.description", value:"Includes our VPN, Personal Information Removal, and Identity Theft Restoration.", comment: "Privacy pro description subtitle in settings") + public static let settingsPProPurchaseUSDescription = NSLocalizedString("settings.subscription.purchase.us.description", value:"Includes our VPN, Personal Information Removal, and Identity Theft Restoration", comment: "Privacy Pro description subtitle in settings") + public static let settingsPProPurchaseROWDescription = NSLocalizedString("settings.subscription.purchase.row.description", value:"Includes our VPN and Identity Theft Restoration", comment: "Privacy Pro description subtitle in settings") public static let settingsPProActivating = NSLocalizedString("settings.subscription.activating", value:"Activating", comment: "Privacy pro description subtitle in settings when the is activating") public static let settingsPProLearnMore = NSLocalizedString("settings.subscription.learn.more", value: "Get Privacy Pro", comment: "Get Privacy Pro button text for privacy pro") diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index fc7d856f4a..315f7079b1 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -2370,6 +2370,12 @@ But if you *do* want a peek under the hood, you can find more information about /* Subscription Settings button text for privacy pro */ "settings.subscription.manage" = "Subscription Settings"; +/* Privacy Pro description subtitle in settings */ +"settings.subscription.purchase.row.description" = "Includes our VPN and Identity Theft Restoration"; + +/* Privacy Pro description subtitle in settings */ +"settings.subscription.purchase.us.description" = "Includes our VPN, Personal Information Removal, and Identity Theft Restoration"; + /* Call to action title for Privacy Pro settings */ "settings.subscription.subscribe" = "Protect your connection and identity with Privacy Pro"; From ac1c49546e600b6e1ee7478e9cd5dae0d62da3a6 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Thu, 21 Nov 2024 18:25:18 +0100 Subject: [PATCH 04/20] Support .identityTheftRestorationGlobal entitlement --- DuckDuckGo/SettingsSubscriptionView.swift | 3 ++- DuckDuckGo/SettingsViewModel.swift | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/DuckDuckGo/SettingsSubscriptionView.swift b/DuckDuckGo/SettingsSubscriptionView.swift index 6ecb8c431a..5510ff81a4 100644 --- a/DuckDuckGo/SettingsSubscriptionView.swift +++ b/DuckDuckGo/SettingsSubscriptionView.swift @@ -188,7 +188,8 @@ struct SettingsSubscriptionView: View { } } - if settingsViewModel.state.subscription.entitlements.contains(.identityTheftRestoration) { + if settingsViewModel.state.subscription.entitlements.contains(.identityTheftRestoration) || + settingsViewModel.state.subscription.entitlements.contains(.identityTheftRestorationGlobal) { NavigationLink( destination: LazyView(SubscriptionITPView()), isActive: $isShowingITP) { diff --git a/DuckDuckGo/SettingsViewModel.swift b/DuckDuckGo/SettingsViewModel.swift index 5f96ce0215..fa8493b56f 100644 --- a/DuckDuckGo/SettingsViewModel.swift +++ b/DuckDuckGo/SettingsViewModel.swift @@ -750,7 +750,7 @@ extension SettingsViewModel { // Check entitlements and update state var currentEntitlements: [Entitlement.ProductName] = [] - let entitlementsToCheck: [Entitlement.ProductName] = [.networkProtection, .dataBrokerProtection, .identityTheftRestoration] + let entitlementsToCheck: [Entitlement.ProductName] = [.networkProtection, .dataBrokerProtection, .identityTheftRestoration, .identityTheftRestorationGlobal] for entitlement in entitlementsToCheck { if case .success(true) = await subscriptionManager.accountManager.hasEntitlement(forProductName: entitlement) { From 24023a7c643afa5070bdbb5e2ffbfbbc61166ef9 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Thu, 21 Nov 2024 19:15:15 +0100 Subject: [PATCH 05/20] Make the features on expired view dynamic --- DuckDuckGo/SettingsState.swift | 2 ++ DuckDuckGo/SettingsSubscriptionView.swift | 44 ++++++++++++++--------- DuckDuckGo/SettingsViewModel.swift | 1 + 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/DuckDuckGo/SettingsState.swift b/DuckDuckGo/SettingsState.swift index 665077e29d..6d1fb7c43d 100644 --- a/DuckDuckGo/SettingsState.swift +++ b/DuckDuckGo/SettingsState.swift @@ -43,6 +43,7 @@ struct SettingsState { var hasActiveSubscription: Bool var isRestoring: Bool var shouldDisplayRestoreSubscriptionError: Bool + var subscriptionFeatures: [Entitlement.ProductName] var entitlements: [Entitlement.ProductName] var platform: DDGSubscription.Platform var isShowingStripeView: Bool @@ -132,6 +133,7 @@ struct SettingsState { hasActiveSubscription: false, isRestoring: false, shouldDisplayRestoreSubscriptionError: false, + subscriptionFeatures: [], entitlements: [], platform: .unknown, isShowingStripeView: false), diff --git a/DuckDuckGo/SettingsSubscriptionView.swift b/DuckDuckGo/SettingsSubscriptionView.swift index 5510ff81a4..fa9004354e 100644 --- a/DuckDuckGo/SettingsSubscriptionView.swift +++ b/DuckDuckGo/SettingsSubscriptionView.swift @@ -105,23 +105,33 @@ struct SettingsSubscriptionView: View { @ViewBuilder private var disabledFeaturesView: some View { - SettingsCellView(label: UserText.settingsPProVPNTitle, - image: Image("SettingsPrivacyProVPN"), - statusIndicator: StatusIndicatorView(status: .off), - isGreyedOut: true - ) - SettingsCellView( - label: UserText.settingsPProDBPTitle, - image: Image("SettingsPrivacyProPIR"), - statusIndicator: StatusIndicatorView(status: .off), - isGreyedOut: true - ) - SettingsCellView( - label: UserText.settingsPProITRTitle, - image: Image("SettingsPrivacyProITP"), - statusIndicator: StatusIndicatorView(status: .off), - isGreyedOut: true - ) + let features = settingsViewModel.state.subscription.subscriptionFeatures + + if features.contains(.networkProtection) { + SettingsCellView(label: UserText.settingsPProVPNTitle, + image: Image("SettingsPrivacyProVPN"), + statusIndicator: StatusIndicatorView(status: .off), + isGreyedOut: true + ) + } + + if features.contains(.dataBrokerProtection) { + SettingsCellView( + label: UserText.settingsPProDBPTitle, + image: Image("SettingsPrivacyProPIR"), + statusIndicator: StatusIndicatorView(status: .off), + isGreyedOut: true + ) + } + + if features.contains(.identityTheftRestoration) || features.contains(.identityTheftRestorationGlobal) { + SettingsCellView( + label: UserText.settingsPProITRTitle, + image: Image("SettingsPrivacyProITP"), + statusIndicator: StatusIndicatorView(status: .off), + isGreyedOut: true + ) + } } @ViewBuilder diff --git a/DuckDuckGo/SettingsViewModel.swift b/DuckDuckGo/SettingsViewModel.swift index fa8493b56f..aa87c2982b 100644 --- a/DuckDuckGo/SettingsViewModel.swift +++ b/DuckDuckGo/SettingsViewModel.swift @@ -759,6 +759,7 @@ extension SettingsViewModel { } self.state.subscription.entitlements = currentEntitlements + self.state.subscription.subscriptionFeatures = await subscriptionManager.currentSubscriptionFeatures() case .failure: break From 5848827cab7e59617f31ffc9d2478b3eeac6bdba Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 22 Nov 2024 10:37:26 +0100 Subject: [PATCH 06/20] For active subscription dynamically build feature rows and disable them when lacking entitlement --- DuckDuckGo/SettingsSubscriptionView.swift | 45 ++++++++++++++--------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/DuckDuckGo/SettingsSubscriptionView.swift b/DuckDuckGo/SettingsSubscriptionView.swift index fa9004354e..aaf888f916 100644 --- a/DuckDuckGo/SettingsSubscriptionView.swift +++ b/DuckDuckGo/SettingsSubscriptionView.swift @@ -177,38 +177,49 @@ struct SettingsSubscriptionView: View { @ViewBuilder private var subscriptionDetailsView: some View { - - if settingsViewModel.state.subscription.entitlements.contains(.networkProtection) { + let subscriptionFeatures = settingsViewModel.state.subscription.subscriptionFeatures + let userEntitlements = settingsViewModel.state.subscription.entitlements + + if subscriptionFeatures.contains(.networkProtection) { + let hasVPNEntitlement = userEntitlements.contains(.networkProtection) + NavigationLink(destination: LazyView(NetworkProtectionRootView()), isActive: $isShowingVPN) { SettingsCellView( label: UserText.settingsPProVPNTitle, image: Image("SettingsPrivacyProVPN"), - statusIndicator: StatusIndicatorView(status: settingsViewModel.state.networkProtectionConnected ? .on : .off) + statusIndicator: StatusIndicatorView(status: settingsViewModel.state.networkProtectionConnected ? .on : .off), + isGreyedOut: !hasVPNEntitlement ) } + .disabled(!hasVPNEntitlement) } - - if settingsViewModel.state.subscription.entitlements.contains(.dataBrokerProtection) { + + if subscriptionFeatures.contains(.dataBrokerProtection) { + let hasDBPEntitlement = userEntitlements.contains(.dataBrokerProtection) + NavigationLink(destination: LazyView(SubscriptionPIRView()), isActive: $isShowingDBP) { SettingsCellView( label: UserText.settingsPProDBPTitle, image: Image("SettingsPrivacyProPIR"), - statusIndicator: StatusIndicatorView(status: .on) + statusIndicator: StatusIndicatorView(status: hasDBPEntitlement ? .on : .off), + isGreyedOut: !hasDBPEntitlement ) } + .disabled(!hasDBPEntitlement) } - - if settingsViewModel.state.subscription.entitlements.contains(.identityTheftRestoration) || - settingsViewModel.state.subscription.entitlements.contains(.identityTheftRestorationGlobal) { - NavigationLink( - destination: LazyView(SubscriptionITPView()), - isActive: $isShowingITP) { - SettingsCellView( - label: UserText.settingsPProITRTitle, - image: Image("SettingsPrivacyProITP"), - statusIndicator: StatusIndicatorView(status: .on) - ) + + if subscriptionFeatures.contains(.identityTheftRestoration) || subscriptionFeatures.contains(.identityTheftRestorationGlobal) { + let hasITREntitlement = userEntitlements.contains(.identityTheftRestoration) || userEntitlements.contains(.identityTheftRestorationGlobal) + + NavigationLink(destination: LazyView(SubscriptionITPView()), isActive: $isShowingITP) { + SettingsCellView( + label: UserText.settingsPProITRTitle, + image: Image("SettingsPrivacyProITP"), + statusIndicator: StatusIndicatorView(status: .on), + isGreyedOut: !hasITREntitlement + ) } + .disabled(!hasITREntitlement) } NavigationLink(destination: LazyView(SubscriptionSettingsView(configuration: .subscribed, settingsViewModel: settingsViewModel)) From 5668603fd6ed7fef00b7caad949aaeca3778d49c Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 22 Nov 2024 10:38:50 +0100 Subject: [PATCH 07/20] Rename --- DuckDuckGo/SettingsSubscriptionView.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/DuckDuckGo/SettingsSubscriptionView.swift b/DuckDuckGo/SettingsSubscriptionView.swift index aaf888f916..91947fd598 100644 --- a/DuckDuckGo/SettingsSubscriptionView.swift +++ b/DuckDuckGo/SettingsSubscriptionView.swift @@ -105,9 +105,9 @@ struct SettingsSubscriptionView: View { @ViewBuilder private var disabledFeaturesView: some View { - let features = settingsViewModel.state.subscription.subscriptionFeatures + let subscriptionFeatures = settingsViewModel.state.subscription.subscriptionFeatures - if features.contains(.networkProtection) { + if subscriptionFeatures.contains(.networkProtection) { SettingsCellView(label: UserText.settingsPProVPNTitle, image: Image("SettingsPrivacyProVPN"), statusIndicator: StatusIndicatorView(status: .off), @@ -115,7 +115,7 @@ struct SettingsSubscriptionView: View { ) } - if features.contains(.dataBrokerProtection) { + if subscriptionFeatures.contains(.dataBrokerProtection) { SettingsCellView( label: UserText.settingsPProDBPTitle, image: Image("SettingsPrivacyProPIR"), @@ -124,7 +124,7 @@ struct SettingsSubscriptionView: View { ) } - if features.contains(.identityTheftRestoration) || features.contains(.identityTheftRestorationGlobal) { + if subscriptionFeatures.contains(.identityTheftRestoration) || subscriptionFeatures.contains(.identityTheftRestorationGlobal) { SettingsCellView( label: UserText.settingsPProITRTitle, image: Image("SettingsPrivacyProITP"), From d95079083a1f929a83fc24fa155db00640cfe3d4 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 22 Nov 2024 11:05:36 +0100 Subject: [PATCH 08/20] Fix on off toggle for ITR on expired state --- DuckDuckGo/SettingsSubscriptionView.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/DuckDuckGo/SettingsSubscriptionView.swift b/DuckDuckGo/SettingsSubscriptionView.swift index 91947fd598..1a6a17486e 100644 --- a/DuckDuckGo/SettingsSubscriptionView.swift +++ b/DuckDuckGo/SettingsSubscriptionView.swift @@ -182,12 +182,13 @@ struct SettingsSubscriptionView: View { if subscriptionFeatures.contains(.networkProtection) { let hasVPNEntitlement = userEntitlements.contains(.networkProtection) + let isVPNConnected = settingsViewModel.state.networkProtectionConnected NavigationLink(destination: LazyView(NetworkProtectionRootView()), isActive: $isShowingVPN) { SettingsCellView( label: UserText.settingsPProVPNTitle, image: Image("SettingsPrivacyProVPN"), - statusIndicator: StatusIndicatorView(status: settingsViewModel.state.networkProtectionConnected ? .on : .off), + statusIndicator: StatusIndicatorView(status: isVPNConnected ? .on : .off), isGreyedOut: !hasVPNEntitlement ) } @@ -215,7 +216,7 @@ struct SettingsSubscriptionView: View { SettingsCellView( label: UserText.settingsPProITRTitle, image: Image("SettingsPrivacyProITP"), - statusIndicator: StatusIndicatorView(status: .on), + statusIndicator: StatusIndicatorView(status: hasITREntitlement ? .on : .off), isGreyedOut: !hasITREntitlement ) } From 5897158a1dcd9795cfb59da50d88af8a9b15a194 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 22 Nov 2024 11:17:10 +0100 Subject: [PATCH 09/20] Allow for the category view to be configured with explicit options list --- DuckDuckGo/SettingsOthersView.swift | 2 +- .../Feedback/UnifiedFeedbackRootView.swift | 22 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/DuckDuckGo/SettingsOthersView.swift b/DuckDuckGo/SettingsOthersView.swift index 96f3cab8d1..ffa3e2a781 100644 --- a/DuckDuckGo/SettingsOthersView.swift +++ b/DuckDuckGo/SettingsOthersView.swift @@ -36,7 +36,7 @@ struct SettingsOthersView: View { if viewModel.usesUnifiedFeedbackForm { let formViewModel = UnifiedFeedbackFormViewModel(vpnMetadataCollector: DefaultVPNMetadataCollector(), source: .settings) NavigationLink { - UnifiedFeedbackCategoryView(UserText.subscriptionFeedback, sources: UnifiedFeedbackFlowCategory.self, selection: $viewModel.selectedFeedbackFlow) { + UnifiedFeedbackCategoryView(UserText.subscriptionFeedback, options: UnifiedFeedbackFlowCategory.allCases, selection: $viewModel.selectedFeedbackFlow) { if let selectedFeedbackFlow = viewModel.selectedFeedbackFlow { switch UnifiedFeedbackFlowCategory(rawValue: selectedFeedbackFlow) { case nil: diff --git a/DuckDuckGo/Subscription/Feedback/UnifiedFeedbackRootView.swift b/DuckDuckGo/Subscription/Feedback/UnifiedFeedbackRootView.swift index 58b43f15d7..a09597ed7b 100644 --- a/DuckDuckGo/Subscription/Feedback/UnifiedFeedbackRootView.swift +++ b/DuckDuckGo/Subscription/Feedback/UnifiedFeedbackRootView.swift @@ -24,7 +24,7 @@ struct UnifiedFeedbackRootView: View { @StateObject var viewModel: UnifiedFeedbackFormViewModel var body: some View { - UnifiedFeedbackCategoryView(UserText.pproFeedbackFormTitle, sources: UnifiedFeedbackReportType.self, selection: $viewModel.selectedReportType) { + UnifiedFeedbackCategoryView(UserText.pproFeedbackFormTitle, options: UnifiedFeedbackReportType.allCases, selection: $viewModel.selectedReportType) { if let selectedReportType = viewModel.selectedReportType { switch UnifiedFeedbackReportType(rawValue: selectedReportType) { case nil: @@ -54,7 +54,7 @@ struct UnifiedFeedbackRootView: View { @ViewBuilder func reportProblemView() -> some View { UnifiedFeedbackCategoryView(UserText.pproFeedbackFormReportProblemTitle, - sources: UnifiedFeedbackCategory.self, + options: UnifiedFeedbackCategory.allCases, selection: $viewModel.selectedCategory) { Group { if let selectedCategory = viewModel.selectedCategory { @@ -63,28 +63,28 @@ struct UnifiedFeedbackRootView: View { EmptyView() case .subscription: UnifiedFeedbackCategoryView(UserText.pproFeedbackFormReportPProProblemTitle, - sources: PrivacyProFeedbackSubcategory.self, + options: PrivacyProFeedbackSubcategory.allCases, selection: $viewModel.selectedSubcategory) { IssueDescriptionFormView(viewModel: viewModel, placeholder: UserText.pproFeedbackFormReportProblemPlaceholder) } case .vpn: UnifiedFeedbackCategoryView(UserText.pproFeedbackFormReportVPNProblemTitle, - sources: VPNFeedbackSubcategory.self, + options: VPNFeedbackSubcategory.allCases, selection: $viewModel.selectedSubcategory) { IssueDescriptionFormView(viewModel: viewModel, placeholder: UserText.pproFeedbackFormReportProblemPlaceholder) } case .pir: UnifiedFeedbackCategoryView(UserText.pproFeedbackFormReportPIRProblemTitle, - sources: PIRFeedbackSubcategory.self, + options: PIRFeedbackSubcategory.allCases, selection: $viewModel.selectedSubcategory) { IssueDescriptionFormView(viewModel: viewModel, placeholder: UserText.pproFeedbackFormReportProblemPlaceholder) } case .itr: UnifiedFeedbackCategoryView(UserText.pproFeedbackFormReportITRProblemTitle, - sources: ITRFeedbackSubcategory.self, + options: ITRFeedbackSubcategory.allCases, selection: $viewModel.selectedSubcategory) { IssueDescriptionFormView(viewModel: viewModel, placeholder: UserText.pproFeedbackFormReportProblemPlaceholder) @@ -106,21 +106,21 @@ struct UnifiedFeedbackRootView: View { } } -struct UnifiedFeedbackCategoryView: View where Category.AllCases == [Category], Category.RawValue == String { +struct UnifiedFeedbackCategoryView: View where Category.RawValue == String { let title: String let prompt: String - let sources: Category.Type + let options: [Category] let selection: Binding let destination: () -> Destination init(_ title: String, prompt: String = UserText.pproFeedbackFormSelectCategoryTitle, - sources: Category.Type, + options: [Category], selection: Binding, @ViewBuilder destination: @escaping () -> Destination) { self.title = title self.prompt = prompt - self.sources = sources + self.options = options self.selection = selection self.destination = destination } @@ -129,7 +129,7 @@ struct UnifiedFeedbackCategoryView Date: Fri, 22 Nov 2024 11:34:51 +0100 Subject: [PATCH 10/20] Dynamically populate categories based on feature avaialable in current subscription --- DuckDuckGo/NetworkProtectionRootView.swift | 4 +++- DuckDuckGo/NetworkProtectionStatusView.swift | 4 +++- .../NetworkProtectionStatusViewModel.swift | 5 ++++- DuckDuckGo/SettingsOthersView.swift | 4 +++- DuckDuckGo/SettingsViewModel.swift | 2 +- .../UnifiedFeedbackFormViewModel.swift | 20 ++++++++++++++++++- .../Feedback/UnifiedFeedbackRootView.swift | 2 +- .../Views/SubscriptionSettingsView.swift | 4 +++- 8 files changed, 37 insertions(+), 8 deletions(-) diff --git a/DuckDuckGo/NetworkProtectionRootView.swift b/DuckDuckGo/NetworkProtectionRootView.swift index 974c03022c..357e302c3d 100644 --- a/DuckDuckGo/NetworkProtectionRootView.swift +++ b/DuckDuckGo/NetworkProtectionRootView.swift @@ -27,6 +27,7 @@ struct NetworkProtectionRootView: View { let statusViewModel: NetworkProtectionStatusViewModel init() { + let subscriptionManager = AppDependencyProvider.shared.subscriptionManager let accountManager = AppDependencyProvider.shared.subscriptionManager.accountManager let locationListRepository = NetworkProtectionLocationListCompositeRepository(accountManager: accountManager) let usesUnifiedFeedbackForm = accountManager.isUserAuthenticated @@ -35,7 +36,8 @@ struct NetworkProtectionRootView: View { statusObserver: AppDependencyProvider.shared.connectionObserver, serverInfoObserver: AppDependencyProvider.shared.serverInfoObserver, locationListRepository: locationListRepository, - usesUnifiedFeedbackForm: usesUnifiedFeedbackForm) + usesUnifiedFeedbackForm: usesUnifiedFeedbackForm, + subscriptionManager: subscriptionManager) } var body: some View { diff --git a/DuckDuckGo/NetworkProtectionStatusView.swift b/DuckDuckGo/NetworkProtectionStatusView.swift index 88e6768dea..f8a51bf3db 100644 --- a/DuckDuckGo/NetworkProtectionStatusView.swift +++ b/DuckDuckGo/NetworkProtectionStatusView.swift @@ -309,7 +309,9 @@ struct NetworkProtectionStatusView: View { @ViewBuilder private func about() -> some View { - let viewModel = UnifiedFeedbackFormViewModel(vpnMetadataCollector: DefaultVPNMetadataCollector(), source: .vpn) + let viewModel = UnifiedFeedbackFormViewModel(vpnMetadataCollector: DefaultVPNMetadataCollector(), + source: .vpn, + subscriptionManager: statusModel.subscriptionManager) Section { NavigationLink(UserText.netPVPNSettingsFAQ, destination: LazyView(NetworkProtectionFAQView())) diff --git a/DuckDuckGo/NetworkProtectionStatusViewModel.swift b/DuckDuckGo/NetworkProtectionStatusViewModel.swift index becccb79aa..566c697227 100644 --- a/DuckDuckGo/NetworkProtectionStatusViewModel.swift +++ b/DuckDuckGo/NetworkProtectionStatusViewModel.swift @@ -176,6 +176,7 @@ final class NetworkProtectionStatusViewModel: ObservableObject { @Published public var animationsOn: Bool = false public let usesUnifiedFeedbackForm: Bool + public let subscriptionManager: SubscriptionManager public init(tunnelController: (TunnelController & TunnelSessionProvider), settings: VPNSettings, @@ -183,13 +184,15 @@ final class NetworkProtectionStatusViewModel: ObservableObject { serverInfoObserver: ConnectionServerInfoObserver, errorObserver: ConnectionErrorObserver = ConnectionErrorObserverThroughSession(), locationListRepository: NetworkProtectionLocationListRepository, - usesUnifiedFeedbackForm: Bool) { + usesUnifiedFeedbackForm: Bool, + subscriptionManager: SubscriptionManager) { self.tunnelController = tunnelController self.settings = settings self.statusObserver = statusObserver self.serverInfoObserver = serverInfoObserver self.errorObserver = errorObserver self.usesUnifiedFeedbackForm = usesUnifiedFeedbackForm + self.subscriptionManager = subscriptionManager statusMessage = Self.message(for: statusObserver.recentValue) self.headerTitle = Self.titleText(status: statusObserver.recentValue) diff --git a/DuckDuckGo/SettingsOthersView.swift b/DuckDuckGo/SettingsOthersView.swift index ffa3e2a781..1e3202ec6e 100644 --- a/DuckDuckGo/SettingsOthersView.swift +++ b/DuckDuckGo/SettingsOthersView.swift @@ -34,7 +34,9 @@ struct SettingsOthersView: View { // Share Feedback if viewModel.usesUnifiedFeedbackForm { - let formViewModel = UnifiedFeedbackFormViewModel(vpnMetadataCollector: DefaultVPNMetadataCollector(), source: .settings) + let formViewModel = UnifiedFeedbackFormViewModel(vpnMetadataCollector: DefaultVPNMetadataCollector(), + source: .settings, + subscriptionManager: viewModel.subscriptionManager) NavigationLink { UnifiedFeedbackCategoryView(UserText.subscriptionFeedback, options: UnifiedFeedbackFlowCategory.allCases, selection: $viewModel.selectedFeedbackFlow) { if let selectedFeedbackFlow = viewModel.selectedFeedbackFlow { diff --git a/DuckDuckGo/SettingsViewModel.swift b/DuckDuckGo/SettingsViewModel.swift index aa87c2982b..a97700ad72 100644 --- a/DuckDuckGo/SettingsViewModel.swift +++ b/DuckDuckGo/SettingsViewModel.swift @@ -46,7 +46,7 @@ final class SettingsViewModel: ObservableObject { let textZoomCoordinator: TextZoomCoordinating // Subscription Dependencies - private let subscriptionManager: SubscriptionManager + let subscriptionManager: SubscriptionManager let subscriptionFeatureAvailability: SubscriptionFeatureAvailability private var subscriptionSignOutObserver: Any? var duckPlayerContingencyHandler: DuckPlayerContingencyHandler { diff --git a/DuckDuckGo/Subscription/Feedback/UnifiedFeedbackFormViewModel.swift b/DuckDuckGo/Subscription/Feedback/UnifiedFeedbackFormViewModel.swift index 6ecfd2aa63..a7c6716fb6 100644 --- a/DuckDuckGo/Subscription/Feedback/UnifiedFeedbackFormViewModel.swift +++ b/DuckDuckGo/Subscription/Feedback/UnifiedFeedbackFormViewModel.swift @@ -19,6 +19,7 @@ import Combine import SwiftUI +import Subscription final class UnifiedFeedbackFormViewModel: ObservableObject { enum Source: String { @@ -104,16 +105,33 @@ final class UnifiedFeedbackFormViewModel: ObservableObject { let source: String + private(set) var availableCategories: [UnifiedFeedbackCategory] = [.subscription] + init(vpnMetadataCollector: any UnifiedMetadataCollector, defaultMetadatCollector: any UnifiedMetadataCollector = DefaultMetadataCollector(), feedbackSender: any UnifiedFeedbackSender = DefaultFeedbackSender(), - source: Source = .unknown) { + source: Source = .unknown, + subscriptionManager: any SubscriptionManager) { self.viewState = .feedbackPending self.vpnMetadataCollector = vpnMetadataCollector self.defaultMetadataCollector = defaultMetadatCollector self.feedbackSender = feedbackSender self.source = source.rawValue + + Task { + let features = await subscriptionManager.currentSubscriptionFeatures() + + if features.contains(.networkProtection) { + availableCategories.append(.vpn) + } + if features.contains(.dataBrokerProtection) { + availableCategories.append(.pir) + } + if features.contains(.identityTheftRestoration) || features.contains(.identityTheftRestorationGlobal) { + availableCategories.append(.itr) + } + } } @MainActor diff --git a/DuckDuckGo/Subscription/Feedback/UnifiedFeedbackRootView.swift b/DuckDuckGo/Subscription/Feedback/UnifiedFeedbackRootView.swift index a09597ed7b..8fc22088f3 100644 --- a/DuckDuckGo/Subscription/Feedback/UnifiedFeedbackRootView.swift +++ b/DuckDuckGo/Subscription/Feedback/UnifiedFeedbackRootView.swift @@ -54,7 +54,7 @@ struct UnifiedFeedbackRootView: View { @ViewBuilder func reportProblemView() -> some View { UnifiedFeedbackCategoryView(UserText.pproFeedbackFormReportProblemTitle, - options: UnifiedFeedbackCategory.allCases, + options: viewModel.availableCategories, selection: $viewModel.selectedCategory) { Group { if let selectedCategory = viewModel.selectedCategory { diff --git a/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift b/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift index 82c7b3f04b..5d6f5c57d8 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift @@ -243,7 +243,9 @@ struct SubscriptionSettingsView: View { @ViewBuilder private var supportButton: some View { - let viewModel = UnifiedFeedbackFormViewModel(vpnMetadataCollector: DefaultVPNMetadataCollector(), source: .ppro) + let viewModel = UnifiedFeedbackFormViewModel(vpnMetadataCollector: DefaultVPNMetadataCollector(), + source: .ppro, + subscriptionManager: settingsViewModel.subscriptionManager) NavigationLink(UserText.subscriptionFeedback, destination: UnifiedFeedbackRootView(viewModel: viewModel)) .daxBodyRegular() .foregroundColor(.init(designSystemColor: .textPrimary)) From 15af65462298454e4ec29e62a912e409c148b537 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 22 Nov 2024 12:43:09 +0100 Subject: [PATCH 11/20] Migrate to product name on feature selected --- DuckDuckGo/Subscription/Subscription.swift | 25 ------------------- ...scriptionPagesUseSubscriptionFeature.swift | 13 +++------- .../SubscriptionEmailViewModel.swift | 8 +++--- .../ViewModel/SubscriptionFlowViewModel.swift | 8 +++--- ...tionPagesUseSubscriptionFeatureTests.swift | 4 +-- 5 files changed, 15 insertions(+), 43 deletions(-) diff --git a/DuckDuckGo/Subscription/Subscription.swift b/DuckDuckGo/Subscription/Subscription.swift index 2adfc0dbea..0673f6be21 100644 --- a/DuckDuckGo/Subscription/Subscription.swift +++ b/DuckDuckGo/Subscription/Subscription.swift @@ -31,28 +31,3 @@ enum SubscriptionPurchaseError: Error { cancelledByUser, generalError } - -enum SubscriptionFeatureName { - static let netP = "vpn" - static let itr = "identity-theft-restoration" - static let dbp = "personal-information-removal" - } - -enum SubscriptionFeatureSelection: Codable { - case netP - case itr - case dbp - - init?(featureName: String) { - switch featureName { - case SubscriptionFeatureName.netP: - self = .netP - case SubscriptionFeatureName.itr: - self = .itr - case SubscriptionFeatureName.dbp: - self = .dbp - default: - return nil - } - } -} diff --git a/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift b/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift index 178c07bd2f..ca148854b1 100644 --- a/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift +++ b/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift @@ -120,11 +120,11 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec // Subscription Activation Actions var onSetSubscription: (() -> Void)? var onBackToSettings: (() -> Void)? - var onFeatureSelected: ((SubscriptionFeatureSelection) -> Void)? + var onFeatureSelected: ((Entitlement.ProductName) -> Void)? var onActivateSubscription: (() -> Void)? struct FeatureSelection: Codable { - let feature: String + let productFeature: Entitlement.ProductName } weak var broker: UserScriptMessageBroker? @@ -330,15 +330,8 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec return nil } - guard let featureSelection = SubscriptionFeatureSelection(featureName: featureSelection.feature) else { - assertionFailure("SubscriptionPagesUserScript: unexpected feature name value") - Logger.subscription.error("SubscriptionPagesUserScript: unexpected feature name value") - setTransactionError(.generalError) - return nil - } + onFeatureSelected?(featureSelection.productFeature) - onFeatureSelected?(featureSelection) - return nil } diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift index 75b49918cb..a059bcae64 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift @@ -170,15 +170,17 @@ final class SubscriptionEmailViewModel: ObservableObject { subFeature.onFeatureSelected = { feature in DispatchQueue.main.async { switch feature { - case .netP: + case .networkProtection: UniquePixel.fire(pixel: .privacyProWelcomeVPN) self.state.selectedFeature = .netP - case .itr: + case .dataBrokerProtection: UniquePixel.fire(pixel: .privacyProWelcomePersonalInformationRemoval) self.state.selectedFeature = .itr - case .dbp: + case .identityTheftRestoration, .identityTheftRestorationGlobal: UniquePixel.fire(pixel: .privacyProWelcomeIdentityRestoration) self.state.selectedFeature = .dbp + case .unknown: + break } } diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift index 04a59ff21c..5dc11256ab 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift @@ -115,15 +115,17 @@ final class SubscriptionFlowViewModel: ObservableObject { subFeature.onFeatureSelected = { feature in DispatchQueue.main.async { switch feature { - case .netP: + case .networkProtection: UniquePixel.fire(pixel: .privacyProWelcomeVPN) self.state.selectedFeature = .netP - case .dbp: + case .dataBrokerProtection: UniquePixel.fire(pixel: .privacyProWelcomePersonalInformationRemoval) self.state.selectedFeature = .dbp - case .itr: + case .identityTheftRestoration, .identityTheftRestorationGlobal: UniquePixel.fire(pixel: .privacyProWelcomeIdentityRestoration) self.state.selectedFeature = .itr + case .unknown: + break } } } diff --git a/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift b/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift index 8636921470..d69e484ad8 100644 --- a/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift +++ b/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift @@ -812,11 +812,11 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { let onFeatureSelectedCalled = expectation(description: "onFeatureSelected") feature.onFeatureSelected = { selection in onFeatureSelectedCalled.fulfill() - XCTAssertEqual(selection, SubscriptionFeatureSelection.itr) + XCTAssertEqual(selection, .identityTheftRestoration) } // When - let featureSelectionParams = ["feature": SubscriptionFeatureName.itr] + let featureSelectionParams = ["productFeature": Entitlement.ProductName.identityTheftRestoration] let result = await feature.featureSelected(params: featureSelectionParams, original: Constants.mockScriptMessage) // Then From 04e58d51f080f84cd4f3e2d7a6eb502935d7f434 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 22 Nov 2024 13:00:23 +0100 Subject: [PATCH 12/20] Update to use new SubscriptionOptions features --- .../SubscriptionPagesUseSubscriptionFeature.swift | 2 +- .../SubscriptionPagesUseSubscriptionFeatureTests.swift | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift b/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift index ca148854b1..7792b22677 100644 --- a/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift +++ b/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift @@ -206,7 +206,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec if subscriptionFeatureAvailability.isSubscriptionPurchaseAllowed { return subscriptionOptions } else { - return SubscriptionOptions.empty + return subscriptionOptions.withoutPurchaseOptions() } } else { Logger.subscription.error("Failed to obtain subscription options") diff --git a/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift b/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift index d69e484ad8..08f07ebac1 100644 --- a/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift +++ b/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift @@ -54,9 +54,9 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { cost: SubscriptionOptionCost(displayPrice: "99 USD", recurrence: "yearly")) ], features: [ - SubscriptionFeature(name: "vpn"), - SubscriptionFeature(name: "personal-information-removal"), - SubscriptionFeature(name: "identity-theft-restoration") + SubscriptionFeature(name: .networkProtection), + SubscriptionFeature(name: .dataBrokerProtection), + SubscriptionFeature(name: .identityTheftRestoration) ]) static let validateTokenResponse = ValidateTokenResponse(account: ValidateTokenResponse.Account(email: Constants.email, From 623c5cf46f2f82d305cb6d33ac343bc403884e8e Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 25 Nov 2024 15:26:04 +0100 Subject: [PATCH 13/20] Add feature flags --- Core/FeatureFlag.swift | 13 ++++++++++++- DuckDuckGo/AppDependencyProvider.swift | 13 ++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/Core/FeatureFlag.swift b/Core/FeatureFlag.swift index da04bcfaf6..05f6782195 100644 --- a/Core/FeatureFlag.swift +++ b/Core/FeatureFlag.swift @@ -56,11 +56,18 @@ public enum FeatureFlag: String { /// https://app.asana.com/0/1208592102886666/1208613627589762/f case crashReportOptInStatusResetting + case isPrivacyProLaunchedROW + case isPrivacyProLaunchedROWOverride } extension FeatureFlag: FeatureFlagDescribing { public var supportsLocalOverriding: Bool { - false + switch self { + case .isPrivacyProLaunchedROWOverride: + return true + default: + return false + } } public var source: FeatureFlagSource { @@ -123,6 +130,10 @@ extension FeatureFlag: FeatureFlagDescribing { return .remoteReleasable(.feature(.adAttributionReporting)) case .crashReportOptInStatusResetting: return .internalOnly + case .isPrivacyProLaunchedROW: + return .remoteReleasable(.subfeature(PrivacyProSubfeature.isLaunchedROW)) + case .isPrivacyProLaunchedROWOverride: + return .remoteReleasable(.subfeature(PrivacyProSubfeature.isLaunchedROWOverride)) } } } diff --git a/DuckDuckGo/AppDependencyProvider.swift b/DuckDuckGo/AppDependencyProvider.swift index 08b976a4a6..0c308cf126 100644 --- a/DuckDuckGo/AppDependencyProvider.swift +++ b/DuckDuckGo/AppDependencyProvider.swift @@ -117,12 +117,23 @@ final class AppDependencyProvider: DependencyProvider { subscriptionEndpointService: subscriptionService, authEndpointService: authService) + let theFeatureFlagger = featureFlagger + let subscriptionFeatureFlagger: FeatureFlaggerMapping = FeatureFlaggerMapping { feature in + switch feature { + case .isLaunchedROW: + return theFeatureFlagger.isFeatureOn(.isPrivacyProLaunchedROW) + case .isLaunchedROWOverride: + return theFeatureFlagger.isFeatureOn(.isPrivacyProLaunchedROWOverride) + } + } + let subscriptionManager = DefaultSubscriptionManager(storePurchaseManager: DefaultStorePurchaseManager(subscriptionFeatureMappingCache: subscriptionFeatureMappingCache), accountManager: accountManager, subscriptionEndpointService: subscriptionService, authEndpointService: authService, subscriptionFeatureMappingCache: subscriptionFeatureMappingCache, - subscriptionEnvironment: subscriptionEnvironment) + subscriptionEnvironment: subscriptionEnvironment, + subscriptionFeatureFlagger: subscriptionFeatureFlagger) accountManager.delegate = subscriptionManager self.subscriptionManager = subscriptionManager From e2e5bbe29a76ed0fbb0c1148a1331c2fdf8b2587 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 25 Nov 2024 16:09:38 +0100 Subject: [PATCH 14/20] Add support for feature flag local override --- Core/FeatureFlag.swift | 3 ++ DuckDuckGo/AppDependencyProvider.swift | 7 +++- .../SubscriptionDebugViewController.swift | 36 +++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/Core/FeatureFlag.swift b/Core/FeatureFlag.swift index 05f6782195..796c0f56fb 100644 --- a/Core/FeatureFlag.swift +++ b/Core/FeatureFlag.swift @@ -61,6 +61,9 @@ public enum FeatureFlag: String { } extension FeatureFlag: FeatureFlagDescribing { + + public static var localOverrideStoreName: String = "com.duckduckgo.app.featureFlag.localOverrides" + public var supportsLocalOverriding: Bool { switch self { case .isPrivacyProLaunchedROWOverride: diff --git a/DuckDuckGo/AppDependencyProvider.swift b/DuckDuckGo/AppDependencyProvider.swift index 0c308cf126..5dacc1637e 100644 --- a/DuckDuckGo/AppDependencyProvider.swift +++ b/DuckDuckGo/AppDependencyProvider.swift @@ -94,7 +94,12 @@ final class AppDependencyProvider: DependencyProvider { private init() { featureFlagger = DefaultFeatureFlagger(internalUserDecider: internalUserDecider, - privacyConfigManager: ContentBlocking.shared.privacyConfigurationManager) + privacyConfigManager: ContentBlocking.shared.privacyConfigurationManager, + localOverrides: FeatureFlagLocalOverrides( + keyValueStore: UserDefaults(suiteName: FeatureFlag.localOverrideStoreName)!, + actionHandler: FeatureFlagOverridesPublishingHandler() + ), + for: FeatureFlag.self) configurationManager = ConfigurationManager(store: configurationStore) diff --git a/DuckDuckGo/SubscriptionDebugViewController.swift b/DuckDuckGo/SubscriptionDebugViewController.swift index 45247ddb76..894136322f 100644 --- a/DuckDuckGo/SubscriptionDebugViewController.swift +++ b/DuckDuckGo/SubscriptionDebugViewController.swift @@ -22,6 +22,7 @@ import UIKit import Subscription import Core import NetworkProtection +import BrowserServicesKit final class SubscriptionDebugViewController: UITableViewController { @@ -29,6 +30,9 @@ final class SubscriptionDebugViewController: UITableViewController { private var subscriptionManager: SubscriptionManager { AppDependencyProvider.shared.subscriptionManager } + private var featureFlagger: FeatureFlagger { + AppDependencyProvider.shared.featureFlagger + } // swiftlint:disable:next force_cast private let reporter = (UIApplication.shared.delegate as! AppDelegate).privacyProDataReporter as! PrivacyProDataReporter @@ -39,6 +43,7 @@ final class SubscriptionDebugViewController: UITableViewController { Sections.appstore: "App Store", Sections.environment: "Environment", Sections.pixels: "Promo Pixel Parameters", + Sections.featureFlags: "Feature flags" ] enum Sections: Int, CaseIterable { @@ -47,6 +52,7 @@ final class SubscriptionDebugViewController: UITableViewController { case appstore case environment case pixels + case featureFlags } enum AuthorizationRows: Int, CaseIterable { @@ -74,6 +80,10 @@ final class SubscriptionDebugViewController: UITableViewController { case randomize } + enum FeatureFlagRows: Int, CaseIterable { + case isLaunchedROW + } + override func numberOfSections(in tableView: UITableView) -> Int { return Sections.allCases.count } @@ -145,6 +155,16 @@ final class SubscriptionDebugViewController: UITableViewController { case .none: break } + + case .featureFlags: + + switch FeatureFlagRows(rawValue: indexPath.row) { + case .isLaunchedROW: + cell.textLabel?.text = "isLaunchedROW" + cell.accessoryType = featureFlagger.isFeatureOn(.isPrivacyProLaunchedROWOverride) ? .checkmark : .none + case .none: + break + } case .none: break } @@ -159,6 +179,7 @@ final class SubscriptionDebugViewController: UITableViewController { case .appstore: return AppStoreRows.allCases.count case .environment: return EnvironmentRows.allCases.count case .pixels: return PixelsRows.allCases.count + case .featureFlags: return FeatureFlagRows.allCases.count case .none: return 0 } @@ -193,6 +214,11 @@ final class SubscriptionDebugViewController: UITableViewController { case .randomize: showRandomizedParamters() default: break } + case .featureFlags: + switch FeatureFlagRows(rawValue: indexPath.row) { + case .isLaunchedROW: toggleIsLaunchedROWFlag() + default: break + } case .none: break } @@ -301,6 +327,16 @@ final class SubscriptionDebugViewController: UITableViewController { showAlert(title: "", message: message) } + private func toggleIsLaunchedROWFlag() { + let flag = FeatureFlag.isPrivacyProLaunchedROWOverride + if featureFlagger.localOverrides?.override(for: flag) == nil { + featureFlagger.localOverrides?.toggleOverride(for: flag) + } else { + featureFlagger.localOverrides?.clearOverride(for: flag) + } + tableView.reloadData() + } + private func syncAppleIDAccount() { Task { do { From 462206d25c38693bdb98a5b1b27fa18cac462d99 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 25 Nov 2024 17:12:04 +0100 Subject: [PATCH 15/20] Update label --- DuckDuckGo/SubscriptionDebugViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/SubscriptionDebugViewController.swift b/DuckDuckGo/SubscriptionDebugViewController.swift index 894136322f..6b4bbbe8e1 100644 --- a/DuckDuckGo/SubscriptionDebugViewController.swift +++ b/DuckDuckGo/SubscriptionDebugViewController.swift @@ -160,7 +160,7 @@ final class SubscriptionDebugViewController: UITableViewController { switch FeatureFlagRows(rawValue: indexPath.row) { case .isLaunchedROW: - cell.textLabel?.text = "isLaunchedROW" + cell.textLabel?.text = "isPrivacyProLaunchedROWOverride" cell.accessoryType = featureFlagger.isFeatureOn(.isPrivacyProLaunchedROWOverride) ? .checkmark : .none case .none: break From fc6d4805c58a5fee34da0c48640e3d0264d6f41a Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 25 Nov 2024 17:13:28 +0100 Subject: [PATCH 16/20] Switch to remote BSK --- DuckDuckGo.xcodeproj/project.pbxproj | 6 ++---- .../xcshareddata/swiftpm/Package.resolved | 9 +++++++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 64b05682a6..9c4f72c74c 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1426,7 +1426,6 @@ 1E9529A02C4E748B006E80D4 /* UINavigationControllerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UINavigationControllerExtension.swift; sourceTree = ""; }; 1EA51375286596A000493C6A /* PrivacyIconLogic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyIconLogic.swift; sourceTree = ""; }; 1EA513772866039400493C6A /* TrackerAnimationLogic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackerAnimationLogic.swift; sourceTree = ""; }; - 1EBC7B462CEF898800DE53B9 /* BrowserServicesKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = BrowserServicesKit; path = ../BrowserServicesKit; sourceTree = SOURCE_ROOT; }; 1EC458452948932500CB2B13 /* UIHostingControllerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIHostingControllerExtension.swift; sourceTree = ""; }; 1EC51CD828D8C0DF00E9D05A /* UIImageExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImageExtension.swift; sourceTree = ""; }; 1EDE39D12705D4A100C99C72 /* FileSizeDebugViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileSizeDebugViewController.swift; sourceTree = ""; }; @@ -4184,7 +4183,6 @@ 84E341891E2F7EFB00BDBA6F = { isa = PBXGroup; children = ( - 1EBC7B462CEF898800DE53B9 /* BrowserServicesKit */, EE3B98EB2A963515002F63A0 /* WidgetsExtensionAlpha.entitlements */, 6FB030C7234331B400A10DB9 /* Configuration.xcconfig */, EEB8FDB92A990AEE00EBEDCF /* Configuration-Alpha.xcconfig */, @@ -11041,8 +11039,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { - kind = exactVersion; - version = 211.0.0; + branch = "michal/non-us-subscriptions"; + kind = branch; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index c88e0ab07e..f361fd0ff1 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -27,6 +27,15 @@ "version" : "3.0.0" } }, + { + "identity" : "browserserviceskit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", + "state" : { + "branch" : "michal/non-us-subscriptions", + "revision" : "c28608cff5bc3369d0277d3d186fa59bfc4b83dc" + } + }, { "identity" : "content-scope-scripts", "kind" : "remoteSourceControl", From db26e74268addd01b37f681aaf41d7b0fea63829 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Thu, 28 Nov 2024 15:23:20 +0100 Subject: [PATCH 17/20] Fix missing period --- DuckDuckGo/SettingsSubscriptionView.swift | 7 ++----- DuckDuckGo/UserText.swift | 3 +-- DuckDuckGo/en.lproj/Localizable.strings | 5 +---- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/DuckDuckGo/SettingsSubscriptionView.swift b/DuckDuckGo/SettingsSubscriptionView.swift index 1a6a17486e..f8e82198b2 100644 --- a/DuckDuckGo/SettingsSubscriptionView.swift +++ b/DuckDuckGo/SettingsSubscriptionView.swift @@ -65,14 +65,11 @@ struct SettingsSubscriptionView: View { private var purchaseSubscriptionView: some View { Group { let subtitleText = { - // TODO: no feature flag (old text) - // UserText.settingsPProDescription - switch subscriptionManager.storePurchaseManager().currentStorefrontRegion { case .usa: - UserText.settingsPProPurchaseUSDescription + UserText.settingsPProDescription case .restOfWorld: - UserText.settingsPProPurchaseROWDescription + UserText.settingsPProROWDescription } }() diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index 7f8396a6a4..55be2ca82a 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -1084,8 +1084,7 @@ But if you *do* want a peek under the hood, you can find more information about public static let settingsPProSectionFooter = NSLocalizedString("settings.ppro.footer", value: "Privacy Policy and Terms of Service", comment: "Title for Link in the Footer of Privacy Pro section") public static let settingsPProSubscribe = NSLocalizedString("settings.subscription.subscribe", value: "Protect your connection and identity with Privacy Pro", comment: "Call to action title for Privacy Pro settings") public static let settingsPProDescription = NSLocalizedString("settings.subscription.description", value:"Includes our VPN, Personal Information Removal, and Identity Theft Restoration.", comment: "Privacy pro description subtitle in settings") - public static let settingsPProPurchaseUSDescription = NSLocalizedString("settings.subscription.purchase.us.description", value:"Includes our VPN, Personal Information Removal, and Identity Theft Restoration", comment: "Privacy Pro description subtitle in settings") - public static let settingsPProPurchaseROWDescription = NSLocalizedString("settings.subscription.purchase.row.description", value:"Includes our VPN and Identity Theft Restoration", comment: "Privacy Pro description subtitle in settings") + public static let settingsPProROWDescription = NSLocalizedString("settings.subscription.row.description", value:"Includes our VPN and Identity Theft Restoration.", comment: "Privacy Pro description subtitle in settings") public static let settingsPProActivating = NSLocalizedString("settings.subscription.activating", value:"Activating", comment: "Privacy pro description subtitle in settings when the is activating") public static let settingsPProLearnMore = NSLocalizedString("settings.subscription.learn.more", value: "Get Privacy Pro", comment: "Get Privacy Pro button text for privacy pro") diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index 315f7079b1..ca98d2afcc 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -2371,10 +2371,7 @@ But if you *do* want a peek under the hood, you can find more information about "settings.subscription.manage" = "Subscription Settings"; /* Privacy Pro description subtitle in settings */ -"settings.subscription.purchase.row.description" = "Includes our VPN and Identity Theft Restoration"; - -/* Privacy Pro description subtitle in settings */ -"settings.subscription.purchase.us.description" = "Includes our VPN, Personal Information Removal, and Identity Theft Restoration"; +"settings.subscription.row.description" = "Includes our VPN and Identity Theft Restoration."; /* Call to action title for Privacy Pro settings */ "settings.subscription.subscribe" = "Protect your connection and identity with Privacy Pro"; From c48b3c372ffba8d1488f3012820673da89f30aa2 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Thu, 28 Nov 2024 15:48:47 +0100 Subject: [PATCH 18/20] Make the Subscription header cell not tappable --- DuckDuckGo/SettingsSubscriptionView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/DuckDuckGo/SettingsSubscriptionView.swift b/DuckDuckGo/SettingsSubscriptionView.swift index f8e82198b2..7c6ce225eb 100644 --- a/DuckDuckGo/SettingsSubscriptionView.swift +++ b/DuckDuckGo/SettingsSubscriptionView.swift @@ -76,6 +76,7 @@ struct SettingsSubscriptionView: View { SettingsCellView(label: UserText.settingsPProSubscribe, subtitle: subtitleText, image: Image("SettingsPrivacyPro")) + .disabled(true) // Get privacy pro SettingsCustomCell(content: { From 7f55d0994385e75eb9fb5b51c58804c81c4230d4 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 29 Nov 2024 11:58:08 +0100 Subject: [PATCH 19/20] Update BSK to release version --- DuckDuckGo.xcodeproj/project.pbxproj | 4 ++-- .../xcshareddata/swiftpm/Package.resolved | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 9c4f72c74c..c5469a8422 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -11039,8 +11039,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { - branch = "michal/non-us-subscriptions"; - kind = branch; + kind = exactVersion; + version = "211.1.3-1"; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index f361fd0ff1..5ef0b5dc49 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "branch" : "michal/non-us-subscriptions", - "revision" : "c28608cff5bc3369d0277d3d186fa59bfc4b83dc" + "revision" : "114cdbfcfae15ad8c7d5e502832e94061aef7cff", + "version" : "211.1.3-1" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "f2caf4ff814f4714d07d6fc2cf02498cb54a1389", - "version" : "6.36.0" + "revision" : "dfef00ef77f5181d1d8a4f7cc88f7b7c0514dd34", + "version" : "6.39.0" } }, { From dfaa6a668a5477662dba534a3fde6d411045e531 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 29 Nov 2024 13:09:23 +0100 Subject: [PATCH 20/20] Fix tests --- .../NetworkProtectionStatusViewModelTests.swift | 12 +++++++++++- .../Subscription/StorePurchaseManagerTests.swift | 5 +++-- .../SubscriptionContainerViewModelTests.swift | 4 +++- .../SubscriptionFlowViewModelTests.swift | 6 ++++-- ...scriptionPagesUseSubscriptionFeatureTests.swift | 14 +++++++++++--- 5 files changed, 32 insertions(+), 9 deletions(-) diff --git a/DuckDuckGoTests/NetworkProtectionStatusViewModelTests.swift b/DuckDuckGoTests/NetworkProtectionStatusViewModelTests.swift index 47428ed53d..de2c62cac0 100644 --- a/DuckDuckGoTests/NetworkProtectionStatusViewModelTests.swift +++ b/DuckDuckGoTests/NetworkProtectionStatusViewModelTests.swift @@ -22,12 +22,14 @@ import NetworkProtection import NetworkExtension import NetworkProtectionTestUtils import SubscriptionTestingUtilities +import Subscription @testable import DuckDuckGo final class NetworkProtectionStatusViewModelTests: XCTestCase { private var tunnelController: MockTunnelController! private var statusObserver: MockConnectionStatusObserver! private var serverInfoObserver: MockConnectionServerInfoObserver! + private var subscriptionManager: SubscriptionManagerMock! private var viewModel: NetworkProtectionStatusViewModel! private var testError: Error { @@ -40,12 +42,20 @@ final class NetworkProtectionStatusViewModelTests: XCTestCase { tunnelController = MockTunnelController() statusObserver = MockConnectionStatusObserver() serverInfoObserver = MockConnectionServerInfoObserver() + subscriptionManager = SubscriptionManagerMock(accountManager: AccountManagerMock(), + subscriptionEndpointService: SubscriptionEndpointServiceMock(), + authEndpointService: AuthEndpointServiceMock(), + storePurchaseManager: StorePurchaseManagerMock(), + currentEnvironment: SubscriptionEnvironment(serviceEnvironment: .production, purchasePlatform: .appStore), + canPurchase: true, + subscriptionFeatureMappingCache: SubscriptionFeatureMappingCacheMock()) viewModel = NetworkProtectionStatusViewModel(tunnelController: tunnelController, settings: VPNSettings(defaults: .networkProtectionGroupDefaults), statusObserver: statusObserver, serverInfoObserver: serverInfoObserver, locationListRepository: MockNetworkProtectionLocationListRepository(), - usesUnifiedFeedbackForm: false) + usesUnifiedFeedbackForm: false, + subscriptionManager: subscriptionManager) } override func tearDown() { diff --git a/DuckDuckGoTests/Subscription/StorePurchaseManagerTests.swift b/DuckDuckGoTests/Subscription/StorePurchaseManagerTests.swift index d8f38e823f..c3004d38fb 100644 --- a/DuckDuckGoTests/Subscription/StorePurchaseManagerTests.swift +++ b/DuckDuckGoTests/Subscription/StorePurchaseManagerTests.swift @@ -41,7 +41,8 @@ final class StorePurchaseManagerTests: XCTestCase { session.disableDialogs = true session.clearTransactions() - storePurchaseManager = DefaultStorePurchaseManager() + let subscriptionFeatureMappingCache = SubscriptionFeatureMappingCacheMock() + storePurchaseManager = DefaultStorePurchaseManager(subscriptionFeatureMappingCache: subscriptionFeatureMappingCache) } override func tearDownWithError() throws { @@ -70,7 +71,7 @@ final class StorePurchaseManagerTests: XCTestCase { // Then XCTAssertEqual(subscriptionOptions.options.count, 2) - XCTAssertEqual(subscriptionOptions.features.count, SubscriptionFeatureName.allCases.count) + XCTAssertEqual(subscriptionOptions.features.count, 3) XCTAssertTrue(storePurchaseManager.areProductsAvailable) let optionIDs = subscriptionOptions.options.map { $0.id } diff --git a/DuckDuckGoTests/Subscription/SubscriptionContainerViewModelTests.swift b/DuckDuckGoTests/Subscription/SubscriptionContainerViewModelTests.swift index 2d4979997f..7efea415d4 100644 --- a/DuckDuckGoTests/Subscription/SubscriptionContainerViewModelTests.swift +++ b/DuckDuckGoTests/Subscription/SubscriptionContainerViewModelTests.swift @@ -30,13 +30,15 @@ final class SubscriptionContainerViewModelTests: XCTestCase { let subscriptionService = SubscriptionEndpointServiceMock() let authService = AuthEndpointServiceMock() let storePurchaseManager = StorePurchaseManagerMock() + let subscriptionFeatureMappingCache = SubscriptionFeatureMappingCacheMock() return SubscriptionManagerMock(accountManager: accountManager, subscriptionEndpointService: subscriptionService, authEndpointService: authService, storePurchaseManager: storePurchaseManager, currentEnvironment: SubscriptionEnvironment(serviceEnvironment: .production, purchasePlatform: .appStore), - canPurchase: true) + canPurchase: true, + subscriptionFeatureMappingCache: subscriptionFeatureMappingCache) }() let subscriptionFeatureAvailability = SubscriptionFeatureAvailabilityMock.enabled diff --git a/DuckDuckGoTests/Subscription/SubscriptionFlowViewModelTests.swift b/DuckDuckGoTests/Subscription/SubscriptionFlowViewModelTests.swift index 67235b541e..cc114e794b 100644 --- a/DuckDuckGoTests/Subscription/SubscriptionFlowViewModelTests.swift +++ b/DuckDuckGoTests/Subscription/SubscriptionFlowViewModelTests.swift @@ -29,14 +29,16 @@ final class SubscriptionFlowViewModelTests: XCTestCase { let accountManager = AccountManagerMock() let subscriptionService = DefaultSubscriptionEndpointService(currentServiceEnvironment: .production) let authService = DefaultAuthEndpointService(currentServiceEnvironment: .production) - let storePurchaseManager = DefaultStorePurchaseManager() + let subscriptionFeatureMappingCache = SubscriptionFeatureMappingCacheMock() + let storePurchaseManager = DefaultStorePurchaseManager(subscriptionFeatureMappingCache: subscriptionFeatureMappingCache) return SubscriptionManagerMock(accountManager: accountManager, subscriptionEndpointService: subscriptionService, authEndpointService: authService, storePurchaseManager: storePurchaseManager, currentEnvironment: SubscriptionEnvironment(serviceEnvironment: .production, purchasePlatform: .appStore), - canPurchase: true) + canPurchase: true, + subscriptionFeatureMappingCache: subscriptionFeatureMappingCache) }() let subscriptionFeatureAvailability = SubscriptionFeatureAvailabilityMock.enabled diff --git a/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift b/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift index 08f07ebac1..0eb8d432a6 100644 --- a/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift +++ b/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift @@ -46,7 +46,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { static let mostRecentTransactionJWS = "dGhpcyBpcyBub3QgYSByZWFsIEFw(...)cCBTdG9yZSB0cmFuc2FjdGlvbiBKV1M=" - static let subscriptionOptions = SubscriptionOptions(platform: SubscriptionPlatformName.ios.rawValue, + static let subscriptionOptions = SubscriptionOptions(platform: SubscriptionPlatformName.ios, options: [ SubscriptionOption(id: "1", cost: SubscriptionOptionCost(displayPrice: "9 USD", recurrence: "monthly")), @@ -81,6 +81,9 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { var storePurchaseManager: StorePurchaseManagerMock! var subscriptionEnvironment: SubscriptionEnvironment! + var subscriptionFeatureMappingCache: SubscriptionFeatureMappingCacheMock! + var subscriptionFeatureFlagger: FeatureFlaggerMapping! + var appStorePurchaseFlow: AppStorePurchaseFlow! var appStoreRestoreFlow: AppStoreRestoreFlow! var appStoreAccountManagementFlow: AppStoreAccountManagementFlow! @@ -129,6 +132,9 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { key: UserDefaultsCacheKey.subscriptionEntitlements, settings: UserDefaultsCacheSettings(defaultExpirationInterval: .minutes(20))) + subscriptionFeatureMappingCache = SubscriptionFeatureMappingCacheMock() + subscriptionFeatureFlagger = FeatureFlaggerMapping(mapping: { $0.defaultState }) + // Real AccountManager accountManager = DefaultAccountManager(storage: accountStorage, accessTokenStorage: accessTokenStorage, @@ -156,7 +162,9 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { accountManager: accountManager, subscriptionEndpointService: subscriptionService, authEndpointService: authService, - subscriptionEnvironment: subscriptionEnvironment) + subscriptionFeatureMappingCache: subscriptionFeatureMappingCache, + subscriptionEnvironment: subscriptionEnvironment, + subscriptionFeatureFlagger: subscriptionFeatureFlagger) feature = SubscriptionPagesUseSubscriptionFeature(subscriptionManager: subscriptionManager, subscriptionFeatureAvailability: subscriptionFeatureAvailability, @@ -816,7 +824,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { } // When - let featureSelectionParams = ["productFeature": Entitlement.ProductName.identityTheftRestoration] + let featureSelectionParams = ["productFeature": Entitlement.ProductName.identityTheftRestoration.rawValue] let result = await feature.featureSelected(params: featureSelectionParams, original: Constants.mockScriptMessage) // Then