diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 61cbca3faf..7082165319 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2591,6 +2591,8 @@ F118EA7E2BEA2B8700F77634 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F118EA7C2BEA2B8700F77634 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift */; }; F118EA852BEACC7000F77634 /* NonStandardPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F118EA842BEACC7000F77634 /* NonStandardPixel.swift */; }; F118EA862BEACC7000F77634 /* NonStandardPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F118EA842BEACC7000F77634 /* NonStandardPixel.swift */; }; + F1476FC02C1359FB00EAE46A /* SubscriptionUIHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1476FBF2C1359FB00EAE46A /* SubscriptionUIHandler.swift */; }; + F1476FC12C1359FB00EAE46A /* SubscriptionUIHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1476FBF2C1359FB00EAE46A /* SubscriptionUIHandler.swift */; }; F188267C2BBEB3AA00D9AC4F /* GeneralPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F188267B2BBEB3AA00D9AC4F /* GeneralPixel.swift */; }; F188267D2BBEB3AA00D9AC4F /* GeneralPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F188267B2BBEB3AA00D9AC4F /* GeneralPixel.swift */; }; F18826802BBEB58100D9AC4F /* PrivacyProPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F188267F2BBEB58100D9AC4F /* PrivacyProPixel.swift */; }; @@ -2615,6 +2617,8 @@ F1B33DF32BAD929D001128B3 /* SubscriptionAppStoreRestorer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B33DF12BAD929D001128B3 /* SubscriptionAppStoreRestorer.swift */; }; F1B33DF62BAD970E001128B3 /* SubscriptionErrorReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B33DF52BAD970E001128B3 /* SubscriptionErrorReporter.swift */; }; F1B33DF72BAD970E001128B3 /* SubscriptionErrorReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B33DF52BAD970E001128B3 /* SubscriptionErrorReporter.swift */; }; + F1C5763E2BFF972900C78647 /* SubscriptionUIHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1C5763D2BFF972900C78647 /* SubscriptionUIHandling.swift */; }; + F1C5763F2BFF972900C78647 /* SubscriptionUIHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1C5763D2BFF972900C78647 /* SubscriptionUIHandling.swift */; }; F1C70D792BFF50A400599292 /* DataBrokerProtectionLoginItemInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1C70D782BFF50A400599292 /* DataBrokerProtectionLoginItemInterface.swift */; }; F1C70D7A2BFF50A400599292 /* DataBrokerProtectionLoginItemInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1C70D782BFF50A400599292 /* DataBrokerProtectionLoginItemInterface.swift */; }; F1C70D7C2BFF510000599292 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1C70D7B2BFF510000599292 /* SubscriptionEnvironment+Default.swift */; }; @@ -3427,8 +3431,8 @@ 7BCB90C12C18626E008E3543 /* VPNControllerXPCClient+ConvenienceInitializers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VPNControllerXPCClient+ConvenienceInitializers.swift"; sourceTree = ""; }; 7BD1688D2AD4A4C400D24876 /* NetworkExtensionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkExtensionController.swift; sourceTree = ""; }; 7BD3AF5C2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeychainType+ClientDefault.swift"; sourceTree = ""; }; - 7BD8679A2A9E9E000063B9F7 /* VPNFeatureGatekeeper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNFeatureGatekeeper.swift; sourceTree = ""; }; 7BD7B0002C19D3830039D20A /* VPNIPCResources.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNIPCResources.swift; sourceTree = ""; }; + 7BD8679A2A9E9E000063B9F7 /* VPNFeatureGatekeeper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNFeatureGatekeeper.swift; sourceTree = ""; }; 7BDA36E52B7E037100AD5388 /* VPNProxyExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = VPNProxyExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 7BDA36EA2B7E037200AD5388 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 7BDA36EB2B7E037200AD5388 /* VPNProxyExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = VPNProxyExtension.entitlements; sourceTree = ""; }; @@ -4143,11 +4147,13 @@ EEF53E172950CED5002D78F4 /* JSAlertViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSAlertViewModelTests.swift; sourceTree = ""; }; F118EA7C2BEA2B8700F77634 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift"; sourceTree = ""; }; F118EA842BEACC7000F77634 /* NonStandardPixel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonStandardPixel.swift; sourceTree = ""; }; + F1476FBF2C1359FB00EAE46A /* SubscriptionUIHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionUIHandler.swift; sourceTree = ""; }; F188267B2BBEB3AA00D9AC4F /* GeneralPixel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralPixel.swift; sourceTree = ""; }; F188267F2BBEB58100D9AC4F /* PrivacyProPixel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyProPixel.swift; sourceTree = ""; }; F18826832BBEE31700D9AC4F /* PixelKit+Assertion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "PixelKit+Assertion.swift"; sourceTree = ""; }; F1B33DF12BAD929D001128B3 /* SubscriptionAppStoreRestorer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionAppStoreRestorer.swift; sourceTree = ""; }; F1B33DF52BAD970E001128B3 /* SubscriptionErrorReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionErrorReporter.swift; sourceTree = ""; }; + F1C5763D2BFF972900C78647 /* SubscriptionUIHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionUIHandling.swift; sourceTree = ""; }; F1C70D782BFF50A400599292 /* DataBrokerProtectionLoginItemInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionLoginItemInterface.swift; sourceTree = ""; }; F1C70D7B2BFF510000599292 /* SubscriptionEnvironment+Default.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SubscriptionEnvironment+Default.swift"; sourceTree = ""; }; F1D042932BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataBrokerProtectionSettings+Environment.swift"; sourceTree = ""; }; @@ -8314,6 +8320,7 @@ isa = PBXGroup; children = ( F1C70D7B2BFF510000599292 /* SubscriptionEnvironment+Default.swift */, + F1476FBF2C1359FB00EAE46A /* SubscriptionUIHandler.swift */, F1D042932BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift */, F118EA7C2BEA2B8700F77634 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift */, F1DA51842BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift */, @@ -8328,6 +8335,7 @@ isa = PBXGroup; children = ( 1E0C72052ABC63BD00802009 /* SubscriptionPagesUserScript.swift */, + F1C5763D2BFF972900C78647 /* SubscriptionUIHandling.swift */, F1B33DF12BAD929D001128B3 /* SubscriptionAppStoreRestorer.swift */, F1B33DF52BAD970E001128B3 /* SubscriptionErrorReporter.swift */, ); @@ -10063,6 +10071,7 @@ 3706FBCB293F65D500E42796 /* BookmarkHTMLImporter.swift in Sources */, 4BF97ADC2B43C5E200EB4240 /* VPNFeedbackSender.swift in Sources */, 987799F72999996B005D8EB6 /* BookmarkDatabase.swift in Sources */, + F1C5763F2BFF972900C78647 /* SubscriptionUIHandling.swift in Sources */, BDA7647D2BC497BE00D0400C /* DefaultVPNLocationFormatter.swift in Sources */, 3706FBCC293F65D500E42796 /* CustomRoundedCornersShape.swift in Sources */, 3706FBCD293F65D500E42796 /* LocaleExtension.swift in Sources */, @@ -10203,6 +10212,7 @@ 3706FC25293F65D500E42796 /* ColorView.swift in Sources */, 3706FC26293F65D500E42796 /* RecentlyClosedCacheItem.swift in Sources */, 3706FC27293F65D500E42796 /* PopupBlockedPopover.swift in Sources */, + F1476FC12C1359FB00EAE46A /* SubscriptionUIHandler.swift in Sources */, 3706FC28293F65D500E42796 /* SaveCredentialsPopover.swift in Sources */, 3707C728294B5D2900682A9F /* WKWebViewConfigurationExtensions.swift in Sources */, 3706FC29293F65D500E42796 /* QuartzIdleStateProvider.swift in Sources */, @@ -11591,6 +11601,7 @@ 566B736C2BECC3C600FF1959 /* SyncPausedStateManaging.swift in Sources */, 4BD18F01283F0BC500058124 /* BookmarksBarViewController.swift in Sources */, 379DE4BD27EA31AC002CC3DE /* PreferencesAutofillView.swift in Sources */, + F1476FC02C1359FB00EAE46A /* SubscriptionUIHandler.swift in Sources */, 1DCFBC8A29ADF32B00313531 /* BurnerHomePageView.swift in Sources */, 858A797F26A79EAA00A75A42 /* UserText+PasswordManager.swift in Sources */, B693954E26F04BEB0015B914 /* LoadingProgressView.swift in Sources */, @@ -11710,6 +11721,7 @@ 31F28C5328C8EECA00119F70 /* DuckURLSchemeHandler.swift in Sources */, AA13DCB4271480B0006D48D3 /* FirePopoverViewModel.swift in Sources */, 1DDC84F72B83558F00670238 /* PreferencesPrivateSearchView.swift in Sources */, + F1C5763E2BFF972900C78647 /* SubscriptionUIHandling.swift in Sources */, 1D43EB38292B636E0065E5D6 /* BWCommand.swift in Sources */, F41D174125CB131900472416 /* NSColorExtension.swift in Sources */, AAC5E4F625D6BF2C007F5990 /* AddressBarButtonsViewController.swift in Sources */, diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index fe67b31e4c..6d6109ae9a 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -87,10 +87,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate { let bookmarksManager = LocalBookmarkManager.shared var privacyDashboardWindow: NSWindow? - private var accountManager: AccountManaging { - subscriptionManager.accountManager - } public let subscriptionManager: SubscriptionManaging + public let subscriptionUIHandler: SubscriptionUIHandling + public let vpnSettings = VPNSettings(defaults: .netP) // MARK: - VPN @@ -126,7 +125,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { ) return VPNRedditSessionWorkaround( - accountManager: accountManager, + accountManager: subscriptionManager.accountManager, ipcClient: ipcClient, statusReporter: statusReporter ) @@ -220,6 +219,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate { // Configure Subscription subscriptionManager = SubscriptionManager() + subscriptionUIHandler = SubscriptionUIHandler(windowControllersManagerProvider: { + return WindowControllersManager.shared + }) // Update VPN environment and match the Subscription environment vpnSettings.alignTo(subscriptionEnvironment: subscriptionManager.currentEnvironment) @@ -347,7 +349,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { #endif #if DBP - DataBrokerProtectionAppEvents(featureGatekeeper: DefaultDataBrokerProtectionFeatureGatekeeper(accountManager: accountManager)).applicationDidFinishLaunching() + DataBrokerProtectionAppEvents(featureGatekeeper: DefaultDataBrokerProtectionFeatureGatekeeper(accountManager: subscriptionManager.accountManager)).applicationDidFinishLaunching() #endif setUpAutoClearHandler() @@ -377,7 +379,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate { NetworkProtectionAppEvents(featureGatekeeper: DefaultVPNFeatureGatekeeper(subscriptionManager: subscriptionManager)).applicationDidBecomeActive() #if DBP - DataBrokerProtectionAppEvents(featureGatekeeper: DefaultDataBrokerProtectionFeatureGatekeeper(accountManager: accountManager)).applicationDidBecomeActive() + DataBrokerProtectionAppEvents(featureGatekeeper: + DefaultDataBrokerProtectionFeatureGatekeeper(accountManager: + subscriptionManager.accountManager)).applicationDidBecomeActive() #endif AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager.toggleProtectionsCounter.sendEventsIfNeeded() diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index a4d9afdbac..1c0f0687b0 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -165,13 +165,9 @@ enum Preferences { let sheetActionHandler = SubscriptionAccessActionHandlers(restorePurchases: { if #available(macOS 12.0, *) { Task { - guard let mainViewController = WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController, - let windowControllerManager = WindowControllersManager.shared.lastKeyMainWindowController else { - return - } - - let subscriptionAppStoreRestorer = SubscriptionAppStoreRestorer(subscriptionManager: Application.appDelegate.subscriptionManager) - await subscriptionAppStoreRestorer.restoreAppStoreSubscription(mainViewController: mainViewController, windowController: windowControllerManager) + let subscriptionAppStoreRestorer = SubscriptionAppStoreRestorer(subscriptionManager: Application.appDelegate.subscriptionManager, + uiHandler: Application.appDelegate.subscriptionUIHandler) + await subscriptionAppStoreRestorer.restoreAppStoreSubscription() } } }, diff --git a/DuckDuckGo/Subscription/SubscriptionUIHandler.swift b/DuckDuckGo/Subscription/SubscriptionUIHandler.swift new file mode 100644 index 0000000000..0ac6b8320c --- /dev/null +++ b/DuckDuckGo/Subscription/SubscriptionUIHandler.swift @@ -0,0 +1,111 @@ +// +// SubscriptionUIHandler.swift +// +// 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 SubscriptionUI + +@MainActor +final class SubscriptionUIHandler: SubscriptionUIHandling { + + fileprivate var currentWindow: NSWindow? { windowControllersManagerProvider().lastKeyMainWindowController?.window } + fileprivate var currentMainViewController: MainViewController? { + windowControllersManagerProvider().lastKeyMainWindowController?.mainViewController + } + fileprivate var windowControllersManager: WindowControllersManager { windowControllersManagerProvider() } + typealias WindowControllersManagerProvider = () -> WindowControllersManager + fileprivate nonisolated let windowControllersManagerProvider: WindowControllersManagerProvider + fileprivate var progressViewController: ProgressViewController? + + nonisolated init(windowControllersManagerProvider: @escaping WindowControllersManagerProvider) { + self.windowControllersManagerProvider = windowControllersManagerProvider + } + + // MARK: - SubscriptionUIHandling + + func presentProgressViewController(withTitle: String) { + progressViewController = ProgressViewController(title: UserText.purchasingSubscriptionTitle) + currentMainViewController?.presentAsSheet(progressViewController!) + } + + func dismissProgressViewController() { + progressViewController?.dismiss() + progressViewController = nil + } + + func updateProgressViewController(title: String) { + progressViewController?.updateTitleText(UserText.completingPurchaseTitle) + } + + func presentSubscriptionAccessViewController(handler: any SubscriptionAccessActionHandling, message: WKScriptMessage) { + let actionHandlers = SubscriptionAccessActionHandlers(restorePurchases: { + handler.subscriptionAccessActionRestorePurchases(message: message) + }, openURLHandler: { url in + handler.subscriptionAccessActionOpenURLHandler(url: url) + }, uiActionHandler: { event in + handler.subscriptionAccessActionHandleAction(event: event) + }) + + let newSubscriptionAccessViewController = SubscriptionAccessViewController(subscriptionManager: Application.appDelegate.subscriptionManager, + actionHandlers: actionHandlers) + currentMainViewController?.presentAsSheet(newSubscriptionAccessViewController) + } + + func show(alertType: SubscriptionAlertType, text: String? = nil, firstButtonAction: (() -> Void)? = nil) { + + var alert: NSAlert? + switch alertType { + case .somethingWentWrong: + alert = .somethingWentWrongAlert() + case .subscriptionNotFound: + alert = .subscriptionNotFoundAlert() + case .subscriptionInactive: + alert = .subscriptionInactiveAlert() + case .subscriptionFound: + alert = .subscriptionFoundAlert() + case .appleIDSyncFailed: + guard let text else { + assertionFailure("Trying to present appleIDSyncFailed alert without required text") + return + } + alert = .appleIDSyncFailedAlert(text: text) + } + + guard let alert else { + assertionFailure("Missing subscription alert") + return + } + + currentWindow?.show(alert, firstButtonAction: firstButtonAction) + } + + func show(alertType: SubscriptionAlertType) { + show(alertType: alertType, text: nil, firstButtonAction: nil) + } + + func show(alertType: SubscriptionAlertType, firstButtonAction: (() -> Void)?) { + show(alertType: alertType, text: nil, firstButtonAction: firstButtonAction) + } + + func show(alertType: SubscriptionAlertType, text: String?) { + show(alertType: alertType, text: text, firstButtonAction: nil) + } + + func showTab(with content: Tab.TabContent) { + self.windowControllersManager.showTab(with: content) + } +} diff --git a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionAppStoreRestorer.swift b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionAppStoreRestorer.swift index 68a02e6372..8b4491fa43 100644 --- a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionAppStoreRestorer.swift +++ b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionAppStoreRestorer.swift @@ -28,23 +28,25 @@ struct SubscriptionAppStoreRestorer { private let subscriptionManager: SubscriptionManaging @MainActor var window: NSWindow? { WindowControllersManager.shared.lastKeyMainWindowController?.window } let subscriptionErrorReporter = SubscriptionErrorReporter() + let uiHandler: SubscriptionUIHandling - public init(subscriptionManager: SubscriptionManaging) { + public init(subscriptionManager: SubscriptionManaging, + uiHandler: SubscriptionUIHandling) { self.subscriptionManager = subscriptionManager + self.uiHandler = uiHandler } - // swiftlint:disable:next cyclomatic_complexity function_body_length - func restoreAppStoreSubscription(mainViewController: MainViewController, windowController: MainWindowController) async { + // swiftlint:disable:next cyclomatic_complexity + func restoreAppStoreSubscription() async { - let progressViewController = await ProgressViewController(title: UserText.restoringSubscriptionTitle) defer { - DispatchQueue.main.async { - mainViewController.dismiss(progressViewController) + Task { @MainActor in + uiHandler.dismissProgressViewController() } } - DispatchQueue.main.async { - mainViewController.presentAsSheet(progressViewController) + Task { @MainActor in + uiHandler.presentProgressViewController(withTitle: UserText.restoringSubscriptionTitle) } let syncResult = await subscriptionManager.storePurchaseManager().syncAppleIDAccount() @@ -60,14 +62,8 @@ struct SubscriptionAppStoreRestorer { break } - let alert = await NSAlert.appleIDSyncFailedAlert(text: error.localizedDescription) - - switch await alert.runModal() { - case .alertFirstButtonReturn: - // Continue button - break - default: - return + Task { @MainActor in + uiHandler.show(alertType: .appleIDSyncFailed, text: error.localizedDescription) } } @@ -102,39 +98,28 @@ struct SubscriptionAppStoreRestorer { @available(macOS 12.0, *) extension SubscriptionAppStoreRestorer { - /* - WARNING: DUPLICATED CODE - This code will be moved as part of https://app.asana.com/0/0/1207157941206686/f - */ - // MARK: - UI interactions @MainActor func showSomethingWentWrongAlert() { PixelKit.fire(PrivacyProPixel.privacyProPurchaseFailure, frequency: .dailyAndCount) - guard let window else { return } - - window.show(.somethingWentWrongAlert()) + uiHandler.show(alertType: .somethingWentWrong) } @MainActor func showSubscriptionNotFoundAlert() { - guard let window else { return } - - window.show(.subscriptionNotFoundAlert(), firstButtonAction: { + uiHandler.show(alertType: .subscriptionNotFound, firstButtonAction: { let url = subscriptionManager.url(for: .purchase) - WindowControllersManager.shared.showTab(with: .subscription(url)) + uiHandler.showTab(with: .subscription(url)) PixelKit.fire(PrivacyProPixel.privacyProOfferScreenImpression) }) } @MainActor func showSubscriptionInactiveAlert() { - guard let window else { return } - - window.show(.subscriptionInactiveAlert(), firstButtonAction: { + uiHandler.show(alertType: .subscriptionInactive, firstButtonAction: { let url = subscriptionManager.url(for: .purchase) - WindowControllersManager.shared.showTab(with: .subscription(url)) + uiHandler.showTab(with: .subscription(url)) PixelKit.fire(PrivacyProPixel.privacyProOfferScreenImpression) }) } diff --git a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUserScript.swift index 32f819fead..ae017978c4 100644 --- a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUserScript.swift @@ -16,15 +16,14 @@ // limitations under the License. // +import Foundation import BrowserServicesKit import Common import Combine -import Foundation import Navigation import WebKit import UserScript import Subscription -import SubscriptionUI import PixelKit public extension Notification.Name { @@ -79,11 +78,6 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { .exact(hostname: "duckduckgo.com"), .exact(hostname: "abrown.duckduckgo.com") ]) - - @MainActor - var window: NSWindow? { - WindowControllersManager.shared.lastKeyMainWindowController?.window - } let subscriptionManager: SubscriptionManaging var accountManager: AccountManaging { subscriptionManager.accountManager } var subscriptionPlatform: SubscriptionEnvironment.PurchasePlatform { subscriptionManager.currentEnvironment.purchasePlatform } @@ -91,12 +85,15 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { let stripePurchaseFlow: StripePurchaseFlow let subscriptionErrorReporter = SubscriptionErrorReporter() let subscriptionSuccessPixelHandler: SubscriptionAttributionPixelHandler + let uiHandler: SubscriptionUIHandling public init(subscriptionManager: SubscriptionManaging, - subscriptionSuccessPixelHandler: SubscriptionAttributionPixelHandler = PrivacyProSubscriptionAttributionPixelHandler()) { + subscriptionSuccessPixelHandler: SubscriptionAttributionPixelHandler = PrivacyProSubscriptionAttributionPixelHandler(), + uiHandler: SubscriptionUIHandling) { self.subscriptionManager = subscriptionManager self.stripePurchaseFlow = StripePurchaseFlow(subscriptionManager: subscriptionManager) self.subscriptionSuccessPixelHandler = subscriptionSuccessPixelHandler + self.uiHandler = uiHandler } func with(broker: UserScriptMessageBroker) { @@ -226,12 +223,9 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { subscriptionSuccessPixelHandler.origin = await originFrom(originalMessage: message) if subscriptionManager.currentEnvironment.purchasePlatform == .appStore { if #available(macOS 12.0, *) { - let mainViewController = await WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController - let progressViewController = await ProgressViewController(title: UserText.purchasingSubscriptionTitle) - defer { - Task { - await mainViewController?.dismiss(progressViewController) + Task { @MainActor in + uiHandler.dismissProgressViewController() } } @@ -243,7 +237,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { os_log(.info, log: .subscription, "[Purchase] Starting purchase for: %{public}s", subscriptionSelection.id) - await mainViewController?.presentAsSheet(progressViewController) + await uiHandler.presentProgressViewController(withTitle: UserText.purchasingSubscriptionTitle) // Check for active subscriptions if await subscriptionManager.storePurchaseManager().hasActiveSubscription() { @@ -289,7 +283,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { return nil } - await progressViewController.updateTitleText(UserText.completingPurchaseTitle) + await uiHandler.updateProgressViewController(title: UserText.completingPurchaseTitle) os_log(.info, log: .subscription, "[Purchase] Completing purchase") @@ -351,39 +345,13 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { return nil } - func activateSubscription(params: Any, original: WKScriptMessage) async throws -> Encodable? { + // MARK: functions used in SubscriptionAccessActionHandlers + func activateSubscription(params: Any, original: WKScriptMessage) async throws -> Encodable? { PixelKit.fire(PrivacyProPixel.privacyProRestorePurchaseOfferPageEntry) - guard let mainViewController = await WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController, - let windowControllerManager = await WindowControllersManager.shared.lastKeyMainWindowController else { - return nil + Task { @MainActor in + uiHandler.presentSubscriptionAccessViewController(handler: self, message: original) } - let message = original - - let actionHandlers = SubscriptionAccessActionHandlers(restorePurchases: { - if #available(macOS 12.0, *) { - Task { @MainActor in - let subscriptionAppStoreRestorer = SubscriptionAppStoreRestorer(subscriptionManager: self.subscriptionManager) - await subscriptionAppStoreRestorer.restoreAppStoreSubscription(mainViewController: mainViewController, windowController: windowControllerManager) - message.webView?.reload() - } - } - }, openURLHandler: { url in - DispatchQueue.main.async { - WindowControllersManager.shared.showTab(with: .subscription(url)) - } - }, uiActionHandler: { event in - switch event { - case .activateAddEmailClick: - PixelKit.fire(PrivacyProPixel.privacyProRestorePurchaseEmailStart, frequency: .dailyAndCount) - default: - break - } - }) - - let subscriptionAccessViewController = await SubscriptionAccessViewController(subscriptionManager: subscriptionManager, actionHandlers: actionHandlers) - await WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.presentAsSheet(subscriptionAccessViewController) - return nil } @@ -417,23 +385,30 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { case .personalInformationRemoval: PixelKit.fire(PrivacyProPixel.privacyProWelcomePersonalInformationRemoval, frequency: .unique) NotificationCenter.default.post(name: .openPersonalInformationRemoval, object: self, userInfo: nil) - await WindowControllersManager.shared.showTab(with: .dataBrokerProtection) + Task { @MainActor in + self.uiHandler.showTab(with: .dataBrokerProtection) + } case .identityTheftRestoration: PixelKit.fire(PrivacyProPixel.privacyProWelcomeIdentityRestoration, frequency: .unique) let url = subscriptionManager.url(for: .identityTheftRestoration) - await WindowControllersManager.shared.showTab(with: .identityTheftRestoration(url)) + Task { @MainActor in + self.uiHandler.showTab(with: .identityTheftRestoration(url)) + } } return nil } func completeStripePayment(params: Any, original: WKScriptMessage) async throws -> Encodable? { - let mainViewController = await WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController - let progressViewController = await ProgressViewController(title: UserText.completingPurchaseTitle) + Task { @MainActor in + uiHandler.presentProgressViewController(withTitle: UserText.completingPurchaseTitle) + } - await mainViewController?.presentAsSheet(progressViewController) await stripePurchaseFlow.completeSubscriptionPurchase() - await mainViewController?.dismiss(progressViewController) + + Task { @MainActor in + uiHandler.dismissProgressViewController() + } PixelKit.fire(PrivacyProPixel.privacyProPurchaseStripeSuccess, frequency: .dailyAndCount) subscriptionSuccessPixelHandler.fireSuccessfulSubscriptionAttributionPixel() @@ -504,48 +479,35 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { extension SubscriptionPagesUseSubscriptionFeature { - /* - WARNING: - This code will be moved as part of https://app.asana.com/0/0/1207157941206686/f - */ - - // MARK: - UI interactions + // MARK: - UI interactions @MainActor func showSomethingWentWrongAlert() { PixelKit.fire(PrivacyProPixel.privacyProPurchaseFailure, frequency: .dailyAndCount) - guard let window else { return } - - window.show(.somethingWentWrongAlert()) + uiHandler.show(alertType: .somethingWentWrong) } @MainActor func showSubscriptionNotFoundAlert() { - guard let window else { return } - - window.show(.subscriptionNotFoundAlert(), firstButtonAction: { + uiHandler.show(alertType: .subscriptionNotFound, firstButtonAction: { let url = self.subscriptionManager.url(for: .purchase) - WindowControllersManager.shared.showTab(with: .subscription(url)) + self.uiHandler.showTab(with: .subscription(url)) PixelKit.fire(PrivacyProPixel.privacyProOfferScreenImpression) }) } @MainActor func showSubscriptionInactiveAlert() { - guard let window else { return } - - window.show(.subscriptionInactiveAlert(), firstButtonAction: { + uiHandler.show(alertType: .subscriptionInactive, firstButtonAction: { let url = self.subscriptionManager.url(for: .purchase) - WindowControllersManager.shared.showTab(with: .subscription(url)) + self.uiHandler.showTab(with: .subscription(url)) PixelKit.fire(PrivacyProPixel.privacyProOfferScreenImpression) }) } @MainActor func showSubscriptionFoundAlert(originalMessage: WKScriptMessage) { - guard let window else { return } - - window.show(.subscriptionFoundAlert(), firstButtonAction: { + uiHandler.show(alertType: .subscriptionFound, firstButtonAction: { if #available(macOS 12.0, *) { Task { let appStoreRestoreFlow = AppStoreRestoreFlow(subscriptionManager: self.subscriptionManager) @@ -560,3 +522,31 @@ extension SubscriptionPagesUseSubscriptionFeature { }) } } + +extension SubscriptionPagesUseSubscriptionFeature: SubscriptionAccessActionHandling { + + func subscriptionAccessActionRestorePurchases(message: WKScriptMessage) { + if #available(macOS 12.0, *) { + Task { @MainActor in + let subscriptionAppStoreRestorer = SubscriptionAppStoreRestorer(subscriptionManager: self.subscriptionManager, + uiHandler: self.uiHandler) + await subscriptionAppStoreRestorer.restoreAppStoreSubscription() + message.webView?.reload() + } + } + } + + func subscriptionAccessActionOpenURLHandler(url: URL) { + Task { @MainActor in + self.uiHandler.showTab(with: .subscription(url)) + } + } + + func subscriptionAccessActionHandleAction(event: SubscriptionAccessActionHandlingEvent) { + switch event { + case .activateAddEmailClick: + PixelKit.fire(PrivacyProPixel.privacyProRestorePurchaseEmailStart, frequency: .dailyAndCount) + default: break + } + } +} diff --git a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionUIHandling.swift b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionUIHandling.swift new file mode 100644 index 0000000000..6176f80e29 --- /dev/null +++ b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionUIHandling.swift @@ -0,0 +1,56 @@ +// +// SubscriptionUIHandling.swift +// +// 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 SubscriptionUI + +@MainActor +protocol SubscriptionUIHandling { + + // MARK: ProgressViewController + func presentProgressViewController(withTitle: String) + func dismissProgressViewController() + func updateProgressViewController(title: String) + + // MARK: SubscriptionAccessViewController + func presentSubscriptionAccessViewController(handler: SubscriptionAccessActionHandling, message: WKScriptMessage) + + // MARK: Alerts + func show(alertType: SubscriptionAlertType) + func show(alertType: SubscriptionAlertType, firstButtonAction: (() -> Void)?) + func show(alertType: SubscriptionAlertType, text: String?) + + // MARK: Tab + func showTab(with content: Tab.TabContent) +} + +enum SubscriptionAlertType { + case somethingWentWrong + case subscriptionNotFound + case subscriptionInactive + case subscriptionFound + case appleIDSyncFailed +} + +typealias SubscriptionAccessActionHandlingEvent = PreferencesSubscriptionModel.UserEvent + +protocol SubscriptionAccessActionHandling { + func subscriptionAccessActionRestorePurchases(message: WKScriptMessage) + func subscriptionAccessActionOpenURLHandler(url: URL) + func subscriptionAccessActionHandleAction(event: SubscriptionAccessActionHandlingEvent) +} diff --git a/DuckDuckGo/Tab/UserScripts/UserScripts.swift b/DuckDuckGo/Tab/UserScripts/UserScripts.swift index 2668a5fde6..4820025ba0 100644 --- a/DuckDuckGo/Tab/UserScripts/UserScripts.swift +++ b/DuckDuckGo/Tab/UserScripts/UserScripts.swift @@ -93,7 +93,9 @@ final class UserScripts: UserScriptsProvider { } if DefaultSubscriptionFeatureAvailability().isFeatureAvailable { - subscriptionPagesUserScript.registerSubfeature(delegate: SubscriptionPagesUseSubscriptionFeature(subscriptionManager: Application.appDelegate.subscriptionManager)) + let delegate = SubscriptionPagesUseSubscriptionFeature(subscriptionManager: Application.appDelegate.subscriptionManager, + uiHandler: Application.appDelegate.subscriptionUIHandler) + subscriptionPagesUserScript.registerSubfeature(delegate: delegate) userScripts.append(subscriptionPagesUserScript) identityTheftRestorationPagesUserScript.registerSubfeature(delegate: IdentityTheftRestorationPagesFeature())