From 4a3c4e8eea03a645fbae7c836cea4a2d718fd59c Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 29 Nov 2024 13:22:15 +0100 Subject: [PATCH 1/2] Client displays correct subscription (#3620) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1208524871249522/1208379950230747/f **Description**: See https://app.asana.com/0/1208524871249522/1208799981662317/f **Steps to test this PR**: See https://app.asana.com/0/0/1208836865988482/f and its parent task. **Definition of Done (Internal Only)**: * [ ] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? **Copy Testing**: * [ ] Use of correct apostrophes in new copy, ie `’` rather than `'` **Orientation Testing**: * [ ] Portrait * [ ] Landscape **Device Testing**: * [ ] iPhone SE (1st Gen) * [ ] iPhone 8 * [ ] iPhone X * [ ] iPhone 14 Pro * [ ] iPad **OS Testing**: * [ ] iOS 15 * [ ] iOS 16 * [ ] iOS 17 **Theme Testing**: * [ ] Light theme * [ ] Dark theme --- ###### Internal references: [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) --- Core/FeatureFlag.swift | 16 ++- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 6 +- DuckDuckGo/AppDependencyProvider.swift | 28 ++++- DuckDuckGo/NetworkProtectionRootView.swift | 4 +- DuckDuckGo/NetworkProtectionStatusView.swift | 4 +- .../NetworkProtectionStatusViewModel.swift | 5 +- DuckDuckGo/SettingsOthersView.swift | 6 +- DuckDuckGo/SettingsState.swift | 2 + DuckDuckGo/SettingsSubscriptionView.swift | 101 ++++++++++++------ DuckDuckGo/SettingsViewModel.swift | 5 +- .../UnifiedFeedbackFormViewModel.swift | 20 +++- .../Feedback/UnifiedFeedbackRootView.swift | 22 ++-- DuckDuckGo/Subscription/Subscription.swift | 25 ----- ...scriptionPagesUseSubscriptionFeature.swift | 15 +-- .../SubscriptionEmailViewModel.swift | 8 +- .../ViewModel/SubscriptionFlowViewModel.swift | 8 +- .../Views/SubscriptionSettingsView.swift | 4 +- .../SubscriptionDebugViewController.swift | 38 ++++++- DuckDuckGo/UserText.swift | 1 + DuckDuckGo/en.lproj/Localizable.strings | 3 + ...etworkProtectionStatusViewModelTests.swift | 12 ++- .../StorePurchaseManagerTests.swift | 5 +- .../SubscriptionContainerViewModelTests.swift | 4 +- .../SubscriptionFlowViewModelTests.swift | 6 +- ...tionPagesUseSubscriptionFeatureTests.swift | 22 ++-- 26 files changed, 254 insertions(+), 118 deletions(-) diff --git a/Core/FeatureFlag.swift b/Core/FeatureFlag.swift index da04bcfaf6..796c0f56fb 100644 --- a/Core/FeatureFlag.swift +++ b/Core/FeatureFlag.swift @@ -56,11 +56,21 @@ public enum FeatureFlag: String { /// https://app.asana.com/0/1208592102886666/1208613627589762/f case crashReportOptInStatusResetting + case isPrivacyProLaunchedROW + case isPrivacyProLaunchedROWOverride } extension FeatureFlag: FeatureFlagDescribing { + + public static var localOverrideStoreName: String = "com.duckduckgo.app.featureFlag.localOverrides" + public var supportsLocalOverriding: Bool { - false + switch self { + case .isPrivacyProLaunchedROWOverride: + return true + default: + return false + } } public var source: FeatureFlagSource { @@ -123,6 +133,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.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 3800ace64b..0250667290 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -11054,7 +11054,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 211.1.3; + 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 4e271933b0..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" : { - "revision" : "f83b1f5ebd328bc2447d1a3793149bb21037d685", - "version" : "211.1.3" + "revision" : "114cdbfcfae15ad8c7d5e502832e94061aef7cff", + "version" : "211.1.3-1" } }, { @@ -138,7 +138,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" diff --git a/DuckDuckGo/AppDependencyProvider.swift b/DuckDuckGo/AppDependencyProvider.swift index 245901da44..b378ab86e9 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) @@ -109,16 +114,33 @@ 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 theFeatureFlagger = featureFlagger + let subscriptionFeatureFlagger: FeatureFlaggerMapping = FeatureFlaggerMapping { feature in + switch feature { + case .isLaunchedROW: + return theFeatureFlagger.isFeatureOn(.isPrivacyProLaunchedROW) + case .isLaunchedROWOverride: + return theFeatureFlagger.isFeatureOn(.isPrivacyProLaunchedROWOverride) + default: + return feature.defaultState + } + } + + let subscriptionManager = DefaultSubscriptionManager(storePurchaseManager: DefaultStorePurchaseManager(subscriptionFeatureMappingCache: subscriptionFeatureMappingCache), accountManager: accountManager, subscriptionEndpointService: subscriptionService, authEndpointService: authService, - subscriptionEnvironment: subscriptionEnvironment) + subscriptionFeatureMappingCache: subscriptionFeatureMappingCache, + subscriptionEnvironment: subscriptionEnvironment, + subscriptionFeatureFlagger: subscriptionFeatureFlagger) accountManager.delegate = subscriptionManager self.subscriptionManager = subscriptionManager 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 96f3cab8d1..1e3202ec6e 100644 --- a/DuckDuckGo/SettingsOthersView.swift +++ b/DuckDuckGo/SettingsOthersView.swift @@ -34,9 +34,11 @@ 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, 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/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 b2414c2855..7c6ce225eb 100644 --- a/DuckDuckGo/SettingsSubscriptionView.swift +++ b/DuckDuckGo/SettingsSubscriptionView.swift @@ -64,9 +64,19 @@ struct SettingsSubscriptionView: View { @ViewBuilder private var purchaseSubscriptionView: some View { Group { + let subtitleText = { + switch subscriptionManager.storePurchaseManager().currentStorefrontRegion { + case .usa: + UserText.settingsPProDescription + case .restOfWorld: + UserText.settingsPProROWDescription + } + }() + SettingsCellView(label: UserText.settingsPProSubscribe, - subtitle: UserText.settingsPProDescription, + subtitle: subtitleText, image: Image("SettingsPrivacyPro")) + .disabled(true) // Get privacy pro SettingsCustomCell(content: { @@ -93,23 +103,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 subscriptionFeatures = settingsViewModel.state.subscription.subscriptionFeatures + + if subscriptionFeatures.contains(.networkProtection) { + SettingsCellView(label: UserText.settingsPProVPNTitle, + image: Image("SettingsPrivacyProVPN"), + statusIndicator: StatusIndicatorView(status: .off), + isGreyedOut: true + ) + } + + if subscriptionFeatures.contains(.dataBrokerProtection) { + SettingsCellView( + label: UserText.settingsPProDBPTitle, + image: Image("SettingsPrivacyProPIR"), + statusIndicator: StatusIndicatorView(status: .off), + isGreyedOut: true + ) + } + + if subscriptionFeatures.contains(.identityTheftRestoration) || subscriptionFeatures.contains(.identityTheftRestorationGlobal) { + SettingsCellView( + label: UserText.settingsPProITRTitle, + image: Image("SettingsPrivacyProITP"), + statusIndicator: StatusIndicatorView(status: .off), + isGreyedOut: true + ) + } } @ViewBuilder @@ -155,37 +175,50 @@ 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) + 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 ) } + .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) { - 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: hasITREntitlement ? .on : .off), + isGreyedOut: !hasITREntitlement + ) } + .disabled(!hasITREntitlement) } NavigationLink(destination: LazyView(SubscriptionSettingsView(configuration: .subscribed, settingsViewModel: settingsViewModel)) diff --git a/DuckDuckGo/SettingsViewModel.swift b/DuckDuckGo/SettingsViewModel.swift index 5f96ce0215..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 { @@ -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) { @@ -759,6 +759,7 @@ extension SettingsViewModel { } self.state.subscription.entitlements = currentEntitlements + self.state.subscription.subscriptionFeatures = await subscriptionManager.currentSubscriptionFeatures() case .failure: break 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 58b43f15d7..8fc22088f3 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: viewModel.availableCategories, 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 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? @@ -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") @@ -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/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)) diff --git a/DuckDuckGo/SubscriptionDebugViewController.swift b/DuckDuckGo/SubscriptionDebugViewController.swift index 5bd7505d64..6dbc713030 100644 --- a/DuckDuckGo/SubscriptionDebugViewController.swift +++ b/DuckDuckGo/SubscriptionDebugViewController.swift @@ -23,6 +23,7 @@ import Subscription import Core import NetworkProtection import StoreKit +import BrowserServicesKit final class SubscriptionDebugViewController: UITableViewController { @@ -30,6 +31,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 @@ -40,7 +44,8 @@ final class SubscriptionDebugViewController: UITableViewController { Sections.appstore: "App Store", Sections.environment: "Environment", Sections.pixels: "Promo Pixel Parameters", - Sections.metadata: "StoreKit Metadata" + Sections.metadata: "StoreKit Metadata", + Sections.featureFlags: "Feature flags" ] enum Sections: Int, CaseIterable { @@ -50,6 +55,7 @@ final class SubscriptionDebugViewController: UITableViewController { case environment case pixels case metadata + case featureFlags } enum AuthorizationRows: Int, CaseIterable { @@ -82,6 +88,10 @@ final class SubscriptionDebugViewController: UITableViewController { case countryCode } + enum FeatureFlagRows: Int, CaseIterable { + case isLaunchedROW + } + private var storefrontID = "Loading" private var storefrontCountryCode = "Loading" @@ -173,6 +183,16 @@ final class SubscriptionDebugViewController: UITableViewController { case .none: break } + + case .featureFlags: + switch FeatureFlagRows(rawValue: indexPath.row) { + case .isLaunchedROW: + cell.textLabel?.text = "isPrivacyProLaunchedROWOverride" + cell.accessoryType = featureFlagger.isFeatureOn(.isPrivacyProLaunchedROWOverride) ? .checkmark : .none + case .none: + break + } + case .none: break } @@ -188,6 +208,7 @@ final class SubscriptionDebugViewController: UITableViewController { case .environment: return EnvironmentRows.allCases.count case .pixels: return PixelsRows.allCases.count case .metadata: return MetadataRows.allCases.count + case .featureFlags: return FeatureFlagRows.allCases.count case .none: return 0 } } @@ -223,6 +244,11 @@ final class SubscriptionDebugViewController: UITableViewController { } case .metadata: break + case .featureFlags: + switch FeatureFlagRows(rawValue: indexPath.row) { + case .isLaunchedROW: toggleIsLaunchedROWFlag() + default: break + } case .none: break } @@ -331,6 +357,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 { diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index b601eb9f28..b6bb75c759 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -1084,6 +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 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 fc7d856f4a..ca98d2afcc 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -2370,6 +2370,9 @@ 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.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"; 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 8636921470..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")), @@ -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, @@ -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, @@ -812,11 +820,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.rawValue] let result = await feature.featureSelected(params: featureSelectionParams, original: Constants.mockScriptMessage) // Then From c28b0022d7dd4b5e5d6500fdf009aeda5c20584d Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 29 Nov 2024 13:42:00 +0100 Subject: [PATCH 2/2] Release 7.147.0-6 (#3652) Please make sure all GH checks passed before merging. It can take around 20 minutes. Briefly review this PR to see if there are no issues or red flags and then merge it. --- DuckDuckGo.xcodeproj/project.pbxproj | 56 ++++++++++++++-------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 0250667290..f4c3a54c31 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -9271,7 +9271,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProvider.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -9308,7 +9308,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9398,7 +9398,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9425,7 +9425,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9572,7 +9572,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9597,7 +9597,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGo/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9666,7 +9666,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -9700,7 +9700,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9733,7 +9733,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9763,7 +9763,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10073,7 +10073,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10104,7 +10104,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -10132,7 +10132,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -10165,7 +10165,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -10195,7 +10195,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProviderAlpha.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -10228,11 +10228,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 5; + DYLIB_CURRENT_VERSION = 6; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10464,7 +10464,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10492,7 +10492,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10524,7 +10524,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10561,7 +10561,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10596,7 +10596,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10631,11 +10631,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 5; + DYLIB_CURRENT_VERSION = 6; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10808,11 +10808,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 5; + DYLIB_CURRENT_VERSION = 6; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10841,10 +10841,10 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 5; + DYLIB_CURRENT_VERSION = 6; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";