From 653bb53b3a92ee0588e9b7a575f9b7553951a860 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Tue, 30 Jan 2024 18:42:14 +0100 Subject: [PATCH 01/17] Barebones integration --- DuckDuckGo.xcodeproj/project.pbxproj | 20 +++++ DuckDuckGo/SettingsSubscriptionView.swift | 2 +- .../Subscription/URL+Subscription.swift | 5 ++ ...ntityTheftRestorationPagesUserScript.swift | 70 +++++++++++++++++ .../UserScripts/Subscription.swift | 24 ++++++ ...scriptionPagesUseSubscriptionFeature.swift | 5 +- ...identityTheftRestorationPagesFeature.swift | 75 +++++++++++++++++++ .../ViewModel/SubscriptionITPViewModel.swift | 53 +++++++++++++ .../Views/SubscriptionITPView.swift | 40 ++++++++++ 9 files changed, 289 insertions(+), 5 deletions(-) create mode 100644 DuckDuckGo/Subscription/UserScripts/IdentityTheftRestorationPagesUserScript.swift create mode 100644 DuckDuckGo/Subscription/UserScripts/Subscription.swift create mode 100644 DuckDuckGo/Subscription/UserScripts/identityTheftRestorationPagesFeature.swift create mode 100644 DuckDuckGo/Subscription/ViewModel/SubscriptionITPViewModel.swift create mode 100644 DuckDuckGo/Subscription/Views/SubscriptionITPView.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 415c2f063b..875737776b 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -788,6 +788,11 @@ D664C7CC2B289AA200CBFA76 /* SubscriptionPagesUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664C7B32B289AA000CBFA76 /* SubscriptionPagesUserScript.swift */; }; D664C7CE2B289AA200CBFA76 /* SubscriptionPagesUseSubscriptionFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664C7B52B289AA000CBFA76 /* SubscriptionPagesUseSubscriptionFeature.swift */; }; D664C7DD2B28A02800CBFA76 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D664C7DC2B28A02800CBFA76 /* StoreKit.framework */; }; + D668D9252B693778008E2FF2 /* SubscriptionITPView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D668D9242B693778008E2FF2 /* SubscriptionITPView.swift */; }; + D668D9272B6937D2008E2FF2 /* SubscriptionITPViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D668D9262B6937D2008E2FF2 /* SubscriptionITPViewModel.swift */; }; + D668D9292B69681C008E2FF2 /* IdentityTheftRestorationPagesUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = D668D9282B69681C008E2FF2 /* IdentityTheftRestorationPagesUserScript.swift */; }; + D668D92B2B696840008E2FF2 /* identityTheftRestorationPagesFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = D668D92A2B696840008E2FF2 /* identityTheftRestorationPagesFeature.swift */; }; + D668D92D2B696945008E2FF2 /* Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = D668D92C2B696945008E2FF2 /* Subscription.swift */; }; D68DF81C2B58302E0023DBEA /* SubscriptionRestoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68DF81B2B58302E0023DBEA /* SubscriptionRestoreView.swift */; }; D68DF81E2B5830380023DBEA /* SubscriptionRestoreViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68DF81D2B5830380023DBEA /* SubscriptionRestoreViewModel.swift */; }; D69FBF762B28BE3600B505F1 /* SettingsSubscriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69FBF752B28BE3600B505F1 /* SettingsSubscriptionView.swift */; }; @@ -2441,6 +2446,11 @@ D664C7B32B289AA000CBFA76 /* SubscriptionPagesUserScript.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionPagesUserScript.swift; sourceTree = ""; }; D664C7B52B289AA000CBFA76 /* SubscriptionPagesUseSubscriptionFeature.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionPagesUseSubscriptionFeature.swift; sourceTree = ""; }; D664C7DC2B28A02800CBFA76 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; + D668D9242B693778008E2FF2 /* SubscriptionITPView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionITPView.swift; sourceTree = ""; }; + D668D9262B6937D2008E2FF2 /* SubscriptionITPViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionITPViewModel.swift; sourceTree = ""; }; + D668D9282B69681C008E2FF2 /* IdentityTheftRestorationPagesUserScript.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentityTheftRestorationPagesUserScript.swift; sourceTree = ""; }; + D668D92A2B696840008E2FF2 /* identityTheftRestorationPagesFeature.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = identityTheftRestorationPagesFeature.swift; sourceTree = ""; }; + D668D92C2B696945008E2FF2 /* Subscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Subscription.swift; sourceTree = ""; }; D68DF81B2B58302E0023DBEA /* SubscriptionRestoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionRestoreView.swift; sourceTree = ""; }; D68DF81D2B5830380023DBEA /* SubscriptionRestoreViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionRestoreViewModel.swift; sourceTree = ""; }; D69FBF752B28BE3600B505F1 /* SettingsSubscriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSubscriptionView.swift; sourceTree = ""; }; @@ -4563,6 +4573,7 @@ isa = PBXGroup; children = ( D664C7942B289AA000CBFA76 /* SubscriptionFlowViewModel.swift */, + D668D9262B6937D2008E2FF2 /* SubscriptionITPViewModel.swift */, D68DF81D2B5830380023DBEA /* SubscriptionRestoreViewModel.swift */, D64648AE2B5993890033090B /* SubscriptionEmailViewModel.swift */, D652498D2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift */, @@ -4584,6 +4595,7 @@ D664C7AF2B289AA000CBFA76 /* HeadlessWebView.swift */, D664C7AD2B289AA000CBFA76 /* PurchaseInProgressView.swift */, D664C7AE2B289AA000CBFA76 /* SubscriptionFlowView.swift */, + D668D9242B693778008E2FF2 /* SubscriptionITPView.swift */, D6F93E3D2B50A8A0004C268D /* SubscriptionSettingsView.swift */, D68DF81B2B58302E0023DBEA /* SubscriptionRestoreView.swift */, D64648AC2B59936B0033090B /* SubscriptionEmailView.swift */, @@ -4594,7 +4606,10 @@ D664C7B02B289AA000CBFA76 /* UserScripts */ = { isa = PBXGroup; children = ( + D668D92C2B696945008E2FF2 /* Subscription.swift */, + D668D9282B69681C008E2FF2 /* IdentityTheftRestorationPagesUserScript.swift */, D664C7B32B289AA000CBFA76 /* SubscriptionPagesUserScript.swift */, + D668D92A2B696840008E2FF2 /* identityTheftRestorationPagesFeature.swift */, D664C7B52B289AA000CBFA76 /* SubscriptionPagesUseSubscriptionFeature.swift */, ); path = UserScripts; @@ -6535,6 +6550,7 @@ 8590CB69268A4E190089F6BF /* DebugEtagStorage.swift in Sources */, D6D12CA62B291CAA0054390C /* AppStoreRestoreFlow.swift in Sources */, C1CDA3162AFB9C7F006D1476 /* AutofillNeverPromptWebsitesManager.swift in Sources */, + D668D9272B6937D2008E2FF2 /* SubscriptionITPViewModel.swift in Sources */, F1CA3C371F045878005FADB3 /* PrivacyStore.swift in Sources */, 37FCAAC029930E26000E420A /* FailedAssertionView.swift in Sources */, 4BBBBA922B03291700D965DA /* VPNWaitlistUserText.swift in Sources */, @@ -6632,6 +6648,7 @@ CB258D1329A4F24E00DEBA24 /* ConfigurationStore.swift in Sources */, 85058370219F424500ED4EDB /* SearchBarExtension.swift in Sources */, 310D09212799FD1A00DC0060 /* MIMEType.swift in Sources */, + D668D9292B69681C008E2FF2 /* IdentityTheftRestorationPagesUserScript.swift in Sources */, D64648AD2B59936B0033090B /* SubscriptionEmailView.swift in Sources */, BD862E032B30DA170073E2EE /* VPNFeedbackFormViewModel.swift in Sources */, F4147354283BF834004AA7A5 /* AutofillContentScopeFeatureToggles.swift in Sources */, @@ -6639,6 +6656,7 @@ 31B524572715BB23002225AB /* WebJSAlert.swift in Sources */, 8536A1FD2ACF114B003AC5BA /* Theme+DesignSystem.swift in Sources */, F114C55B1E66EB020018F95F /* NibLoading.swift in Sources */, + D668D9252B693778008E2FF2 /* SubscriptionITPView.swift in Sources */, C10CB5F32A1A5BDF0048E503 /* AutofillViews.swift in Sources */, 982E5630222C3D5B008D861B /* FeedbackPickerViewController.swift in Sources */, 37FCAABC2992F592000E420A /* MultilineScrollableTextFix.swift in Sources */, @@ -6679,6 +6697,7 @@ F4F6DFB426E6B63700ED7E12 /* BookmarkFolderCell.swift in Sources */, D6F93E3E2B50A8A0004C268D /* SubscriptionSettingsView.swift in Sources */, 851B12CC22369931004781BC /* AtbAndVariantCleanup.swift in Sources */, + D668D92B2B696840008E2FF2 /* identityTheftRestorationPagesFeature.swift in Sources */, 85F2FFCF2211F8E5006BB258 /* TabSwitcherViewController+KeyCommands.swift in Sources */, 3157B43327F497E90042D3D7 /* SaveLoginView.swift in Sources */, F17922E01E71BB59006E3D97 /* AutocompleteViewControllerDelegate.swift in Sources */, @@ -6916,6 +6935,7 @@ EE72CA852A862D000043B5B3 /* NetworkProtectionDebugViewController.swift in Sources */, C18ED43A2AB6F77600BF3805 /* AutofillSettingsEnableFooterView.swift in Sources */, CB84C7BD29A3EF530088A5B8 /* AppConfigurationURLProvider.swift in Sources */, + D668D92D2B696945008E2FF2 /* Subscription.swift in Sources */, AA3D854723D9E88E00788410 /* AppIconSettingsCell.swift in Sources */, 316931D927BD22A80095F5ED /* DownloadActionMessageViewHelper.swift in Sources */, 9838059F2228208E00385F1A /* PositiveFeedbackViewController.swift in Sources */, diff --git a/DuckDuckGo/SettingsSubscriptionView.swift b/DuckDuckGo/SettingsSubscriptionView.swift index 11e681147d..52fe5916dc 100644 --- a/DuckDuckGo/SettingsSubscriptionView.swift +++ b/DuckDuckGo/SettingsSubscriptionView.swift @@ -72,7 +72,7 @@ struct SettingsSubscriptionView: View { SettingsCellView(label: UserText.settingsPProDBPTitle, subtitle: UserText.settingsPProDBPSubTitle) } - NavigationLink(destination: Text("Identity Theft Restoration"), isActive: $viewModel.shouldNavigateToITP) { + NavigationLink(destination: SubscriptionITPView(viewModel: SubscriptionITPViewModel()), isActive: $viewModel.shouldNavigateToITP) { SettingsCellView(label: UserText.settingsPProITRTitle, subtitle: UserText.settingsPProITRSubTitle) } diff --git a/DuckDuckGo/Subscription/Subscription/URL+Subscription.swift b/DuckDuckGo/Subscription/Subscription/URL+Subscription.swift index 2451ab5055..d8f35b7312 100644 --- a/DuckDuckGo/Subscription/Subscription/URL+Subscription.swift +++ b/DuckDuckGo/Subscription/Subscription/URL+Subscription.swift @@ -51,4 +51,9 @@ public extension URL { static var manageSubscriptionsIniOSAppStoreAppURL: URL { URL(string: "https://apps.apple.com/account/subscriptions")! } + + // MARK: - Identity Theft Protection + static var manageITP: URL { + URL(string: "https://abrown.duckduckgo.com/identity-theft-restoration")! + } } diff --git a/DuckDuckGo/Subscription/UserScripts/IdentityTheftRestorationPagesUserScript.swift b/DuckDuckGo/Subscription/UserScripts/IdentityTheftRestorationPagesUserScript.swift new file mode 100644 index 0000000000..cfbafec477 --- /dev/null +++ b/DuckDuckGo/Subscription/UserScripts/IdentityTheftRestorationPagesUserScript.swift @@ -0,0 +1,70 @@ +// +// IdentityTheftRestorationPagesUserScript.swift +// DuckDuckGo +// +// Copyright © 2023 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. +// + +#if SUBSCRIPTION + +import BrowserServicesKit +import Common +import Combine +import Foundation +import WebKit +import UserScript + +/// +/// The user script that will be the broker for all identity theft protection features +/// +public final class IdentityTheftRestorationPagesUserScript: NSObject, UserScript, UserScriptMessaging { + public var source: String = "" + + public static let context = "identityTheftRestorationPages" + + // special pages messaging cannot be isolated as we'll want regular page-scripts to be able to communicate + public let broker = UserScriptMessageBroker(context: IdentityTheftRestorationPagesUserScript.context, requiresRunInPageContentWorld: false ) + + public let messageNames: [String] = [ + IdentityTheftRestorationPagesUserScript.context + ] + + public let injectionTime: WKUserScriptInjectionTime = .atDocumentStart + public let forMainFrameOnly = true + public let requiresRunInPageContentWorld = true +} + +extension IdentityTheftRestorationPagesUserScript: WKScriptMessageHandlerWithReply { + @MainActor + public func userContentController(_ userContentController: WKUserContentController, + didReceive message: WKScriptMessage) async -> (Any?, String?) { + let action = broker.messageHandlerFor(message) + do { + let json = try await broker.execute(action: action, original: message) + return (json, nil) + } catch { + // forward uncaught errors to the client + return (nil, error.localizedDescription) + } + } +} + +extension IdentityTheftRestorationPagesUserScript: WKScriptMessageHandler { + public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + // unsupported + } +} + +#endif diff --git a/DuckDuckGo/Subscription/UserScripts/Subscription.swift b/DuckDuckGo/Subscription/UserScripts/Subscription.swift new file mode 100644 index 0000000000..9306a531c5 --- /dev/null +++ b/DuckDuckGo/Subscription/UserScripts/Subscription.swift @@ -0,0 +1,24 @@ +// +// Subscription.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 + +struct Subscription: Encodable { + let token: String +} diff --git a/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift b/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift index 3b85bd54b5..780b317fbe 100644 --- a/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift +++ b/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift @@ -103,10 +103,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec } } - struct Subscription: Encodable { - let token: String - } - + /// Values that the Frontend can use to determine the current state. // swiftlint:disable nesting struct SubscriptionValues: Codable { diff --git a/DuckDuckGo/Subscription/UserScripts/identityTheftRestorationPagesFeature.swift b/DuckDuckGo/Subscription/UserScripts/identityTheftRestorationPagesFeature.swift new file mode 100644 index 0000000000..d7c2f8ecf1 --- /dev/null +++ b/DuckDuckGo/Subscription/UserScripts/identityTheftRestorationPagesFeature.swift @@ -0,0 +1,75 @@ +// +// identityTheftRestorationPagesFeature.swift +// DuckDuckGo +// +// Copyright © 2023 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. +// + +#if SUBSCRIPTION +import BrowserServicesKit +import Common +import Foundation +import WebKit +import UserScript +import Combine + +@available(iOS 15.0, *) +final class IdentityTheftRestorationPagesFeature: Subfeature, ObservableObject { + + struct Constants { + static let featureName = "useIdentityTheftRestoration" + static let os = "ios" + static let empty = "" + } + + struct OriginDomains { + static let duckduckgo = "duckduckgo.com" + static let abrown = "abrown.duckduckgo.com" + } + + struct Handlers { + static let getAccessToken = "getAccessToken" + } + + + var broker: UserScriptMessageBroker? + var featureName: String = Constants.featureName + + var messageOriginPolicy: MessageOriginPolicy = .only(rules: [ + .exact(hostname: OriginDomains.duckduckgo), + .exact(hostname: OriginDomains.abrown) + ]) + + var originalMessage: WKScriptMessage? + + func with(broker: UserScriptMessageBroker) { + self.broker = broker + } + + func handler(forMethodNamed methodName: String) -> Subfeature.Handler? { + switch methodName { + case Handlers.getAccessToken: return getAccessToken + default: + return nil + } + } + + func getAccessToken(params: Any, original: WKScriptMessage) async throws -> Encodable? { + let authToken = AccountManager().authToken ?? Constants.empty + return Subscription(token: authToken) + } + +} +#endif diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionITPViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionITPViewModel.swift new file mode 100644 index 0000000000..63d3a95c41 --- /dev/null +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionITPViewModel.swift @@ -0,0 +1,53 @@ +// +// SubscriptionITPViewModel.swift +// DuckDuckGo +// +// Copyright © 2023 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 +import UserScript +import Combine +import Core + +#if SUBSCRIPTION +@available(iOS 15.0, *) +final class SubscriptionITPViewModel: ObservableObject { + + let userScript: IdentityTheftRestorationPagesUserScript + let subFeature: IdentityTheftRestorationPagesFeature + let purchaseManager: PurchaseManager + let viewTitle = UserText.settingsPProSection + private var cancellables = Set() + + // State variables + var itpURL = URL.manageITP + @Published var shouldReloadWebView = false + + init(userScript: IdentityTheftRestorationPagesUserScript = IdentityTheftRestorationPagesUserScript(), + subFeature: IdentityTheftRestorationPagesFeature = IdentityTheftRestorationPagesFeature(), + purchaseManager: PurchaseManager = PurchaseManager.shared) { + self.userScript = userScript + self.subFeature = subFeature + self.purchaseManager = purchaseManager + } + + // Observe transaction status + private func setupTransactionObserver() async { + + } + +} +#endif diff --git a/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift b/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift new file mode 100644 index 0000000000..a0f46a7961 --- /dev/null +++ b/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift @@ -0,0 +1,40 @@ +// +// SubscriptionFlowView.swift +// DuckDuckGo +// +// Copyright © 2023 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. +// + +#if SUBSCRIPTION +import SwiftUI +import Foundation + +@available(iOS 15.0, *) +struct SubscriptionITPView: View { + + @Environment(\.dismiss) var dismiss + @ObservedObject var viewModel: SubscriptionITPViewModel + + var body: some View { + ZStack { + AsyncHeadlessWebView(url: $viewModel.itpURL, + userScript: viewModel.userScript, + subFeature: viewModel.subFeature, + shouldReload: $viewModel.shouldReloadWebView).background() + } + .navigationTitle(viewModel.viewTitle) + } +} +#endif From 6c9e0f7a42c7f127630ee521d67cd54e11f1b7aa Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Wed, 31 Jan 2024 20:17:01 +0100 Subject: [PATCH 02/17] Full screen modal for subscription --- DuckDuckGo.xcodeproj/project.pbxproj | 12 ++ DuckDuckGo/SettingsSubscriptionView.swift | 10 +- .../Extensions/View+NavbarAppereance.swift | 27 ++++ .../Subscription.xcassets/Contents.json | 6 + .../Contents.json | 15 ++ .../Sync-128.svg | 21 +++ .../Subscription/Views/HeadlessWebView.swift | 63 +++++++- .../Views/NavigationViewStyle.swift | 40 +++++ .../Views/SubscriptionEmailView.swift | 5 +- .../Views/SubscriptionFlowView.swift | 142 +++++++++++++++--- .../Views/SubscriptionITPView.swift | 2 +- .../Views/SubscriptionRestoreView.swift | 2 +- DuckDuckGo/UserText.swift | 3 + DuckDuckGo/en.lproj/Localizable.strings | 6 + 14 files changed, 319 insertions(+), 35 deletions(-) create mode 100644 DuckDuckGo/Subscription/Extensions/View+NavbarAppereance.swift create mode 100644 DuckDuckGo/Subscription/Subscription.xcassets/Contents.json create mode 100644 DuckDuckGo/Subscription/Subscription.xcassets/ManageSubscriptionHero.imageset/Contents.json create mode 100644 DuckDuckGo/Subscription/Subscription.xcassets/ManageSubscriptionHero.imageset/Sync-128.svg create mode 100644 DuckDuckGo/Subscription/Views/NavigationViewStyle.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 875737776b..4115e132af 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -779,6 +779,9 @@ D64648AD2B59936B0033090B /* SubscriptionEmailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64648AC2B59936B0033090B /* SubscriptionEmailView.swift */; }; D64648AF2B5993890033090B /* SubscriptionEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64648AE2B5993890033090B /* SubscriptionEmailViewModel.swift */; }; D652498E2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D652498D2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift */; }; + D65CEA6C2B6A871B008A759B /* View+NavbarAppereance.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65CEA6B2B6A871B008A759B /* View+NavbarAppereance.swift */; }; + D65CEA6E2B6ABA29008A759B /* NavigationViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65CEA6D2B6ABA29008A759B /* NavigationViewStyle.swift */; }; + D65CEA702B6AC6C9008A759B /* Subscription.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D65CEA6F2B6AC6C9008A759B /* Subscription.xcassets */; }; D664C7B62B289AA200CBFA76 /* SubscriptionFlowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664C7942B289AA000CBFA76 /* SubscriptionFlowViewModel.swift */; }; D664C7B72B289AA200CBFA76 /* Subscription.storekit in Resources */ = {isa = PBXBuildFile; fileRef = D664C7952B289AA000CBFA76 /* Subscription.storekit */; }; D664C7B92B289AA200CBFA76 /* WKUserContentController+Handler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664C7982B289AA000CBFA76 /* WKUserContentController+Handler.swift */; }; @@ -2437,6 +2440,9 @@ D64648AC2B59936B0033090B /* SubscriptionEmailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionEmailView.swift; sourceTree = ""; }; D64648AE2B5993890033090B /* SubscriptionEmailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionEmailViewModel.swift; sourceTree = ""; }; D652498D2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionSettingsViewModel.swift; sourceTree = ""; }; + D65CEA6B2B6A871B008A759B /* View+NavbarAppereance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+NavbarAppereance.swift"; sourceTree = ""; }; + D65CEA6D2B6ABA29008A759B /* NavigationViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewStyle.swift; sourceTree = ""; }; + D65CEA6F2B6AC6C9008A759B /* Subscription.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Subscription.xcassets; sourceTree = ""; }; D664C7942B289AA000CBFA76 /* SubscriptionFlowViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionFlowViewModel.swift; sourceTree = ""; }; D664C7952B289AA000CBFA76 /* Subscription.storekit */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Subscription.storekit; sourceTree = ""; }; D664C7982B289AA000CBFA76 /* WKUserContentController+Handler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "WKUserContentController+Handler.swift"; sourceTree = ""; }; @@ -4565,6 +4571,7 @@ D664C7B02B289AA000CBFA76 /* UserScripts */, D664C7962B289AA000CBFA76 /* Extensions */, D6D12C8A2B291CA90054390C /* Subscription */, + D65CEA6F2B6AC6C9008A759B /* Subscription.xcassets */, ); path = Subscription; sourceTree = ""; @@ -4585,6 +4592,7 @@ isa = PBXGroup; children = ( D664C7982B289AA000CBFA76 /* WKUserContentController+Handler.swift */, + D65CEA6B2B6A871B008A759B /* View+NavbarAppereance.swift */, ); path = Extensions; sourceTree = ""; @@ -4599,6 +4607,7 @@ D6F93E3D2B50A8A0004C268D /* SubscriptionSettingsView.swift */, D68DF81B2B58302E0023DBEA /* SubscriptionRestoreView.swift */, D64648AC2B59936B0033090B /* SubscriptionEmailView.swift */, + D65CEA6D2B6ABA29008A759B /* NavigationViewStyle.swift */, ); path = Views; sourceTree = ""; @@ -6138,6 +6147,7 @@ AA4D6A9423DE49A5007E8790 /* AppIconBlack29x29@2x.png in Resources */, 98B001B3251EABB40090EC07 /* InfoPlist.strings in Resources */, AA4D6ACE23DE4D27007E8790 /* AppIconPurple60x60@3x.png in Resources */, + D65CEA702B6AC6C9008A759B /* Subscription.xcassets in Resources */, F1E4A4451EE89460006F2EAE /* Bookmarks.storyboard in Resources */, AA4D6ABB23DE4D15007E8790 /* AppIconYellow40x40@2x.png in Resources */, 84E341A01E2F7EFB00BDBA6F /* LaunchScreen.storyboard in Resources */, @@ -6853,6 +6863,7 @@ 8563A03C1F9288D600F04442 /* BrowserChromeManager.swift in Sources */, 980891A32237146B00313A70 /* Feedback.swift in Sources */, F1D796F01E7B07610019D451 /* BookmarksViewControllerCells.swift in Sources */, + D65CEA6E2B6ABA29008A759B /* NavigationViewStyle.swift in Sources */, 85058369219F424500ED4EDB /* UIColorExtension.swift in Sources */, D6E83C312B1EA309006C8AFB /* SettingsCell.swift in Sources */, 85058368219C49E000ED4EDB /* HomeViewSectionRenderers.swift in Sources */, @@ -6880,6 +6891,7 @@ 8540BBA22440857A00017FE4 /* PreserveLoginsWorker.swift in Sources */, 85DFEDF924CF3D0E00973FE7 /* TabsBarCell.swift in Sources */, F17922DB1E717C8D006E3D97 /* Suggestion.swift in Sources */, + D65CEA6C2B6A871B008A759B /* View+NavbarAppereance.swift in Sources */, 020108A729A6ABF600644F9D /* AppTPToggleView.swift in Sources */, 02A54A982A093126000C8FED /* AppTPHomeViewModel.swift in Sources */, F1617C191E573EA800DEDCAF /* TabSwitcherDelegate.swift in Sources */, diff --git a/DuckDuckGo/SettingsSubscriptionView.swift b/DuckDuckGo/SettingsSubscriptionView.swift index 52fe5916dc..ff58a2afc9 100644 --- a/DuckDuckGo/SettingsSubscriptionView.swift +++ b/DuckDuckGo/SettingsSubscriptionView.swift @@ -25,6 +25,9 @@ import UIKit struct SettingsSubscriptionView: View { @EnvironmentObject var viewModel: SettingsViewModel + @State var isShowingsubScriptionFlow = false + @State var isShowingDBP = false + @State var isShowingITP = false private var subscriptionDescriptionView: some View { VStack(alignment: .leading) { @@ -54,8 +57,11 @@ struct SettingsSubscriptionView: View { let viewModel = SubscriptionFlowViewModel(onFeatureSelected: { value in self.viewModel.onAppearNavigationTarget = value }) - NavigationLink(destination: SubscriptionFlowView(viewModel: viewModel)) { - SettingsCustomCell(content: { learnMoreView }) + SettingsCustomCell(content: { learnMoreView }, + action: { isShowingsubScriptionFlow = true }, + isButton: true ) + .sheet(isPresented: $isShowingsubScriptionFlow) { + SubscriptionFlowView(viewModel: viewModel) } } } diff --git a/DuckDuckGo/Subscription/Extensions/View+NavbarAppereance.swift b/DuckDuckGo/Subscription/Extensions/View+NavbarAppereance.swift new file mode 100644 index 0000000000..2d3f2cd2e9 --- /dev/null +++ b/DuckDuckGo/Subscription/Extensions/View+NavbarAppereance.swift @@ -0,0 +1,27 @@ +// +// View+NavbarAppereance.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 +import SwiftUI +import UIKit + +extension View { + + +} diff --git a/DuckDuckGo/Subscription/Subscription.xcassets/Contents.json b/DuckDuckGo/Subscription/Subscription.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/DuckDuckGo/Subscription/Subscription.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Subscription/Subscription.xcassets/ManageSubscriptionHero.imageset/Contents.json b/DuckDuckGo/Subscription/Subscription.xcassets/ManageSubscriptionHero.imageset/Contents.json new file mode 100644 index 0000000000..49070f6b76 --- /dev/null +++ b/DuckDuckGo/Subscription/Subscription.xcassets/ManageSubscriptionHero.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Sync-128.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/DuckDuckGo/Subscription/Subscription.xcassets/ManageSubscriptionHero.imageset/Sync-128.svg b/DuckDuckGo/Subscription/Subscription.xcassets/ManageSubscriptionHero.imageset/Sync-128.svg new file mode 100644 index 0000000000..c00e6ee0ed --- /dev/null +++ b/DuckDuckGo/Subscription/Subscription.xcassets/ManageSubscriptionHero.imageset/Sync-128.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/DuckDuckGo/Subscription/Views/HeadlessWebView.swift b/DuckDuckGo/Subscription/Views/HeadlessWebView.swift index 116e34f771..d91af222ad 100644 --- a/DuckDuckGo/Subscription/Views/HeadlessWebView.swift +++ b/DuckDuckGo/Subscription/Views/HeadlessWebView.swift @@ -24,11 +24,14 @@ import SwiftUI import DesignResourcesKit import Core -struct HeadlessWebview: UIViewRepresentable { +struct HeadlessWebView: UIViewRepresentable { let userScript: UserScriptMessaging let subFeature: Subfeature @Binding var url: URL @Binding var shouldReload: Bool + let ignoreTopSafeAreaInsets: Bool + let onScroll: ((CGPoint) -> Void)? + let bounces: Bool func makeUIView(context: Context) -> WKWebView { let configuration = WKWebViewConfiguration() @@ -43,6 +46,8 @@ struct HeadlessWebview: UIViewRepresentable { } webView.uiDelegate = context.coordinator + webView.scrollView.delegate = context.coordinator + webView.scrollView.bounces = bounces #if DEBUG @@ -54,6 +59,13 @@ struct HeadlessWebview: UIViewRepresentable { } func updateUIView(_ uiView: WKWebView, context: Context) { + // Adjust content insets + if ignoreTopSafeAreaInsets { + let insets = UIEdgeInsets(top: -uiView.safeAreaInsets.top, left: 0, bottom: 0, right: 0) + uiView.scrollView.contentInset = insets + uiView.scrollView.scrollIndicatorInsets = insets + } + if shouldReload { reloadView(uiView: uiView) } @@ -68,7 +80,7 @@ struct HeadlessWebview: UIViewRepresentable { } func makeCoordinator() -> Coordinator { - Coordinator() + Coordinator(self) } @MainActor @@ -80,9 +92,14 @@ struct HeadlessWebview: UIViewRepresentable { return userContentController } - class Coordinator: NSObject, WKUIDelegate { + class Coordinator: NSObject, WKUIDelegate, UIScrollViewDelegate { + var parent: HeadlessWebView var webView: WKWebView? + init(_ parent: HeadlessWebView) { + self.parent = parent + } + private func topMostViewController() -> UIViewController? { var topController: UIViewController? = UIApplication.shared.windows.filter { $0.isKeyWindow } .first? @@ -109,6 +126,14 @@ struct HeadlessWebview: UIViewRepresentable { completionHandler(false) } } + + // MARK: UIScrollViewDelegate + + // Detect scroll events and call onScroll function with the current scroll position + func scrollViewDidScroll(_ scrollView: UIScrollView) { + guard let onScroll = parent.onScroll else { return } + onScroll(scrollView.contentOffset) + } } } @@ -117,15 +142,37 @@ struct AsyncHeadlessWebView: View { let userScript: UserScriptMessaging let subFeature: Subfeature @Binding var shouldReload: Bool - + var ignoreTopSafeAreaInsets: Bool + let onScroll: ((CGPoint) -> Void)? + let bounces: Bool + + init(url: Binding, + userScript: UserScriptMessaging, + subFeature: Subfeature, + shouldReload: Binding, + ignoreTopSafeAreaInsets: Bool = false, + onScroll: ((CGPoint) -> Void)? = nil, + bounces: Bool = false) { + self._url = url + self.userScript = userScript + self.subFeature = subFeature + self._shouldReload = shouldReload + self.ignoreTopSafeAreaInsets = ignoreTopSafeAreaInsets + self.onScroll = onScroll + self.bounces = bounces + } + var body: some View { GeometryReader { geometry in - HeadlessWebview(userScript: userScript, + HeadlessWebView(userScript: userScript, subFeature: subFeature, url: $url, - shouldReload: $shouldReload) + shouldReload: $shouldReload, + ignoreTopSafeAreaInsets: ignoreTopSafeAreaInsets, + onScroll: onScroll, + bounces: bounces) .frame(width: geometry.size.width, height: geometry.size.height) - } + .edgesIgnoringSafeArea(.all) + }.edgesIgnoringSafeArea(.all) } - } diff --git a/DuckDuckGo/Subscription/Views/NavigationViewStyle.swift b/DuckDuckGo/Subscription/Views/NavigationViewStyle.swift new file mode 100644 index 0000000000..e9f9ab3a57 --- /dev/null +++ b/DuckDuckGo/Subscription/Views/NavigationViewStyle.swift @@ -0,0 +1,40 @@ +// +// NavigationViewStyle.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 SwiftUI +import DesignResourcesKit + +@available(iOS 16.0, *) +struct NavigationBarModifier: ViewModifier { + + func body(content: Content) -> some View { + content + .toolbarBackground(Color(designSystemColor: .surface), for: .navigationBar) + .navigationBarTitleDisplayMode(.inline) + .tint(Color(designSystemColor: .textPrimary)) + } +} + +// Extension to easily apply the custom modifier +@available(iOS 16.0, *) +extension View { + func applyNavigationStyle() -> some View { + self.modifier(NavigationBarModifier()) + } +} diff --git a/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift b/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift index 714e9696ea..a44d8fad1d 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift @@ -34,7 +34,10 @@ struct SubscriptionEmailView: View { AsyncHeadlessWebView(url: $viewModel.emailURL, userScript: viewModel.userScript, subFeature: viewModel.subFeature, - shouldReload: $viewModel.shouldReloadWebView).background() + shouldReload: $viewModel.shouldReloadWebView, + onScroll: { position in + print(position)} + ).background() } } .onChange(of: viewModel.activateSubscription) { active in diff --git a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift index e9b06406ac..a2ff2abf7e 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift @@ -23,10 +23,18 @@ import Foundation @available(iOS 15.0, *) struct SubscriptionFlowView: View { - + @Environment(\.dismiss) var dismiss @ObservedObject var viewModel: SubscriptionFlowViewModel @State private var isAlertVisible = false + @State private var shouldShowNavigationBar = false + + enum Constants { + static let navigationTranslucentThreshold = 40.0 + static let daxLogo = "Home" + static let daxLogoSize: CGFloat = 24.0 + static let empty = "" + } private func getTransactionStatus() -> String { switch viewModel.transactionStatus { @@ -40,26 +48,58 @@ struct SubscriptionFlowView: View { return "" } } - + var body: some View { - ZStack { - AsyncHeadlessWebView(url: $viewModel.purchaseURL, - userScript: viewModel.userScript, - subFeature: viewModel.subFeature, - shouldReload: $viewModel.shouldReloadWebView).background() - - // Overlay that appears when transaction is in progress - if viewModel.transactionStatus != .idle { - PurchaseInProgressView(status: getTransactionStatus()) + if #available(iOS 16.0, *) { + NavigationStack { + baseView + .applyNavigationStyle() + .toolbar { + ToolbarItem(placement: .principal) { + Group { + if shouldShowNavigationBar { + HStack { + Image(Constants.daxLogo) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: Constants.daxLogoSize, height: Constants.daxLogoSize) + Text(viewModel.viewTitle).daxBodyRegular() + } + } else { + Text(Constants.empty) + } + } + } + } + .toolbar(shouldShowNavigationBar ? .visible : .hidden, for: .navigationBar) + .animation(.bouncy) } - - // Activation View - NavigationLink(destination: SubscriptionRestoreView(viewModel: SubscriptionRestoreViewModel(), - isActivatingSubscription: $viewModel.activatingSubscription), - isActive: $viewModel.activatingSubscription) { - EmptyView() + } else { + NavigationView { + baseView + .navigationTitle(viewModel.viewTitle) + } + } + } + + + @ViewBuilder + private var baseView: some View { + ZStack(alignment: .top) { + webView + + // Show a dismiss button while the bar is not visible + if !shouldShowNavigationBar { + HStack { + Spacer() + Button(action: { dismiss() }) { + Text(UserText.subscriptionCloseButton).daxBodyRegular() + }.transition(.opacity) + }.animation(.easeInOut, value: shouldShowNavigationBar) + .padding() } } + .onChange(of: viewModel.shouldReloadWebView) { shouldReload in if shouldReload { viewModel.shouldReloadWebView = false @@ -75,14 +115,14 @@ struct SubscriptionFlowView: View { dismiss() } } - .onAppear(perform: { Task { await viewModel.initializeViewData() } + + // Fall back to old customization + if #unavailable(iOS 16.0) { + setUpAppearances() + } }) - .navigationTitle(viewModel.viewTitle) - .navigationBarBackButtonHidden(viewModel.transactionStatus != .idle) - - // Active subscription found Alert .alert(isPresented: $isAlertVisible) { Alert( title: Text(UserText.subscriptionFoundTitle), @@ -95,6 +135,64 @@ struct SubscriptionFlowView: View { ) } .navigationBarBackButtonHidden(viewModel.transactionStatus != .idle) + .navigationBarItems(trailing: Button(UserText.subscriptionCloseButton) { + dismiss() + }) } + + @ViewBuilder + private var webView: some View { + + var ignoreTopSafeAreaInsets = true + + // No transparent navbar pre iOS 16 + if #unavailable(iOS 16.0) { + var ignoreTopSafeAreaInsets = false + } + + ZStack(alignment: .top) { + AsyncHeadlessWebView(url: $viewModel.purchaseURL, + userScript: viewModel.userScript, + subFeature: viewModel.subFeature, + shouldReload: $viewModel.shouldReloadWebView, + ignoreTopSafeAreaInsets: false, + onScroll: { position in + updateNavigationBarWithScrollPosition(position) + }, + bounces: false) + + if viewModel.transactionStatus != .idle { + PurchaseInProgressView(status: getTransactionStatus()) + } + + NavigationLink(destination: SubscriptionRestoreView(viewModel: SubscriptionRestoreViewModel(), + isActivatingSubscription: $viewModel.activatingSubscription), + isActive: $viewModel.activatingSubscription) { + EmptyView() + } + } + } + + private func updateNavigationBarWithScrollPosition(_ position: CGPoint) { + DispatchQueue.main.async { + if position.y > Constants.navigationTranslucentThreshold && !shouldShowNavigationBar { + withAnimation { + shouldShowNavigationBar = true + } + } else if position.y <= Constants.navigationTranslucentThreshold && shouldShowNavigationBar { + withAnimation { + shouldShowNavigationBar = false + } + } + } + } + + private func setUpAppearances() { + let navAppearance = UINavigationBar.appearance() + navAppearance.backgroundColor = UIColor(designSystemColor: .background) + navAppearance.barTintColor = UIColor(designSystemColor: .background) + navAppearance.shadowImage = UIImage() + } + } #endif diff --git a/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift b/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift index a0f46a7961..32cf5e7b99 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift @@ -1,5 +1,5 @@ // -// SubscriptionFlowView.swift +// SubscriptionITPView.swift // DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. diff --git a/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift b/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift index 3006cf4ace..1be7aad511 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift @@ -34,7 +34,7 @@ struct SubscriptionRestoreView: View { @Binding var isActivatingSubscription: Bool private enum Constants { - static let heroImage = "SyncTurnOnSyncHero" + static let heroImage = "ManageSubscriptionHero" static let appleIDIcon = "Platform-Apple-16" static let emailIcon = "Email-16" static let headerLineSpacing = 10.0 diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index 531900ac59..c0c39412be 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -1040,6 +1040,9 @@ But if you *do* want a peek under the hood, you can find more information about static let subscriptionCompletingPurchaseTitle = NSLocalizedString("subscription.progress.view.completing.purchase", value: "Completing purchase...", comment: "Progress view title when completing the purchase") // Subscription Settings + public static let subscriptionTitle = NSLocalizedString("subscription.title", value: "Privacy Pro", comment: "Navigation bar Title for subscriptions") + public static let subscriptionCloseButton = NSLocalizedString("subscription.close", value: "Close", comment: "Navigation Button for closing subscription view") + static func subscriptionInfo(expiration: String) -> String { let localized = NSLocalizedString("subscription.subscription.active.caption", value: "Your Privacy Pro subscription renews on %@", comment: "Subscription Expiration Data") return String(format: localized, expiration) diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index 815afd61b6..2ebdb62857 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -1992,6 +1992,9 @@ But if you *do* want a peek under the hood, you can find more information about /* Change plan or billing title */ "subscription.change.plan" = "Change Plan Or Billing"; +/* Navigation Button for closing subscription view */ +"subscription.close" = "Close"; + /* FAQ Button */ "subscription.faq" = "Privacy Pro FAQ"; @@ -2067,6 +2070,9 @@ But if you *do* want a peek under the hood, you can find more information about /* Subscription Expiration Data */ "subscription.subscription.active.caption" = "Your Privacy Pro subscription renews on %@"; +/* Navigation bar Title for subscriptions */ +"subscription.title" = "Privacy Pro"; + /* Message confirming that recovery code was copied to clipboard */ "sync.code.copied" = "Recovery code copied to clipboard"; From 326e4946e54a1326fdd4b81dc0ef9a8a444531f6 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Wed, 31 Jan 2024 21:26:16 +0100 Subject: [PATCH 03/17] Close button works as expected --- .../Views/SubscriptionFlowView.swift | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift index a2ff2abf7e..b4a27d6d7c 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift @@ -34,6 +34,7 @@ struct SubscriptionFlowView: View { static let daxLogo = "Home" static let daxLogoSize: CGFloat = 24.0 static let empty = "" + static let closeButtonPadding: CGFloat = 16.0 } private func getTransactionStatus() -> String { @@ -63,7 +64,7 @@ struct SubscriptionFlowView: View { .resizable() .aspectRatio(contentMode: .fit) .frame(width: Constants.daxLogoSize, height: Constants.daxLogoSize) - Text(viewModel.viewTitle).daxBodyRegular() + Text(viewModel.viewTitle).daxBodyRegular() } } else { Text(Constants.empty) @@ -72,7 +73,6 @@ struct SubscriptionFlowView: View { } } .toolbar(shouldShowNavigationBar ? .visible : .hidden, for: .navigationBar) - .animation(.bouncy) } } else { NavigationView { @@ -89,14 +89,18 @@ struct SubscriptionFlowView: View { webView // Show a dismiss button while the bar is not visible - if !shouldShowNavigationBar { + // But it should be hidden while performing a transaction + if !shouldShowNavigationBar && viewModel.transactionStatus == .idle { HStack { Spacer() - Button(action: { dismiss() }) { - Text(UserText.subscriptionCloseButton).daxBodyRegular() - }.transition(.opacity) - }.animation(.easeInOut, value: shouldShowNavigationBar) - .padding() + Button(action: { + dismiss() + }) { + Text(UserText.subscriptionCloseButton) + } + .padding(Constants.closeButtonPadding) + .contentShape(Rectangle()) + } } } @@ -134,10 +138,10 @@ struct SubscriptionFlowView: View { } ) } - .navigationBarBackButtonHidden(viewModel.transactionStatus != .idle) - .navigationBarItems(trailing: Button(UserText.subscriptionCloseButton) { - dismiss() - }) + // The trailing close button should be hidden when a transaction is in progress + .navigationBarItems(trailing: viewModel.transactionStatus == .idle + ? Button(UserText.subscriptionCloseButton) { self.dismiss() } + : nil) } @ViewBuilder @@ -179,7 +183,7 @@ struct SubscriptionFlowView: View { withAnimation { shouldShowNavigationBar = true } - } else if position.y <= Constants.navigationTranslucentThreshold && shouldShowNavigationBar { + } else if position.y <= Constants.navigationTranslucentThreshold && position.y > 0 && shouldShowNavigationBar { withAnimation { shouldShowNavigationBar = false } From 475ee0fd30206247898b00243ae3e7d5e1adfb10 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Wed, 31 Jan 2024 23:43:42 +0100 Subject: [PATCH 04/17] Setup Subscription environment after dismissing subscription flow --- DuckDuckGo/SettingsSubscriptionView.swift | 7 ++++++- DuckDuckGo/SettingsViewModel.swift | 2 +- .../ViewModel/SubscriptionFlowViewModel.swift | 9 ++++++++- .../Subscription/Views/SubscriptionFlowView.swift | 12 ++++++------ .../Subscription/Views/SubscriptionRestoreView.swift | 6 ------ 5 files changed, 21 insertions(+), 15 deletions(-) diff --git a/DuckDuckGo/SettingsSubscriptionView.swift b/DuckDuckGo/SettingsSubscriptionView.swift index ff58a2afc9..8cf08325bc 100644 --- a/DuckDuckGo/SettingsSubscriptionView.swift +++ b/DuckDuckGo/SettingsSubscriptionView.swift @@ -97,7 +97,12 @@ struct SettingsSubscriptionView: View { } else { purchaseSubscriptionView } - } + // Refresh subscription when dismissing the Subscription Flow + }.onChange(of: isShowingsubScriptionFlow, perform: { value in + if !value { + Task { await viewModel.setupSubscriptionEnvironment() } + } + }) } } } diff --git a/DuckDuckGo/SettingsViewModel.swift b/DuckDuckGo/SettingsViewModel.swift index 899b4eef6f..5b180f5999 100644 --- a/DuckDuckGo/SettingsViewModel.swift +++ b/DuckDuckGo/SettingsViewModel.swift @@ -309,7 +309,7 @@ extension SettingsViewModel { #if SUBSCRIPTION @available(iOS 15.0, *) @MainActor - private func setupSubscriptionEnvironment() async { + func setupSubscriptionEnvironment() async { // Active subscription check if let token = accountManager.accessToken { diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift index 89cd53837b..d15dfafdbd 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift @@ -119,7 +119,8 @@ final class SubscriptionFlowViewModel: ObservableObject { func initializeViewData() async { await self.setupTransactionObserver() - await MainActor.run { shouldReloadWebView = true } + await self.updateSubscriptionStatus() + } func restoreAppstoreTransaction() { @@ -133,5 +134,11 @@ final class SubscriptionFlowViewModel: ObservableObject { } } + func updateSubscriptionStatus() async { + if AccountManager().isUserAuthenticated && hasActiveSubscription == false { + await MainActor.run { shouldReloadWebView = true } + } + } + } #endif diff --git a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift index b4a27d6d7c..46b4faa0aa 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift @@ -34,7 +34,7 @@ struct SubscriptionFlowView: View { static let daxLogo = "Home" static let daxLogoSize: CGFloat = 24.0 static let empty = "" - static let closeButtonPadding: CGFloat = 16.0 + static let closeButtonPadding: CGFloat = 20.0 } private func getTransactionStatus() -> String { @@ -81,7 +81,6 @@ struct SubscriptionFlowView: View { } } } - @ViewBuilder private var baseView: some View { @@ -116,16 +115,17 @@ struct SubscriptionFlowView: View { } .onChange(of: viewModel.shouldDismissView) { result in if result { - dismiss() + self.dismiss() } } .onAppear(perform: { - Task { await viewModel.initializeViewData() } - // Fall back to old customization if #unavailable(iOS 16.0) { - setUpAppearances() + // setUpAppearances() } + + Task { await viewModel.initializeViewData() } + }) .alert(isPresented: $isAlertVisible) { Alert( diff --git a/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift b/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift index 1be7aad511..939271c6cf 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift @@ -138,12 +138,6 @@ struct SubscriptionRestoreView: View { HStack { getCellButton(buttonText: UserText.subscriptionManageEmailButton, action: buttonAction) - /* TO BE IMPLEMENTED ?? - Spacer() - Button(action: {}, label: { - Text(UserText.subscriptionManageEmailResendInstructions).daxButton().daxBodyBold() - }) - */ } } } From 434e2b36eb1d71f1aa43538fe007d290641eed06 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Fri, 2 Feb 2024 19:19:01 +0100 Subject: [PATCH 05/17] Update navigation Management to support iOS 15 --- DuckDuckGo.xcodeproj/project.pbxproj | 18 ++-- DuckDuckGo/SettingsSubscriptionView.swift | 11 ++- .../SubscriptionEmailViewModel.swift | 9 +- .../ViewModel/SubscriptionFlowViewModel.swift | 25 ++--- .../SubscriptionRestoreViewModel.swift | 5 +- ...tyle.swift => NavigationBarModifier.swift} | 8 +- .../Views/RootPresentationMode.swift | 45 +++++++++ .../Views/SubscriptionEmailView.swift | 15 +-- .../Views/SubscriptionFlowView.swift | 92 +++++++++---------- .../Views/SubscriptionRestoreView.swift | 29 +++--- .../Views/SubscriptionSettingsView.swift | 9 +- 11 files changed, 158 insertions(+), 108 deletions(-) rename DuckDuckGo/Subscription/Views/{NavigationViewStyle.swift => NavigationBarModifier.swift} (82%) create mode 100644 DuckDuckGo/Subscription/Views/RootPresentationMode.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 4115e132af..549e4e3c59 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -780,7 +780,7 @@ D64648AF2B5993890033090B /* SubscriptionEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64648AE2B5993890033090B /* SubscriptionEmailViewModel.swift */; }; D652498E2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D652498D2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift */; }; D65CEA6C2B6A871B008A759B /* View+NavbarAppereance.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65CEA6B2B6A871B008A759B /* View+NavbarAppereance.swift */; }; - D65CEA6E2B6ABA29008A759B /* NavigationViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65CEA6D2B6ABA29008A759B /* NavigationViewStyle.swift */; }; + D65CEA6E2B6ABA29008A759B /* NavigationBarModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65CEA6D2B6ABA29008A759B /* NavigationBarModifier.swift */; }; D65CEA702B6AC6C9008A759B /* Subscription.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D65CEA6F2B6AC6C9008A759B /* Subscription.xcassets */; }; D664C7B62B289AA200CBFA76 /* SubscriptionFlowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664C7942B289AA000CBFA76 /* SubscriptionFlowViewModel.swift */; }; D664C7B72B289AA200CBFA76 /* Subscription.storekit in Resources */ = {isa = PBXBuildFile; fileRef = D664C7952B289AA000CBFA76 /* Subscription.storekit */; }; @@ -813,6 +813,7 @@ 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 */; }; + D6D95CE12B6D52DA00960317 /* RootPresentationMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D95CE02B6D52DA00960317 /* RootPresentationMode.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 */; }; @@ -2441,7 +2442,7 @@ D64648AE2B5993890033090B /* SubscriptionEmailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionEmailViewModel.swift; sourceTree = ""; }; D652498D2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionSettingsViewModel.swift; sourceTree = ""; }; D65CEA6B2B6A871B008A759B /* View+NavbarAppereance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+NavbarAppereance.swift"; sourceTree = ""; }; - D65CEA6D2B6ABA29008A759B /* NavigationViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewStyle.swift; sourceTree = ""; }; + D65CEA6D2B6ABA29008A759B /* NavigationBarModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarModifier.swift; sourceTree = ""; }; D65CEA6F2B6AC6C9008A759B /* Subscription.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Subscription.xcassets; sourceTree = ""; }; D664C7942B289AA000CBFA76 /* SubscriptionFlowViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionFlowViewModel.swift; sourceTree = ""; }; D664C7952B289AA000CBFA76 /* Subscription.storekit */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Subscription.storekit; sourceTree = ""; }; @@ -2474,6 +2475,7 @@ 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 = ""; }; + D6D95CE02B6D52DA00960317 /* RootPresentationMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootPresentationMode.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 = ""; }; @@ -4580,8 +4582,8 @@ isa = PBXGroup; children = ( D664C7942B289AA000CBFA76 /* SubscriptionFlowViewModel.swift */, - D668D9262B6937D2008E2FF2 /* SubscriptionITPViewModel.swift */, D68DF81D2B5830380023DBEA /* SubscriptionRestoreViewModel.swift */, + D668D9262B6937D2008E2FF2 /* SubscriptionITPViewModel.swift */, D64648AE2B5993890033090B /* SubscriptionEmailViewModel.swift */, D652498D2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift */, ); @@ -4603,11 +4605,12 @@ D664C7AF2B289AA000CBFA76 /* HeadlessWebView.swift */, D664C7AD2B289AA000CBFA76 /* PurchaseInProgressView.swift */, D664C7AE2B289AA000CBFA76 /* SubscriptionFlowView.swift */, - D668D9242B693778008E2FF2 /* SubscriptionITPView.swift */, - D6F93E3D2B50A8A0004C268D /* SubscriptionSettingsView.swift */, D68DF81B2B58302E0023DBEA /* SubscriptionRestoreView.swift */, D64648AC2B59936B0033090B /* SubscriptionEmailView.swift */, - D65CEA6D2B6ABA29008A759B /* NavigationViewStyle.swift */, + D668D9242B693778008E2FF2 /* SubscriptionITPView.swift */, + D6F93E3D2B50A8A0004C268D /* SubscriptionSettingsView.swift */, + D65CEA6D2B6ABA29008A759B /* NavigationBarModifier.swift */, + D6D95CE02B6D52DA00960317 /* RootPresentationMode.swift */, ); path = Views; sourceTree = ""; @@ -6863,7 +6866,7 @@ 8563A03C1F9288D600F04442 /* BrowserChromeManager.swift in Sources */, 980891A32237146B00313A70 /* Feedback.swift in Sources */, F1D796F01E7B07610019D451 /* BookmarksViewControllerCells.swift in Sources */, - D65CEA6E2B6ABA29008A759B /* NavigationViewStyle.swift in Sources */, + D65CEA6E2B6ABA29008A759B /* NavigationBarModifier.swift in Sources */, 85058369219F424500ED4EDB /* UIColorExtension.swift in Sources */, D6E83C312B1EA309006C8AFB /* SettingsCell.swift in Sources */, 85058368219C49E000ED4EDB /* HomeViewSectionRenderers.swift in Sources */, @@ -6942,6 +6945,7 @@ D6E83C682B23B6A3006C8AFB /* FontSettings.swift in Sources */, 31EF52E1281B3BDC0034796E /* AutofillLoginListItemViewModel.swift in Sources */, 1E4FAA6627D8DFC800ADC5B3 /* CompleteDownloadRowViewModel.swift in Sources */, + D6D95CE12B6D52DA00960317 /* RootPresentationMode.swift in Sources */, 83004E862193E5ED00DA013C /* TabViewControllerBrowsingMenuExtension.swift in Sources */, EE01EB402AFBD0000096AAC9 /* NetworkProtectionVPNSettingsViewModel.swift in Sources */, EE72CA852A862D000043B5B3 /* NetworkProtectionDebugViewController.swift in Sources */, diff --git a/DuckDuckGo/SettingsSubscriptionView.swift b/DuckDuckGo/SettingsSubscriptionView.swift index 8cf08325bc..c43c06e50d 100644 --- a/DuckDuckGo/SettingsSubscriptionView.swift +++ b/DuckDuckGo/SettingsSubscriptionView.swift @@ -54,14 +54,11 @@ struct SettingsSubscriptionView: View { private var purchaseSubscriptionView: some View { return Group { SettingsCustomCell(content: { subscriptionDescriptionView }) - let viewModel = SubscriptionFlowViewModel(onFeatureSelected: { value in - self.viewModel.onAppearNavigationTarget = value - }) SettingsCustomCell(content: { learnMoreView }, action: { isShowingsubScriptionFlow = true }, isButton: true ) .sheet(isPresented: $isShowingsubScriptionFlow) { - SubscriptionFlowView(viewModel: viewModel) + SubscriptionFlowView() } } } @@ -74,17 +71,21 @@ struct SettingsSubscriptionView: View { disclosureIndicator: true, isButton: true) + /* NavigationLink(destination: Text("Data Broker Protection"), isActive: $viewModel.shouldNavigateToDBP) { SettingsCellView(label: UserText.settingsPProDBPTitle, subtitle: UserText.settingsPProDBPSubTitle) } + NavigationLink(destination: SubscriptionITPView(viewModel: SubscriptionITPViewModel()), isActive: $viewModel.shouldNavigateToITP) { SettingsCellView(label: UserText.settingsPProITRTitle, subtitle: UserText.settingsPProITRSubTitle) } + */ - NavigationLink(destination: SubscriptionSettingsView(viewModel: SubscriptionSettingsViewModel())) { + NavigationLink(destination: SubscriptionSettingsView()) { SettingsCustomCell(content: { manageSubscriptionView }) } + } } diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift index 7e883f8e06..7923208d8d 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift @@ -39,9 +39,9 @@ final class SubscriptionEmailViewModel: ObservableObject { private var cancellables = Set() - init(userScript: SubscriptionPagesUserScript, - subFeature: SubscriptionPagesUseSubscriptionFeature, - accountManager: AccountManager) { + init(userScript: SubscriptionPagesUserScript = SubscriptionPagesUserScript(), + subFeature: SubscriptionPagesUseSubscriptionFeature = SubscriptionPagesUseSubscriptionFeature(), + accountManager: AccountManager = AccountManager()) { self.userScript = userScript self.subFeature = subFeature self.accountManager = accountManager @@ -76,5 +76,8 @@ final class SubscriptionEmailViewModel: ObservableObject { activateSubscription = true } + deinit { + print("Subscription Email ViewModel Deallocated") + } } #endif diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift index d15dfafdbd..74d83aad3b 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift @@ -38,7 +38,7 @@ final class SubscriptionFlowViewModel: ObservableObject { // Closure passed to navigate to a specific section // after returning to settings - var onFeatureSelected: ((SettingsViewModel.SettingsSection) -> Void) + // var onFeatureSelected: ((SettingsViewModel.SettingsSection) -> Void) enum FeatureName { static let netP = "vpn" @@ -55,12 +55,12 @@ final class SubscriptionFlowViewModel: ObservableObject { init(userScript: SubscriptionPagesUserScript = SubscriptionPagesUserScript(), subFeature: SubscriptionPagesUseSubscriptionFeature = SubscriptionPagesUseSubscriptionFeature(), - purchaseManager: PurchaseManager = PurchaseManager.shared, - onFeatureSelected: @escaping ((SettingsViewModel.SettingsSection) -> Void)) { + purchaseManager: PurchaseManager = PurchaseManager.shared + /*onFeatureSelected: @escaping ((SettingsViewModel.SettingsSection) -> Void)*/) { self.userScript = userScript self.subFeature = subFeature self.purchaseManager = purchaseManager - self.onFeatureSelected = onFeatureSelected + // self.onFeatureSelected = onFeatureSelected } // Observe transaction status @@ -97,12 +97,12 @@ final class SubscriptionFlowViewModel: ObservableObject { 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) + case FeatureName.netP: break + // self?.onFeatureSelected(.netP) + case FeatureName.itp: break + // self?.onFeatureSelected(.itp) + case FeatureName.dbp: break + // self?.onFeatureSelected(.dbp) default: return } @@ -120,7 +120,6 @@ final class SubscriptionFlowViewModel: ObservableObject { func initializeViewData() async { await self.setupTransactionObserver() await self.updateSubscriptionStatus() - } func restoreAppstoreTransaction() { @@ -140,5 +139,9 @@ final class SubscriptionFlowViewModel: ObservableObject { } } + deinit { + print("Subscription Flow ViewModel Deallocated") + } + } #endif diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift index 458be8fad4..4b17d5cf37 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift @@ -39,7 +39,6 @@ final class SubscriptionRestoreViewModel: ObservableObject { @Published var transactionStatus: SubscriptionPagesUseSubscriptionFeature.TransactionStatus = .idle @Published var activationResult: SubscriptionActivationResult = .unknown @Published var subscriptionEmail: String? - @Published var isManagingEmailSubscription: Bool = false init(userScript: SubscriptionPagesUserScript = SubscriptionPagesUserScript(), subFeature: SubscriptionPagesUseSubscriptionFeature = SubscriptionPagesUseSubscriptionFeature(), @@ -80,8 +79,8 @@ final class SubscriptionRestoreViewModel: ObservableObject { } } - func manageEmailSubscription() { - isManagingEmailSubscription = true + deinit { + print("Subscription Restore ViewModel Deallocated") } } diff --git a/DuckDuckGo/Subscription/Views/NavigationViewStyle.swift b/DuckDuckGo/Subscription/Views/NavigationBarModifier.swift similarity index 82% rename from DuckDuckGo/Subscription/Views/NavigationViewStyle.swift rename to DuckDuckGo/Subscription/Views/NavigationBarModifier.swift index e9f9ab3a57..3b35407737 100644 --- a/DuckDuckGo/Subscription/Views/NavigationViewStyle.swift +++ b/DuckDuckGo/Subscription/Views/NavigationBarModifier.swift @@ -1,5 +1,5 @@ // -// NavigationViewStyle.swift +// NavigationBarModifier.swift // DuckDuckGo // // Copyright © 2024 DuckDuckGo. All rights reserved. @@ -20,19 +20,17 @@ import SwiftUI import DesignResourcesKit -@available(iOS 16.0, *) + struct NavigationBarModifier: ViewModifier { func body(content: Content) -> some View { content - .toolbarBackground(Color(designSystemColor: .surface), for: .navigationBar) .navigationBarTitleDisplayMode(.inline) - .tint(Color(designSystemColor: .textPrimary)) } } // Extension to easily apply the custom modifier -@available(iOS 16.0, *) + extension View { func applyNavigationStyle() -> some View { self.modifier(NavigationBarModifier()) diff --git a/DuckDuckGo/Subscription/Views/RootPresentationMode.swift b/DuckDuckGo/Subscription/Views/RootPresentationMode.swift new file mode 100644 index 0000000000..e43fc4bc08 --- /dev/null +++ b/DuckDuckGo/Subscription/Views/RootPresentationMode.swift @@ -0,0 +1,45 @@ +// +// RootPresentationMode.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 SwiftUI + +/* + iOS15 does not support NavigationStack navigation so this creates a 'RootPresentationMode' + environment that views can use to create a binding for dismissal of the whole stack of views + See: https://stackoverflow.com/questions/57334455/how-can-i-pop-to-the-root-view-using-swiftui + */ +struct RootPresentationModeKey: EnvironmentKey { + static let defaultValue: Binding = .constant(RootPresentationMode()) +} + +extension EnvironmentValues { + var rootPresentationMode: Binding { + get { return self[RootPresentationModeKey.self] } + set { self[RootPresentationModeKey.self] = newValue } + } +} + +typealias RootPresentationMode = Bool + +extension RootPresentationMode { + + public mutating func dismiss() { + self.toggle() + } +} diff --git a/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift b/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift index a44d8fad1d..a4b1d23323 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift @@ -24,9 +24,11 @@ import Foundation @available(iOS 15.0, *) struct SubscriptionEmailView: View { - @ObservedObject var viewModel: SubscriptionEmailViewModel - @Binding var isActivatingSubscription: Bool + @StateObject var viewModel = SubscriptionEmailViewModel() @Environment(\.dismiss) var dismiss + @Environment(\.rootPresentationMode) private var rootPresentationMode: Binding + @State private var isActive: Bool = false + @State var isAddingDevice = false var body: some View { ZStack { @@ -42,13 +44,12 @@ struct SubscriptionEmailView: View { } .onChange(of: viewModel.activateSubscription) { active in if active { - // We just need to dismiss the current view - if viewModel.managingSubscriptionEmail { + // If updating email, just go back + if isAddingDevice { dismiss() } else { - // Update the binding to tear down the entire view stack - // This dismisses all views in between and takes you back to the welcome page - isActivatingSubscription = false + // Dismiss the whole stack + self.rootPresentationMode.wrappedValue.dismiss() } } } diff --git a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift index 46b4faa0aa..0d7fa657c2 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift @@ -25,10 +25,13 @@ import Foundation struct SubscriptionFlowView: View { @Environment(\.dismiss) var dismiss - @ObservedObject var viewModel: SubscriptionFlowViewModel + @StateObject var viewModel = SubscriptionFlowViewModel() + @State private var showNestedViews = true @State private var isAlertVisible = false @State private var shouldShowNavigationBar = false + @State private var isActive: Bool = false + enum Constants { static let navigationTranslucentThreshold = 40.0 static let daxLogo = "Home" @@ -49,44 +52,33 @@ struct SubscriptionFlowView: View { return "" } } - + var body: some View { - if #available(iOS 16.0, *) { - NavigationStack { - baseView - .applyNavigationStyle() - .toolbar { - ToolbarItem(placement: .principal) { - Group { - if shouldShowNavigationBar { - HStack { - Image(Constants.daxLogo) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: Constants.daxLogoSize, height: Constants.daxLogoSize) - Text(viewModel.viewTitle).daxBodyRegular() - } - } else { - Text(Constants.empty) - } - } + NavigationView { + baseView + .toolbar { + ToolbarItem(placement: .principal) { + HStack { + Image(Constants.daxLogo) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: Constants.daxLogoSize, height: Constants.daxLogoSize) + Text(viewModel.viewTitle).daxBodyRegular() } } - .toolbar(shouldShowNavigationBar ? .visible : .hidden, for: .navigationBar) - } - } else { - NavigationView { - baseView - .navigationTitle(viewModel.viewTitle) - } - } + } + .navigationBarTitleDisplayMode(.inline) + .navigationBarHidden(!shouldShowNavigationBar) + }.environment(\.rootPresentationMode, self.$isActive) + } + @ViewBuilder private var baseView: some View { ZStack(alignment: .top) { webView - + // Show a dismiss button while the bar is not visible // But it should be hidden while performing a transaction if !shouldShowNavigationBar && viewModel.transactionStatus == .idle { @@ -101,6 +93,7 @@ struct SubscriptionFlowView: View { .contentShape(Rectangle()) } } + } .onChange(of: viewModel.shouldReloadWebView) { shouldReload in @@ -108,25 +101,32 @@ struct SubscriptionFlowView: View { viewModel.shouldReloadWebView = false } } + .onChange(of: viewModel.hasActiveSubscription) { result in if result { isAlertVisible = true } } + .onChange(of: viewModel.shouldDismissView) { result in if result { - self.dismiss() + dismiss() } } - .onAppear(perform: { - // Fall back to old customization - if #unavailable(iOS 16.0) { - // setUpAppearances() + + .onChange(of: viewModel.activatingSubscription) { value in + if value { + isActive = true + viewModel.activatingSubscription = false } - + } + + .onAppear(perform: { + setUpAppearances() Task { await viewModel.initializeViewData() } }) + .alert(isPresented: $isAlertVisible) { Alert( title: Text(UserText.subscriptionFoundTitle), @@ -155,25 +155,25 @@ struct SubscriptionFlowView: View { } ZStack(alignment: .top) { + // Restore View Hidden Link + NavigationLink(destination: SubscriptionRestoreView(), isActive: $isActive) { + EmptyView() + }.isDetailLink(false) + AsyncHeadlessWebView(url: $viewModel.purchaseURL, userScript: viewModel.userScript, subFeature: viewModel.subFeature, shouldReload: $viewModel.shouldReloadWebView, - ignoreTopSafeAreaInsets: false, + ignoreTopSafeAreaInsets: true, onScroll: { position in - updateNavigationBarWithScrollPosition(position) - }, + updateNavigationBarWithScrollPosition(position) + }, bounces: false) if viewModel.transactionStatus != .idle { PurchaseInProgressView(status: getTransactionStatus()) } - - NavigationLink(destination: SubscriptionRestoreView(viewModel: SubscriptionRestoreViewModel(), - isActivatingSubscription: $viewModel.activatingSubscription), - isActive: $viewModel.activatingSubscription) { - EmptyView() - } + } } @@ -183,7 +183,7 @@ struct SubscriptionFlowView: View { withAnimation { shouldShowNavigationBar = true } - } else if position.y <= Constants.navigationTranslucentThreshold && position.y > 0 && shouldShowNavigationBar { + } else if position.y <= Constants.navigationTranslucentThreshold && shouldShowNavigationBar { withAnimation { shouldShowNavigationBar = false } diff --git a/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift b/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift index 939271c6cf..97af3278db 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift @@ -26,12 +26,11 @@ import DesignResourcesKit struct SubscriptionRestoreView: View { @Environment(\.dismiss) var dismiss - @StateObject var viewModel: SubscriptionRestoreViewModel + @Environment(\.rootPresentationMode) private var rootPresentationMode: Binding + @StateObject var viewModel = SubscriptionRestoreViewModel() @State private var expandedItemId: Int = 0 @State private var isAlertVisible = false - - // Binding used to dismiss the entire stack (Go back to settings from several levels down) - @Binding var isActivatingSubscription: Bool + @State private var isActive: Bool = false private enum Constants { static let heroImage = "ManageSubscriptionHero" @@ -48,8 +47,15 @@ struct SubscriptionRestoreView: View { } var body: some View { - ZStack { + print("Restore View rendered") + return ZStack { VStack { + + // Email Activation View Hidden link + NavigationLink(destination: SubscriptionEmailView(isAddingDevice: viewModel.isAddingDevice), isActive: $isActive) { + EmptyView() + }.isDetailLink(false) + headerView listView } @@ -58,6 +64,7 @@ struct SubscriptionRestoreView: View { .navigationBarBackButtonHidden(viewModel.transactionStatus != .idle) .applyInsetGroupedListStyle() .alert(isPresented: $isAlertVisible) { getAlert() } + .onChange(of: viewModel.activationResult) { result in if result != .unknown { isAlertVisible = true @@ -72,16 +79,6 @@ struct SubscriptionRestoreView: View { } } - // Activation View - NavigationLink(destination: SubscriptionEmailView( - viewModel: SubscriptionEmailViewModel( - userScript: viewModel.userScript, - subFeature: viewModel.subFeature, - accountManager: viewModel.accountManager), - isActivatingSubscription: $isActivatingSubscription), - isActive: $viewModel.isManagingEmailSubscription) { - EmptyView() - } } private var listItems: [ListItem] { @@ -93,7 +90,7 @@ struct SubscriptionRestoreView: View { .init(id: 1, content: getCellTitle(icon: Constants.emailIcon, text: UserText.subscriptionActivateEmail), - expandedContent: getEmailCellContent(buttonAction: viewModel.manageEmailSubscription )) + expandedContent: getEmailCellContent(buttonAction: { isActive = true })) ] } diff --git a/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift b/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift index 8e44ce451c..aa4e227686 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift @@ -29,10 +29,9 @@ class SceneEnvironment: ObservableObject { @available(iOS 15.0, *) struct SubscriptionSettingsView: View { - @ObservedObject var viewModel: SubscriptionSettingsViewModel @Environment(\.presentationMode) var presentationMode + @StateObject var viewModel = SubscriptionSettingsViewModel() @StateObject var sceneEnvironment = SceneEnvironment() - @State private var isActivatingSubscription = false var body: some View { List { @@ -46,15 +45,15 @@ struct SubscriptionSettingsView: View { }.textCase(nil) Section(header: Text(UserText.subscriptionManageDevices)) { - NavigationLink(destination: SubscriptionRestoreView( - viewModel: SubscriptionRestoreViewModel(isAddingDevice: true), - isActivatingSubscription: $isActivatingSubscription)) { + /* + NavigationLink(destination: SubscriptionRestoreView(router: viewRouter, isAddingDevice: true)) { SettingsCustomCell(content: { Text(UserText.subscriptionAddDeviceButton) .daxBodyRegular() .foregroundColor(Color.init(designSystemColor: .accent)) }) } + */ SettingsCustomCell(content: { Text(UserText.subscriptionRemoveFromDevice) From 3c1c04fc46c0623806a366cdb469d3dc73a18b6d Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Fri, 2 Feb 2024 19:32:16 +0100 Subject: [PATCH 06/17] Cleanup --- DuckDuckGo/SettingsViewModel.swift | 2 +- .../Subscription/ViewModel/SubscriptionEmailViewModel.swift | 5 +---- .../Subscription/ViewModel/SubscriptionFlowViewModel.swift | 4 ---- .../ViewModel/SubscriptionRestoreViewModel.swift | 4 ---- DuckDuckGo/Subscription/Views/NavigationBarModifier.swift | 1 + DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift | 4 +--- DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift | 3 +-- 7 files changed, 5 insertions(+), 18 deletions(-) diff --git a/DuckDuckGo/SettingsViewModel.swift b/DuckDuckGo/SettingsViewModel.swift index 5b180f5999..899b4eef6f 100644 --- a/DuckDuckGo/SettingsViewModel.swift +++ b/DuckDuckGo/SettingsViewModel.swift @@ -309,7 +309,7 @@ extension SettingsViewModel { #if SUBSCRIPTION @available(iOS 15.0, *) @MainActor - func setupSubscriptionEnvironment() async { + private func setupSubscriptionEnvironment() async { // Active subscription check if let token = accountManager.accessToken { diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift index 7923208d8d..80cf1a2fb1 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift @@ -75,9 +75,6 @@ final class SubscriptionEmailViewModel: ObservableObject { subFeature.emailActivationComplete = false activateSubscription = true } - - deinit { - print("Subscription Email ViewModel Deallocated") - } + } #endif diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift index 74d83aad3b..d884df1b7b 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift @@ -139,9 +139,5 @@ final class SubscriptionFlowViewModel: ObservableObject { } } - deinit { - print("Subscription Flow ViewModel Deallocated") - } - } #endif diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift index 4b17d5cf37..0733cd2e83 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift @@ -79,9 +79,5 @@ final class SubscriptionRestoreViewModel: ObservableObject { } } - deinit { - print("Subscription Restore ViewModel Deallocated") - } - } #endif diff --git a/DuckDuckGo/Subscription/Views/NavigationBarModifier.swift b/DuckDuckGo/Subscription/Views/NavigationBarModifier.swift index 3b35407737..a340cfe265 100644 --- a/DuckDuckGo/Subscription/Views/NavigationBarModifier.swift +++ b/DuckDuckGo/Subscription/Views/NavigationBarModifier.swift @@ -26,6 +26,7 @@ struct NavigationBarModifier: ViewModifier { func body(content: Content) -> some View { content .navigationBarTitleDisplayMode(.inline) + } } diff --git a/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift b/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift index a4b1d23323..3743923a43 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift @@ -48,14 +48,12 @@ struct SubscriptionEmailView: View { if isAddingDevice { dismiss() } else { - // Dismiss the whole stack + // Pop to Root view self.rootPresentationMode.wrappedValue.dismiss() } } } .navigationTitle(viewModel.viewTitle) } - - } #endif diff --git a/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift b/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift index 97af3278db..2301cf7daa 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift @@ -47,8 +47,7 @@ struct SubscriptionRestoreView: View { } var body: some View { - print("Restore View rendered") - return ZStack { + ZStack { VStack { // Email Activation View Hidden link From f3ccda3307f735a1fa91c0bbef20ca1dea229f90 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Sat, 3 Feb 2024 02:14:07 +0100 Subject: [PATCH 07/17] Improved memory management and navigation for AsyncVIews --- DuckDuckGo.xcodeproj/project.pbxproj | 20 +- DuckDuckGo/SettingsSubscriptionView.swift | 14 +- DuckDuckGo/SettingsViewModel.swift | 2 +- .../AsyncHeadlessWebView.swift | 146 ++++++++++++++ .../AsyncHeadlessWebViewModel.swift | 37 ++++ .../SubscriptionEmailViewModel.swift | 8 + .../ViewModel/SubscriptionFlowViewModel.swift | 38 +++- .../Subscription/Views/HeadlessWebView.swift | 178 ------------------ .../Views/SubscriptionEmailView.swift | 13 +- .../Views/SubscriptionFlowView.swift | 42 ++--- .../Views/SubscriptionITPView.swift | 6 +- .../Views/SubscriptionSettingsView.swift | 6 +- 12 files changed, 272 insertions(+), 238 deletions(-) create mode 100644 DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebView.swift create mode 100644 DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebViewModel.swift delete mode 100644 DuckDuckGo/Subscription/Views/HeadlessWebView.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 549e4e3c59..ceb9ed3abe 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -787,7 +787,7 @@ D664C7B92B289AA200CBFA76 /* WKUserContentController+Handler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664C7982B289AA000CBFA76 /* WKUserContentController+Handler.swift */; }; D664C7C72B289AA200CBFA76 /* PurchaseInProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664C7AD2B289AA000CBFA76 /* PurchaseInProgressView.swift */; }; D664C7C82B289AA200CBFA76 /* SubscriptionFlowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664C7AE2B289AA000CBFA76 /* SubscriptionFlowView.swift */; }; - D664C7C92B289AA200CBFA76 /* HeadlessWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664C7AF2B289AA000CBFA76 /* HeadlessWebView.swift */; }; + D664C7C92B289AA200CBFA76 /* AsyncHeadlessWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664C7AF2B289AA000CBFA76 /* AsyncHeadlessWebView.swift */; }; D664C7CC2B289AA200CBFA76 /* SubscriptionPagesUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664C7B32B289AA000CBFA76 /* SubscriptionPagesUserScript.swift */; }; D664C7CE2B289AA200CBFA76 /* SubscriptionPagesUseSubscriptionFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664C7B52B289AA000CBFA76 /* SubscriptionPagesUseSubscriptionFeature.swift */; }; D664C7DD2B28A02800CBFA76 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D664C7DC2B28A02800CBFA76 /* StoreKit.framework */; }; @@ -814,6 +814,7 @@ D6D12CAC2B291CAA0054390C /* AuthService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12C9D2B291CA90054390C /* AuthService.swift */; }; D6D12CAD2B291CAA0054390C /* PurchaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12C9E2B291CA90054390C /* PurchaseManager.swift */; }; D6D95CE12B6D52DA00960317 /* RootPresentationMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D95CE02B6D52DA00960317 /* RootPresentationMode.swift */; }; + D6D95CE32B6D9F8800960317 /* AsyncHeadlessWebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D95CE22B6D9F8800960317 /* AsyncHeadlessWebViewModel.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 */; }; @@ -2449,7 +2450,7 @@ D664C7982B289AA000CBFA76 /* WKUserContentController+Handler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "WKUserContentController+Handler.swift"; sourceTree = ""; }; D664C7AD2B289AA000CBFA76 /* PurchaseInProgressView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PurchaseInProgressView.swift; sourceTree = ""; }; D664C7AE2B289AA000CBFA76 /* SubscriptionFlowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionFlowView.swift; sourceTree = ""; }; - D664C7AF2B289AA000CBFA76 /* HeadlessWebView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeadlessWebView.swift; sourceTree = ""; }; + D664C7AF2B289AA000CBFA76 /* AsyncHeadlessWebView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AsyncHeadlessWebView.swift; sourceTree = ""; }; D664C7B32B289AA000CBFA76 /* SubscriptionPagesUserScript.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionPagesUserScript.swift; sourceTree = ""; }; D664C7B52B289AA000CBFA76 /* SubscriptionPagesUseSubscriptionFeature.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionPagesUseSubscriptionFeature.swift; sourceTree = ""; }; D664C7DC2B28A02800CBFA76 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; @@ -2476,6 +2477,7 @@ 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 = ""; }; D6D95CE02B6D52DA00960317 /* RootPresentationMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootPresentationMode.swift; sourceTree = ""; }; + D6D95CE22B6D9F8800960317 /* AsyncHeadlessWebViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncHeadlessWebViewModel.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 = ""; }; @@ -4567,6 +4569,7 @@ D664C7922B289AA000CBFA76 /* Subscription */ = { isa = PBXGroup; children = ( + D6D95CE42B6DA3F200960317 /* AsyncHeadlessWebview */, D664C7952B289AA000CBFA76 /* Subscription.storekit */, D664C7932B289AA000CBFA76 /* ViewModel */, D664C7AC2B289AA000CBFA76 /* Views */, @@ -4602,7 +4605,6 @@ D664C7AC2B289AA000CBFA76 /* Views */ = { isa = PBXGroup; children = ( - D664C7AF2B289AA000CBFA76 /* HeadlessWebView.swift */, D664C7AD2B289AA000CBFA76 /* PurchaseInProgressView.swift */, D664C7AE2B289AA000CBFA76 /* SubscriptionFlowView.swift */, D68DF81B2B58302E0023DBEA /* SubscriptionRestoreView.swift */, @@ -4680,6 +4682,15 @@ path = Services; sourceTree = ""; }; + D6D95CE42B6DA3F200960317 /* AsyncHeadlessWebview */ = { + isa = PBXGroup; + children = ( + D6D95CE22B6D9F8800960317 /* AsyncHeadlessWebViewModel.swift */, + D664C7AF2B289AA000CBFA76 /* AsyncHeadlessWebView.swift */, + ); + path = AsyncHeadlessWebview; + sourceTree = ""; + }; D6E83C322B1F1279006C8AFB /* Sections */ = { isa = PBXGroup; children = ( @@ -6604,7 +6615,7 @@ 85047C752A0D3C2900D2FF3F /* SyncSettingsViewController+Themable.swift in Sources */, F44D279F27F331BB0037F371 /* AutofillLoginPromptViewController.swift in Sources */, C1BF0BA529B63D7200482B73 /* AutofillLoginPromptHelper.swift in Sources */, - D664C7C92B289AA200CBFA76 /* HeadlessWebView.swift in Sources */, + D664C7C92B289AA200CBFA76 /* AsyncHeadlessWebView.swift in Sources */, F1F5337C1F26A9EF00D80D4F /* UserText.swift in Sources */, D6E83C5E2B224676006C8AFB /* SettingsCustomizeView.swift in Sources */, 1E8AD1C727BE9B2900ABA377 /* DownloadsListDataSource.swift in Sources */, @@ -6645,6 +6656,7 @@ EE276BEA2A77F823009167B6 /* NetworkProtectionRootViewController.swift in Sources */, 310ECFDD282A8BB0005029B3 /* EnableAutofillSettingsTableViewCell.swift in Sources */, 1E908BF329827C480008C8F3 /* AutoconsentManagement.swift in Sources */, + D6D95CE32B6D9F8800960317 /* AsyncHeadlessWebViewModel.swift in Sources */, CB9B8739278C8E72001F4906 /* WidgetEducationViewController.swift in Sources */, F4D9C4FA25117A0F00814B71 /* HomeMessageStorage.swift in Sources */, D69FBF762B28BE3600B505F1 /* SettingsSubscriptionView.swift in Sources */, diff --git a/DuckDuckGo/SettingsSubscriptionView.swift b/DuckDuckGo/SettingsSubscriptionView.swift index c43c06e50d..a5e74e8127 100644 --- a/DuckDuckGo/SettingsSubscriptionView.swift +++ b/DuckDuckGo/SettingsSubscriptionView.swift @@ -25,6 +25,7 @@ import UIKit struct SettingsSubscriptionView: View { @EnvironmentObject var viewModel: SettingsViewModel + @StateObject var subscriptionFlowViewModel = SubscriptionFlowViewModel() @State var isShowingsubScriptionFlow = false @State var isShowingDBP = false @State var isShowingITP = false @@ -58,7 +59,7 @@ struct SettingsSubscriptionView: View { action: { isShowingsubScriptionFlow = true }, isButton: true ) .sheet(isPresented: $isShowingsubScriptionFlow) { - SubscriptionFlowView() + SubscriptionFlowView(viewModel: subscriptionFlowViewModel) } } } @@ -98,12 +99,19 @@ struct SettingsSubscriptionView: View { } else { purchaseSubscriptionView } + + } // Refresh subscription when dismissing the Subscription Flow - }.onChange(of: isShowingsubScriptionFlow, perform: { value in + .onChange(of: isShowingsubScriptionFlow, perform: { value in if !value { - Task { await viewModel.setupSubscriptionEnvironment() } + Task { viewModel.onAppear() } } }) + + .onReceive(subscriptionFlowViewModel.$selectedFeature) { value in + guard let value else { return } + viewModel.onAppearNavigationTarget = value + } } } } diff --git a/DuckDuckGo/SettingsViewModel.swift b/DuckDuckGo/SettingsViewModel.swift index 899b4eef6f..1993226671 100644 --- a/DuckDuckGo/SettingsViewModel.swift +++ b/DuckDuckGo/SettingsViewModel.swift @@ -422,7 +422,7 @@ extension SettingsViewModel { 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) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) { switch self.onAppearNavigationTarget { case .netP: self.presentLegacyView(.netP) diff --git a/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebView.swift b/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebView.swift new file mode 100644 index 0000000000..ded5fd79c2 --- /dev/null +++ b/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebView.swift @@ -0,0 +1,146 @@ +// +// AsyncHeadlessWebView.swift +// DuckDuckGo +// +// Copyright © 2023 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 +import WebKit +import UserScript +import SwiftUI +import DesignResourcesKit +import Core + +struct AsyncHeadlessWebViewSettings { + let bounces: Bool + + init(bounces: Bool = false) { + self.bounces = bounces + } +} + +class NavigationCoordinator { + weak var webView: WKWebView? + + init(webView: WKWebView?) { + self.webView = webView + } + + var canGoBack: Bool { + return webView?.canGoBack ?? false + } + + func reload() async { + await MainActor.run { + self.webView?.reload() + } + } + + func navigateTo(url: URL) { + guard let webView else { return } + DispatchQueue.main.asyncAfter(deadline: .now() + 0) { + DefaultUserAgentManager.shared.update(webView: webView, isDesktop: false, url: url) + webView.load(URLRequest(url: url)) + } + } + + func goBack() async { + guard await webView?.canGoBack == true else { return } + await MainActor.run { + self.webView?.goBack() + } + } + + func goForward() async { + guard await webView?.canGoForward == true else { return } + await MainActor.run { + self.webView?.goForward() + } + } +} + +struct HeadlessWebView: UIViewRepresentable { + let userScript: UserScriptMessaging? + let subFeature: Subfeature? + let settings: AsyncHeadlessWebViewSettings + var onScroll: ((CGPoint) -> Void)? + var navigationCoordinator: NavigationCoordinator + + + func makeUIView(context: Context) -> WKWebView { + let configuration = WKWebViewConfiguration() + configuration.userContentController = makeUserContentController() + let webView = WKWebView(frame: .zero, configuration: configuration) + + navigationCoordinator.webView = webView + webView.uiDelegate = context.coordinator + webView.scrollView.delegate = context.coordinator + webView.scrollView.bounces = settings.bounces + + return webView + } + + func updateUIView(_ uiView: WKWebView, context: Context) {} + + func makeCoordinator() -> Coordinator { + Coordinator(self, onScroll: onScroll) + } + + @MainActor + private func makeUserContentController() -> WKUserContentController { + let userContentController = WKUserContentController() + if let userScript, let subFeature { + userContentController.addUserScript(userScript.makeWKUserScriptSync()) + userContentController.addHandler(userScript) + userScript.registerSubfeature(delegate: subFeature) + } + return userContentController + } + + class Coordinator: NSObject, WKUIDelegate, UIScrollViewDelegate { + var parent: HeadlessWebView + var onScroll: ((CGPoint) -> Void)? + + init(_ parent: HeadlessWebView, onScroll: ((CGPoint) -> Void)?) { + self.parent = parent + self.onScroll = onScroll + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + let contentOffset = scrollView.contentOffset + onScroll?(contentOffset) + } + } +} + +struct AsyncHeadlessWebView: View { + @StateObject var viewModel: AsyncHeadlessWebViewViewModel + + var body: some View { + GeometryReader { geometry in + HeadlessWebView( + userScript: viewModel.userScript, + subFeature: viewModel.subFeature, + settings: viewModel.settings, + onScroll: { newPosition in + viewModel.scrollPosition = newPosition + }, + navigationCoordinator: viewModel.navigationCoordinator + ) + .frame(width: geometry.size.width, height: geometry.size.height) + } + } +} diff --git a/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebViewModel.swift b/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebViewModel.swift new file mode 100644 index 0000000000..6bcc01d970 --- /dev/null +++ b/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebViewModel.swift @@ -0,0 +1,37 @@ +// +// AsyncHeadlessWebViewModel.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 UserScript +import Core + +class AsyncHeadlessWebViewViewModel: ObservableObject { + let userScript: UserScriptMessaging? + let subFeature: Subfeature? + let settings: AsyncHeadlessWebViewSettings + + @Published var scrollPosition: CGPoint = .zero + + var navigationCoordinator = NavigationCoordinator(webView: nil) + + init(userScript: UserScriptMessaging?, subFeature: Subfeature?, settings: AsyncHeadlessWebViewSettings) { + self.userScript = userScript + self.subFeature = subFeature + self.settings = settings + } +} diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift index 80cf1a2fb1..abe47f3fc5 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift @@ -36,6 +36,7 @@ final class SubscriptionEmailViewModel: ObservableObject { @Published var shouldReloadWebView = false @Published var activateSubscription = false @Published var managingSubscriptionEmail = false + @Published var webViewModel: AsyncHeadlessWebViewViewModel private var cancellables = Set() @@ -45,6 +46,9 @@ final class SubscriptionEmailViewModel: ObservableObject { self.userScript = userScript self.subFeature = subFeature self.accountManager = accountManager + self.webViewModel = AsyncHeadlessWebViewViewModel(userScript: userScript, + subFeature: subFeature, + settings: AsyncHeadlessWebViewSettings(bounces: false)) initializeView() setupTransactionObservers() } @@ -75,6 +79,10 @@ final class SubscriptionEmailViewModel: ObservableObject { subFeature.emailActivationComplete = false activateSubscription = true } + + func loadURL() { + webViewModel.navigationCoordinator.navigateTo(url: emailURL ) + } } #endif diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift index d884df1b7b..892a513450 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift @@ -31,6 +31,10 @@ final class SubscriptionFlowViewModel: ObservableObject { let purchaseManager: PurchaseManager let viewTitle = UserText.settingsPProSection + enum Constants { + static let navigationBarHideThreshold = 40.0 + } + private var cancellables = Set() // State variables @@ -52,15 +56,24 @@ final class SubscriptionFlowViewModel: ObservableObject { @Published var shouldReloadWebView = false @Published var activatingSubscription = false @Published var shouldDismissView = false + @Published var webViewModel: AsyncHeadlessWebViewViewModel + @Published var shouldShowNavigationBar: Bool = false + @Published var selectedFeature: SettingsViewModel.SettingsSection? init(userScript: SubscriptionPagesUserScript = SubscriptionPagesUserScript(), subFeature: SubscriptionPagesUseSubscriptionFeature = SubscriptionPagesUseSubscriptionFeature(), - purchaseManager: PurchaseManager = PurchaseManager.shared + purchaseManager: PurchaseManager = PurchaseManager.shared, + selectedFeature: SettingsViewModel.SettingsSection? = nil /*onFeatureSelected: @escaping ((SettingsViewModel.SettingsSection) -> Void)*/) { self.userScript = userScript self.subFeature = subFeature self.purchaseManager = purchaseManager + self.selectedFeature = selectedFeature // self.onFeatureSelected = onFeatureSelected + + self.webViewModel = AsyncHeadlessWebViewViewModel(userScript: userScript, + subFeature: subFeature, + settings: AsyncHeadlessWebViewSettings(bounces: false)) } // Observe transaction status @@ -97,19 +110,25 @@ final class SubscriptionFlowViewModel: ObservableObject { if value != nil { self?.shouldDismissView = true switch value?.feature { - case FeatureName.netP: break - // self?.onFeatureSelected(.netP) - case FeatureName.itp: break - // self?.onFeatureSelected(.itp) - case FeatureName.dbp: break - // self?.onFeatureSelected(.dbp) + case FeatureName.netP: + self?.selectedFeature = .netP + case FeatureName.itp: + self?.selectedFeature = .itp + case FeatureName.dbp: + self?.selectedFeature = .dbp default: - return + self?.selectedFeature = Optional.none } } } .store(in: &cancellables) - + + webViewModel.$scrollPosition + .receive(on: DispatchQueue.main) + .sink { [weak self] value in + self?.shouldShowNavigationBar = value.y > Constants.navigationBarHideThreshold + } + .store(in: &cancellables) } @MainActor @@ -120,6 +139,7 @@ final class SubscriptionFlowViewModel: ObservableObject { func initializeViewData() async { await self.setupTransactionObserver() await self.updateSubscriptionStatus() + webViewModel.navigationCoordinator.navigateTo(url: purchaseURL ) } func restoreAppstoreTransaction() { diff --git a/DuckDuckGo/Subscription/Views/HeadlessWebView.swift b/DuckDuckGo/Subscription/Views/HeadlessWebView.swift deleted file mode 100644 index d91af222ad..0000000000 --- a/DuckDuckGo/Subscription/Views/HeadlessWebView.swift +++ /dev/null @@ -1,178 +0,0 @@ -// -// HeadlessWebView.swift -// DuckDuckGo -// -// Copyright © 2023 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 -import WebKit -import UserScript -import SwiftUI -import DesignResourcesKit -import Core - -struct HeadlessWebView: UIViewRepresentable { - let userScript: UserScriptMessaging - let subFeature: Subfeature - @Binding var url: URL - @Binding var shouldReload: Bool - let ignoreTopSafeAreaInsets: Bool - let onScroll: ((CGPoint) -> Void)? - let bounces: Bool - - func makeUIView(context: Context) -> WKWebView { - let configuration = WKWebViewConfiguration() - configuration.userContentController = makeUserContentController() - - let webView = WKWebView(frame: .zero, configuration: configuration) - 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)) - } - - webView.uiDelegate = context.coordinator - webView.scrollView.delegate = context.coordinator - webView.scrollView.bounces = bounces - - -#if DEBUG - if #available(iOS 16.4, *) { - webView.isInspectable = true - } -#endif - return webView - } - - func updateUIView(_ uiView: WKWebView, context: Context) { - // Adjust content insets - if ignoreTopSafeAreaInsets { - let insets = UIEdgeInsets(top: -uiView.safeAreaInsets.top, left: 0, bottom: 0, right: 0) - uiView.scrollView.contentInset = insets - uiView.scrollView.scrollIndicatorInsets = insets - } - - if shouldReload { - reloadView(uiView: uiView) - } - } - - @MainActor - func reloadView(uiView: WKWebView) { - uiView.reload() - DispatchQueue.main.async { - shouldReload = false - } - } - - func makeCoordinator() -> Coordinator { - Coordinator(self) - } - - @MainActor - private func makeUserContentController() -> WKUserContentController { - let userContentController = WKUserContentController() - userContentController.addUserScript(userScript.makeWKUserScriptSync()) - userContentController.addHandler(userScript) - userScript.registerSubfeature(delegate: subFeature) - return userContentController - } - - class Coordinator: NSObject, WKUIDelegate, UIScrollViewDelegate { - var parent: HeadlessWebView - var webView: WKWebView? - - init(_ parent: HeadlessWebView) { - self.parent = parent - } - - private func topMostViewController() -> UIViewController? { - var topController: UIViewController? = UIApplication.shared.windows.filter { $0.isKeyWindow } - .first? - .rootViewController - while let presentedViewController = topController?.presentedViewController { - topController = presentedViewController - } - return topController - } - - // MARK: WKUIDelegate - - // Enables presenting Javascript alerts via the native layer (window.confirm()) - func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, - initiatedByFrame frame: WKFrameInfo, - completionHandler: @escaping (Bool) -> Void) { - let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert) - alertController.addAction(UIAlertAction(title: UserText.actionCancel, style: .cancel, handler: { _ in completionHandler(false) })) - alertController.addAction(UIAlertAction(title: UserText.actionOK, style: .default, handler: { _ in completionHandler(true) })) - - if let topController = topMostViewController() { - topController.present(alertController, animated: true, completion: nil) - } else { - completionHandler(false) - } - } - - // MARK: UIScrollViewDelegate - - // Detect scroll events and call onScroll function with the current scroll position - func scrollViewDidScroll(_ scrollView: UIScrollView) { - guard let onScroll = parent.onScroll else { return } - onScroll(scrollView.contentOffset) - } - } -} - -struct AsyncHeadlessWebView: View { - @Binding var url: URL - let userScript: UserScriptMessaging - let subFeature: Subfeature - @Binding var shouldReload: Bool - var ignoreTopSafeAreaInsets: Bool - let onScroll: ((CGPoint) -> Void)? - let bounces: Bool - - init(url: Binding, - userScript: UserScriptMessaging, - subFeature: Subfeature, - shouldReload: Binding, - ignoreTopSafeAreaInsets: Bool = false, - onScroll: ((CGPoint) -> Void)? = nil, - bounces: Bool = false) { - self._url = url - self.userScript = userScript - self.subFeature = subFeature - self._shouldReload = shouldReload - self.ignoreTopSafeAreaInsets = ignoreTopSafeAreaInsets - self.onScroll = onScroll - self.bounces = bounces - } - - var body: some View { - GeometryReader { geometry in - HeadlessWebView(userScript: userScript, - subFeature: subFeature, - url: $url, - shouldReload: $shouldReload, - ignoreTopSafeAreaInsets: ignoreTopSafeAreaInsets, - onScroll: onScroll, - bounces: bounces) - .frame(width: geometry.size.width, height: geometry.size.height) - .edgesIgnoringSafeArea(.all) - }.edgesIgnoringSafeArea(.all) - } -} diff --git a/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift b/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift index 3743923a43..50811f81a6 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift @@ -33,15 +33,14 @@ struct SubscriptionEmailView: View { var body: some View { ZStack { VStack { - AsyncHeadlessWebView(url: $viewModel.emailURL, - userScript: viewModel.userScript, - subFeature: viewModel.subFeature, - shouldReload: $viewModel.shouldReloadWebView, - onScroll: { position in - print(position)} - ).background() + AsyncHeadlessWebView(viewModel: viewModel.webViewModel) + .background() } } + .onAppear { + viewModel.loadURL() + } + .onChange(of: viewModel.activateSubscription) { active in if active { // If updating email, just go back diff --git a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift index 0d7fa657c2..88aa77e4a1 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift @@ -20,6 +20,7 @@ #if SUBSCRIPTION import SwiftUI import Foundation +import DesignResourcesKit @available(iOS 15.0, *) struct SubscriptionFlowView: View { @@ -29,11 +30,9 @@ struct SubscriptionFlowView: View { @State private var showNestedViews = true @State private var isAlertVisible = false @State private var shouldShowNavigationBar = false - @State private var isActive: Bool = false enum Constants { - static let navigationTranslucentThreshold = 40.0 static let daxLogo = "Home" static let daxLogoSize: CGFloat = 24.0 static let empty = "" @@ -67,9 +66,12 @@ struct SubscriptionFlowView: View { } } } + .edgesIgnoringSafeArea(.top) .navigationBarTitleDisplayMode(.inline) - .navigationBarHidden(!shouldShowNavigationBar) - }.environment(\.rootPresentationMode, self.$isActive) + .navigationBarHidden(!viewModel.shouldShowNavigationBar).animation(.easeOut) + } + .tint(Color(designSystemColor: .textPrimary)) + .environment(\.rootPresentationMode, self.$isActive) } @@ -91,6 +93,7 @@ struct SubscriptionFlowView: View { } .padding(Constants.closeButtonPadding) .contentShape(Rectangle()) + .tint(Color(designSystemColor: .textPrimary)) } } @@ -111,6 +114,7 @@ struct SubscriptionFlowView: View { .onChange(of: viewModel.shouldDismissView) { result in if result { dismiss() + viewModel.shouldDismissView = false } } @@ -160,15 +164,8 @@ struct SubscriptionFlowView: View { EmptyView() }.isDetailLink(false) - AsyncHeadlessWebView(url: $viewModel.purchaseURL, - userScript: viewModel.userScript, - subFeature: viewModel.subFeature, - shouldReload: $viewModel.shouldReloadWebView, - ignoreTopSafeAreaInsets: true, - onScroll: { position in - updateNavigationBarWithScrollPosition(position) - }, - bounces: false) + AsyncHeadlessWebView(viewModel: viewModel.webViewModel) + .background() if viewModel.transactionStatus != .idle { PurchaseInProgressView(status: getTransactionStatus()) @@ -176,26 +173,13 @@ struct SubscriptionFlowView: View { } } - - private func updateNavigationBarWithScrollPosition(_ position: CGPoint) { - DispatchQueue.main.async { - if position.y > Constants.navigationTranslucentThreshold && !shouldShowNavigationBar { - withAnimation { - shouldShowNavigationBar = true - } - } else if position.y <= Constants.navigationTranslucentThreshold && shouldShowNavigationBar { - withAnimation { - shouldShowNavigationBar = false - } - } - } - } private func setUpAppearances() { let navAppearance = UINavigationBar.appearance() - navAppearance.backgroundColor = UIColor(designSystemColor: .background) - navAppearance.barTintColor = UIColor(designSystemColor: .background) + navAppearance.backgroundColor = UIColor(designSystemColor: .surface) + navAppearance.barTintColor = UIColor(designSystemColor: .surface) navAppearance.shadowImage = UIImage() + navAppearance.tintColor = UIColor(designSystemColor: .textPrimary) } } diff --git a/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift b/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift index 32cf5e7b99..611099c78e 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift @@ -29,10 +29,8 @@ struct SubscriptionITPView: View { var body: some View { ZStack { - AsyncHeadlessWebView(url: $viewModel.itpURL, - userScript: viewModel.userScript, - subFeature: viewModel.subFeature, - shouldReload: $viewModel.shouldReloadWebView).background() + // AsyncHeadlessWebView() + // .background() } .navigationTitle(viewModel.viewTitle) } diff --git a/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift b/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift index aa4e227686..49b2b76aa3 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift @@ -45,15 +45,15 @@ struct SubscriptionSettingsView: View { }.textCase(nil) Section(header: Text(UserText.subscriptionManageDevices)) { - /* - NavigationLink(destination: SubscriptionRestoreView(router: viewRouter, isAddingDevice: true)) { + + NavigationLink(destination: SubscriptionRestoreView()) { SettingsCustomCell(content: { Text(UserText.subscriptionAddDeviceButton) .daxBodyRegular() .foregroundColor(Color.init(designSystemColor: .accent)) }) } - */ + SettingsCustomCell(content: { Text(UserText.subscriptionRemoveFromDevice) From 4746fff57d52704dbd3b25936f99acecea2bf009 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Sat, 3 Feb 2024 11:04:44 +0100 Subject: [PATCH 08/17] Back Button Navigation --- DuckDuckGo/MainViewController+Segues.swift | 2 - DuckDuckGo/SettingsSubscriptionView.swift | 2 +- .../AsyncHeadlessWebView.swift | 37 +++++++-- .../AsyncHeadlessWebViewModel.swift | 2 + .../ViewModel/SubscriptionFlowViewModel.swift | 12 +++ .../Views/SubscriptionFlowView.swift | 75 +++++++++++-------- 6 files changed, 89 insertions(+), 41 deletions(-) diff --git a/DuckDuckGo/MainViewController+Segues.swift b/DuckDuckGo/MainViewController+Segues.swift index 0d994f5699..94b4be4591 100644 --- a/DuckDuckGo/MainViewController+Segues.swift +++ b/DuckDuckGo/MainViewController+Segues.swift @@ -244,8 +244,6 @@ extension MainViewController { let navController = UINavigationController(rootViewController: settingsController) navController.applyTheme(ThemeManager.shared.currentTheme) settingsController.modalPresentationStyle = .automatic - - settingsController.isModalInPresentation = true present(navController, animated: true) { completion?(settingsViewModel) diff --git a/DuckDuckGo/SettingsSubscriptionView.swift b/DuckDuckGo/SettingsSubscriptionView.swift index a5e74e8127..ac8e4e47b2 100644 --- a/DuckDuckGo/SettingsSubscriptionView.swift +++ b/DuckDuckGo/SettingsSubscriptionView.swift @@ -59,7 +59,7 @@ struct SettingsSubscriptionView: View { action: { isShowingsubScriptionFlow = true }, isButton: true ) .sheet(isPresented: $isShowingsubScriptionFlow) { - SubscriptionFlowView(viewModel: subscriptionFlowViewModel) + SubscriptionFlowView(viewModel: subscriptionFlowViewModel).interactiveDismissDisabled() } } } diff --git a/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebView.swift b/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebView.swift index ded5fd79c2..b426603c9a 100644 --- a/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebView.swift +++ b/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebView.swift @@ -39,10 +39,6 @@ class NavigationCoordinator { self.webView = webView } - var canGoBack: Bool { - return webView?.canGoBack ?? false - } - func reload() async { await MainActor.run { self.webView?.reload() @@ -77,6 +73,8 @@ struct HeadlessWebView: UIViewRepresentable { let subFeature: Subfeature? let settings: AsyncHeadlessWebViewSettings var onScroll: ((CGPoint) -> Void)? + var onURLChange: ((URL) -> Void)? + var onCanGoBack: ((Bool) -> Void)? var navigationCoordinator: NavigationCoordinator @@ -89,6 +87,7 @@ struct HeadlessWebView: UIViewRepresentable { webView.uiDelegate = context.coordinator webView.scrollView.delegate = context.coordinator webView.scrollView.bounces = settings.bounces + webView.navigationDelegate = context.coordinator return webView } @@ -96,7 +95,7 @@ struct HeadlessWebView: UIViewRepresentable { func updateUIView(_ uiView: WKWebView, context: Context) {} func makeCoordinator() -> Coordinator { - Coordinator(self, onScroll: onScroll) + Coordinator(self, onScroll: onScroll, onURLChange: onURLChange, onCanGoBack: onCanGoBack) } @MainActor @@ -110,19 +109,37 @@ struct HeadlessWebView: UIViewRepresentable { return userContentController } - class Coordinator: NSObject, WKUIDelegate, UIScrollViewDelegate { + class Coordinator: NSObject, WKUIDelegate, UIScrollViewDelegate, WKNavigationDelegate { var parent: HeadlessWebView var onScroll: ((CGPoint) -> Void)? + var onURLChange: ((URL) -> Void)? + var onCanGoBack: ((Bool) -> Void)? - init(_ parent: HeadlessWebView, onScroll: ((CGPoint) -> Void)?) { + init(_ parent: HeadlessWebView, + onScroll: ((CGPoint) -> Void)?, + onURLChange: ((URL) -> Void)?, + onCanGoBack: ((Bool) -> Void)?) { self.parent = parent self.onScroll = onScroll + self.onURLChange = onURLChange + self.onCanGoBack = onCanGoBack } func scrollViewDidScroll(_ scrollView: UIScrollView) { let contentOffset = scrollView.contentOffset onScroll?(contentOffset) } + + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + if let url = navigationAction.request.url, + let onURLChange { + onURLChange(url) + } + if let onCanGoBack { + onCanGoBack(webView.canGoBack) + } + decisionHandler(.allow) + } } } @@ -138,6 +155,12 @@ struct AsyncHeadlessWebView: View { onScroll: { newPosition in viewModel.scrollPosition = newPosition }, + onURLChange: { newURL in + viewModel.url = newURL + }, + onCanGoBack: { value in + viewModel.canGoBack = value + }, navigationCoordinator: viewModel.navigationCoordinator ) .frame(width: geometry.size.width, height: geometry.size.height) diff --git a/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebViewModel.swift b/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebViewModel.swift index 6bcc01d970..d772924e8b 100644 --- a/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebViewModel.swift +++ b/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebViewModel.swift @@ -26,6 +26,8 @@ class AsyncHeadlessWebViewViewModel: ObservableObject { let settings: AsyncHeadlessWebViewSettings @Published var scrollPosition: CGPoint = .zero + @Published var url: URL? + @Published var canGoBack: Bool = false var navigationCoordinator = NavigationCoordinator(webView: nil) diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift index 892a513450..179c80292e 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift @@ -59,6 +59,7 @@ final class SubscriptionFlowViewModel: ObservableObject { @Published var webViewModel: AsyncHeadlessWebViewViewModel @Published var shouldShowNavigationBar: Bool = false @Published var selectedFeature: SettingsViewModel.SettingsSection? + @Published var canNavigateBack: Bool = false init(userScript: SubscriptionPagesUserScript = SubscriptionPagesUserScript(), subFeature: SubscriptionPagesUseSubscriptionFeature = SubscriptionPagesUseSubscriptionFeature(), @@ -129,6 +130,13 @@ final class SubscriptionFlowViewModel: ObservableObject { self?.shouldShowNavigationBar = value.y > Constants.navigationBarHideThreshold } .store(in: &cancellables) + + webViewModel.$canGoBack + .receive(on: DispatchQueue.main) + .sink { [weak self] value in + self?.canNavigateBack = value + } + .store(in: &cancellables) } @MainActor @@ -159,5 +167,9 @@ final class SubscriptionFlowViewModel: ObservableObject { } } + func navigateBack() async { + await webViewModel.navigationCoordinator.goBack() + } + } #endif diff --git a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift index 88aa77e4a1..ae7ec8daf2 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift @@ -36,26 +36,17 @@ struct SubscriptionFlowView: View { static let daxLogo = "Home" static let daxLogoSize: CGFloat = 24.0 static let empty = "" - static let closeButtonPadding: CGFloat = 20.0 - } - - private func getTransactionStatus() -> String { - switch viewModel.transactionStatus { - case .polling: - return UserText.subscriptionCompletingPurchaseTitle - case .purchasing: - return UserText.subscriptionPurchasingTitle - case .restoring: - return UserText.subscriptionRestoringTitle - case .idle: - return "" - } + static let navButtonPadding: CGFloat = 20.0 + static let backButtonImage = "chevron.left" } var body: some View { NavigationView { baseView .toolbar { + ToolbarItemGroup(placement: .navigationBarLeading) { + backButton + } ToolbarItem(placement: .principal) { HStack { Image(Constants.daxLogo) @@ -74,7 +65,43 @@ struct SubscriptionFlowView: View { .environment(\.rootPresentationMode, self.$isActive) } - + + @ViewBuilder + private var dismissButton: some View { + Button(action: { dismiss() }, label: { Text(UserText.subscriptionCloseButton) }) + .padding(Constants.navButtonPadding) + .contentShape(Rectangle()) + .tint(Color(designSystemColor: .textPrimary)) + } + + @ViewBuilder + private var backButton: some View { + if viewModel.canNavigateBack { + Button(action: { + Task { await viewModel.navigateBack() } + }, label: { + HStack(spacing: 0) { + Image(systemName: Constants.backButtonImage) + Text(UserText.backButtonTitle) + } + + }) + } + } + + private func getTransactionStatus() -> String { + switch viewModel.transactionStatus { + case .polling: + return UserText.subscriptionCompletingPurchaseTitle + case .purchasing: + return UserText.subscriptionPurchasingTitle + case .restoring: + return UserText.subscriptionRestoringTitle + case .idle: + return "" + } + } + @ViewBuilder private var baseView: some View { @@ -85,18 +112,11 @@ struct SubscriptionFlowView: View { // But it should be hidden while performing a transaction if !shouldShowNavigationBar && viewModel.transactionStatus == .idle { HStack { + backButton.padding(.leading, Constants.navButtonPadding) Spacer() - Button(action: { - dismiss() - }) { - Text(UserText.subscriptionCloseButton) - } - .padding(Constants.closeButtonPadding) - .contentShape(Rectangle()) - .tint(Color(designSystemColor: .textPrimary)) + dismissButton } } - } .onChange(of: viewModel.shouldReloadWebView) { shouldReload in @@ -150,13 +170,6 @@ struct SubscriptionFlowView: View { @ViewBuilder private var webView: some View { - - var ignoreTopSafeAreaInsets = true - - // No transparent navbar pre iOS 16 - if #unavailable(iOS 16.0) { - var ignoreTopSafeAreaInsets = false - } ZStack(alignment: .top) { // Restore View Hidden Link From 9ee45acb9207a6c81c07d84f2491329be2cea907 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Sat, 3 Feb 2024 11:17:45 +0100 Subject: [PATCH 09/17] Delay publishing scroll updates --- .../AsyncHeadlessWebView.swift | 8 +++---- .../AsyncHeadlessWebViewModel.swift | 23 +++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebView.swift b/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebView.swift index b426603c9a..b8a6d134d3 100644 --- a/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebView.swift +++ b/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebView.swift @@ -40,7 +40,7 @@ class NavigationCoordinator { } func reload() async { - await MainActor.run { + _ = await MainActor.run { self.webView?.reload() } } @@ -55,14 +55,14 @@ class NavigationCoordinator { func goBack() async { guard await webView?.canGoBack == true else { return } - await MainActor.run { + _ = await MainActor.run { self.webView?.goBack() } } func goForward() async { guard await webView?.canGoForward == true else { return } - await MainActor.run { + _ = await MainActor.run { self.webView?.goForward() } } @@ -153,7 +153,7 @@ struct AsyncHeadlessWebView: View { subFeature: viewModel.subFeature, settings: viewModel.settings, onScroll: { newPosition in - viewModel.scrollPosition = newPosition + viewModel.updateScrollPosition(newPosition) }, onURLChange: { newURL in viewModel.url = newURL diff --git a/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebViewModel.swift b/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebViewModel.swift index d772924e8b..b4ada5592d 100644 --- a/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebViewModel.swift +++ b/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebViewModel.swift @@ -19,12 +19,19 @@ import UserScript import Core +import Combine class AsyncHeadlessWebViewViewModel: ObservableObject { let userScript: UserScriptMessaging? let subFeature: Subfeature? let settings: AsyncHeadlessWebViewSettings + private var initialScrollPositionSubject = PassthroughSubject() + private var subsequentScrollPositionSubject = PassthroughSubject() + private var cancellables = Set() + private var isFirstUpdate = true + private var initialDelay = 1 + @Published var scrollPosition: CGPoint = .zero @Published var url: URL? @Published var canGoBack: Bool = false @@ -35,5 +42,21 @@ class AsyncHeadlessWebViewViewModel: ObservableObject { self.userScript = userScript self.subFeature = subFeature self.settings = settings + + // Delayed publishing first update for scrollPosition + // To avoid publishing events on view updates + initialScrollPositionSubject + .delay(for: .seconds(initialDelay), scheduler: RunLoop.main) + .merge(with: subsequentScrollPositionSubject) + .assign(to: &$scrollPosition) + } + + func updateScrollPosition(_ newPosition: CGPoint) { + if isFirstUpdate { + initialScrollPositionSubject.send(newPosition) + isFirstUpdate = false + } else { + subsequentScrollPositionSubject.send(newPosition) + } } } From 4d5c00b0ad6fd9844f16365287cae297038d5056 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Sat, 3 Feb 2024 12:28:16 +0100 Subject: [PATCH 10/17] Reset Subscription Flow after subscription is restored --- DuckDuckGo/SettingsSubscriptionView.swift | 6 +-- .../ViewModel/SubscriptionFlowViewModel.swift | 44 ++++++++++++------- .../Views/SubscriptionFlowView.swift | 10 +---- .../Views/SubscriptionITPView.swift | 2 +- 4 files changed, 35 insertions(+), 27 deletions(-) diff --git a/DuckDuckGo/SettingsSubscriptionView.swift b/DuckDuckGo/SettingsSubscriptionView.swift index ac8e4e47b2..ff2c98bf5e 100644 --- a/DuckDuckGo/SettingsSubscriptionView.swift +++ b/DuckDuckGo/SettingsSubscriptionView.swift @@ -76,12 +76,12 @@ struct SettingsSubscriptionView: View { NavigationLink(destination: Text("Data Broker Protection"), isActive: $viewModel.shouldNavigateToDBP) { SettingsCellView(label: UserText.settingsPProDBPTitle, subtitle: UserText.settingsPProDBPSubTitle) } + */ - - NavigationLink(destination: SubscriptionITPView(viewModel: SubscriptionITPViewModel()), isActive: $viewModel.shouldNavigateToITP) { + NavigationLink(destination: SubscriptionITPView(), isActive: $viewModel.shouldNavigateToITP) { SettingsCellView(label: UserText.settingsPProITRTitle, subtitle: UserText.settingsPProITRSubTitle) } - */ + NavigationLink(destination: SubscriptionSettingsView()) { SettingsCustomCell(content: { manageSubscriptionView }) diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift index 179c80292e..5f197b1f81 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift @@ -36,14 +36,11 @@ final class SubscriptionFlowViewModel: ObservableObject { } private var cancellables = Set() + private var canGoBackCancellable: 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" @@ -53,7 +50,6 @@ final class SubscriptionFlowViewModel: ObservableObject { // Published properties @Published var hasActiveSubscription = false @Published var transactionStatus: SubscriptionPagesUseSubscriptionFeature.TransactionStatus = .idle - @Published var shouldReloadWebView = false @Published var activatingSubscription = false @Published var shouldDismissView = false @Published var webViewModel: AsyncHeadlessWebViewViewModel @@ -64,14 +60,11 @@ final class SubscriptionFlowViewModel: ObservableObject { init(userScript: SubscriptionPagesUserScript = SubscriptionPagesUserScript(), subFeature: SubscriptionPagesUseSubscriptionFeature = SubscriptionPagesUseSubscriptionFeature(), purchaseManager: PurchaseManager = PurchaseManager.shared, - selectedFeature: SettingsViewModel.SettingsSection? = nil - /*onFeatureSelected: @escaping ((SettingsViewModel.SettingsSection) -> Void)*/) { + selectedFeature: SettingsViewModel.SettingsSection? = nil) { self.userScript = userScript self.subFeature = subFeature self.purchaseManager = purchaseManager self.selectedFeature = selectedFeature - // self.onFeatureSelected = onFeatureSelected - self.webViewModel = AsyncHeadlessWebViewViewModel(userScript: userScript, subFeature: subFeature, settings: AsyncHeadlessWebViewSettings(bounces: false)) @@ -109,7 +102,6 @@ final class SubscriptionFlowViewModel: ObservableObject { .receive(on: DispatchQueue.main) .sink { [weak self] value in if value != nil { - self?.shouldDismissView = true switch value?.feature { case FeatureName.netP: self?.selectedFeature = .netP @@ -118,9 +110,11 @@ final class SubscriptionFlowViewModel: ObservableObject { case FeatureName.dbp: self?.selectedFeature = .dbp default: - self?.selectedFeature = Optional.none + break } + self?.finalizeSubscriptionFlow() } + } .store(in: &cancellables) @@ -131,18 +125,23 @@ final class SubscriptionFlowViewModel: ObservableObject { } .store(in: &cancellables) - webViewModel.$canGoBack + canGoBackCancellable = webViewModel.$canGoBack .receive(on: DispatchQueue.main) .sink { [weak self] value in self?.canNavigateBack = value } - .store(in: &cancellables) } @MainActor private func setTransactionStatus(_ status: SubscriptionPagesUseSubscriptionFeature.TransactionStatus) { self.transactionStatus = status } + + @MainActor + private func disableGoBack() { + canGoBackCancellable?.cancel() + canNavigateBack = false + } func initializeViewData() async { await self.setupTransactionObserver() @@ -150,10 +149,24 @@ final class SubscriptionFlowViewModel: ObservableObject { webViewModel.navigationCoordinator.navigateTo(url: purchaseURL ) } + func finalizeSubscriptionFlow() { + canGoBackCancellable?.cancel() + cancellables.removeAll() + subFeature.selectedFeature = nil + hasActiveSubscription = false + transactionStatus = .idle + activatingSubscription = false + shouldShowNavigationBar = false + selectedFeature = nil + canNavigateBack = false + shouldDismissView = true + } + func restoreAppstoreTransaction() { Task { if await subFeature.restoreAccountFromAppStorePurchase() { - await MainActor.run { shouldReloadWebView = true } + await disableGoBack() + await webViewModel.navigationCoordinator.reload() } else { await MainActor.run { } @@ -163,7 +176,8 @@ final class SubscriptionFlowViewModel: ObservableObject { func updateSubscriptionStatus() async { if AccountManager().isUserAuthenticated && hasActiveSubscription == false { - await MainActor.run { shouldReloadWebView = true } + await disableGoBack() + await webViewModel.navigationCoordinator.reload() } } diff --git a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift index ae7ec8daf2..603fb5e00f 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift @@ -68,7 +68,7 @@ struct SubscriptionFlowView: View { @ViewBuilder private var dismissButton: some View { - Button(action: { dismiss() }, label: { Text(UserText.subscriptionCloseButton) }) + Button(action: { viewModel.finalizeSubscriptionFlow() }, label: { Text(UserText.subscriptionCloseButton) }) .padding(Constants.navButtonPadding) .contentShape(Rectangle()) .tint(Color(designSystemColor: .textPrimary)) @@ -119,12 +119,6 @@ struct SubscriptionFlowView: View { } } - .onChange(of: viewModel.shouldReloadWebView) { shouldReload in - if shouldReload { - viewModel.shouldReloadWebView = false - } - } - .onChange(of: viewModel.hasActiveSubscription) { result in if result { isAlertVisible = true @@ -164,7 +158,7 @@ struct SubscriptionFlowView: View { } // The trailing close button should be hidden when a transaction is in progress .navigationBarItems(trailing: viewModel.transactionStatus == .idle - ? Button(UserText.subscriptionCloseButton) { self.dismiss() } + ? Button(UserText.subscriptionCloseButton) { viewModel.finalizeSubscriptionFlow() } : nil) } diff --git a/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift b/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift index 611099c78e..09e8b3b0db 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift @@ -25,7 +25,7 @@ import Foundation struct SubscriptionITPView: View { @Environment(\.dismiss) var dismiss - @ObservedObject var viewModel: SubscriptionITPViewModel + @ObservedObject var viewModel = SubscriptionITPViewModel() var body: some View { ZStack { From 939c53778e56d1516245e1a7ebb14e139d9c0aba Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Mon, 5 Feb 2024 16:18:53 +0100 Subject: [PATCH 11/17] Navigation Updates --- DuckDuckGo/SettingsSubscriptionView.swift | 7 +- .../AsyncHeadlessWebView.swift | 42 ++++++-- .../AsyncHeadlessWebViewModel.swift | 2 + .../ViewModel/SubscriptionFlowViewModel.swift | 1 + .../ViewModel/SubscriptionITPViewModel.swift | 53 ++++++++-- .../Views/SubscriptionFlowView.swift | 2 - .../Views/SubscriptionITPView.swift | 97 ++++++++++++++++++- 7 files changed, 179 insertions(+), 25 deletions(-) diff --git a/DuckDuckGo/SettingsSubscriptionView.swift b/DuckDuckGo/SettingsSubscriptionView.swift index ff2c98bf5e..b74e57c6b9 100644 --- a/DuckDuckGo/SettingsSubscriptionView.swift +++ b/DuckDuckGo/SettingsSubscriptionView.swift @@ -78,8 +78,11 @@ struct SettingsSubscriptionView: View { } */ - NavigationLink(destination: SubscriptionITPView(), isActive: $viewModel.shouldNavigateToITP) { - SettingsCellView(label: UserText.settingsPProITRTitle, subtitle: UserText.settingsPProITRSubTitle) + SettingsCellView(label: UserText.settingsPProITRTitle, + subtitle: UserText.settingsPProITRSubTitle, + action: { isShowingITP.toggle() }, isButton: true) + .sheet(isPresented: $isShowingITP) { + SubscriptionITPView() } diff --git a/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebView.swift b/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebView.swift index b8a6d134d3..8be555c236 100644 --- a/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebView.swift +++ b/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebView.swift @@ -81,6 +81,7 @@ struct HeadlessWebView: UIViewRepresentable { func makeUIView(context: Context) -> WKWebView { let configuration = WKWebViewConfiguration() configuration.userContentController = makeUserContentController() + let webView = WKWebView(frame: .zero, configuration: configuration) navigationCoordinator.webView = webView @@ -89,6 +90,13 @@ struct HeadlessWebView: UIViewRepresentable { webView.scrollView.bounces = settings.bounces webView.navigationDelegate = context.coordinator +#if DEBUG + if #available(iOS 16.4, *) { + webView.isInspectable = true + } +#endif + + context.coordinator.setupWebViewObservation(webView) return webView } @@ -114,6 +122,10 @@ struct HeadlessWebView: UIViewRepresentable { var onScroll: ((CGPoint) -> Void)? var onURLChange: ((URL) -> Void)? var onCanGoBack: ((Bool) -> Void)? + var lastURL: URL? + + private var webViewURLObservation: NSKeyValueObservation? + private var webViewCanGoBackObservation: NSKeyValueObservation? init(_ parent: HeadlessWebView, onScroll: ((CGPoint) -> Void)?, @@ -125,20 +137,34 @@ struct HeadlessWebView: UIViewRepresentable { self.onCanGoBack = onCanGoBack } + func setupWebViewObservation(_ webView: WKWebView) { + webViewURLObservation = webView.observe(\.url, options: [.new]) { [weak self] _, change in + if let newURL = change.newValue as? URL { + self?.onURLChange?(newURL) + self?.onCanGoBack?(webView.canGoBack) + } + } + + webViewCanGoBackObservation = webView.observe(\.canGoBack, options: [.new]) { [weak self] _, change in + if let canGoBack = change.newValue { + self?.onCanGoBack?(canGoBack) + } + } + } + func scrollViewDidScroll(_ scrollView: UIScrollView) { let contentOffset = scrollView.contentOffset onScroll?(contentOffset) } - func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { - if let url = navigationAction.request.url, - let onURLChange { - onURLChange(url) - } - if let onCanGoBack { - onCanGoBack(webView.canGoBack) + func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { + if let url = webView.url, url != lastURL { + onURLChange?(url) + lastURL = url + if let onCanGoBack { + onCanGoBack(webView.canGoBack) + } } - decisionHandler(.allow) } } } diff --git a/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebViewModel.swift b/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebViewModel.swift index b4ada5592d..d56b934697 100644 --- a/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebViewModel.swift +++ b/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebViewModel.swift @@ -59,4 +59,6 @@ class AsyncHeadlessWebViewViewModel: ObservableObject { subsequentScrollPositionSubject.send(newPosition) } } + + } diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift index 5f197b1f81..4838050c47 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift @@ -181,6 +181,7 @@ final class SubscriptionFlowViewModel: ObservableObject { } } + @MainActor func navigateBack() async { await webViewModel.navigationCoordinator.goBack() } diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionITPViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionITPViewModel.swift index 63d3a95c41..26d06b3411 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionITPViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionITPViewModel.swift @@ -28,25 +28,62 @@ final class SubscriptionITPViewModel: ObservableObject { let userScript: IdentityTheftRestorationPagesUserScript let subFeature: IdentityTheftRestorationPagesFeature - let purchaseManager: PurchaseManager - let viewTitle = UserText.settingsPProSection - private var cancellables = Set() + var manageITPURL = URL.manageITP + var viewTitle = UserText.settingsPProITRTitle + + enum Constants { + static let navigationBarHideThreshold = 40.0 + } // State variables var itpURL = URL.manageITP - @Published var shouldReloadWebView = false + @Published var webViewModel: AsyncHeadlessWebViewViewModel + @Published var shouldShowNavigationBar: Bool = false + @Published var canNavigateBack: Bool = false + + private var cancellables = Set() + private var canGoBackCancellable: AnyCancellable? init(userScript: IdentityTheftRestorationPagesUserScript = IdentityTheftRestorationPagesUserScript(), - subFeature: IdentityTheftRestorationPagesFeature = IdentityTheftRestorationPagesFeature(), - purchaseManager: PurchaseManager = PurchaseManager.shared) { + subFeature: IdentityTheftRestorationPagesFeature = IdentityTheftRestorationPagesFeature()) { self.userScript = userScript self.subFeature = subFeature - self.purchaseManager = purchaseManager + self.webViewModel = AsyncHeadlessWebViewViewModel(userScript: userScript, + subFeature: subFeature, + settings: AsyncHeadlessWebViewSettings(bounces: false)) } // Observe transaction status - private func setupTransactionObserver() async { + private func setupSubscribers() async { + + webViewModel.$scrollPosition + .receive(on: DispatchQueue.main) + .sink { [weak self] value in + self?.shouldShowNavigationBar = value.y > Constants.navigationBarHideThreshold + } + .store(in: &cancellables) + canGoBackCancellable = webViewModel.$canGoBack + .receive(on: DispatchQueue.main) + .sink { [weak self] value in + self?.canNavigateBack = value + } + } + + func initializeView() { + webViewModel.navigationCoordinator.navigateTo(url: manageITPURL ) + Task { await setupSubscribers() } + } + + @MainActor + private func disableGoBack() { + canGoBackCancellable?.cancel() + canNavigateBack = false + } + + @MainActor + func navigateBack() async { + await webViewModel.navigationCoordinator.goBack() } } diff --git a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift index 603fb5e00f..d8a2064a8d 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift @@ -27,7 +27,6 @@ struct SubscriptionFlowView: View { @Environment(\.dismiss) var dismiss @StateObject var viewModel = SubscriptionFlowViewModel() - @State private var showNestedViews = true @State private var isAlertVisible = false @State private var shouldShowNavigationBar = false @State private var isActive: Bool = false @@ -63,7 +62,6 @@ struct SubscriptionFlowView: View { } .tint(Color(designSystemColor: .textPrimary)) .environment(\.rootPresentationMode, self.$isActive) - } @ViewBuilder diff --git a/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift b/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift index 09e8b3b0db..4d824ff4b6 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift @@ -20,19 +20,106 @@ #if SUBSCRIPTION import SwiftUI import Foundation +import DesignResourcesKit @available(iOS 15.0, *) struct SubscriptionITPView: View { @Environment(\.dismiss) var dismiss - @ObservedObject var viewModel = SubscriptionITPViewModel() + @StateObject var viewModel = SubscriptionITPViewModel() + @State private var shouldShowNavigationBar = false + + enum Constants { + static let daxLogo = "Home" + static let daxLogoSize: CGFloat = 24.0 + static let empty = "" + static let navButtonPadding: CGFloat = 20.0 + static let backButtonImage = "chevron.left" + } var body: some View { - ZStack { - // AsyncHeadlessWebView() - // .background() + NavigationView { + baseView + .toolbar { + ToolbarItemGroup(placement: .navigationBarLeading) { + backButton + } + ToolbarItem(placement: .principal) { + HStack { + Image(Constants.daxLogo) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: Constants.daxLogoSize, height: Constants.daxLogoSize) + Text(viewModel.viewTitle).daxBodyRegular() + } + } + } + .edgesIgnoringSafeArea(.top) + .navigationBarTitleDisplayMode(.inline) + .navigationBarHidden(!viewModel.shouldShowNavigationBar).animation(.easeOut) + + .onAppear(perform: { + setUpAppearances() + viewModel.initializeView() + }) + }.tint(Color(designSystemColor: .textPrimary)) + } + + private var baseView: some View { + ZStack(alignment: .top) { + webView + + // Show a dismiss button while the bar is not visible + // But it should be hidden while performing a transaction + if !shouldShowNavigationBar { + HStack { + backButton.padding(.leading, Constants.navButtonPadding) + Spacer() + dismissButton + } + } + } + } + + @ViewBuilder + private var webView: some View { + + ZStack(alignment: .top) { + AsyncHeadlessWebView(viewModel: viewModel.webViewModel) + .background() } - .navigationTitle(viewModel.viewTitle) + } + + @ViewBuilder + private var backButton: some View { + if viewModel.canNavigateBack { + Button(action: { + Task { await viewModel.navigateBack() } + }, label: { + HStack(spacing: 0) { + Image(systemName: Constants.backButtonImage) + Text(UserText.backButtonTitle) + } + + }) + } + } + + @ViewBuilder + private var dismissButton: some View { + Button(action: { dismiss() }, label: { Text(UserText.subscriptionCloseButton) }) + .padding(Constants.navButtonPadding) + .contentShape(Rectangle()) + .tint(Color(designSystemColor: .textPrimary)) + } + + + private func setUpAppearances() { + let navAppearance = UINavigationBar.appearance() + navAppearance.backgroundColor = UIColor(designSystemColor: .surface) + navAppearance.barTintColor = UIColor(designSystemColor: .surface) + navAppearance.shadowImage = UIImage() + navAppearance.tintColor = UIColor(designSystemColor: .textPrimary) } } #endif From fe59f8b689d096a4bb654934c9c3f09c6819554c Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Mon, 5 Feb 2024 18:48:45 +0100 Subject: [PATCH 12/17] Added Close button to navbar --- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 2 +- DuckDuckGo/Subscription/Views/SubscriptionITPView.swift | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 31faa8367e..4b3ba67da8 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -156,7 +156,7 @@ { "identity" : "trackerradarkit", "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/TrackerRadarKit.git", + "location" : "https://github.com/duckduckgo/TrackerRadarKit", "state" : { "revision" : "a6b7ba151d9dc6684484f3785293875ec01cc1ff", "version" : "1.2.2" diff --git a/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift b/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift index 4d824ff4b6..2281922f7f 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift @@ -78,7 +78,8 @@ struct SubscriptionITPView: View { dismissButton } } - } + // The trailing close button should be hidden when a transaction is in progress + }.navigationBarItems(trailing: Button(UserText.subscriptionCloseButton) { dismiss() }) } @ViewBuilder From ba126613e43f0724a992c8112c8ccf85fc09544a Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Thu, 8 Feb 2024 05:55:58 +0100 Subject: [PATCH 13/17] Share PDF --- DuckDuckGo.xcodeproj/project.pbxproj | 4 + .../AsyncHeadlessWebView.swift | 70 +++++++++++++++++- .../AsyncHeadlessWebViewModel.swift | 2 + .../Extensions/View+TopMostController.swift | 38 ++++++++++ .../Contents.json | 25 +++++++ .../share-dark.pdf | Bin 0 -> 3115 bytes .../SubscriptionShareIcon.imageset/share.pdf | Bin 0 -> 3162 bytes .../ViewModel/SubscriptionITPViewModel.swift | 54 ++++++++++++++ .../Views/SubscriptionITPView.swift | 35 ++++++++- 9 files changed, 222 insertions(+), 6 deletions(-) create mode 100644 DuckDuckGo/Subscription/Extensions/View+TopMostController.swift create mode 100644 DuckDuckGo/Subscription/Subscription.xcassets/SubscriptionShareIcon.imageset/Contents.json create mode 100644 DuckDuckGo/Subscription/Subscription.xcassets/SubscriptionShareIcon.imageset/share-dark.pdf create mode 100644 DuckDuckGo/Subscription/Subscription.xcassets/SubscriptionShareIcon.imageset/share.pdf diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index ceb9ed3abe..242103ef25 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -798,6 +798,7 @@ D668D92D2B696945008E2FF2 /* Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = D668D92C2B696945008E2FF2 /* Subscription.swift */; }; D68DF81C2B58302E0023DBEA /* SubscriptionRestoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68DF81B2B58302E0023DBEA /* SubscriptionRestoreView.swift */; }; D68DF81E2B5830380023DBEA /* SubscriptionRestoreViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68DF81D2B5830380023DBEA /* SubscriptionRestoreViewModel.swift */; }; + D69DBB502B72B1D300156310 /* View+TopMostController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69DBB4F2B72B1D200156310 /* View+TopMostController.swift */; }; D69FBF762B28BE3600B505F1 /* SettingsSubscriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69FBF752B28BE3600B505F1 /* SettingsSubscriptionView.swift */; }; D6D12C9F2B291CA90054390C /* URL+Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12C8B2B291CA90054390C /* URL+Subscription.swift */; }; D6D12CA02B291CA90054390C /* SubscriptionPurchaseEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12C8C2B291CA90054390C /* SubscriptionPurchaseEnvironment.swift */; }; @@ -2461,6 +2462,7 @@ D668D92C2B696945008E2FF2 /* Subscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Subscription.swift; sourceTree = ""; }; D68DF81B2B58302E0023DBEA /* SubscriptionRestoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionRestoreView.swift; sourceTree = ""; }; D68DF81D2B5830380023DBEA /* SubscriptionRestoreViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionRestoreViewModel.swift; sourceTree = ""; }; + D69DBB4F2B72B1D200156310 /* View+TopMostController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+TopMostController.swift"; sourceTree = ""; }; D69FBF752B28BE3600B505F1 /* SettingsSubscriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSubscriptionView.swift; sourceTree = ""; }; D6D12C8B2B291CA90054390C /* URL+Subscription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URL+Subscription.swift"; sourceTree = ""; }; D6D12C8C2B291CA90054390C /* SubscriptionPurchaseEnvironment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionPurchaseEnvironment.swift; sourceTree = ""; }; @@ -4598,6 +4600,7 @@ children = ( D664C7982B289AA000CBFA76 /* WKUserContentController+Handler.swift */, D65CEA6B2B6A871B008A759B /* View+NavbarAppereance.swift */, + D69DBB4F2B72B1D200156310 /* View+TopMostController.swift */, ); path = Extensions; sourceTree = ""; @@ -6774,6 +6777,7 @@ 31C70B5528045E3500FB6AD1 /* SecureVaultErrorReporter.swift in Sources */, F4CE6D1B257EA33C00D0A6AA /* FireButtonAnimator.swift in Sources */, 85582E0029D7409700E9AE35 /* SyncSettingsViewController+PDFRendering.swift in Sources */, + D69DBB502B72B1D300156310 /* View+TopMostController.swift in Sources */, EE0153EF2A70021E002A8B26 /* NetworkProtectionInviteView.swift in Sources */, 9888F77B2224980500C46159 /* FeedbackViewController.swift in Sources */, D6E83C662B23936F006C8AFB /* SettingsDebugView.swift in Sources */, diff --git a/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebView.swift b/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebView.swift index 8be555c236..edc49b65e8 100644 --- a/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebView.swift +++ b/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebView.swift @@ -75,6 +75,8 @@ struct HeadlessWebView: UIViewRepresentable { var onScroll: ((CGPoint) -> Void)? var onURLChange: ((URL) -> Void)? var onCanGoBack: ((Bool) -> Void)? + var onCanGoForward: ((Bool) -> Void)? + var onContentType: ((String) -> Void)? var navigationCoordinator: NavigationCoordinator @@ -103,7 +105,12 @@ struct HeadlessWebView: UIViewRepresentable { func updateUIView(_ uiView: WKWebView, context: Context) {} func makeCoordinator() -> Coordinator { - Coordinator(self, onScroll: onScroll, onURLChange: onURLChange, onCanGoBack: onCanGoBack) + Coordinator(self, + onScroll: onScroll, + onURLChange: onURLChange, + onCanGoBack: onCanGoBack, + onCanGoForward: onCanGoForward, + onContentType: onContentType) } @MainActor @@ -122,19 +129,32 @@ struct HeadlessWebView: UIViewRepresentable { var onScroll: ((CGPoint) -> Void)? var onURLChange: ((URL) -> Void)? var onCanGoBack: ((Bool) -> Void)? - var lastURL: URL? + var onCanGoForward: ((Bool) -> Void)? + var onContentType: ((String) -> Void)? + + private var lastURL: URL? + + enum Constants { + static let contentTypeJS = "document.contentType" + static let externalSchemes = ["tel", "sms", "facetime"] + } private var webViewURLObservation: NSKeyValueObservation? private var webViewCanGoBackObservation: NSKeyValueObservation? + private var webViewCanGoForwardObservation: NSKeyValueObservation? init(_ parent: HeadlessWebView, onScroll: ((CGPoint) -> Void)?, onURLChange: ((URL) -> Void)?, - onCanGoBack: ((Bool) -> Void)?) { + onCanGoBack: ((Bool) -> Void)?, + onCanGoForward: ((Bool) -> Void)?, + onContentType: ((String) -> Void)?) { self.parent = parent self.onScroll = onScroll self.onURLChange = onURLChange self.onCanGoBack = onCanGoBack + self.onCanGoForward = onCanGoForward + self.onContentType = onContentType } func setupWebViewObservation(_ webView: WKWebView) { @@ -150,6 +170,12 @@ struct HeadlessWebView: UIViewRepresentable { self?.onCanGoBack?(canGoBack) } } + + webViewCanGoForwardObservation = webView.observe(\.canGoForward, options: [.new]) { [weak self] _, change in + if let onCanGoForward = change.newValue { + self?.onCanGoForward?(onCanGoForward) + } + } } func scrollViewDidScroll(_ scrollView: UIScrollView) { @@ -164,6 +190,38 @@ struct HeadlessWebView: UIViewRepresentable { if let onCanGoBack { onCanGoBack(webView.canGoBack) } + if let onCanGoForward { + onCanGoForward(webView.canGoForward) + } + } + } + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + webView.evaluateJavaScript(Constants.contentTypeJS) { result, error in + guard error == nil, let contentType = result as? String else { + return + } + self.onContentType?(contentType) + } + } + + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + guard let url = navigationAction.request.url else { + + decisionHandler(.allow) + return + } + + guard let scheme = url.scheme else { + decisionHandler(.cancel) + return + } + + if Constants.externalSchemes.contains(scheme) && UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url, options: [:], completionHandler: nil) + decisionHandler(.cancel) + } else { + decisionHandler(.allow) } } } @@ -187,6 +245,12 @@ struct AsyncHeadlessWebView: View { onCanGoBack: { value in viewModel.canGoBack = value }, + onCanGoForward: { value in + viewModel.canGoForward = value + }, + onContentType: { value in + viewModel.contentType = value + }, navigationCoordinator: viewModel.navigationCoordinator ) .frame(width: geometry.size.width, height: geometry.size.height) diff --git a/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebViewModel.swift b/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebViewModel.swift index d56b934697..78aaaa15f8 100644 --- a/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebViewModel.swift +++ b/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebViewModel.swift @@ -35,6 +35,8 @@ class AsyncHeadlessWebViewViewModel: ObservableObject { @Published var scrollPosition: CGPoint = .zero @Published var url: URL? @Published var canGoBack: Bool = false + @Published var canGoForward: Bool = false + @Published var contentType: String = "" var navigationCoordinator = NavigationCoordinator(webView: nil) diff --git a/DuckDuckGo/Subscription/Extensions/View+TopMostController.swift b/DuckDuckGo/Subscription/Extensions/View+TopMostController.swift new file mode 100644 index 0000000000..398dacb699 --- /dev/null +++ b/DuckDuckGo/Subscription/Extensions/View+TopMostController.swift @@ -0,0 +1,38 @@ +// +// View+TopMostController.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 SwiftUI + +extension View { + func topMostViewController() -> UIViewController? { + guard let keyWindow = UIApplication.shared.connectedScenes + .filter({ $0.activationState == .foregroundActive }) + .compactMap({ $0 as? UIWindowScene }) + .first?.windows + .filter({ $0.isKeyWindow }).first else { + return nil + } + + var topController = keyWindow.rootViewController + while let presentedController = topController?.presentedViewController { + topController = presentedController + } + return topController + } +} diff --git a/DuckDuckGo/Subscription/Subscription.xcassets/SubscriptionShareIcon.imageset/Contents.json b/DuckDuckGo/Subscription/Subscription.xcassets/SubscriptionShareIcon.imageset/Contents.json new file mode 100644 index 0000000000..9c8f34e6e8 --- /dev/null +++ b/DuckDuckGo/Subscription/Subscription.xcassets/SubscriptionShareIcon.imageset/Contents.json @@ -0,0 +1,25 @@ +{ + "images" : [ + { + "filename" : "share.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "share-dark.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/DuckDuckGo/Subscription/Subscription.xcassets/SubscriptionShareIcon.imageset/share-dark.pdf b/DuckDuckGo/Subscription/Subscription.xcassets/SubscriptionShareIcon.imageset/share-dark.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4d647e1794694cde14008eebdc76487774c76c0f GIT binary patch literal 3115 zcmZWrO^+Kl487}D=n^0)5Lu+YfgnJ$+Z07xbnEmM^x)1;cDL}_t?d+Pe|;aN8O`iw z4#xP6L_U&_lwRCizkf|8DMi}q;~#%2t>3)SZ{Mo%a4UZmukp=K&%o1Xx3{SFhj>dr{s8xZ^ad-Vz=8=+h)jBhAO0qlH z=o>~p=8U+oC;O199a|z%XudgeqK5k5JaUCbaBQjWu~Zle1GZ{_hQT*O0I8J9cU~!7 zkvLa75Jk1f(u7{>06X0`nx(i9TeD0o87vuvkd<>Zla^Zyt#1caI-%a-`vpY*OTS2i z_l41SHBcy(kQf-6%t0C%a{tfh3 zIl=@4C{!T3Vmu+usI2sm3@tzbITPWBX3{PYY8_hiX#oM%6w%6BXqn7_Ca-mCO}GbW zy$=OCKyvCMsV{JfW?n%P5p3-sBr=BnX2Z*!PYQM&&OxmiXAeEZZ zZJQRP#=R=GsZxqONx?QHNSS6O%>Fup6gC~i^{Og^l_lI8q@RRA_eZVkgu}#&XX{-9 zE2K-HV?7U&_bCsI69uQ6mm|>OK#h2b90CfA+C&sqPDh7h)k{y~BgVI@CuWE;_F#ar z$5e#LmeF44jv(ePhz`~h$7aGnSoT6o4#OYn1E1zVIpUrW08VtF%;zyJ zP!GK-cv`p8z_PNZ5|GT3Aa$CX?(2~-sC)J48+A~>D!JE9m$vzaCzrW)^JAZUzxeqM z<3hOk_HZ~}&ido;xFF)${Pyo(qu$)??#2oDdAxnty*qx;k2XnDFS;S_lW<&~|=Y4@-nPfDuy!!^?J@_c+8U+I?*x35}KHrwOzq9x#(;LV4J Y|35-~ICbf6*XyHvj+t literal 0 HcmV?d00001 diff --git a/DuckDuckGo/Subscription/Subscription.xcassets/SubscriptionShareIcon.imageset/share.pdf b/DuckDuckGo/Subscription/Subscription.xcassets/SubscriptionShareIcon.imageset/share.pdf new file mode 100644 index 0000000000000000000000000000000000000000..cd5ff551f6c99c0ea1905008f1c1bb6d71f6748f GIT binary patch literal 3162 zcmZWr+in~;41L#E=p{f>AX<^S13`esxu9r^x^~}!K5WMm+r@gf-Q5&vzrKgkjAm>P z410V=A`i(!N-r+oe|$|QDMi}q!ykVtt>3)Sn-Aa4pRUi_^Vt5Q`e)c`YqFI`zkS<& z)VRNqZHgc5|7qOcp6|2+c!h=PbUu#T2eo-W{&PQ!SD)VLi|h4&!%6+EHXlgeo)nqt z!ou0~^18qEZ+`wprX*id)Wd`7%bvfShP_H{4{M4hQm{7Y2jxs6xB=*l6ENx>z`zts z5s)}0OWGNZ(OEB$Wn0Jts{HEK2^tiet867Zdkxk(3;(8+ zP^el@E-6a#WLuk)}>&L+KB^CtKj!{XJ0WsP+}R%R+?26q5m4kVO|$sDbZ zIvJ#y1E>H(&Os`f$SsOuil3+?lcV#|X+{+Qa?sh3i^0<)$cd7iSnFG}!SWOkV|S5J zDW9bhok(sKt6O^%NNuen==Iok3TU=2F}8aXJzHOkl~X3r0u~(kh851az_T~rmXKK^ zsSesIsJft>Q{ws^x&d{;D4@_;-WF^r$%2AK;Ud--i{Kj>J}Wl)cHjt#rTLy&LX4E* zNmkC$IIsk@D$qIZuHVW$QnIdEXC_Wbb_W}M!^p>+5f}DkA5yhrOC$=-H%Cs?P#>H} zuFwdME!91i3PWMQRt?ZF_+|(ol~Vc6E2S$E=V}L{s1{k8&`TX)r~5{;6c=J^mWd^U zC8H3sa*k%wa*Lt$?Vw60)H{5)farhe7isXmFq#xf$^{xl`6F~5k*45X1d=RrTYxx9 zXjh~L3Z)Vf14ENJNF!q|U|7hi$$FkibVQmgISc`@KCMDKW$WF_k~AXcB5S(8gpm+j zw7@1-eTa4D9qI9jzqvB2MBFLSvG`zJ3_B)An1BF<3S?J|C!`sbl^&9z1t=hABK*)y z+66+bLyJBwAfTEeT3HJ%lNr$DwQj8m_W-T;p+E;nPJJZx1y0e-D`+ButsR6MQe}r$ z4;X>TMaJOsU=Inv#)u=(V00I0X|y5{VKiWf zaf&btUBefi3RSNI4hm7!8rxxv!+Q>Swbg{^k}HdScn>~P0@7)~xes*)&1~fum#dwOeCTCT&$xkV;MIwoMCC<6afpR4GNCq+pv8q)f9C<|=grDQr53 z>s3_-D@(X$NIwaK?vGm635SUl&(^yJR!EmX$9f(l?@|fWf^nkYbn|iqIvl7GFOfq) zfl-@?!piCBaIAXiX?(=^cJ;&zQN|t&Q1+OLFxfKN>)a8<+y&9Wdg9nj7zoQ=Xvtyt zLw(@W9EiLeR3cF40We4069T}AE|mE+rUmMuR|QY&RvK7V_EZ9rc@m^fbJIN^34^** zU%pd&^{bNW-E?=GFM4vPYZpa^g8Ti?cUbO^-|zQ_^GSdH9rsB*o8SKZYt);|?d>=L zKaV%}+joa=`m?l2eU=wq!tQUgrK|CD_<9`1la?Fn)nxACus@G`c$91C3|?-Jz;u`( zTtnv=1X?b;Ki=Pfs+I_@^z7&2@D+RM8UGWIm8JN|k_33_4wt|$w&(5caQk@cx8wLl zS)Ei1Uw-QtP#q0;dI-}-9=-2S0?$Vf1{YHC1ai+-Wpv(?RWc-J=zQGX@5ZB&>izH@ y>3DfMJdCgO%ln&GEh(F;!{MwY;F{pg_5FV%*w@?r_IR%0aPd~Xc=6$vkN*S7%7AA8 literal 0 HcmV?d00001 diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionITPViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionITPViewModel.swift index 26d06b3411..a076f08341 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionITPViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionITPViewModel.swift @@ -33,6 +33,7 @@ final class SubscriptionITPViewModel: ObservableObject { enum Constants { static let navigationBarHideThreshold = 40.0 + static let downloadableContent = ["application/pdf"] } // State variables @@ -40,6 +41,10 @@ final class SubscriptionITPViewModel: ObservableObject { @Published var webViewModel: AsyncHeadlessWebViewViewModel @Published var shouldShowNavigationBar: Bool = false @Published var canNavigateBack: Bool = false + @Published var isDownloadableContent: Bool = false + @Published var activityItems: [Any] = [] + @Published var attachmentURL: URL? + private var currentURL: URL? private var cancellables = Set() private var canGoBackCancellable: AnyCancellable? @@ -63,6 +68,35 @@ final class SubscriptionITPViewModel: ObservableObject { } .store(in: &cancellables) + webViewModel.$contentType + .receive(on: DispatchQueue.main) + .sink { [weak self] value in + guard let strongSelf = self else { return } + + if Constants.downloadableContent.contains(value) { + strongSelf.isDownloadableContent = true + guard let url = strongSelf.currentURL else { return } + Task { + // We are using a dummy PDF for testing, as the real PDF's are behind the internal user login + if let downloadURL = URL(string: "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf") { + await strongSelf.downloadAttachment(from: downloadURL) + } + // if let downloadURL = url { + // await strongSelf.downloadAttachment(from: downloadURL) + } + } + } + .store(in: &cancellables) + + webViewModel.$url + .receive(on: DispatchQueue.main) + .sink { [weak self] value in + self?.isDownloadableContent = false + self?.currentURL = value + } + .store(in: &cancellables) + + canGoBackCancellable = webViewModel.$canGoBack .receive(on: DispatchQueue.main) .sink { [weak self] value in @@ -75,6 +109,26 @@ final class SubscriptionITPViewModel: ObservableObject { Task { await setupSubscribers() } } + private func downloadAttachment(from url: URL) async { + if let (temporaryURL, _) = try? await URLSession.shared.download(from: url) { + let fileManager = FileManager.default + + let fileName = url.lastPathComponent + + let tempDirectory = fileManager.temporaryDirectory + let tempFileURL = tempDirectory.appendingPathComponent(fileName) + + if fileManager.fileExists(atPath: tempFileURL.path) { + try? fileManager.removeItem(at: tempFileURL) + } + try? fileManager.moveItem(at: temporaryURL, to: tempFileURL) + DispatchQueue.main.async { + self.attachmentURL = tempFileURL + } + } + } + + @MainActor private func disableGoBack() { canGoBackCancellable?.cancel() diff --git a/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift b/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift index 2281922f7f..7d8c83c7b6 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift @@ -22,12 +22,24 @@ import SwiftUI import Foundation import DesignResourcesKit +struct SubscriptionActivityViewController: UIViewControllerRepresentable { + var activityItems: [Any] + var applicationActivities: [UIActivity]? + + func makeUIViewController(context: Context) -> UIActivityViewController { + return UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities) + } + + func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {} +} + @available(iOS 15.0, *) struct SubscriptionITPView: View { @Environment(\.dismiss) var dismiss @StateObject var viewModel = SubscriptionITPViewModel() @State private var shouldShowNavigationBar = false + @State private var isShowingActivityView = false enum Constants { static let daxLogo = "Home" @@ -35,6 +47,7 @@ struct SubscriptionITPView: View { static let empty = "" static let navButtonPadding: CGFloat = 20.0 static let backButtonImage = "chevron.left" + static let shareImage = "SubscriptionShareIcon" } var body: some View { @@ -53,10 +66,16 @@ struct SubscriptionITPView: View { Text(viewModel.viewTitle).daxBodyRegular() } } + ToolbarItem(placement: .navigationBarTrailing) { + shareButton + } + ToolbarItem(placement: .navigationBarTrailing) { + Button(UserText.subscriptionCloseButton) { dismiss() } + } } .edgesIgnoringSafeArea(.top) .navigationBarTitleDisplayMode(.inline) - .navigationBarHidden(!viewModel.shouldShowNavigationBar).animation(.easeOut) + .navigationBarHidden(!viewModel.shouldShowNavigationBar && !viewModel.isDownloadableContent).animation(.easeOut) .onAppear(perform: { setUpAppearances() @@ -78,8 +97,8 @@ struct SubscriptionITPView: View { dismissButton } } - // The trailing close button should be hidden when a transaction is in progress - }.navigationBarItems(trailing: Button(UserText.subscriptionCloseButton) { dismiss() }) + + } } @ViewBuilder @@ -106,6 +125,16 @@ struct SubscriptionITPView: View { } } + @ViewBuilder + private var shareButton: some View { + if viewModel.isDownloadableContent { + Button(action: { isShowingActivityView = true }, label: { Image(Constants.shareImage) }) + .popover(isPresented: $isShowingActivityView, arrowEdge: .bottom) { + SubscriptionActivityViewController(activityItems: [viewModel.attachmentURL], applicationActivities: nil) + } + } + } + @ViewBuilder private var dismissButton: some View { Button(action: { dismiss() }, label: { Text(UserText.subscriptionCloseButton) }) From 5749f5e485a6cbda67ac55823438fff424099ae8 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Thu, 8 Feb 2024 06:29:32 +0100 Subject: [PATCH 14/17] Support window.open links --- DuckDuckGo.xcodeproj/project.pbxproj | 12 ++ .../AsyncHeadlessWebView.swift | 195 ------------------ .../AsyncHeadlessWebViewModel.swift | 2 +- .../HeadlessWebView.swift | 81 ++++++++ .../HeadlessWebViewCoordinator.swift | 138 +++++++++++++ .../HeadlessWebViewNavCoordinator.swift | 58 ++++++ 6 files changed, 290 insertions(+), 196 deletions(-) create mode 100644 DuckDuckGo/Subscription/AsyncHeadlessWebview/HeadlessWebView.swift create mode 100644 DuckDuckGo/Subscription/AsyncHeadlessWebview/HeadlessWebViewCoordinator.swift create mode 100644 DuckDuckGo/Subscription/AsyncHeadlessWebview/HeadlessWebViewNavCoordinator.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 242103ef25..e77864fdc8 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -834,6 +834,9 @@ D6E83C682B23B6A3006C8AFB /* FontSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E83C672B23B6A3006C8AFB /* FontSettings.swift */; }; D6F93E3C2B4FFA97004C268D /* SubscriptionDebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F93E3B2B4FFA97004C268D /* SubscriptionDebugViewController.swift */; }; D6F93E3E2B50A8A0004C268D /* SubscriptionSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F93E3D2B50A8A0004C268D /* SubscriptionSettingsView.swift */; }; + D6FEB8B12B7498A300C3615F /* HeadlessWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6FEB8B02B7498A300C3615F /* HeadlessWebView.swift */; }; + D6FEB8B32B74990D00C3615F /* HeadlessWebViewNavCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6FEB8B22B74990D00C3615F /* HeadlessWebViewNavCoordinator.swift */; }; + D6FEB8B52B74994000C3615F /* HeadlessWebViewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6FEB8B42B74994000C3615F /* HeadlessWebViewCoordinator.swift */; }; EA39B7E2268A1A35000C62CD /* privacy-reference-tests in Resources */ = {isa = PBXBuildFile; fileRef = EA39B7E1268A1A35000C62CD /* privacy-reference-tests */; }; EAB19EDA268963510015D3EA /* DomainMatchingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB19ED9268963510015D3EA /* DomainMatchingTests.swift */; }; EE0153E12A6EABE0002A8B26 /* NetworkProtectionConvenienceInitialisers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0153E02A6EABE0002A8B26 /* NetworkProtectionConvenienceInitialisers.swift */; }; @@ -2498,6 +2501,9 @@ D6E83C672B23B6A3006C8AFB /* FontSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontSettings.swift; sourceTree = ""; }; D6F93E3B2B4FFA97004C268D /* SubscriptionDebugViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionDebugViewController.swift; sourceTree = ""; }; D6F93E3D2B50A8A0004C268D /* SubscriptionSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionSettingsView.swift; sourceTree = ""; }; + D6FEB8B02B7498A300C3615F /* HeadlessWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlessWebView.swift; sourceTree = ""; }; + D6FEB8B22B74990D00C3615F /* HeadlessWebViewNavCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlessWebViewNavCoordinator.swift; sourceTree = ""; }; + D6FEB8B42B74994000C3615F /* HeadlessWebViewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlessWebViewCoordinator.swift; sourceTree = ""; }; EA39B7E1268A1A35000C62CD /* privacy-reference-tests */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = "privacy-reference-tests"; path = "submodules/privacy-reference-tests"; sourceTree = SOURCE_ROOT; }; EAB19ED9268963510015D3EA /* DomainMatchingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DomainMatchingTests.swift; sourceTree = ""; }; EE0153E02A6EABE0002A8B26 /* NetworkProtectionConvenienceInitialisers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionConvenienceInitialisers.swift; sourceTree = ""; }; @@ -4690,6 +4696,9 @@ children = ( D6D95CE22B6D9F8800960317 /* AsyncHeadlessWebViewModel.swift */, D664C7AF2B289AA000CBFA76 /* AsyncHeadlessWebView.swift */, + D6FEB8B02B7498A300C3615F /* HeadlessWebView.swift */, + D6FEB8B22B74990D00C3615F /* HeadlessWebViewNavCoordinator.swift */, + D6FEB8B42B74994000C3615F /* HeadlessWebViewCoordinator.swift */, ); path = AsyncHeadlessWebview; sourceTree = ""; @@ -6546,6 +6555,7 @@ files = ( EE4FB1862A28CE7200E5CBA7 /* NetworkProtectionStatusView.swift in Sources */, C17B59592A03AAD30055F2D1 /* PasswordGenerationPromptViewModel.swift in Sources */, + D6FEB8B52B74994000C3615F /* HeadlessWebViewCoordinator.swift in Sources */, 8528AE81212F15D600D0BD74 /* AppRatingPrompt.xcdatamodeld in Sources */, 1E24295E293F57FA00584836 /* LottieView.swift in Sources */, 8577A1C5255D2C0D00D43FCD /* HitTestingToolbar.swift in Sources */, @@ -6589,6 +6599,7 @@ 9874F9EE2187AFCE00CAF33D /* Themable.swift in Sources */, F44D279E27F331BB0037F371 /* AutofillLoginPromptViewModel.swift in Sources */, 3151F0F02735802800226F58 /* VoiceSearchViewController.swift in Sources */, + D6FEB8B32B74990D00C3615F /* HeadlessWebViewNavCoordinator.swift in Sources */, 85BDC310243359040053DB07 /* FindInPageUserScript.swift in Sources */, F1DE78581E5CAE350058895A /* TabViewGridCell.swift in Sources */, 984D035824ACCC6F0066CFB8 /* TabViewListCell.swift in Sources */, @@ -6860,6 +6871,7 @@ 980891A222369ADB00313A70 /* FeedbackUserText.swift in Sources */, 4BCD14692B05BDD5000B1E4C /* AppDelegate+Waitlists.swift in Sources */, 988F3DD3237DE8D900AEE34C /* ForgetDataAlert.swift in Sources */, + D6FEB8B12B7498A300C3615F /* HeadlessWebView.swift in Sources */, 850ABD012AC3961100A733DF /* MainViewController+Segues.swift in Sources */, 9817C9C321EF594700884F65 /* AutoClear.swift in Sources */, 310C4B47281B60E300BA79A9 /* AutofillLoginDetailsViewModel.swift in Sources */, diff --git a/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebView.swift b/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebView.swift index edc49b65e8..74b5224f9d 100644 --- a/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebView.swift +++ b/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebView.swift @@ -32,201 +32,6 @@ struct AsyncHeadlessWebViewSettings { } } -class NavigationCoordinator { - weak var webView: WKWebView? - - init(webView: WKWebView?) { - self.webView = webView - } - - func reload() async { - _ = await MainActor.run { - self.webView?.reload() - } - } - - func navigateTo(url: URL) { - guard let webView else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0) { - DefaultUserAgentManager.shared.update(webView: webView, isDesktop: false, url: url) - webView.load(URLRequest(url: url)) - } - } - - func goBack() async { - guard await webView?.canGoBack == true else { return } - _ = await MainActor.run { - self.webView?.goBack() - } - } - - func goForward() async { - guard await webView?.canGoForward == true else { return } - _ = await MainActor.run { - self.webView?.goForward() - } - } -} - -struct HeadlessWebView: UIViewRepresentable { - let userScript: UserScriptMessaging? - let subFeature: Subfeature? - let settings: AsyncHeadlessWebViewSettings - var onScroll: ((CGPoint) -> Void)? - var onURLChange: ((URL) -> Void)? - var onCanGoBack: ((Bool) -> Void)? - var onCanGoForward: ((Bool) -> Void)? - var onContentType: ((String) -> Void)? - var navigationCoordinator: NavigationCoordinator - - - func makeUIView(context: Context) -> WKWebView { - let configuration = WKWebViewConfiguration() - configuration.userContentController = makeUserContentController() - - let webView = WKWebView(frame: .zero, configuration: configuration) - - navigationCoordinator.webView = webView - webView.uiDelegate = context.coordinator - webView.scrollView.delegate = context.coordinator - webView.scrollView.bounces = settings.bounces - webView.navigationDelegate = context.coordinator - -#if DEBUG - if #available(iOS 16.4, *) { - webView.isInspectable = true - } -#endif - - context.coordinator.setupWebViewObservation(webView) - return webView - } - - func updateUIView(_ uiView: WKWebView, context: Context) {} - - func makeCoordinator() -> Coordinator { - Coordinator(self, - onScroll: onScroll, - onURLChange: onURLChange, - onCanGoBack: onCanGoBack, - onCanGoForward: onCanGoForward, - onContentType: onContentType) - } - - @MainActor - private func makeUserContentController() -> WKUserContentController { - let userContentController = WKUserContentController() - if let userScript, let subFeature { - userContentController.addUserScript(userScript.makeWKUserScriptSync()) - userContentController.addHandler(userScript) - userScript.registerSubfeature(delegate: subFeature) - } - return userContentController - } - - class Coordinator: NSObject, WKUIDelegate, UIScrollViewDelegate, WKNavigationDelegate { - var parent: HeadlessWebView - var onScroll: ((CGPoint) -> Void)? - var onURLChange: ((URL) -> Void)? - var onCanGoBack: ((Bool) -> Void)? - var onCanGoForward: ((Bool) -> Void)? - var onContentType: ((String) -> Void)? - - private var lastURL: URL? - - enum Constants { - static let contentTypeJS = "document.contentType" - static let externalSchemes = ["tel", "sms", "facetime"] - } - - private var webViewURLObservation: NSKeyValueObservation? - private var webViewCanGoBackObservation: NSKeyValueObservation? - private var webViewCanGoForwardObservation: NSKeyValueObservation? - - init(_ parent: HeadlessWebView, - onScroll: ((CGPoint) -> Void)?, - onURLChange: ((URL) -> Void)?, - onCanGoBack: ((Bool) -> Void)?, - onCanGoForward: ((Bool) -> Void)?, - onContentType: ((String) -> Void)?) { - self.parent = parent - self.onScroll = onScroll - self.onURLChange = onURLChange - self.onCanGoBack = onCanGoBack - self.onCanGoForward = onCanGoForward - self.onContentType = onContentType - } - - func setupWebViewObservation(_ webView: WKWebView) { - webViewURLObservation = webView.observe(\.url, options: [.new]) { [weak self] _, change in - if let newURL = change.newValue as? URL { - self?.onURLChange?(newURL) - self?.onCanGoBack?(webView.canGoBack) - } - } - - webViewCanGoBackObservation = webView.observe(\.canGoBack, options: [.new]) { [weak self] _, change in - if let canGoBack = change.newValue { - self?.onCanGoBack?(canGoBack) - } - } - - webViewCanGoForwardObservation = webView.observe(\.canGoForward, options: [.new]) { [weak self] _, change in - if let onCanGoForward = change.newValue { - self?.onCanGoForward?(onCanGoForward) - } - } - } - - func scrollViewDidScroll(_ scrollView: UIScrollView) { - let contentOffset = scrollView.contentOffset - onScroll?(contentOffset) - } - - func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { - if let url = webView.url, url != lastURL { - onURLChange?(url) - lastURL = url - if let onCanGoBack { - onCanGoBack(webView.canGoBack) - } - if let onCanGoForward { - onCanGoForward(webView.canGoForward) - } - } - } - - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - webView.evaluateJavaScript(Constants.contentTypeJS) { result, error in - guard error == nil, let contentType = result as? String else { - return - } - self.onContentType?(contentType) - } - } - - func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { - guard let url = navigationAction.request.url else { - - decisionHandler(.allow) - return - } - - guard let scheme = url.scheme else { - decisionHandler(.cancel) - return - } - - if Constants.externalSchemes.contains(scheme) && UIApplication.shared.canOpenURL(url) { - UIApplication.shared.open(url, options: [:], completionHandler: nil) - decisionHandler(.cancel) - } else { - decisionHandler(.allow) - } - } - } -} - struct AsyncHeadlessWebView: View { @StateObject var viewModel: AsyncHeadlessWebViewViewModel diff --git a/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebViewModel.swift b/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebViewModel.swift index 78aaaa15f8..7e4fe8185f 100644 --- a/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebViewModel.swift +++ b/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebViewModel.swift @@ -38,7 +38,7 @@ class AsyncHeadlessWebViewViewModel: ObservableObject { @Published var canGoForward: Bool = false @Published var contentType: String = "" - var navigationCoordinator = NavigationCoordinator(webView: nil) + var navigationCoordinator = HeadlessWebViewNavCoordinator(webView: nil) init(userScript: UserScriptMessaging?, subFeature: Subfeature?, settings: AsyncHeadlessWebViewSettings) { self.userScript = userScript diff --git a/DuckDuckGo/Subscription/AsyncHeadlessWebview/HeadlessWebView.swift b/DuckDuckGo/Subscription/AsyncHeadlessWebview/HeadlessWebView.swift new file mode 100644 index 0000000000..5aa39ae262 --- /dev/null +++ b/DuckDuckGo/Subscription/AsyncHeadlessWebview/HeadlessWebView.swift @@ -0,0 +1,81 @@ +// +// HeadlessWebView.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 +import SwiftUI +import WebKit +import UserScript + +struct HeadlessWebView: UIViewRepresentable { + let userScript: UserScriptMessaging? + let subFeature: Subfeature? + let settings: AsyncHeadlessWebViewSettings + var onScroll: ((CGPoint) -> Void)? + var onURLChange: ((URL) -> Void)? + var onCanGoBack: ((Bool) -> Void)? + var onCanGoForward: ((Bool) -> Void)? + var onContentType: ((String) -> Void)? + var navigationCoordinator: HeadlessWebViewNavCoordinator + + + func makeUIView(context: Context) -> WKWebView { + let configuration = WKWebViewConfiguration() + configuration.userContentController = makeUserContentController() + + let webView = WKWebView(frame: .zero, configuration: configuration) + + navigationCoordinator.webView = webView + webView.uiDelegate = context.coordinator + webView.scrollView.delegate = context.coordinator + webView.scrollView.bounces = settings.bounces + webView.navigationDelegate = context.coordinator + +#if DEBUG + if #available(iOS 16.4, *) { + webView.isInspectable = true + } +#endif + + context.coordinator.setupWebViewObservation(webView) + return webView + } + + func updateUIView(_ uiView: WKWebView, context: Context) {} + + func makeCoordinator() -> HeadlessWebViewCoordinator { + HeadlessWebViewCoordinator(self, + onScroll: onScroll, + onURLChange: onURLChange, + onCanGoBack: onCanGoBack, + onCanGoForward: onCanGoForward, + onContentType: onContentType) + } + + @MainActor + private func makeUserContentController() -> WKUserContentController { + let userContentController = WKUserContentController() + if let userScript, let subFeature { + userContentController.addUserScript(userScript.makeWKUserScriptSync()) + userContentController.addHandler(userScript) + userScript.registerSubfeature(delegate: subFeature) + } + return userContentController + } + +} diff --git a/DuckDuckGo/Subscription/AsyncHeadlessWebview/HeadlessWebViewCoordinator.swift b/DuckDuckGo/Subscription/AsyncHeadlessWebview/HeadlessWebViewCoordinator.swift new file mode 100644 index 0000000000..e7262ae0a1 --- /dev/null +++ b/DuckDuckGo/Subscription/AsyncHeadlessWebview/HeadlessWebViewCoordinator.swift @@ -0,0 +1,138 @@ +// +// HeadlessWebviewCoordinator.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 +import WebKit + +final class HeadlessWebViewCoordinator: NSObject { + var parent: HeadlessWebView + var onScroll: ((CGPoint) -> Void)? + var onURLChange: ((URL) -> Void)? + var onCanGoBack: ((Bool) -> Void)? + var onCanGoForward: ((Bool) -> Void)? + var onContentType: ((String) -> Void)? + + private var lastURL: URL? + + enum Constants { + static let contentTypeJS = "document.contentType" + static let externalSchemes = ["tel", "sms", "facetime"] + } + + private var webViewURLObservation: NSKeyValueObservation? + private var webViewCanGoBackObservation: NSKeyValueObservation? + private var webViewCanGoForwardObservation: NSKeyValueObservation? + + init(_ parent: HeadlessWebView, + onScroll: ((CGPoint) -> Void)?, + onURLChange: ((URL) -> Void)?, + onCanGoBack: ((Bool) -> Void)?, + onCanGoForward: ((Bool) -> Void)?, + onContentType: ((String) -> Void)?) { + self.parent = parent + self.onScroll = onScroll + self.onURLChange = onURLChange + self.onCanGoBack = onCanGoBack + self.onCanGoForward = onCanGoForward + self.onContentType = onContentType + } + + func setupWebViewObservation(_ webView: WKWebView) { + webViewURLObservation = webView.observe(\.url, options: [.new]) { [weak self] _, change in + if let newURL = change.newValue as? URL { + self?.onURLChange?(newURL) + self?.onCanGoBack?(webView.canGoBack) + } + } + + webViewCanGoBackObservation = webView.observe(\.canGoBack, options: [.new]) { [weak self] _, change in + if let canGoBack = change.newValue { + self?.onCanGoBack?(canGoBack) + } + } + + webViewCanGoForwardObservation = webView.observe(\.canGoForward, options: [.new]) { [weak self] _, change in + if let onCanGoForward = change.newValue { + self?.onCanGoForward?(onCanGoForward) + } + } + } + +} + +extension HeadlessWebViewCoordinator: WKUIDelegate {} + +extension HeadlessWebViewCoordinator: UIScrollViewDelegate { + func scrollViewDidScroll(_ scrollView: UIScrollView) { + let contentOffset = scrollView.contentOffset + onScroll?(contentOffset) + } +} + +extension HeadlessWebViewCoordinator: WKNavigationDelegate { + + func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? { + // Force all requests for new windows or frame to be loaded in the View Itself (No popups or new windows) + webView.load(navigationAction.request) + return nil + } + + func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { + if let url = webView.url, url != lastURL { + onURLChange?(url) + lastURL = url + if let onCanGoBack { + onCanGoBack(webView.canGoBack) + } + if let onCanGoForward { + onCanGoForward(webView.canGoForward) + } + } + } + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + webView.evaluateJavaScript(Constants.contentTypeJS) { result, error in + guard error == nil, let contentType = result as? String else { + return + } + self.onContentType?(contentType) + } + } + + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + guard let url = navigationAction.request.url else { + + decisionHandler(.allow) + return + } + + guard let scheme = url.scheme else { + decisionHandler(.cancel) + return + } + + if Constants.externalSchemes.contains(scheme) && UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url, options: [:], completionHandler: nil) + decisionHandler(.cancel) + } else { + decisionHandler(.allow) + } + } + +} diff --git a/DuckDuckGo/Subscription/AsyncHeadlessWebview/HeadlessWebViewNavCoordinator.swift b/DuckDuckGo/Subscription/AsyncHeadlessWebview/HeadlessWebViewNavCoordinator.swift new file mode 100644 index 0000000000..94f7695be9 --- /dev/null +++ b/DuckDuckGo/Subscription/AsyncHeadlessWebview/HeadlessWebViewNavCoordinator.swift @@ -0,0 +1,58 @@ +// +// HeadlessWebViewNavCoordinator.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 +import WebKit +import Core + +final class HeadlessWebViewNavCoordinator { + weak var webView: WKWebView? + + init(webView: WKWebView?) { + self.webView = webView + } + + func reload() async { + _ = await MainActor.run { + self.webView?.reload() + } + } + + func navigateTo(url: URL) { + guard let webView else { return } + DispatchQueue.main.asyncAfter(deadline: .now() + 0) { + DefaultUserAgentManager.shared.update(webView: webView, isDesktop: false, url: url) + webView.load(URLRequest(url: url)) + } + } + + func goBack() async { + guard await webView?.canGoBack == true else { return } + _ = await MainActor.run { + self.webView?.goBack() + } + } + + func goForward() async { + guard await webView?.canGoForward == true else { return } + _ = await MainActor.run { + self.webView?.goForward() + } + } +} From 5fe2ff516b93b35032d6cce1ec12ca47db7addf1 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Thu, 8 Feb 2024 06:35:52 +0100 Subject: [PATCH 15/17] Cleanup --- DuckDuckGo.xcodeproj/project.pbxproj | 4 --- .../AsyncHeadlessWebViewModel.swift | 2 +- .../Extensions/View+NavbarAppereance.swift | 27 ------------------- .../Extensions/View+TopMostController.swift | 2 ++ 4 files changed, 3 insertions(+), 32 deletions(-) delete mode 100644 DuckDuckGo/Subscription/Extensions/View+NavbarAppereance.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index e77864fdc8..ef6bd11c9b 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -779,7 +779,6 @@ D64648AD2B59936B0033090B /* SubscriptionEmailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64648AC2B59936B0033090B /* SubscriptionEmailView.swift */; }; D64648AF2B5993890033090B /* SubscriptionEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64648AE2B5993890033090B /* SubscriptionEmailViewModel.swift */; }; D652498E2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D652498D2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift */; }; - D65CEA6C2B6A871B008A759B /* View+NavbarAppereance.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65CEA6B2B6A871B008A759B /* View+NavbarAppereance.swift */; }; D65CEA6E2B6ABA29008A759B /* NavigationBarModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65CEA6D2B6ABA29008A759B /* NavigationBarModifier.swift */; }; D65CEA702B6AC6C9008A759B /* Subscription.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D65CEA6F2B6AC6C9008A759B /* Subscription.xcassets */; }; D664C7B62B289AA200CBFA76 /* SubscriptionFlowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664C7942B289AA000CBFA76 /* SubscriptionFlowViewModel.swift */; }; @@ -2446,7 +2445,6 @@ D64648AC2B59936B0033090B /* SubscriptionEmailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionEmailView.swift; sourceTree = ""; }; D64648AE2B5993890033090B /* SubscriptionEmailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionEmailViewModel.swift; sourceTree = ""; }; D652498D2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionSettingsViewModel.swift; sourceTree = ""; }; - D65CEA6B2B6A871B008A759B /* View+NavbarAppereance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+NavbarAppereance.swift"; sourceTree = ""; }; D65CEA6D2B6ABA29008A759B /* NavigationBarModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarModifier.swift; sourceTree = ""; }; D65CEA6F2B6AC6C9008A759B /* Subscription.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Subscription.xcassets; sourceTree = ""; }; D664C7942B289AA000CBFA76 /* SubscriptionFlowViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionFlowViewModel.swift; sourceTree = ""; }; @@ -4605,7 +4603,6 @@ isa = PBXGroup; children = ( D664C7982B289AA000CBFA76 /* WKUserContentController+Handler.swift */, - D65CEA6B2B6A871B008A759B /* View+NavbarAppereance.swift */, D69DBB4F2B72B1D200156310 /* View+TopMostController.swift */, ); path = Extensions; @@ -6922,7 +6919,6 @@ 8540BBA22440857A00017FE4 /* PreserveLoginsWorker.swift in Sources */, 85DFEDF924CF3D0E00973FE7 /* TabsBarCell.swift in Sources */, F17922DB1E717C8D006E3D97 /* Suggestion.swift in Sources */, - D65CEA6C2B6A871B008A759B /* View+NavbarAppereance.swift in Sources */, 020108A729A6ABF600644F9D /* AppTPToggleView.swift in Sources */, 02A54A982A093126000C8FED /* AppTPHomeViewModel.swift in Sources */, F1617C191E573EA800DEDCAF /* TabSwitcherDelegate.swift in Sources */, diff --git a/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebViewModel.swift b/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebViewModel.swift index 7e4fe8185f..304908891c 100644 --- a/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebViewModel.swift +++ b/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebViewModel.swift @@ -21,7 +21,7 @@ import UserScript import Core import Combine -class AsyncHeadlessWebViewViewModel: ObservableObject { +final class AsyncHeadlessWebViewViewModel: ObservableObject { let userScript: UserScriptMessaging? let subFeature: Subfeature? let settings: AsyncHeadlessWebViewSettings diff --git a/DuckDuckGo/Subscription/Extensions/View+NavbarAppereance.swift b/DuckDuckGo/Subscription/Extensions/View+NavbarAppereance.swift deleted file mode 100644 index 2d3f2cd2e9..0000000000 --- a/DuckDuckGo/Subscription/Extensions/View+NavbarAppereance.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// View+NavbarAppereance.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 -import SwiftUI -import UIKit - -extension View { - - -} diff --git a/DuckDuckGo/Subscription/Extensions/View+TopMostController.swift b/DuckDuckGo/Subscription/Extensions/View+TopMostController.swift index 398dacb699..2d26949767 100644 --- a/DuckDuckGo/Subscription/Extensions/View+TopMostController.swift +++ b/DuckDuckGo/Subscription/Extensions/View+TopMostController.swift @@ -20,6 +20,8 @@ import SwiftUI extension View { + + // Grabs the topMost controller so we can properly present sheets anywhere func topMostViewController() -> UIViewController? { guard let keyWindow = UIApplication.shared.connectedScenes .filter({ $0.activationState == .foregroundActive }) From db47486734ee49b87f7a5ac6b1753ce5301b4938 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Fri, 9 Feb 2024 12:22:29 +0100 Subject: [PATCH 16/17] PR Feedback --- DuckDuckGo.xcodeproj/project.pbxproj | 4 -- DuckDuckGo/SettingsViewModel.swift | 4 +- .../HeadlessWebViewCoordinator.swift | 2 +- ...identityTheftRestorationPagesFeature.swift | 5 +-- .../ViewModel/SubscriptionFlowViewModel.swift | 6 +-- .../Views/NavigationBarModifier.swift | 39 ------------------- 6 files changed, 8 insertions(+), 52 deletions(-) delete mode 100644 DuckDuckGo/Subscription/Views/NavigationBarModifier.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 818e0f582d..c89d3b9ec1 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -774,7 +774,6 @@ D64648AD2B59936B0033090B /* SubscriptionEmailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64648AC2B59936B0033090B /* SubscriptionEmailView.swift */; }; D64648AF2B5993890033090B /* SubscriptionEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64648AE2B5993890033090B /* SubscriptionEmailViewModel.swift */; }; D652498E2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D652498D2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift */; }; - D65CEA6E2B6ABA29008A759B /* NavigationBarModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65CEA6D2B6ABA29008A759B /* NavigationBarModifier.swift */; }; D65CEA702B6AC6C9008A759B /* Subscription.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D65CEA6F2B6AC6C9008A759B /* Subscription.xcassets */; }; D664C7B62B289AA200CBFA76 /* SubscriptionFlowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664C7942B289AA000CBFA76 /* SubscriptionFlowViewModel.swift */; }; D664C7B72B289AA200CBFA76 /* Subscription.storekit in Resources */ = {isa = PBXBuildFile; fileRef = D664C7952B289AA000CBFA76 /* Subscription.storekit */; }; @@ -2435,7 +2434,6 @@ D64648AC2B59936B0033090B /* SubscriptionEmailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionEmailView.swift; sourceTree = ""; }; D64648AE2B5993890033090B /* SubscriptionEmailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionEmailViewModel.swift; sourceTree = ""; }; D652498D2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionSettingsViewModel.swift; sourceTree = ""; }; - D65CEA6D2B6ABA29008A759B /* NavigationBarModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarModifier.swift; sourceTree = ""; }; D65CEA6F2B6AC6C9008A759B /* Subscription.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Subscription.xcassets; sourceTree = ""; }; D664C7942B289AA000CBFA76 /* SubscriptionFlowViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionFlowViewModel.swift; sourceTree = ""; }; D664C7952B289AA000CBFA76 /* Subscription.storekit */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Subscription.storekit; sourceTree = ""; }; @@ -4579,7 +4577,6 @@ D64648AC2B59936B0033090B /* SubscriptionEmailView.swift */, D668D9242B693778008E2FF2 /* SubscriptionITPView.swift */, D6F93E3D2B50A8A0004C268D /* SubscriptionSettingsView.swift */, - D65CEA6D2B6ABA29008A759B /* NavigationBarModifier.swift */, D6D95CE02B6D52DA00960317 /* RootPresentationMode.swift */, ); path = Views; @@ -6848,7 +6845,6 @@ 8563A03C1F9288D600F04442 /* BrowserChromeManager.swift in Sources */, 980891A32237146B00313A70 /* Feedback.swift in Sources */, F1D796F01E7B07610019D451 /* BookmarksViewControllerCells.swift in Sources */, - D65CEA6E2B6ABA29008A759B /* NavigationBarModifier.swift in Sources */, 85058369219F424500ED4EDB /* UIColorExtension.swift in Sources */, D6E83C312B1EA309006C8AFB /* SettingsCell.swift in Sources */, 85058368219C49E000ED4EDB /* HomeViewSectionRenderers.swift in Sources */, diff --git a/DuckDuckGo/SettingsViewModel.swift b/DuckDuckGo/SettingsViewModel.swift index 1993226671..c379365941 100644 --- a/DuckDuckGo/SettingsViewModel.swift +++ b/DuckDuckGo/SettingsViewModel.swift @@ -91,7 +91,7 @@ final class SettingsViewModel: ObservableObject { // Used to automatically navigate on Appear to a specific section enum SettingsSection: String { - case none, netP, dbp, itp + case none, netP, dbp, itr } @Published var onAppearNavigationTarget: SettingsSection @@ -428,7 +428,7 @@ extension SettingsViewModel { self.presentLegacyView(.netP) case .dbp: self.shouldNavigateToDBP = true - case .itp: + case .itr: self.shouldNavigateToITP = true default: break diff --git a/DuckDuckGo/Subscription/AsyncHeadlessWebview/HeadlessWebViewCoordinator.swift b/DuckDuckGo/Subscription/AsyncHeadlessWebview/HeadlessWebViewCoordinator.swift index e7262ae0a1..a2fb375dc7 100644 --- a/DuckDuckGo/Subscription/AsyncHeadlessWebview/HeadlessWebViewCoordinator.swift +++ b/DuckDuckGo/Subscription/AsyncHeadlessWebview/HeadlessWebViewCoordinator.swift @@ -1,5 +1,5 @@ // -// HeadlessWebviewCoordinator.swift +// HeadlessWebViewCoordinator.swift // DuckDuckGo // // Copyright © 2024 DuckDuckGo. All rights reserved. diff --git a/DuckDuckGo/Subscription/UserScripts/identityTheftRestorationPagesFeature.swift b/DuckDuckGo/Subscription/UserScripts/identityTheftRestorationPagesFeature.swift index d7c2f8ecf1..28aa9881f0 100644 --- a/DuckDuckGo/Subscription/UserScripts/identityTheftRestorationPagesFeature.swift +++ b/DuckDuckGo/Subscription/UserScripts/identityTheftRestorationPagesFeature.swift @@ -1,5 +1,5 @@ // -// identityTheftRestorationPagesFeature.swift +// IdentityTheftRestorationPagesFeature.swift // DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. @@ -31,7 +31,6 @@ final class IdentityTheftRestorationPagesFeature: Subfeature, ObservableObject { struct Constants { static let featureName = "useIdentityTheftRestoration" static let os = "ios" - static let empty = "" } struct OriginDomains { @@ -67,7 +66,7 @@ final class IdentityTheftRestorationPagesFeature: Subfeature, ObservableObject { } func getAccessToken(params: Any, original: WKScriptMessage) async throws -> Encodable? { - let authToken = AccountManager().authToken ?? Constants.empty + let authToken = AccountManager().authToken ?? "" return Subscription(token: authToken) } diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift index 4838050c47..084717c1b3 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift @@ -43,7 +43,7 @@ final class SubscriptionFlowViewModel: ObservableObject { enum FeatureName { static let netP = "vpn" - static let itp = "identity-theft-restoration" + static let itr = "identity-theft-restoration" static let dbp = "personal-information-removal" } @@ -105,8 +105,8 @@ final class SubscriptionFlowViewModel: ObservableObject { switch value?.feature { case FeatureName.netP: self?.selectedFeature = .netP - case FeatureName.itp: - self?.selectedFeature = .itp + case FeatureName.itr: + self?.selectedFeature = .itr case FeatureName.dbp: self?.selectedFeature = .dbp default: diff --git a/DuckDuckGo/Subscription/Views/NavigationBarModifier.swift b/DuckDuckGo/Subscription/Views/NavigationBarModifier.swift deleted file mode 100644 index a340cfe265..0000000000 --- a/DuckDuckGo/Subscription/Views/NavigationBarModifier.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// NavigationBarModifier.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 SwiftUI -import DesignResourcesKit - - -struct NavigationBarModifier: ViewModifier { - - func body(content: Content) -> some View { - content - .navigationBarTitleDisplayMode(.inline) - - } -} - -// Extension to easily apply the custom modifier - -extension View { - func applyNavigationStyle() -> some View { - self.modifier(NavigationBarModifier()) - } -} From 2555d4fa1329a52a80be62ecb80bec791e96d7ec Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Fri, 9 Feb 2024 12:26:55 +0100 Subject: [PATCH 17/17] Fix file name --- DuckDuckGo.xcodeproj/project.pbxproj | 8 ++++---- ...e.swift => IdentityTheftRestorationPagesFeature.swift} | 0 2 files changed, 4 insertions(+), 4 deletions(-) rename DuckDuckGo/Subscription/UserScripts/{identityTheftRestorationPagesFeature.swift => IdentityTheftRestorationPagesFeature.swift} (100%) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index c89d3b9ec1..bbd10ceb25 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -787,7 +787,7 @@ D668D9252B693778008E2FF2 /* SubscriptionITPView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D668D9242B693778008E2FF2 /* SubscriptionITPView.swift */; }; D668D9272B6937D2008E2FF2 /* SubscriptionITPViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D668D9262B6937D2008E2FF2 /* SubscriptionITPViewModel.swift */; }; D668D9292B69681C008E2FF2 /* IdentityTheftRestorationPagesUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = D668D9282B69681C008E2FF2 /* IdentityTheftRestorationPagesUserScript.swift */; }; - D668D92B2B696840008E2FF2 /* identityTheftRestorationPagesFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = D668D92A2B696840008E2FF2 /* identityTheftRestorationPagesFeature.swift */; }; + D668D92B2B696840008E2FF2 /* IdentityTheftRestorationPagesFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = D668D92A2B696840008E2FF2 /* IdentityTheftRestorationPagesFeature.swift */; }; D668D92D2B696945008E2FF2 /* Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = D668D92C2B696945008E2FF2 /* Subscription.swift */; }; D68DF81C2B58302E0023DBEA /* SubscriptionRestoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68DF81B2B58302E0023DBEA /* SubscriptionRestoreView.swift */; }; D68DF81E2B5830380023DBEA /* SubscriptionRestoreViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68DF81D2B5830380023DBEA /* SubscriptionRestoreViewModel.swift */; }; @@ -2447,7 +2447,7 @@ D668D9242B693778008E2FF2 /* SubscriptionITPView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionITPView.swift; sourceTree = ""; }; D668D9262B6937D2008E2FF2 /* SubscriptionITPViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionITPViewModel.swift; sourceTree = ""; }; D668D9282B69681C008E2FF2 /* IdentityTheftRestorationPagesUserScript.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentityTheftRestorationPagesUserScript.swift; sourceTree = ""; }; - D668D92A2B696840008E2FF2 /* identityTheftRestorationPagesFeature.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = identityTheftRestorationPagesFeature.swift; sourceTree = ""; }; + D668D92A2B696840008E2FF2 /* IdentityTheftRestorationPagesFeature.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentityTheftRestorationPagesFeature.swift; sourceTree = ""; }; D668D92C2B696945008E2FF2 /* Subscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Subscription.swift; sourceTree = ""; }; D68DF81B2B58302E0023DBEA /* SubscriptionRestoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionRestoreView.swift; sourceTree = ""; }; D68DF81D2B5830380023DBEA /* SubscriptionRestoreViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionRestoreViewModel.swift; sourceTree = ""; }; @@ -4588,7 +4588,7 @@ D668D92C2B696945008E2FF2 /* Subscription.swift */, D668D9282B69681C008E2FF2 /* IdentityTheftRestorationPagesUserScript.swift */, D664C7B32B289AA000CBFA76 /* SubscriptionPagesUserScript.swift */, - D668D92A2B696840008E2FF2 /* identityTheftRestorationPagesFeature.swift */, + D668D92A2B696840008E2FF2 /* IdentityTheftRestorationPagesFeature.swift */, D664C7B52B289AA000CBFA76 /* SubscriptionPagesUseSubscriptionFeature.swift */, ); path = UserScripts; @@ -6688,7 +6688,7 @@ F4F6DFB426E6B63700ED7E12 /* BookmarkFolderCell.swift in Sources */, D6F93E3E2B50A8A0004C268D /* SubscriptionSettingsView.swift in Sources */, 851B12CC22369931004781BC /* AtbAndVariantCleanup.swift in Sources */, - D668D92B2B696840008E2FF2 /* identityTheftRestorationPagesFeature.swift in Sources */, + D668D92B2B696840008E2FF2 /* IdentityTheftRestorationPagesFeature.swift in Sources */, 85F2FFCF2211F8E5006BB258 /* TabSwitcherViewController+KeyCommands.swift in Sources */, 3157B43327F497E90042D3D7 /* SaveLoginView.swift in Sources */, F17922E01E71BB59006E3D97 /* AutocompleteViewControllerDelegate.swift in Sources */, diff --git a/DuckDuckGo/Subscription/UserScripts/identityTheftRestorationPagesFeature.swift b/DuckDuckGo/Subscription/UserScripts/IdentityTheftRestorationPagesFeature.swift similarity index 100% rename from DuckDuckGo/Subscription/UserScripts/identityTheftRestorationPagesFeature.swift rename to DuckDuckGo/Subscription/UserScripts/IdentityTheftRestorationPagesFeature.swift