diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 91f0ebe1a5..69bb109f32 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -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 */; }; @@ -2458,7 +2457,6 @@ D6D12C9C2B291CA90054390C /* APIService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = ""; }; D6D12C9D2B291CA90054390C /* AuthService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthService.swift; sourceTree = ""; }; D6D12C9E2B291CA90054390C /* PurchaseManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PurchaseManager.swift; sourceTree = ""; }; - D6D4B77B2B5AE99500996546 /* SubscriptionFlowNavController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionFlowNavController.swift; sourceTree = ""; }; D6E83C112B1E6AB3006C8AFB /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; D6E83C2D2B1EA06E006C8AFB /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; D6E83C302B1EA309006C8AFB /* SettingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsCell.swift; sourceTree = ""; }; @@ -4562,7 +4560,6 @@ D664C7932B289AA000CBFA76 /* ViewModel */ = { isa = PBXGroup; children = ( - D6D4B77B2B5AE99500996546 /* SubscriptionFlowNavController.swift */, D664C7942B289AA000CBFA76 /* SubscriptionFlowViewModel.swift */, D68DF81D2B5830380023DBEA /* SubscriptionRestoreViewModel.swift */, D64648AE2B5993890033090B /* SubscriptionEmailViewModel.swift */, @@ -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 */, diff --git a/DuckDuckGo/SettingsState.swift b/DuckDuckGo/SettingsState.swift index 9d9ff181a5..e6568efc7d 100644 --- a/DuckDuckGo/SettingsState.swift +++ b/DuckDuckGo/SettingsState.swift @@ -87,7 +87,7 @@ struct SettingsState { // Subscriptions Properties var subscription: Subscription - // Sync Propertiers + // Sync Properties var sync: SyncSettings static var defaults: SettingsState { diff --git a/DuckDuckGo/SettingsSubscriptionView.swift b/DuckDuckGo/SettingsSubscriptionView.swift index 53069a89c2..11e681147d 100644 --- a/DuckDuckGo/SettingsSubscriptionView.swift +++ b/DuckDuckGo/SettingsSubscriptionView.swift @@ -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 }) } } @@ -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) } diff --git a/DuckDuckGo/SettingsViewModel.swift b/DuckDuckGo/SettingsViewModel.swift index bff53aae81..fd452ddeca 100644 --- a/DuckDuckGo/SettingsViewModel.swift +++ b/DuckDuckGo/SettingsViewModel.swift @@ -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 @@ -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 { Binding( @@ -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 } } @@ -287,7 +303,6 @@ extension SettingsViewModel { completion(true) } } - #if SUBSCRIPTION @available(iOS 15.0, *) @@ -348,7 +363,6 @@ extension SettingsViewModel { } } #endif - } // MARK: Subscribers @@ -365,7 +379,7 @@ extension SettingsViewModel { } .store(in: &cancellables) #endif - + } } @@ -374,6 +388,7 @@ extension SettingsViewModel { func onAppear() { initState() + Task { await MainActor.run { navigateOnAppear() } } } func setAsDefaultBrowser() { @@ -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 diff --git a/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift b/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift index a481a14598..3b85bd54b5 100644 --- a/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift +++ b/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift @@ -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? @@ -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 } diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowNavController.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowNavController.swift deleted file mode 100644 index ddd207de0d..0000000000 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowNavController.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// SubscriptionFlowNavController.swift -// DuckDuckGo -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -class SubscriptionFlowNavController: ObservableObject { - @Published var shouldDisplayRestoreView: Bool = false { - didSet { - print("shouldDisplayRestoreView changed to: \(shouldDisplayRestoreView)") - } - } -} diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift index d0638d4862..89cd53837b 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift @@ -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() // 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 @@ -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 diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift index 85998e1bd5..458be8fad4 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift @@ -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 @@ -56,6 +56,9 @@ final class SubscriptionRestoreViewModel: ObservableObject { func initializeView() { subscriptionEmail = accountManager.email + if accountManager.isUserAuthenticated { + isAddingDevice = true + } } @MainActor diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionSettingsViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionSettingsViewModel.swift index d7f5d98351..4cf960c44f 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionSettingsViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionSettingsViewModel.swift @@ -63,6 +63,7 @@ final class SubscriptionSettingsViewModel: ObservableObject { func removeSubscription() { AccountManager().signOut() + ActionMessageView.present(message: UserText.subscriptionRemovalConfirmation) } func manageSubscription() { diff --git a/DuckDuckGo/Subscription/Views/HeadlessWebView.swift b/DuckDuckGo/Subscription/Views/HeadlessWebView.swift index e80e3f5bdc..116e34f771 100644 --- a/DuckDuckGo/Subscription/Views/HeadlessWebView.swift +++ b/DuckDuckGo/Subscription/Views/HeadlessWebView.swift @@ -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)) } diff --git a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift index 6538daedea..e9b06406ac 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift @@ -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 @@ -69,6 +70,11 @@ struct SubscriptionFlowView: View { isAlertVisible = true } } + .onChange(of: viewModel.shouldDismissView) { result in + if result { + dismiss() + } + } .onAppear(perform: { Task { await viewModel.initializeViewData() } diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index cd5f49ad45..531900ac59 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -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") @@ -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") @@ -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") - - + } diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index f50749e44b..815afd61b6 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -1986,6 +1986,9 @@ But if you *do* want a peek under the hood, you can find more information about /* Subscription availability message on Apple devices */ "subscription.available.apple" = "Privacy Pro is available on any device signed in to the same Apple ID."; +/* Subscription Removal confirmation message */ +"subscription.cancel.message" = "Your subscription has been removed from this device."; + /* Change plan or billing title */ "subscription.change.plan" = "Change Plan Or Billing";