Skip to content

Commit

Permalink
Implements Welcome Links Navigation in Subscriptions (#2371)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/1200019156869587/1205017548855433/f

Description:

Implements onAppear navigation for Settings, so you can navigate to a view programmatically when the view is shown
Implements navigation from the Subscription Welcome page to each feature
  • Loading branch information
afterxleep authored Jan 24, 2024
1 parent 2061bde commit 4a3c10e
Show file tree
Hide file tree
Showing 13 changed files with 107 additions and 58 deletions.
4 changes: 0 additions & 4 deletions DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -805,7 +805,6 @@
D6D12CAB2B291CAA0054390C /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12C9C2B291CA90054390C /* APIService.swift */; };
D6D12CAC2B291CAA0054390C /* AuthService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12C9D2B291CA90054390C /* AuthService.swift */; };
D6D12CAD2B291CAA0054390C /* PurchaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12C9E2B291CA90054390C /* PurchaseManager.swift */; };
D6D4B77C2B5AE99500996546 /* SubscriptionFlowNavController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4B77B2B5AE99500996546 /* SubscriptionFlowNavController.swift */; };
D6E83C122B1E6AB3006C8AFB /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E83C112B1E6AB3006C8AFB /* SettingsView.swift */; };
D6E83C2E2B1EA06E006C8AFB /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E83C2D2B1EA06E006C8AFB /* SettingsViewModel.swift */; };
D6E83C312B1EA309006C8AFB /* SettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E83C302B1EA309006C8AFB /* SettingsCell.swift */; };
Expand Down Expand Up @@ -2458,7 +2457,6 @@
D6D12C9C2B291CA90054390C /* APIService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = "<group>"; };
D6D12C9D2B291CA90054390C /* AuthService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthService.swift; sourceTree = "<group>"; };
D6D12C9E2B291CA90054390C /* PurchaseManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PurchaseManager.swift; sourceTree = "<group>"; };
D6D4B77B2B5AE99500996546 /* SubscriptionFlowNavController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionFlowNavController.swift; sourceTree = "<group>"; };
D6E83C112B1E6AB3006C8AFB /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
D6E83C2D2B1EA06E006C8AFB /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = "<group>"; };
D6E83C302B1EA309006C8AFB /* SettingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsCell.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4562,7 +4560,6 @@
D664C7932B289AA000CBFA76 /* ViewModel */ = {
isa = PBXGroup;
children = (
D6D4B77B2B5AE99500996546 /* SubscriptionFlowNavController.swift */,
D664C7942B289AA000CBFA76 /* SubscriptionFlowViewModel.swift */,
D68DF81D2B5830380023DBEA /* SubscriptionRestoreViewModel.swift */,
D64648AE2B5993890033090B /* SubscriptionEmailViewModel.swift */,
Expand Down Expand Up @@ -6534,7 +6531,6 @@
1E162610296C5C630004127F /* CustomDaxDialogViewModel.swift in Sources */,
8590CB69268A4E190089F6BF /* DebugEtagStorage.swift in Sources */,
D6D12CA62B291CAA0054390C /* AppStoreRestoreFlow.swift in Sources */,
D6D4B77C2B5AE99500996546 /* SubscriptionFlowNavController.swift in Sources */,
C1CDA3162AFB9C7F006D1476 /* AutofillNeverPromptWebsitesManager.swift in Sources */,
F1CA3C371F045878005FADB3 /* PrivacyStore.swift in Sources */,
37FCAAC029930E26000E420A /* FailedAssertionView.swift in Sources */,
Expand Down
2 changes: 1 addition & 1 deletion DuckDuckGo/SettingsState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ struct SettingsState {
// Subscriptions Properties
var subscription: Subscription

// Sync Propertiers
// Sync Properties
var sync: SyncSettings

static var defaults: SettingsState {
Expand Down
12 changes: 7 additions & 5 deletions DuckDuckGo/SettingsSubscriptionView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,14 @@ struct SettingsSubscriptionView: View {
.daxBodyRegular()
.foregroundColor(Color.init(designSystemColor: .accent))
}



private var purchaseSubscriptionView: some View {
return Group {
SettingsCustomCell(content: { subscriptionDescriptionView })
NavigationLink(destination: SubscriptionFlowView(viewModel: SubscriptionFlowViewModel())) {
let viewModel = SubscriptionFlowViewModel(onFeatureSelected: { value in
self.viewModel.onAppearNavigationTarget = value
})
NavigationLink(destination: SubscriptionFlowView(viewModel: viewModel)) {
SettingsCustomCell(content: { learnMoreView })
}
}
Expand All @@ -66,11 +68,11 @@ struct SettingsSubscriptionView: View {
disclosureIndicator: true,
isButton: true)

NavigationLink(destination: Text("Data Broker Protection")) {
NavigationLink(destination: Text("Data Broker Protection"), isActive: $viewModel.shouldNavigateToDBP) {
SettingsCellView(label: UserText.settingsPProDBPTitle, subtitle: UserText.settingsPProDBPSubTitle)
}

NavigationLink(destination: Text("Identity Theft Restoration")) {
NavigationLink(destination: Text("Identity Theft Restoration"), isActive: $viewModel.shouldNavigateToITP) {
SettingsCellView(label: UserText.settingsPProITRTitle, subtitle: UserText.settingsPProITRSubTitle)
}

Expand Down
44 changes: 39 additions & 5 deletions DuckDuckGo/SettingsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,18 @@ final class SettingsViewModel: ObservableObject {
@UserDefaultsWrapper(key: .subscriptionIsActive, defaultValue: false)
static private var cachedHasActiveSubscription: Bool

// Closures to interact with legacy view controllers throught the container
// Closures to interact with legacy view controllers through the container
var onRequestPushLegacyView: ((UIViewController) -> Void)?
var onRequestPresentLegacyView: ((UIViewController, _ modal: Bool) -> Void)?
var onRequestPopLegacyView: (() -> Void)?
var onRequestDismissSettings: (() -> Void)?

// SwiftUI Programatic Navigation Variables
// Add more views as needed here...
@Published var shouldNavigateToDBP = false
@Published var shouldNavigateToITP = false

// Subscription Entitlement names: TBD
static let entitlementNames = ["dummy1", "dummy2", "dummy3"]

// Our View State
Expand All @@ -82,6 +88,12 @@ final class SettingsViewModel: ObservableObject {

var shouldShowNoMicrophonePermissionAlert: Bool = false

// Used to automatically navigate on Appear to a specific section
enum SettingsSection: String {
case none, netP, dbp, itp
}
@Published var onAppearNavigationTarget: SettingsSection

// MARK: Bindings
var themeBinding: Binding<ThemeName> {
Binding<ThemeName>(
Expand Down Expand Up @@ -183,10 +195,14 @@ final class SettingsViewModel: ObservableObject {
}

// MARK: Default Init
init(state: SettingsState? = nil, legacyViewProvider: SettingsLegacyViewProvider, accountManager: AccountManager) {
init(state: SettingsState? = nil,
legacyViewProvider: SettingsLegacyViewProvider,
accountManager: AccountManager,
navigateOnAppearDestination: SettingsSection = .none) {
self.state = SettingsState.defaults
self.legacyViewProvider = legacyViewProvider
self.accountManager = accountManager
self.onAppearNavigationTarget = navigateOnAppearDestination
}
}

Expand Down Expand Up @@ -287,7 +303,6 @@ extension SettingsViewModel {
completion(true)
}
}


#if SUBSCRIPTION
@available(iOS 15.0, *)
Expand Down Expand Up @@ -348,7 +363,6 @@ extension SettingsViewModel {
}
}
#endif

}

// MARK: Subscribers
Expand All @@ -365,7 +379,7 @@ extension SettingsViewModel {
}
.store(in: &cancellables)
#endif

}
}

Expand All @@ -374,6 +388,7 @@ extension SettingsViewModel {

func onAppear() {
initState()
Task { await MainActor.run { navigateOnAppear() } }
}

func setAsDefaultBrowser() {
Expand Down Expand Up @@ -401,6 +416,25 @@ extension SettingsViewModel {
onRequestDismissSettings?()
}

@MainActor
private func navigateOnAppear() {
// We need a short delay to let the SwifttUI view lifecycle complete
// Otherwise the transition can be inconsistent
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
switch self.onAppearNavigationTarget {
case .netP:
self.presentLegacyView(.netP)
case .dbp:
self.shouldNavigateToDBP = true
case .itp:
self.shouldNavigateToITP = true
default:
break
}
self.onAppearNavigationTarget = .none
}
}

}

// MARK: Legacy View Presentation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,16 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec
case idle, purchasing, restoring, polling
}

struct FeatureSelection: Codable {
let feature: String
}

@Published var transactionStatus: TransactionStatus = .idle
@Published var hasActiveSubscription = false
@Published var purchaseError: AppStorePurchaseFlow.Error?
@Published var activateSubscription: Bool = false
@Published var emailActivationComplete: Bool = false
@Published var selectedFeature: FeatureSelection?

var broker: UserScriptMessageBroker?

Expand Down Expand Up @@ -232,14 +237,11 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec
}

func featureSelected(params: Any, original: WKScriptMessage) async throws -> Encodable? {
struct FeatureSelection: Codable {
let feature: String
}

guard let featureSelection: FeatureSelection = DecodableHelper.decode(from: params) else {
assertionFailure("SubscriptionPagesUserScript: expected JSON representation of FeatureSelection")
return nil
}
selectedFeature = featureSelection

return nil
}
Expand Down

This file was deleted.

38 changes: 36 additions & 2 deletions DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,24 +29,38 @@ final class SubscriptionFlowViewModel: ObservableObject {
let userScript: SubscriptionPagesUserScript
let subFeature: SubscriptionPagesUseSubscriptionFeature
let purchaseManager: PurchaseManager

let viewTitle = UserText.settingsPProSection

private var cancellables = Set<AnyCancellable>()

// State variables
var purchaseURL = URL.purchaseSubscription

// Closure passed to navigate to a specific section
// after returning to settings
var onFeatureSelected: ((SettingsViewModel.SettingsSection) -> Void)

enum FeatureName {
static let netP = "vpn"
static let itp = "identity-theft-restoration"
static let dbp = "personal-information-removal"
}

// Published properties
@Published var hasActiveSubscription = false
@Published var transactionStatus: SubscriptionPagesUseSubscriptionFeature.TransactionStatus = .idle
@Published var shouldReloadWebView = false
@Published var activatingSubscription = false
@Published var shouldDismissView = false

init(userScript: SubscriptionPagesUserScript = SubscriptionPagesUserScript(),
subFeature: SubscriptionPagesUseSubscriptionFeature = SubscriptionPagesUseSubscriptionFeature(),
purchaseManager: PurchaseManager = PurchaseManager.shared) {
purchaseManager: PurchaseManager = PurchaseManager.shared,
onFeatureSelected: @escaping ((SettingsViewModel.SettingsSection) -> Void)) {
self.userScript = userScript
self.subFeature = subFeature
self.purchaseManager = purchaseManager
self.onFeatureSelected = onFeatureSelected
}

// Observe transaction status
Expand Down Expand Up @@ -76,6 +90,26 @@ final class SubscriptionFlowViewModel: ObservableObject {
}
}
.store(in: &cancellables)

subFeature.$selectedFeature
.receive(on: DispatchQueue.main)
.sink { [weak self] value in
if value != nil {
self?.shouldDismissView = true
switch value?.feature {
case FeatureName.netP:
self?.onFeatureSelected(.netP)
case FeatureName.itp:
self?.onFeatureSelected(.itp)
case FeatureName.dbp:
self?.onFeatureSelected(.dbp)
default:
return
}
}
}
.store(in: &cancellables)

}

@MainActor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ final class SubscriptionRestoreViewModel: ObservableObject {
let subFeature: SubscriptionPagesUseSubscriptionFeature
let purchaseManager: PurchaseManager
let accountManager: AccountManager
let isAddingDevice: Bool
var isAddingDevice: Bool

enum SubscriptionActivationResult {
case unknown, activated, notFound, error
Expand All @@ -56,6 +56,9 @@ final class SubscriptionRestoreViewModel: ObservableObject {

func initializeView() {
subscriptionEmail = accountManager.email
if accountManager.isUserAuthenticated {
isAddingDevice = true
}
}

@MainActor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ final class SubscriptionSettingsViewModel: ObservableObject {

func removeSubscription() {
AccountManager().signOut()
ActionMessageView.present(message: UserText.subscriptionRemovalConfirmation)
}

func manageSubscription() {
Expand Down
6 changes: 2 additions & 4 deletions DuckDuckGo/Subscription/Views/HeadlessWebView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,9 @@ struct HeadlessWebview: UIViewRepresentable {
configuration.userContentController = makeUserContentController()

let webView = WKWebView(frame: .zero, configuration: configuration)
DefaultUserAgentManager.shared.update(webView: webView, isDesktop: false, url: url)

// We're using the macOS agent as the config for iOS has not been deployed in test env
webView.customUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko)"
// DefaultUserAgentManager.shared.update(webView: webView, isDesktop: false, url: url)

// Just add time if you need to hook the WebView inspector
DispatchQueue.main.asyncAfter(deadline: .now() + 0) {
webView.load(URLRequest(url: url))
}
Expand Down
6 changes: 6 additions & 0 deletions DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import Foundation
@available(iOS 15.0, *)
struct SubscriptionFlowView: View {

@Environment(\.dismiss) var dismiss
@ObservedObject var viewModel: SubscriptionFlowViewModel
@State private var isAlertVisible = false

Expand Down Expand Up @@ -69,6 +70,11 @@ struct SubscriptionFlowView: View {
isAlertVisible = true
}
}
.onChange(of: viewModel.shouldDismissView) { result in
if result {
dismiss()
}
}

.onAppear(perform: {
Task { await viewModel.initializeViewData() }
Expand Down
6 changes: 2 additions & 4 deletions DuckDuckGo/UserText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1058,6 +1058,7 @@ But if you *do* want a peek under the hood, you can find more information about
public static let subscriptionRemoveFromDeviceConfirmText = NSLocalizedString("subscription.remove.from.device.text", value: "You will no longer be able to access your Privacy Pro subscription on this device. This will not cancel your subscription, and it will remain active on your other devices.", comment: "Remove from device confirmation dialog text")
public static let subscriptionRemove = NSLocalizedString("subscription.remove.subscription", value: "Remove Subscription", comment: "Remove subscription button text")
public static let subscriptionRemoveCancel = NSLocalizedString("subscription.remove.subscription.cancel", value: "Cancel", comment: "Remove subscription cancel button text")
public static let subscriptionRemovalConfirmation = NSLocalizedString("subscription.cancel.message", value: "Your subscription has been removed from this device.", comment: "Subscription Removal confirmation message")

// Subscription Restore
public static let subscriptionActivate = NSLocalizedString("subscription.activate", value: "Activate Subscription", comment: "Subscription Activation Window Title")
Expand Down Expand Up @@ -1091,8 +1092,6 @@ But if you *do* want a peek under the hood, you can find more information about
public static let subscriptionManageEmailCancelButton = NSLocalizedString("subscription.activate.manage.email.cancel", value: "Cancel", comment: "Button title for cancelling email deletion")
public static let subscriptionManageEmailOKButton = NSLocalizedString("subscription.activate.manage.email.OK", value: "OK", comment: "Button title for confirming email deletion")



// Subscribe & Restore Flow
public static let subscriptionFoundTitle = NSLocalizedString("subscription.found.title", value: "Subscription Found", comment: "Title for the existing subscription dialog")
public static let subscriptionFoundText = NSLocalizedString("subscription.found.text", value: "We found a subscription associated with this Apple ID.", comment: "Message for the existing subscription dialog")
Expand All @@ -1104,6 +1103,5 @@ But if you *do* want a peek under the hood, you can find more information about
public static let subscriptionRestoreSuccessfulTitle = NSLocalizedString("subscription.restore.success.alert.title", value: "You’re all set.", comment: "Alert title for restored purchase")
public static let subscriptionRestoreSuccessfulMessage = NSLocalizedString("subscription.restore.success.alert.message", value: "Your purchases have been restored.", comment: "Alert message for restored purchase")
public static let subscriptionRestoreSuccessfulButton = NSLocalizedString("subscription.restore.success.alert.button", value: "OK", comment: "Alert button text for restored purchase alert")



}
Loading

0 comments on commit 4a3c10e

Please sign in to comment.