Skip to content

Commit

Permalink
Client displays correct subscription (#3620)
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
miasma13 authored Nov 29, 2024
1 parent 286e5b9 commit 4a3c4e8
Show file tree
Hide file tree
Showing 26 changed files with 254 additions and 118 deletions.
16 changes: 15 additions & 1 deletion Core/FeatureFlag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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))
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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" */ = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
},
{
Expand Down Expand Up @@ -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"
Expand Down
28 changes: 25 additions & 3 deletions DuckDuckGo/AppDependencyProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<FeatureFlag>()
),
for: FeatureFlag.self)

configurationManager = ConfigurationManager(store: configurationStore)

Expand All @@ -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<SubscriptionFeatureFlags> = 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
Expand Down
4 changes: 3 additions & 1 deletion DuckDuckGo/NetworkProtectionRootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down
4 changes: 3 additions & 1 deletion DuckDuckGo/NetworkProtectionStatusView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
Expand Down
5 changes: 4 additions & 1 deletion DuckDuckGo/NetworkProtectionStatusViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -176,20 +176,23 @@ 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,
statusObserver: ConnectionStatusObserver,
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)
Expand Down
6 changes: 4 additions & 2 deletions DuckDuckGo/SettingsOthersView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions DuckDuckGo/SettingsState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -132,6 +133,7 @@ struct SettingsState {
hasActiveSubscription: false,
isRestoring: false,
shouldDisplayRestoreSubscriptionError: false,
subscriptionFeatures: [],
entitlements: [],
platform: .unknown,
isShowingStripeView: false),
Expand Down
101 changes: 67 additions & 34 deletions DuckDuckGo/SettingsSubscriptionView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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
Expand Down Expand Up @@ -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))
Expand Down
5 changes: 3 additions & 2 deletions DuckDuckGo/SettingsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand All @@ -759,6 +759,7 @@ extension SettingsViewModel {
}

self.state.subscription.entitlements = currentEntitlements
self.state.subscription.subscriptionFeatures = await subscriptionManager.currentSubscriptionFeatures()

case .failure:
break
Expand Down
Loading

0 comments on commit 4a3c4e8

Please sign in to comment.