diff --git a/DuckDuckGo/Subscription/Extensions/Logger+Subscription.swift b/Core/Logger+Pixel.swift similarity index 77% rename from DuckDuckGo/Subscription/Extensions/Logger+Subscription.swift rename to Core/Logger+Pixel.swift index 11ec2a3b73..2e22d2e774 100644 --- a/DuckDuckGo/Subscription/Extensions/Logger+Subscription.swift +++ b/Core/Logger+Pixel.swift @@ -1,5 +1,5 @@ // -// Logger+Subscription.swift +// Logger+Pixel.swift // DuckDuckGo // // Copyright © 2024 DuckDuckGo. All rights reserved. @@ -20,9 +20,6 @@ import Foundation import os.log -extension Logger { - - static var subscription: Logger = { - Logger(subsystem: Bundle.main.bundleIdentifier ?? "DuckDuckGo", category: "SubscriptionPro") - }() +public extension Logger { + static var pixels = { Logger(subsystem: "Pixels", category: "") }() } diff --git a/Core/PersistentPixel.swift b/Core/PersistentPixel.swift index d26dd31cba..bbba933836 100644 --- a/Core/PersistentPixel.swift +++ b/Core/PersistentPixel.swift @@ -98,7 +98,7 @@ public final class PersistentPixel: PersistentPixelFiring { var additionalParameters = additionalParameters additionalParameters[PixelParameters.originalPixelTimestamp] = dateString - Logger.general.debug("Firing persistent pixel named \(pixel.name)") + Logger.pixels.debug("Firing persistent pixel named \(pixel.name)") pixelFiring.fire(pixel: pixel, error: error, diff --git a/Core/Pixel.swift b/Core/Pixel.swift index ffe7b063cc..0b43fbf7c8 100644 --- a/Core/Pixel.swift +++ b/Core/Pixel.swift @@ -245,7 +245,7 @@ public class Pixel { } guard !isDryRun else { - Logger.general.debug("Pixel fired \(pixelName.replacingOccurrences(of: "_", with: "."), privacy: .public) \(params.count > 0 ? "\(params)" : "", privacy: .public)") + Logger.pixels.debug("Pixel fired \(pixelName.replacingOccurrences(of: "_", with: "."), privacy: .public) \(params.count > 0 ? "\(params)" : "", privacy: .public)") // simulate server response time for Dry Run mode DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { onComplete(nil) @@ -276,7 +276,7 @@ public class Pixel { headers: headers) let request = APIRequest(configuration: configuration, urlSession: .session(useMainThreadCallbackQueue: true)) request.fetch { _, error in - Logger.general.debug("Pixel fired \(pixelName, privacy: .public) \(params, privacy: .public)") + Logger.pixels.debug("Pixel fired \(pixelName, privacy: .public) \(params, privacy: .public)") onComplete(error) } } diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index 620f7cdf25..126405ece3 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -801,6 +801,9 @@ extension Pixel { case privacyProSubscriptionCookieRefreshedWithAccessToken case privacyProSubscriptionCookieRefreshedWithEmptyValue case privacyProSubscriptionCookieFailedToSetSubscriptionCookie + case privacyProDeadTokenDetected + case authV1MigrationFailed + case authV1MigrationSucceeded // MARK: Pixel Experiment case pixelExperimentEnrollment @@ -1755,6 +1758,9 @@ extension Pixel.Event { case .privacyProSubscriptionCookieRefreshedWithAccessToken: return "m_privacy-pro_subscription-cookie-refreshed_with_access_token" case .privacyProSubscriptionCookieRefreshedWithEmptyValue: return "m_privacy-pro_subscription-cookie-refreshed_with_empty_value" case .privacyProSubscriptionCookieFailedToSetSubscriptionCookie: return "m_privacy-pro_subscription-cookie-failed_to_set_subscription_cookie" + case .privacyProDeadTokenDetected: return "m_privacy-pro_dead_token_detected" + case .authV1MigrationFailed: return "m_privacy-pro_v1migration_failed" + case .authV1MigrationSucceeded: return "m_privacy-pro_v1migration_succeeded" // MARK: Pixel Experiment case .pixelExperimentEnrollment: return "pixel_experiment_enrollment" diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 9c36cafd8c..f96b8f28a2 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -73,7 +73,6 @@ 1E4F4A5A297193DE00625985 /* MainViewController+CookiesManaged.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4F4A59297193DE00625985 /* MainViewController+CookiesManaged.swift */; }; 1E4FAA6427D8DFB900ADC5B3 /* OngoingDownloadRowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4FAA6327D8DFB900ADC5B3 /* OngoingDownloadRowViewModel.swift */; }; 1E4FAA6627D8DFC800ADC5B3 /* CompleteDownloadRowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4FAA6527D8DFC800ADC5B3 /* CompleteDownloadRowViewModel.swift */; }; - 1E53508F2C7C9A1F00818DAA /* DefaultSubscriptionManager+AccountManagerKeychainAccessDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E53508E2C7C9A1F00818DAA /* DefaultSubscriptionManager+AccountManagerKeychainAccessDelegate.swift */; }; 1E5918472CA422A7008ED2B3 /* Navigation in Frameworks */ = {isa = PBXBuildFile; productRef = 1E5918462CA422A7008ED2B3 /* Navigation */; }; 1E60989B290009C700A508F9 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 1E7060BD28F88EE200E4CCDB /* Common */; }; 1E60989D290011E600A508F9 /* ContentBlocking in Frameworks */ = {isa = PBXBuildFile; productRef = 1E60989C290011E600A508F9 /* ContentBlocking */; }; @@ -700,7 +699,6 @@ 98424A982CED4F430071C7DB /* ContentBlocking in Frameworks */ = {isa = PBXBuildFile; productRef = 984249C62CED4F430071C7DB /* ContentBlocking */; }; 98424A992CED4F430071C7DB /* SubscriptionTestingUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = 984249C92CED4F430071C7DB /* SubscriptionTestingUtilities */; }; 98424A9A2CED4F430071C7DB /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = 984249C82CED4F430071C7DB /* Subscription */; }; - 98424A9B2CED4F430071C7DB /* TestUtils in Frameworks */ = {isa = PBXBuildFile; productRef = 984249C52CED4F430071C7DB /* TestUtils */; }; 98424A9C2CED4F430071C7DB /* NetworkProtectionTestUtils in Frameworks */ = {isa = PBXBuildFile; productRef = 984249C32CED4F430071C7DB /* NetworkProtectionTestUtils */; }; 98424A9D2CED4F430071C7DB /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 984249C72CED4F430071C7DB /* Common */; }; 98424AAB2CED4FF10071C7DB /* CookieStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85AD49ED2B6149110085D2D1 /* CookieStorageTests.swift */; }; @@ -882,7 +880,6 @@ B6BA95C528894A28004ABA20 /* BrowsingMenuViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B6BA95C428894A28004ABA20 /* BrowsingMenuViewController.storyboard */; }; B6BA95E828924730004ABA20 /* JSAlertController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B6BA95E728924730004ABA20 /* JSAlertController.storyboard */; }; BBFF18B12C76448100C48D7D /* QuerySubmittedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBFF18B02C76448100C48D7D /* QuerySubmittedTests.swift */; }; - BD10B8AA2C7629740033115D /* Logger+Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD10B8A92C7629740033115D /* Logger+Subscription.swift */; }; BD15DB852B959CFD00821457 /* BundleExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD15DB842B959CFD00821457 /* BundleExtension.swift */; }; BD2F39EB2C19F955005B19E7 /* NetworkProtectionDNSSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD2F39EA2C19F955005B19E7 /* NetworkProtectionDNSSettingsView.swift */; }; BD862E032B30DA170073E2EE /* VPNFeedbackFormViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD862E022B30DA170073E2EE /* VPNFeedbackFormViewModel.swift */; }; @@ -902,10 +899,8 @@ BDE91CE02C6515420005CB74 /* UnifiedFeedbackFormViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDE91CDF2C6515410005CB74 /* UnifiedFeedbackFormViewModel.swift */; }; BDF8D0022C1B87F4003E3B27 /* NetworkProtectionDNSSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDF8D0012C1B87F4003E3B27 /* NetworkProtectionDNSSettingsViewModel.swift */; }; BDFF031D2BA3D2BD00F324C9 /* DefaultNetworkProtectionVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFF031C2BA3D2BD00F324C9 /* DefaultNetworkProtectionVisibility.swift */; }; - BDFF03212BA3D3CF00F324C9 /* NetworkProtectionVisibilityForTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFF03202BA3D3CF00F324C9 /* NetworkProtectionVisibilityForTunnelProvider.swift */; }; BDFF03222BA3D8E200F324C9 /* NetworkProtectionFeatureVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFF03192BA39C5A00F324C9 /* NetworkProtectionFeatureVisibility.swift */; }; BDFF03232BA3D8E300F324C9 /* NetworkProtectionFeatureVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFF03192BA39C5A00F324C9 /* NetworkProtectionFeatureVisibility.swift */; }; - BDFF03262BA3DA4900F324C9 /* NetworkProtectionFeatureVisibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFF03242BA3D92E00F324C9 /* NetworkProtectionFeatureVisibilityTests.swift */; }; C10CB5F32A1A5BDF0048E503 /* AutofillViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10CB5F22A1A5BDF0048E503 /* AutofillViews.swift */; }; C1106F312D0EFD8B0054A221 /* FreeTrialsFeatureFlagExperimentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1106F302D0EFD8B0054A221 /* FreeTrialsFeatureFlagExperimentTests.swift */; }; C111B26927F579EF006558B1 /* BookmarkOrFolderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C111B26827F579EF006558B1 /* BookmarkOrFolderTests.swift */; }; @@ -1173,7 +1168,6 @@ F1134ED21F40EF3A00B73467 /* JsonTestDataLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1134ECF1F40EBE200B73467 /* JsonTestDataLoader.swift */; }; F1134ED61F40F29F00B73467 /* StatisticsUserDefaultsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1134ED41F40F15800B73467 /* StatisticsUserDefaultsTests.swift */; }; F114C55B1E66EB020018F95F /* NibLoading.swift in Sources */ = {isa = PBXBuildFile; fileRef = F114C55A1E66EB020018F95F /* NibLoading.swift */; }; - F115ED9C2B4EFC8E001A0453 /* TestUtils in Frameworks */ = {isa = PBXBuildFile; productRef = F115ED9B2B4EFC8E001A0453 /* TestUtils */; }; F130D73A1E5776C500C45811 /* OmniBarDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F130D7391E5776C500C45811 /* OmniBarDelegate.swift */; }; F132D6A52C62239B00D85426 /* SubscriptionSettingsHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F132D6A42C62239B00D85426 /* SubscriptionSettingsHeaderView.swift */; }; F1386BA41E6846C40062FC3C /* TabDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1386BA31E6846C40062FC3C /* TabDelegate.swift */; }; @@ -1196,6 +1190,10 @@ F15531922BF215ED0029ED04 /* SubscriptionTestingUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = F15531912BF215ED0029ED04 /* SubscriptionTestingUtilities */; }; F15531942BF215F60029ED04 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = F15531932BF215F60029ED04 /* Subscription */; }; F15531962BF215F60029ED04 /* SubscriptionTestingUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = F15531952BF215F60029ED04 /* SubscriptionTestingUtilities */; }; + F15764C02D3E6AE60086AA21 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = F15764BF2D3E6AE60086AA21 /* Networking */; }; + F15764C22D3E6AF60086AA21 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = F15764C12D3E6AF60086AA21 /* Networking */; }; + F15764C42D3E6AF60086AA21 /* NetworkingTestingUtils in Frameworks */ = {isa = PBXBuildFile; productRef = F15764C32D3E6AF60086AA21 /* NetworkingTestingUtils */; }; + F15764C62D3E6B010086AA21 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = F15764C52D3E6B010086AA21 /* Networking */; }; F15D43201E706CC500BF2CDC /* AutocompleteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F15D431F1E706CC500BF2CDC /* AutocompleteViewController.swift */; }; F1617C131E572E0300DEDCAF /* TabSwitcherViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1617C121E572E0300DEDCAF /* TabSwitcherViewController.swift */; }; F1617C151E57336D00DEDCAF /* TabManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1617C141E57336D00DEDCAF /* TabManager.swift */; }; @@ -1215,6 +1213,10 @@ F198D78E1E39762C0088DA8A /* StringExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F198D78D1E39762C0088DA8A /* StringExtensionTests.swift */; }; F1A886781F29394E0096251E /* WebCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1A886771F29394E0096251E /* WebCacheManager.swift */; }; F1AE54E81F0425FC00D9A700 /* AuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1AE54E71F0425FC00D9A700 /* AuthenticationViewController.swift */; }; + F1AFDF802D2EE10A00DB313F /* PersistenceTestingUtils in Frameworks */ = {isa = PBXBuildFile; productRef = F1AFDF7F2D2EE10A00DB313F /* PersistenceTestingUtils */; }; + F1AFDF822D2EE1D100DB313F /* PersistenceTestingUtils in Frameworks */ = {isa = PBXBuildFile; productRef = F1AFDF812D2EE1D100DB313F /* PersistenceTestingUtils */; }; + F1B3DEC12D2D90BB00D33FA5 /* NetworkingTestingUtils in Frameworks */ = {isa = PBXBuildFile; productRef = F1B3DEC02D2D90BB00D33FA5 /* NetworkingTestingUtils */; }; + F1B3DEC32D2D90D500D33FA5 /* NetworkingTestingUtils in Frameworks */ = {isa = PBXBuildFile; productRef = F1B3DEC22D2D90D500D33FA5 /* NetworkingTestingUtils */; }; F1BDDBFD2C340D9C00459306 /* SubscriptionContainerViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1BDDBF92C340D9C00459306 /* SubscriptionContainerViewModelTests.swift */; }; F1BDDBFE2C340D9C00459306 /* SubscriptionFlowViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1BDDBFA2C340D9C00459306 /* SubscriptionFlowViewModelTests.swift */; }; F1BDDBFF2C340D9C00459306 /* SubscriptionPagesUseSubscriptionFeatureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1BDDBFB2C340D9C00459306 /* SubscriptionPagesUseSubscriptionFeatureTests.swift */; }; @@ -1229,6 +1231,8 @@ F1D477C61F2126CC0031ED49 /* OmniBarState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D477C51F2126CC0031ED49 /* OmniBarState.swift */; }; F1D477C91F2139410031ED49 /* SmallOmniBarStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D477C81F2139410031ED49 /* SmallOmniBarStateTests.swift */; }; F1D477CB1F2149C40031ED49 /* Type.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D477CA1F2149C40031ED49 /* Type.swift */; }; + F1D5FEC32CF74B6800E00C3A /* Logger+Pixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D5FEC22CF74B6800E00C3A /* Logger+Pixel.swift */; }; + F1D5FEC52CF75B4900E00C3A /* TokenBackgroundRefreshTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D5FEC42CF75B4900E00C3A /* TokenBackgroundRefreshTask.swift */; }; F1D796EC1E7AB8930019D451 /* SaveBookmarkActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D796EB1E7AB8930019D451 /* SaveBookmarkActivity.swift */; }; F1D796EE1E7AF2EB0019D451 /* UIViewControllerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F143C32C1E4A9A4800CFDE3A /* UIViewControllerExtension.swift */; }; F1D796F01E7B07610019D451 /* BookmarksViewControllerCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D796EF1E7B07610019D451 /* BookmarksViewControllerCells.swift */; }; @@ -1502,7 +1506,6 @@ 1E4F4A59297193DE00625985 /* MainViewController+CookiesManaged.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainViewController+CookiesManaged.swift"; sourceTree = ""; }; 1E4FAA6327D8DFB900ADC5B3 /* OngoingDownloadRowViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OngoingDownloadRowViewModel.swift; sourceTree = ""; }; 1E4FAA6527D8DFC800ADC5B3 /* CompleteDownloadRowViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompleteDownloadRowViewModel.swift; sourceTree = ""; }; - 1E53508E2C7C9A1F00818DAA /* DefaultSubscriptionManager+AccountManagerKeychainAccessDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DefaultSubscriptionManager+AccountManagerKeychainAccessDelegate.swift"; sourceTree = ""; }; 1E6A4D682984208800A371D3 /* LocaleExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocaleExtension.swift; sourceTree = ""; }; 1E7A71162934EB6400B7EA19 /* OmniBarNotificationAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmniBarNotificationAnimator.swift; sourceTree = ""; }; 1E7A71182934EC6100B7EA19 /* OmniBarNotificationContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmniBarNotificationContainerView.swift; sourceTree = ""; }; @@ -2774,7 +2777,6 @@ B6DFE6CF2BC7E47500A9CE59 /* SwiftLintTool.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftLintTool.bundle; sourceTree = BUILT_PRODUCTS_DIR; }; B6DFE6D92BC7E61B00A9CE59 /* SwiftLintToolBundleConfiguration.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = SwiftLintToolBundleConfiguration.xcconfig; sourceTree = ""; }; BBFF18B02C76448100C48D7D /* QuerySubmittedTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QuerySubmittedTests.swift; sourceTree = ""; }; - BD10B8A92C7629740033115D /* Logger+Subscription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Logger+Subscription.swift"; sourceTree = ""; }; BD15DB842B959CFD00821457 /* BundleExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleExtension.swift; sourceTree = ""; }; BD2F39EA2C19F955005B19E7 /* NetworkProtectionDNSSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionDNSSettingsView.swift; sourceTree = ""; }; BD862E022B30DA170073E2EE /* VPNFeedbackFormViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNFeedbackFormViewModel.swift; sourceTree = ""; }; @@ -2794,8 +2796,6 @@ BDF8D0012C1B87F4003E3B27 /* NetworkProtectionDNSSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionDNSSettingsViewModel.swift; sourceTree = ""; }; BDFF03192BA39C5A00F324C9 /* NetworkProtectionFeatureVisibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionFeatureVisibility.swift; sourceTree = ""; }; BDFF031C2BA3D2BD00F324C9 /* DefaultNetworkProtectionVisibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultNetworkProtectionVisibility.swift; sourceTree = ""; }; - BDFF03202BA3D3CF00F324C9 /* NetworkProtectionVisibilityForTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionVisibilityForTunnelProvider.swift; sourceTree = ""; }; - BDFF03242BA3D92E00F324C9 /* NetworkProtectionFeatureVisibilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionFeatureVisibilityTests.swift; sourceTree = ""; }; C10CB5F22A1A5BDF0048E503 /* AutofillViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillViews.swift; sourceTree = ""; }; C1106F302D0EFD8B0054A221 /* FreeTrialsFeatureFlagExperimentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FreeTrialsFeatureFlagExperimentTests.swift; sourceTree = ""; }; C111B26827F579EF006558B1 /* BookmarkOrFolderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkOrFolderTests.swift; sourceTree = ""; }; @@ -3205,6 +3205,8 @@ F1D477C51F2126CC0031ED49 /* OmniBarState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OmniBarState.swift; sourceTree = ""; }; F1D477C81F2139410031ED49 /* SmallOmniBarStateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SmallOmniBarStateTests.swift; sourceTree = ""; }; F1D477CA1F2149C40031ED49 /* Type.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Type.swift; sourceTree = ""; }; + F1D5FEC22CF74B6800E00C3A /* Logger+Pixel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+Pixel.swift"; sourceTree = ""; }; + F1D5FEC42CF75B4900E00C3A /* TokenBackgroundRefreshTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenBackgroundRefreshTask.swift; sourceTree = ""; }; F1D796EB1E7AB8930019D451 /* SaveBookmarkActivity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SaveBookmarkActivity.swift; sourceTree = ""; }; F1D796EF1E7B07610019D451 /* BookmarksViewControllerCells.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarksViewControllerCells.swift; sourceTree = ""; }; F1D796F31E7C2A410019D451 /* BookmarksDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarksDelegate.swift; sourceTree = ""; }; @@ -3307,8 +3309,10 @@ 4BE67B052B96B9AB007335F7 /* ContentBlocking in Frameworks */, F15531922BF215ED0029ED04 /* SubscriptionTestingUtilities in Frameworks */, F15531902BF215ED0029ED04 /* Subscription in Frameworks */, - F115ED9C2B4EFC8E001A0453 /* TestUtils in Frameworks */, + F1B3DEC12D2D90BB00D33FA5 /* NetworkingTestingUtils in Frameworks */, + F1AFDF822D2EE1D100DB313F /* PersistenceTestingUtils in Frameworks */, EEFAB4672A73C230008A38E4 /* NetworkProtectionTestUtils in Frameworks */, + F15764C02D3E6AE60086AA21 /* Networking in Frameworks */, 4BE67B072B96B9B0007335F7 /* Common in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -3343,6 +3347,8 @@ 4BE67B012B96B741007335F7 /* Common in Frameworks */, F15531962BF215F60029ED04 /* SubscriptionTestingUtilities in Frameworks */, F15531942BF215F60029ED04 /* Subscription in Frameworks */, + F15764C42D3E6AF60086AA21 /* NetworkingTestingUtils in Frameworks */, + F15764C22D3E6AF60086AA21 /* Networking in Frameworks */, 4BE67B032B96B864007335F7 /* ContentBlocking in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -3369,13 +3375,15 @@ buildActionMask = 2147483647; files = ( 98424AB42CEDD61C0071C7DB /* BrowserServicesKitTestsUtils in Frameworks */, + F1AFDF802D2EE10A00DB313F /* PersistenceTestingUtils in Frameworks */, + F1B3DEC32D2D90D500D33FA5 /* NetworkingTestingUtils in Frameworks */, 98424A962CED4F430071C7DB /* OHHTTPStubs in Frameworks */, 98424A972CED4F430071C7DB /* OHHTTPStubsSwift in Frameworks */, 98424A982CED4F430071C7DB /* ContentBlocking in Frameworks */, + F15764C62D3E6B010086AA21 /* Networking in Frameworks */, 98424AB22CEDD6150071C7DB /* BrowserServicesKit in Frameworks */, 98424A992CED4F430071C7DB /* SubscriptionTestingUtilities in Frameworks */, 98424A9A2CED4F430071C7DB /* Subscription in Frameworks */, - 98424A9B2CED4F430071C7DB /* TestUtils in Frameworks */, 98424A9C2CED4F430071C7DB /* NetworkProtectionTestUtils in Frameworks */, 98424A9D2CED4F430071C7DB /* Common in Frameworks */, ); @@ -5406,7 +5414,6 @@ children = ( BDFF03192BA39C5A00F324C9 /* NetworkProtectionFeatureVisibility.swift */, BDFF031C2BA3D2BD00F324C9 /* DefaultNetworkProtectionVisibility.swift */, - BDFF03202BA3D3CF00F324C9 /* NetworkProtectionVisibilityForTunnelProvider.swift */, ); name = "Feature Visibility"; sourceTree = ""; @@ -5845,7 +5852,6 @@ isa = PBXGroup; children = ( D6D95CE42B6DA3F200960317 /* AsyncHeadlessWebview */, - 1E53508E2C7C9A1F00818DAA /* DefaultSubscriptionManager+AccountManagerKeychainAccessDelegate.swift */, D664C7962B289AA000CBFA76 /* Extensions */, BDE91CD42C6292BF0005CB74 /* Feedback */, C12552952D0B066B00A0FDAA /* FreeTrialsExperiment */, @@ -5854,6 +5860,7 @@ D65CEA6F2B6AC6C9008A759B /* Subscription.xcassets */, 1E39BEAF2CC9477200496FBA /* SubscriptionCookieManageEventPixelMapping.swift */, F1FDC92F2BF4E0B3006B1435 /* SubscriptionEnvironment+Default.swift */, + F1D5FEC42CF75B4900E00C3A /* TokenBackgroundRefreshTask.swift */, D664C7B02B289AA000CBFA76 /* UserScripts */, D664C7932B289AA000CBFA76 /* ViewModel */, D664C7AC2B289AA000CBFA76 /* Views */, @@ -5879,7 +5886,6 @@ D664C7962B289AA000CBFA76 /* Extensions */ = { isa = PBXGroup; children = ( - BD10B8A92C7629740033115D /* Logger+Subscription.swift */, F1FDC9342BF51E41006B1435 /* VPNSettings+Environment.swift */, D664C7982B289AA000CBFA76 /* WKUserContentController+Handler.swift */, ); @@ -6051,7 +6057,6 @@ children = ( EEFE9C722A603CE9005B0A26 /* NetworkProtectionStatusViewModelTests.swift */, EEC02C152B065BE00045CE11 /* NetworkProtectionVPNLocationViewModelTests.swift */, - BDFF03242BA3D92E00F324C9 /* NetworkProtectionFeatureVisibilityTests.swift */, ); name = NetworkProtection; sourceTree = ""; @@ -6425,6 +6430,7 @@ F143C3191E4A99DD00CFDE3A /* Utilities */ = { isa = PBXGroup; children = ( + F1D5FEC22CF74B6800E00C3A /* Logger+Pixel.swift */, 6F9857332CD27F98001BE9A0 /* BoolFileMarker.swift */, 9FCFCD7D2C6AF52A006EB7A0 /* LaunchOptionsHandler.swift */, B603974829C19F6F00902A34 /* Assertions.swift */, @@ -7106,11 +7112,13 @@ F486D3352506A037002D07D7 /* OHHTTPStubs */, F486D3372506A225002D07D7 /* OHHTTPStubsSwift */, EEFAB4662A73C230008A38E4 /* NetworkProtectionTestUtils */, - F115ED9B2B4EFC8E001A0453 /* TestUtils */, 4BE67B042B96B9AB007335F7 /* ContentBlocking */, 4BE67B062B96B9B0007335F7 /* Common */, F155318F2BF215ED0029ED04 /* Subscription */, F15531912BF215ED0029ED04 /* SubscriptionTestingUtilities */, + F1B3DEC02D2D90BB00D33FA5 /* NetworkingTestingUtils */, + F1AFDF812D2EE1D100DB313F /* PersistenceTestingUtils */, + F15764BF2D3E6AE60086AA21 /* Networking */, ); productName = DuckDuckGoTests; productReference = 84E341A61E2F7EFB00BDBA6F /* UnitTests.xctest */; @@ -7176,6 +7184,8 @@ 4BE67B022B96B864007335F7 /* ContentBlocking */, F15531932BF215F60029ED04 /* Subscription */, F15531952BF215F60029ED04 /* SubscriptionTestingUtilities */, + F15764C12D3E6AF60086AA21 /* Networking */, + F15764C32D3E6AF60086AA21 /* NetworkingTestingUtils */, ); productName = IntegrationTests; productReference = 85D33FCB25C97B6E002B91A6 /* IntegrationTests.xctest */; @@ -7240,13 +7250,15 @@ 984249C02CED4F430071C7DB /* OHHTTPStubs */, 984249C22CED4F430071C7DB /* OHHTTPStubsSwift */, 984249C32CED4F430071C7DB /* NetworkProtectionTestUtils */, - 984249C52CED4F430071C7DB /* TestUtils */, 984249C62CED4F430071C7DB /* ContentBlocking */, 984249C72CED4F430071C7DB /* Common */, 984249C82CED4F430071C7DB /* Subscription */, 984249C92CED4F430071C7DB /* SubscriptionTestingUtilities */, 98424AB12CEDD6150071C7DB /* BrowserServicesKit */, 98424AB32CEDD61C0071C7DB /* BrowserServicesKitTestsUtils */, + F1B3DEC22D2D90D500D33FA5 /* NetworkingTestingUtils */, + F1AFDF7F2D2EE10A00DB313F /* PersistenceTestingUtils */, + F15764C52D3E6B010086AA21 /* Networking */, ); productName = DuckDuckGoTests; productReference = 98424AA92CED4F430071C7DB /* WebViewUnitTests.xctest */; @@ -7867,7 +7879,6 @@ 0214407A2C7FB28F00426724 /* VPNAgentConfigurationURLProvider.swift in Sources */, EE3766DE2AC5945500AAB575 /* NetworkProtectionUNNotificationPresenter.swift in Sources */, F1FDC9362BF51E41006B1435 /* VPNSettings+Environment.swift in Sources */, - BDFF03212BA3D3CF00F324C9 /* NetworkProtectionVisibilityForTunnelProvider.swift in Sources */, EEFC6A602AC0F2F80065027D /* UserText.swift in Sources */, 7B4DC5C42CB2B1D000EE5CC2 /* WidgetKind.swift in Sources */, 7BC571212BDBB977003B0CCE /* VPNActivationDateStore.swift in Sources */, @@ -8084,6 +8095,7 @@ 986DA94A24884B18004A7E39 /* WebViewTransition.swift in Sources */, 31B524572715BB23002225AB /* WebJSAlert.swift in Sources */, C1641EB32BC2F53C0012607A /* ImportPasswordsViewModel.swift in Sources */, + F1D5FEC52CF75B4900E00C3A /* TokenBackgroundRefreshTask.swift in Sources */, 9FDEC7BF2C91264C00C7A692 /* OnboardingAddressBarPositionPicker.swift in Sources */, 8536A1FD2ACF114B003AC5BA /* Theme+DesignSystem.swift in Sources */, F114C55B1E66EB020018F95F /* NibLoading.swift in Sources */, @@ -8375,7 +8387,6 @@ 9820EAF522613CD30089094D /* WebProgressWorker.swift in Sources */, 1EEF387D285B1A1100383393 /* TrackerImageCache.swift in Sources */, 3151F0EC27357FEE00226F58 /* VoiceSearchFeedbackViewModel.swift in Sources */, - 1E53508F2C7C9A1F00818DAA /* DefaultSubscriptionManager+AccountManagerKeychainAccessDelegate.swift in Sources */, 1DDF402D2BA09482006850D9 /* SettingsMainSettingsView.swift in Sources */, 85010502292FB1000033978F /* FireproofFaviconUpdater.swift in Sources */, F1C4A70E1E57725800A6CA1B /* OmniBar.swift in Sources */, @@ -8507,7 +8518,6 @@ D664C7B92B289AA200CBFA76 /* WKUserContentController+Handler.swift in Sources */, 1E8AD1D727C2E24E00ABA377 /* DownloadsListRowViewModel.swift in Sources */, 9FEA222E2C324ECD006B03BF /* ViewVisibility.swift in Sources */, - BD10B8AA2C7629740033115D /* Logger+Subscription.swift in Sources */, D6E0C1892B7A2E0D00D5E1E9 /* DesktopDownloadViewModel.swift in Sources */, 8524CC9A246DA81700E59D45 /* FullscreenDaxDialogViewController.swift in Sources */, 9F23B8012C2BC94400950875 /* OnboardingBackground.swift in Sources */, @@ -8632,7 +8642,6 @@ EEC02C162B065BE00045CE11 /* NetworkProtectionVPNLocationViewModelTests.swift in Sources */, 6F934F862C58DB00008364E4 /* NewTabPageSettingsPersistentStorageTests.swift in Sources */, 987130C5294AAB9F00AB05E0 /* BookmarkEditorViewModelTests.swift in Sources */, - BDFF03262BA3DA4900F324C9 /* NetworkProtectionFeatureVisibilityTests.swift in Sources */, 9F8E0F332CCA642D001EA7C5 /* VideoPlayerViewModelTests.swift in Sources */, 6F1422842D314DD100B6D3DE /* TabInteractionStateDiskSourceTests.swift in Sources */, D62EC3BA2C246A7000FC9D04 /* YoutublePlayerNavigationHandlerTests.swift in Sources */, @@ -8973,6 +8982,7 @@ 4BE2756827304F57006B20B0 /* URLRequestExtension.swift in Sources */, 85BA79911F6FF75000F59015 /* ContentBlockerStoreConstants.swift in Sources */, 85E242172AB1B54D000F3E28 /* ReturnUserMeasurement.swift in Sources */, + F1D5FEC32CF74B6800E00C3A /* Logger+Pixel.swift in Sources */, 85BDC3142434D8F80053DB07 /* DebugUserScript.swift in Sources */, 85011867290028C400BDEE27 /* BookmarksDatabase.swift in Sources */, 1D8F727F2BA86D8000E31493 /* PixelExperiment.swift in Sources */, @@ -11952,8 +11962,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { - kind = exactVersion; - version = 226.0.0; + branch = fcappelli/subscription_oauth_api_v2; + kind = branch; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { @@ -12185,11 +12195,6 @@ package = 984249C42CED4F430071C7DB /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = NetworkProtectionTestUtils; }; - 984249C52CED4F430071C7DB /* TestUtils */ = { - isa = XCSwiftPackageProductDependency; - package = 984249C42CED4F430071C7DB /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; - productName = TestUtils; - }; 984249C62CED4F430071C7DB /* ContentBlocking */ = { isa = XCSwiftPackageProductDependency; package = 984249C42CED4F430071C7DB /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; @@ -12307,11 +12312,6 @@ package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = NetworkProtectionTestUtils; }; - F115ED9B2B4EFC8E001A0453 /* TestUtils */ = { - isa = XCSwiftPackageProductDependency; - package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; - productName = TestUtils; - }; F155318F2BF215ED0029ED04 /* Subscription */ = { isa = XCSwiftPackageProductDependency; package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; @@ -12332,6 +12332,46 @@ package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = SubscriptionTestingUtilities; }; + F15764BF2D3E6AE60086AA21 /* Networking */ = { + isa = XCSwiftPackageProductDependency; + package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Networking; + }; + F15764C12D3E6AF60086AA21 /* Networking */ = { + isa = XCSwiftPackageProductDependency; + package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Networking; + }; + F15764C32D3E6AF60086AA21 /* NetworkingTestingUtils */ = { + isa = XCSwiftPackageProductDependency; + package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = NetworkingTestingUtils; + }; + F15764C52D3E6B010086AA21 /* Networking */ = { + isa = XCSwiftPackageProductDependency; + package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Networking; + }; + F1AFDF7F2D2EE10A00DB313F /* PersistenceTestingUtils */ = { + isa = XCSwiftPackageProductDependency; + package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = PersistenceTestingUtils; + }; + F1AFDF812D2EE1D100DB313F /* PersistenceTestingUtils */ = { + isa = XCSwiftPackageProductDependency; + package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = PersistenceTestingUtils; + }; + F1B3DEC02D2D90BB00D33FA5 /* NetworkingTestingUtils */ = { + isa = XCSwiftPackageProductDependency; + package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = NetworkingTestingUtils; + }; + F1B3DEC22D2D90D500D33FA5 /* NetworkingTestingUtils */ = { + isa = XCSwiftPackageProductDependency; + package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = NetworkingTestingUtils; + }; F1D43AF92B99C1D300BAB743 /* BareBonesBrowserKit */ = { isa = XCSwiftPackageProductDependency; package = F1D43AF82B99C1D300BAB743 /* XCRemoteSwiftPackageReference "BareBonesBrowser" */; diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index d2d80a070b..2a6af9a009 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "revision" : "d1275238d88f25cd31a29be2b4f20bbba5de9aa1", - "version" : "226.0.0" + "branch" : "fcappelli/subscription_oauth_api_v2", + "revision" : "ecf8c5d3cee2eab26c04da06979f3ff70ab2c6fe" } }, { @@ -90,6 +90,15 @@ "version" : "2.0.0" } }, + { + "identity" : "jwt-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/jwt-kit.git", + "state" : { + "revision" : "c2595b9ad7f512d7f334830b4df1fed6e917946a", + "version" : "4.13.4" + } + }, { "identity" : "kingfisher", "kind" : "remoteSourceControl", @@ -144,6 +153,24 @@ "version" : "1.4.0" } }, + { + "identity" : "swift-asn1", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-asn1.git", + "state" : { + "revision" : "ae33e5941bb88d88538d0a6b19ca0b01e6c76dcf", + "version" : "1.3.1" + } + }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto.git", + "state" : { + "revision" : "ff0f781cf7c6a22d52957e50b104f5768b50c779", + "version" : "3.10.0" + } + }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/AdhocDebug.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/AdhocDebug.xcscheme index 5750e6f61e..6d74bb82b3 100644 --- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/AdhocDebug.xcscheme +++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/AdhocDebug.xcscheme @@ -1,6 +1,6 @@ (userDefaults: subscriptionUserDefaults, - key: UserDefaultsCacheKey.subscriptionEntitlements, - settings: UserDefaultsCacheSettings(defaultExpirationInterval: .minutes(20))) - let accessTokenStorage = SubscriptionTokenKeychainStorage(keychainType: .dataProtection(.named(subscriptionAppGroup))) - let subscriptionService = DefaultSubscriptionEndpointService(currentServiceEnvironment: subscriptionEnvironment.serviceEnvironment) - let authService = DefaultAuthEndpointService(currentServiceEnvironment: subscriptionEnvironment.serviceEnvironment) - let subscriptionFeatureMappingCache = DefaultSubscriptionFeatureMappingCache(subscriptionEndpointService: subscriptionService, - userDefaults: subscriptionUserDefaults) - - let accountManager = DefaultAccountManager(accessTokenStorage: accessTokenStorage, - entitlementsCache: entitlementsCache, - subscriptionEndpointService: subscriptionService, - authEndpointService: authService) - - let storePurchaseManager = DefaultStorePurchaseManager(subscriptionFeatureMappingCache: subscriptionFeatureMappingCache) - + let configuration = URLSessionConfiguration.default + configuration.httpCookieStorage = nil + configuration.requestCachePolicy = .reloadIgnoringLocalCacheData + let urlSession = URLSession(configuration: configuration, + delegate: SessionDelegate(), + delegateQueue: nil) + let apiService = DefaultAPIService(urlSession: urlSession) + let authEnvironment: OAuthEnvironment = subscriptionEnvironment.serviceEnvironment == .production ? .production : .staging + + let authService = DefaultOAuthService(baseURL: authEnvironment.url, apiService: apiService) + let theFeatureFlagger = featureFlagger + + // keychain storage + let subscriptionAppGroup = Bundle.main.appGroup(bundle: .subs) + let tokenStorage = SubscriptionTokenKeychainStorageV2(keychainType: .dataProtection(.named(subscriptionAppGroup))) { keychainType, error in + Pixel.fire(.privacyProKeychainAccessError, withAdditionalParameters: ["type": keychainType.rawValue, "error": error.errorDescription]) + } + let legacyAccountStorage = SubscriptionTokenKeychainStorage(keychainType: .dataProtection(.named(subscriptionAppGroup))) + + let authClient = DefaultOAuthClient(tokensStorage: tokenStorage, + legacyTokenStorage: legacyAccountStorage, + authService: authService) + + apiService.authorizationRefresherCallback = { _ in + guard let tokenContainer = tokenStorage.tokenContainer else { + throw OAuthClientError.internalError("Missing refresh token") + } + + if tokenContainer.decodedAccessToken.isExpired() { + Logger.OAuth.debug("Refreshing tokens") + let tokens = try await authClient.getTokens(policy: .localForceRefresh) + return tokens.accessToken + } else { + Logger.general.debug("Trying to refresh valid token, using the old one") + return tokenContainer.accessToken + } + } + let subscriptionEndpointService = DefaultSubscriptionEndpointService(apiService: apiService, + baseURL: subscriptionEnvironment.serviceEnvironment.url) + let storePurchaseManager = DefaultStorePurchaseManager(subscriptionFeatureMappingCache: subscriptionEndpointService) + let pixelHandler: SubscriptionManager.PixelHandler = { type in + switch type { + case .deadToken: + Pixel.fire(pixel: .privacyProDeadTokenDetected) + case .subscriptionIsActive: + DailyPixel.fire(pixel: .privacyProSubscriptionActive) + case .v1MigrationFailed: + Pixel.fire(pixel: .authV1MigrationFailed) + case .v1MigrationSuccessful: + Pixel.fire(pixel: .authV1MigrationSucceeded) + } + } let subscriptionManager = DefaultSubscriptionManager(storePurchaseManager: storePurchaseManager, - accountManager: accountManager, - subscriptionEndpointService: subscriptionService, - authEndpointService: authService, - subscriptionFeatureMappingCache: subscriptionFeatureMappingCache, - subscriptionEnvironment: subscriptionEnvironment) - accountManager.delegate = subscriptionManager - + oAuthClient: authClient, + subscriptionEndpointService: subscriptionEndpointService, + subscriptionEnvironment: subscriptionEnvironment, + pixelHandler: pixelHandler) self.subscriptionManager = subscriptionManager - - let accessTokenProvider: () -> String? = { - return { accountManager.accessToken } - }() - - networkProtectionKeychainTokenStore = NetworkProtectionKeychainTokenStore(accessTokenProvider: accessTokenProvider) - - networkProtectionTunnelController = NetworkProtectionTunnelController(accountManager: accountManager, - tokenStore: networkProtectionKeychainTokenStore, + networkProtectionTunnelController = NetworkProtectionTunnelController(tokenProvider: subscriptionManager, featureFlagger: featureFlagger, persistentPixel: persistentPixel, settings: vpnSettings) vpnFeatureVisibility = DefaultNetworkProtectionVisibility(userDefaults: .networkProtectionGroupDefaults, - accountManager: accountManager) + oAuthClient: authClient) } - } diff --git a/DuckDuckGo/AppLifecycle/AppStates/Foreground.swift b/DuckDuckGo/AppLifecycle/AppStates/Foreground.swift index 165e972944..5ebad11774 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Foreground.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Foreground.swift @@ -182,7 +182,7 @@ struct Foreground: AppState { Task { await stopAndRemoveVPNIfNotAuthenticated() await application.refreshVPNShortcuts(vpnFeatureVisibility: appDependencies.vpnFeatureVisibility, - accountManager: appDependencies.accountManager) + subscriptionManager: appDependencies.subscriptionManager) await appDependencies.vpnWorkaround.installRedditSessionWorkaround() if #available(iOS 17.0, *) { @@ -190,11 +190,11 @@ struct Foreground: AppState { } } - AppDependencyProvider.shared.subscriptionManager.refreshCachedSubscriptionAndEntitlements { isSubscriptionActive in - if isSubscriptionActive { - DailyPixel.fire(pixel: .privacyProSubscriptionActive) - } - } +// AppDependencyProvider.shared.subscriptionManager.refreshCachedSubscription { isSubscriptionActive in +// if isSubscriptionActive { +// DailyPixel.fire(pixel: .privacyProSubscriptionActive) +// } +// } Task { await appDependencies.subscriptionService.subscriptionCookieManager.refreshSubscriptionCookie() @@ -378,7 +378,8 @@ struct Foreground: AppState { private func stopAndRemoveVPNIfNotAuthenticated() async { // Only remove the VPN if the user is not authenticated, and it's installed: - guard !appDependencies.accountManager.isUserAuthenticated, await AppDependencyProvider.shared.networkProtectionTunnelController.isInstalled else { + guard !AppDependencyProvider.shared.subscriptionManager.isUserAuthenticated, + await AppDependencyProvider.shared.networkProtectionTunnelController.isInstalled else { return } @@ -463,10 +464,14 @@ struct Foreground: AppState { @MainActor func presentNetworkProtectionStatusSettingsModal() { Task { - if case .success(let hasEntitlements) = await appDependencies.accountManager.hasEntitlement(forProductName: .networkProtection), hasEntitlements { - (window.rootViewController as? MainViewController)?.segueToVPN() + if await AppDependencyProvider.shared.subscriptionManager.isFeatureAvailableForUser(.networkProtection) { + Task { @MainActor in + (window.rootViewController as? MainViewController)?.segueToVPN() + } } else { - (window.rootViewController as? MainViewController)?.segueToPrivacyPro() + Task { @MainActor in + (window.rootViewController as? MainViewController)?.segueToPrivacyPro() + } } } } diff --git a/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift b/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift new file mode 100644 index 0000000000..93ec94b6ad --- /dev/null +++ b/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift @@ -0,0 +1,61 @@ +// +// Inactive.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 UIKit + +struct Inactive: AppState { + + private let application: UIApplication + private let appDependencies: AppDependencies + + var urlToOpen: URL? + + init(stateContext: Active.StateContext) { + application = stateContext.application + appDependencies = stateContext.appDependencies + + let vpnFeatureVisibility = appDependencies.vpnFeatureVisibility + let subscriptionManager = appDependencies.subscriptionManager + let vpnWorkaround = appDependencies.vpnWorkaround + Task { @MainActor [application] in + await application.refreshVPNShortcuts(vpnFeatureVisibility: vpnFeatureVisibility, + subscriptionManager: subscriptionManager) + await vpnWorkaround.removeRedditSessionWorkaround() + } + } + +} + +extension Inactive { + + struct StateContext { + + let application: UIApplication + let urlToOpen: URL? + let appDependencies: AppDependencies + + } + + func makeStateContext() -> StateContext { + .init(application: application, + urlToOpen: urlToOpen, + appDependencies: appDependencies) + } + +} diff --git a/DuckDuckGo/AppLifecycle/AppStates/Launching.swift b/DuckDuckGo/AppLifecycle/AppStates/Launching.swift index 5c6fa26108..34f4b301c9 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Launching.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Launching.swift @@ -59,7 +59,7 @@ struct Launching: AppState { crashCollectionStorage: UserDefaults()) private let bookmarksDatabase = BookmarksDatabase.make() private let marketplaceAdPostbackManager = MarketplaceAdPostbackManager() - private let accountManager = AppDependencyProvider.shared.accountManager + private let subscriptionManager = AppDependencyProvider.shared.subscriptionManager private let tunnelController = AppDependencyProvider.shared.networkProtectionTunnelController private let vpnFeatureVisibility = AppDependencyProvider.shared.vpnFeatureVisibility private let appSettings = AppDependencyProvider.shared.appSettings @@ -80,6 +80,7 @@ struct Launching: AppState { // These should ideally be let properties instead of force-unwrapped. However, due to various initialization paths, such as database completion blocks, setting them up in advance is currently not feasible. Refactoring will be done once this code is streamlined. private let uiService: UIService private let unService: UNService + private var tokenBackgroundRefreshTask: TokenBackgroundRefreshTask? private let syncDataProviders: SyncDataProviders private let syncService: DDGSync private let isSyncInProgressCancellable: AnyCancellable @@ -102,9 +103,14 @@ struct Launching: AppState { @UserDefaultsWrapper(key: .privacyConfigCustomURL, defaultValue: nil) var privacyConfigCustomURL: String? + Task { + await AppDependencyProvider.shared.subscriptionManager.loadInitialData() + } + application = stateContext.application privacyProDataReporter = PrivacyProDataReporter(fireproofing: fireproofing) - vpnWorkaround = VPNRedditSessionWorkaround(accountManager: accountManager, tunnelController: tunnelController) + vpnWorkaround = VPNRedditSessionWorkaround(subscriptionManager: AppDependencyProvider.shared.subscriptionManager, + tunnelController: AppDependencyProvider.shared.networkProtectionTunnelController) crashReportUploaderOnboarding = CrashCollectionOnboarding(appSettings: AppDependencyProvider.shared.appSettings) defer { @@ -323,14 +329,11 @@ struct Launching: AppState { let url = URL.pixelUrl(forPixelNamed: pixelName) let apiHeaders = APIRequestV2.HeadersV2(additionalHeaders: headers) - let request = APIRequestV2(url: url, method: .get, queryItems: parameters, headers: apiHeaders) - Task { - do { - _ = try await DefaultAPIService().fetch(request: request) - onComplete(true, nil) - } catch { - onComplete(false, error) - } + guard let request = APIRequestV2(url: url, method: .get, + queryItems: parameters.map({ (key, value) in QueryItem(key: key, value: value) }), + headers: apiHeaders) else { + onComplete(false, nil) + return } } PixelKit.configureExperimentKit(featureFlagger: AppDependencyProvider.shared.featureFlagger, @@ -455,7 +458,7 @@ struct Launching: AppState { mainViewController?.tabManager.removeLeftoverInteractionStates() } } - unService = UNService(window: window, accountManager: accountManager) + unService = UNService(window: window, subscriptionManager: subscriptionManager) uiService = UIService(window: window) voiceSearchHelper.migrateSettingsFlagIfNecessary() @@ -464,6 +467,9 @@ struct Launching: AppState { // Having both in `didBecomeActive` can sometimes cause the exception when running on a physical device, so registration happens here. AppConfigurationFetch.registerBackgroundRefreshTaskHandler() + tokenBackgroundRefreshTask = TokenBackgroundRefreshTask(subscriptionManager: AppDependencyProvider.shared.subscriptionManager) + tokenBackgroundRefreshTask?.registerBackgroundRefreshTaskHandler() + UNUserNotificationCenter.current().delegate = unService window.windowScene?.screenshotService?.delegate = uiService @@ -478,8 +484,6 @@ struct Launching: AppState { widgetRefreshModel.beginObservingVPNStatus() - AppDependencyProvider.shared.subscriptionManager.loadInitialData() - let autofillUsageMonitor = AutofillUsageMonitor() autofillPixelReporter = AutofillPixelReporter( userDefaults: .standard, @@ -528,7 +532,7 @@ struct Launching: AppState { private var appDependencies: AppDependencies { AppDependencies( - accountManager: accountManager, + subscriptionManager: subscriptionManager, vpnWorkaround: vpnWorkaround, vpnFeatureVisibility: vpnFeatureVisibility, appSettings: appSettings, diff --git a/DuckDuckGo/AppLifecycle/AppStates/Suspending.swift b/DuckDuckGo/AppLifecycle/AppStates/Suspending.swift index 11944b7ea8..45ac75821f 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Suspending.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Suspending.swift @@ -49,11 +49,11 @@ struct Suspending: AppState { appDependencies = stateContext.appDependencies let vpnFeatureVisibility = appDependencies.vpnFeatureVisibility - let accountManager = appDependencies.accountManager + let subscriptionManager = appDependencies.subscriptionManager let vpnWorkaround = appDependencies.vpnWorkaround Task { @MainActor [application] in await application.refreshVPNShortcuts(vpnFeatureVisibility: vpnFeatureVisibility, - accountManager: accountManager) + subscriptionManager: subscriptionManager) await vpnWorkaround.removeRedditSessionWorkaround() } } diff --git a/DuckDuckGo/AppServices/UNService.swift b/DuckDuckGo/AppServices/UNService.swift index f7874d03f7..a8509e825c 100644 --- a/DuckDuckGo/AppServices/UNService.swift +++ b/DuckDuckGo/AppServices/UNService.swift @@ -25,12 +25,12 @@ import Subscription final class UNService: NSObject { let window: UIWindow - let accountManager: AccountManager + let subscriptionManager: any SubscriptionManager init(window: UIWindow, - accountManager: AccountManager) { + subscriptionManager: any SubscriptionManager) { self.window = window - self.accountManager = accountManager + self.subscriptionManager = subscriptionManager } } @@ -58,11 +58,15 @@ extension UNService: UNUserNotificationCenterDelegate { } private func presentNetworkProtectionStatusSettingsModal() { - Task { @MainActor in - if case .success(let hasEntitlements) = await accountManager.hasEntitlement(forProductName: .networkProtection), hasEntitlements { - (window.rootViewController as? MainViewController)?.segueToVPN() + Task { + if await AppDependencyProvider.shared.subscriptionManager.isFeatureAvailableForUser(.networkProtection) { + Task { @MainActor in + (window.rootViewController as? MainViewController)?.segueToVPN() + } } else { - (window.rootViewController as? MainViewController)?.segueToPrivacyPro() + Task { @MainActor in + (window.rootViewController as? MainViewController)?.segueToPrivacyPro() + } } } } diff --git a/DuckDuckGo/AppShortcuts.swift b/DuckDuckGo/AppShortcuts.swift index aff44e84cf..3104d70341 100644 --- a/DuckDuckGo/AppShortcuts.swift +++ b/DuckDuckGo/AppShortcuts.swift @@ -22,10 +22,9 @@ import UIKit extension UIApplication { - func refreshVPNShortcuts(vpnFeatureVisibility: DefaultNetworkProtectionVisibility, accountManager: AccountManager) async { + func refreshVPNShortcuts(vpnFeatureVisibility: DefaultNetworkProtectionVisibility, subscriptionManager: any SubscriptionManager) async { guard vpnFeatureVisibility.shouldShowVPNShortcut(), - case .success(true) = await accountManager.hasEntitlement(forProductName: .networkProtection, - cachePolicy: .returnCacheDataDontLoad) + await AppDependencyProvider.shared.subscriptionManager.isFeatureAvailableForUser(.networkProtection) else { shortcutItems = nil return diff --git a/DuckDuckGo/DefaultNetworkProtectionVisibility.swift b/DuckDuckGo/DefaultNetworkProtectionVisibility.swift index baa0cebf53..5b1661ad76 100644 --- a/DuckDuckGo/DefaultNetworkProtectionVisibility.swift +++ b/DuckDuckGo/DefaultNetworkProtectionVisibility.swift @@ -23,21 +23,22 @@ import Waitlist import NetworkProtection import Core import Subscription +import Networking struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { private let userDefaults: UserDefaults - private let accountManager: AccountManager + private let oAuthClient: any OAuthClient - init(userDefaults: UserDefaults, accountManager: AccountManager) { + init(userDefaults: UserDefaults, oAuthClient: any OAuthClient) { self.userDefaults = userDefaults - self.accountManager = accountManager + self.oAuthClient = oAuthClient } var token: String? { - return accountManager.accessToken + return oAuthClient.currentTokenContainer?.accessToken } func shouldShowVPNShortcut() -> Bool { - return accountManager.isUserAuthenticated + oAuthClient.isUserAuthenticated } } diff --git a/DuckDuckGo/Feedback/VPNMetadataCollector.swift b/DuckDuckGo/Feedback/VPNMetadataCollector.swift index f77dcfddd2..c38fbe34e4 100644 --- a/DuckDuckGo/Feedback/VPNMetadataCollector.swift +++ b/DuckDuckGo/Feedback/VPNMetadataCollector.swift @@ -102,18 +102,18 @@ protocol VPNMetadataCollector { final class DefaultVPNMetadataCollector: VPNMetadataCollector { private let statusObserver: ConnectionStatusObserver private let serverInfoObserver: ConnectionServerInfoObserver - private let accountManager: AccountManager + private let subscriptionManager: any SubscriptionManager private let settings: VPNSettings private let defaults: UserDefaults init(statusObserver: ConnectionStatusObserver, - serverInfoObserver: ConnectionServerInfoObserver, - accountManager: AccountManager = AppDependencyProvider.shared.subscriptionManager.accountManager, + serverInfoObserver: ConnectionServerInfoObserver,// ConnectionServerInfoObserverThroughSession(), + subscriptionManager: any SubscriptionManager = AppDependencyProvider.shared.subscriptionManager, settings: VPNSettings = .init(defaults: .networkProtectionGroupDefaults), defaults: UserDefaults = .networkProtectionGroupDefaults) { self.statusObserver = statusObserver self.serverInfoObserver = serverInfoObserver - self.accountManager = accountManager + self.subscriptionManager = subscriptionManager self.settings = settings self.defaults = defaults } @@ -242,13 +242,11 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { } func collectPrivacyProInfo() async -> VPNMetadata.PrivacyProInfo { - let hasVPNEntitlement = (try? await accountManager.hasEntitlement(forProductName: .networkProtection).get()) ?? false return .init( - hasPrivacyProAccount: accountManager.isUserAuthenticated, - hasVPNEntitlement: hasVPNEntitlement + hasPrivacyProAccount: subscriptionManager.isUserAuthenticated, + hasVPNEntitlement: await subscriptionManager.isFeatureAvailableForUser(.networkProtection) ) } - } private extension NSError { diff --git a/DuckDuckGo/Info.plist b/DuckDuckGo/Info.plist index 5e88ebbac6..895a2aff77 100644 --- a/DuckDuckGo/Info.plist +++ b/DuckDuckGo/Info.plist @@ -10,6 +10,7 @@ com.duckduckgo.app.configurationRefresh com.duckduckgo.app.remoteMessageRefresh + com.duckduckgo.app.backgroundTokenRefresh CFBundleDevelopmentRegion en diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index 3017205678..f462afb4b8 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -509,8 +509,8 @@ class MainViewController: UIViewController { func presentNetworkProtectionStatusSettingsModal() { Task { - let accountManager = AppDependencyProvider.shared.subscriptionManager.accountManager - if case .success(let hasEntitlements) = await accountManager.hasEntitlement(forProductName: .networkProtection), hasEntitlements { + let subscriptionManager = AppDependencyProvider.shared.subscriptionManager + if await subscriptionManager.isFeatureAvailableForUser(.networkProtection) { segueToVPN() } else { segueToPrivacyPro() @@ -1661,8 +1661,10 @@ class MainViewController: UIViewController { @objc private func onEntitlementsChange(_ notification: Notification) { Task { - let accountManager = AppDependencyProvider.shared.subscriptionManager.accountManager - guard case .success(false) = await accountManager.hasEntitlement(forProductName: .networkProtection) else { return } + let subscriptionManager = AppDependencyProvider.shared.subscriptionManager + + guard let tokenContainer = try? await subscriptionManager.getTokenContainer(policy: .local), + tokenContainer.decodedAccessToken.hasEntitlement(.networkProtection) == false else { return } if await networkProtectionTunnelController.isInstalled { tunnelDefaults.enableEntitlementMessaging() diff --git a/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift b/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift index a72c4f0ab0..5362a5290f 100644 --- a/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift +++ b/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift @@ -67,11 +67,11 @@ extension NetworkProtectionVPNSettingsViewModel { extension NetworkProtectionLocationListCompositeRepository { - convenience init(accountManager: AccountManager) { + convenience init() { let settings = AppDependencyProvider.shared.vpnSettings self.init( environment: settings.selectedEnvironment, - tokenStore: AppDependencyProvider.shared.networkProtectionKeychainTokenStore, + tokenProvider: AppDependencyProvider.shared.subscriptionManager, errorEvents: .networkProtectionAppDebugEvents ) } @@ -79,8 +79,8 @@ extension NetworkProtectionLocationListCompositeRepository { extension NetworkProtectionVPNLocationViewModel { - convenience init(accountManager: AccountManager) { - let locationListRepository = NetworkProtectionLocationListCompositeRepository(accountManager: accountManager) + convenience init() { + let locationListRepository = NetworkProtectionLocationListCompositeRepository() self.init( locationListRepository: locationListRepository, settings: AppDependencyProvider.shared.vpnSettings diff --git a/DuckDuckGo/NetworkProtectionDebugViewController.swift b/DuckDuckGo/NetworkProtectionDebugViewController.swift index fb38929911..313b489512 100644 --- a/DuckDuckGo/NetworkProtectionDebugViewController.swift +++ b/DuckDuckGo/NetworkProtectionDebugViewController.swift @@ -107,7 +107,6 @@ final class NetworkProtectionDebugViewController: UITableViewController { // MARK: Properties private let debugFeatures: NetworkProtectionDebugFeatures - private let tokenStore: NetworkProtectionTokenStore private let pathMonitor = NWPathMonitor() private var currentNetworkPath: String? @@ -125,25 +124,19 @@ final class NetworkProtectionDebugViewController: UITableViewController { private var connectionTestResults: [ConnectionTestResult] = [] private var connectionTestResultError: String? private let connectionTestQueue = DispatchQueue(label: "com.duckduckgo.ios.vpnDebugConnectionTestQueue") - private let accountManager: AccountManager // MARK: Lifecycle required init?(coder: NSCoder, - tokenStore: NetworkProtectionTokenStore, - debugFeatures: NetworkProtectionDebugFeatures = NetworkProtectionDebugFeatures(), - accountManager: AccountManager) { + debugFeatures: NetworkProtectionDebugFeatures = NetworkProtectionDebugFeatures()) { self.debugFeatures = debugFeatures - self.tokenStore = tokenStore - self.accountManager = accountManager super.init(coder: coder) } required convenience init?(coder: NSCoder) { - self.init(coder: coder, tokenStore: AppDependencyProvider.shared.networkProtectionKeychainTokenStore, - accountManager: AppDependencyProvider.shared.subscriptionManager.accountManager) + self.init(coder: coder, debugFeatures: NetworkProtectionDebugFeatures()) } override func viewWillAppear(_ animated: Bool) { @@ -686,9 +679,11 @@ shouldShowVPNShortcut: \(vpnVisibility.shouldShowVPNShortcut() ? "YES" : "NO") if let subscriptionOverrideEnabled = defaults.subscriptionOverrideEnabled { if subscriptionOverrideEnabled { defaults.subscriptionOverrideEnabled = false - accountManager.signOut() + Task { + await AppDependencyProvider.shared.subscriptionManager.signOut(notifyUI: true) + } } else { - defaults.resetsubscriptionOverrideEnabled() + defaults.resetSubscriptionOverrideEnabled() } } else { defaults.subscriptionOverrideEnabled = true diff --git a/DuckDuckGo/NetworkProtectionRootView.swift b/DuckDuckGo/NetworkProtectionRootView.swift index 357e302c3d..c9df1b154b 100644 --- a/DuckDuckGo/NetworkProtectionRootView.swift +++ b/DuckDuckGo/NetworkProtectionRootView.swift @@ -28,9 +28,9 @@ struct NetworkProtectionRootView: View { init() { let subscriptionManager = AppDependencyProvider.shared.subscriptionManager - let accountManager = AppDependencyProvider.shared.subscriptionManager.accountManager - let locationListRepository = NetworkProtectionLocationListCompositeRepository(accountManager: accountManager) - let usesUnifiedFeedbackForm = accountManager.isUserAuthenticated + + let locationListRepository = NetworkProtectionLocationListCompositeRepository() + let usesUnifiedFeedbackForm = subscriptionManager.isUserAuthenticated statusViewModel = NetworkProtectionStatusViewModel(tunnelController: AppDependencyProvider.shared.networkProtectionTunnelController, settings: AppDependencyProvider.shared.vpnSettings, statusObserver: AppDependencyProvider.shared.connectionObserver, diff --git a/DuckDuckGo/NetworkProtectionStatusView.swift b/DuckDuckGo/NetworkProtectionStatusView.swift index 24dd47e31c..24df86aae5 100644 --- a/DuckDuckGo/NetworkProtectionStatusView.swift +++ b/DuckDuckGo/NetworkProtectionStatusView.swift @@ -283,7 +283,7 @@ struct NetworkProtectionStatusView: View { @ViewBuilder private func about() -> some View { - let viewModel = UnifiedFeedbackFormViewModel(subscriptionManager: statusModel.subscriptionManager, + let viewModel = UnifiedFeedbackFormViewModel(subscriptionManager: AppDependencyProvider.shared.subscriptionManager, apiService: DefaultAPIService(), vpnMetadataCollector: DefaultVPNMetadataCollector(), source: .vpn) diff --git a/DuckDuckGo/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtectionTunnelController.swift index ed22d7cd91..172dcc00a7 100644 --- a/DuckDuckGo/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtectionTunnelController.swift @@ -38,7 +38,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr private let featureFlagger: FeatureFlagger private var internalManager: NETunnelProviderManager? private let debugFeatures = NetworkProtectionDebugFeatures() - private let tokenStore: NetworkProtectionKeychainTokenStore + private let tokenProvider: any SubscriptionTokenProvider private let errorStore = NetworkProtectionTunnelErrorStore() private let snoozeTimingStore = NetworkProtectionSnoozeTimingStore(userDefaults: .networkProtectionGroupDefaults) private let notificationCenter: NotificationCenter = .default @@ -92,7 +92,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr case loadFromPreferencesFailed(Error) case saveToPreferencesFailed(Error) case startVPNFailed(Error) - case fetchAuthTokenFailed(Error) + case fetchAuthTokenFailed case configSystemPermissionsDenied(Error) public var errorCode: Int { @@ -109,13 +109,13 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr public var errorUserInfo: [String: Any] { switch self { case - .simulateControllerFailureError: + .simulateControllerFailureError, + .fetchAuthTokenFailed: return [:] case .loadFromPreferencesFailed(let error), .saveToPreferencesFailed(let error), .startVPNFailed(let error), - .fetchAuthTokenFailed(let error), .configSystemPermissionsDenied(let error): return [NSUnderlyingErrorKey: error] } @@ -130,8 +130,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr // MARK: - Initializers - init(accountManager: AccountManager, - tokenStore: NetworkProtectionKeychainTokenStore, + init(tokenProvider: any SubscriptionTokenProvider, featureFlagger: FeatureFlagger, persistentPixel: PersistentPixelFiring, settings: VPNSettings) { @@ -139,7 +138,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr self.featureFlagger = featureFlagger self.persistentPixel = persistentPixel self.settings = settings - self.tokenStore = tokenStore + self.tokenProvider = tokenProvider subscribeToSnoozeTimingChanges() subscribeToStatusChanges() @@ -268,7 +267,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr // Intentional no-op break default: - try start(tunnelManager) + try await start(tunnelManager) } } @@ -276,7 +275,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr internalManager = nil } - private func start(_ tunnelManager: NETunnelProviderManager) throws { + private func start(_ tunnelManager: NETunnelProviderManager) async throws { var options = [String: NSObject]() if Self.shouldSimulateFailure { @@ -285,10 +284,11 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr } options["activationAttemptId"] = UUID().uuidString as NSString - do { - options["authToken"] = try tokenStore.fetchToken() as NSString? - } catch { - throw StartError.fetchAuthTokenFailed(error) + + if let token = try await tokenProvider.getTokenContainer(policy: .localValid).accessToken as NSString? { + options["authToken"] = token + } else { + throw StartError.fetchAuthTokenFailed } options[NetworkProtectionOptionKey.selectedEnvironment] = AppDependencyProvider.shared.vpnSettings .selectedEnvironment.rawValue as NSString diff --git a/DuckDuckGo/NetworkProtectionVPNLocationView.swift b/DuckDuckGo/NetworkProtectionVPNLocationView.swift index eb49cbdff8..a722c54d45 100644 --- a/DuckDuckGo/NetworkProtectionVPNLocationView.swift +++ b/DuckDuckGo/NetworkProtectionVPNLocationView.swift @@ -21,7 +21,7 @@ import Foundation import SwiftUI struct NetworkProtectionVPNLocationView: View { - @StateObject var model = NetworkProtectionVPNLocationViewModel(accountManager: AppDependencyProvider.shared.subscriptionManager.accountManager) + @StateObject var model = NetworkProtectionVPNLocationViewModel() var body: some View { List { diff --git a/DuckDuckGo/NetworkProtectionVisibilityForTunnelProvider.swift b/DuckDuckGo/NetworkProtectionVisibilityForTunnelProvider.swift deleted file mode 100644 index 1944d2a806..0000000000 --- a/DuckDuckGo/NetworkProtectionVisibilityForTunnelProvider.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// NetworkProtectionVisibilityForTunnelProvider.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 Subscription - -struct NetworkProtectionVisibilityForTunnelProvider: NetworkProtectionFeatureVisibility { - - private let accountManager: AccountManager - - init(accountManager: AccountManager) { - self.accountManager = accountManager - } - - func isPrivacyProLaunched() -> Bool { - accountManager.isUserAuthenticated - } - - func shouldMonitorEntitlement() -> Bool { - isPrivacyProLaunched() - } - - func shouldShowVPNShortcut() -> Bool { - guard isPrivacyProLaunched() else { - return false - } - - return accountManager.isUserAuthenticated - } -} diff --git a/DuckDuckGo/OldAppDelegate.swift b/DuckDuckGo/OldAppDelegate.swift new file mode 100644 index 0000000000..de0995d233 --- /dev/null +++ b/DuckDuckGo/OldAppDelegate.swift @@ -0,0 +1,1275 @@ +// +// OldAppDelegate.swift +// DuckDuckGo +// +// Copyright © 2017 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 UIKit +import Combine +import Common +import Core +import UserNotifications +import Kingfisher +import WidgetKit +import BackgroundTasks +import BrowserServicesKit +import Bookmarks +import Persistence +import Crashes +import Configuration +import Networking +import DDGSync +import RemoteMessaging +import SyncDataProviders +import Subscription +import NetworkProtection +import PixelKit +import PixelExperimentKit +import WebKit +import os.log + +@MainActor +final class OldAppDelegate: NSObject, UIApplicationDelegate, DDGApp { + + private var testing = false + var appIsLaunching = false + var overlayWindow: UIWindow? + var window: UIWindow? { + get { + appDelegate?.window + } + set { + appDelegate?.window = newValue + } + } + + private lazy var privacyStore = PrivacyUserDefaults() + private var bookmarksDatabase: CoreDataDatabase = BookmarksDatabase.make() + + private let widgetRefreshModel = NetworkProtectionWidgetRefreshModel() + private let tunnelDefaults = UserDefaults.networkProtectionGroupDefaults + + @MainActor + private lazy var vpnWorkaround: VPNRedditSessionWorkaround = { + return VPNRedditSessionWorkaround( + subscriptionManager: AppDependencyProvider.shared.subscriptionManager, + tunnelController: AppDependencyProvider.shared.networkProtectionTunnelController + ) + }() + + private var autoClear: AutoClear? + private var showKeyboardIfSettingOn = true + private var lastBackgroundDate: Date? + + private(set) var homePageConfiguration: HomePageConfiguration! + + private(set) var remoteMessagingClient: RemoteMessagingClient! + + private(set) var syncService: DDGSync! + private(set) var syncDataProviders: SyncDataProviders! + private var syncDidFinishCancellable: AnyCancellable? + private var syncStateCancellable: AnyCancellable? + private var isSyncInProgressCancellable: AnyCancellable? + + private let crashCollection = CrashCollection(crashReportSender: CrashReportSender(platform: .iOS, + pixelEvents: CrashReportSender.pixelEvents), + crashCollectionStorage: UserDefaults()) + private var crashReportUploaderOnboarding: CrashCollectionOnboarding? + + private var autofillPixelReporter: AutofillPixelReporter? + private var autofillUsageMonitor = AutofillUsageMonitor() + + private(set) var subscriptionFeatureAvailability: SubscriptionFeatureAvailability! + private var subscriptionCookieManager: SubscriptionCookieManaging! + private var subscriptionCookieManagerFeatureFlagCancellable: AnyCancellable? + var privacyProDataReporter: PrivacyProDataReporting? + private var tokenBackgroundRefreshTask: TokenBackgroundRefreshTask? + + // MARK: - Feature specific app event handlers + + private let tipKitAppEventsHandler = TipKitAppEventHandler() + + // MARK: lifecycle + + @UserDefaultsWrapper(key: .privacyConfigCustomURL, defaultValue: nil) + private var privacyConfigCustomURL: String? + + @UserDefaultsWrapper(key: .didCrashDuringCrashHandlersSetUp, defaultValue: false) + private var didCrashDuringCrashHandlersSetUp: Bool + + private let launchOptionsHandler = LaunchOptionsHandler() + private let onboardingPixelReporter = OnboardingPixelReporter() + + private let voiceSearchHelper = VoiceSearchHelper() + + private let marketplaceAdPostbackManager = MarketplaceAdPostbackManager() + + private var didFinishLaunchingStartTime: CFAbsoluteTime? + + private weak var appDelegate: AppDelegate? + init(with appDelegate: AppDelegate) { + self.appDelegate = appDelegate + } + + func initialize() { + if !didCrashDuringCrashHandlersSetUp { + didCrashDuringCrashHandlersSetUp = true + CrashLogMessageExtractor.setUp(swapCxaThrow: false) + didCrashDuringCrashHandlersSetUp = false + } + } + + // swiftlint:disable:next function_body_length + // swiftlint:disable:next cyclomatic_complexity + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + didFinishLaunchingStartTime = CFAbsoluteTimeGetCurrent() + defer { + if let didFinishLaunchingStartTime { + let launchTime = CFAbsoluteTimeGetCurrent() - didFinishLaunchingStartTime + Pixel.fire(pixel: .appDidFinishLaunchingTime(time: Pixel.Event.BucketAggregation(number: launchTime)), + withAdditionalParameters: [PixelParameters.time: String(launchTime)]) + } + } + + +#if targetEnvironment(simulator) + if ProcessInfo.processInfo.environment["UITESTING"] == "true" { + // Disable hardware keyboards. + let setHardwareLayout = NSSelectorFromString("setHardwareLayout:") + UITextInputMode.activeInputModes + // Filter `UIKeyboardInputMode`s. + .filter({ $0.responds(to: setHardwareLayout) }) + .forEach { $0.perform(setHardwareLayout, with: nil) } + } +#endif + +#if DEBUG + Pixel.isDryRun = true +#else + Pixel.isDryRun = false +#endif + + ContentBlocking.shared.onCriticalError = presentPreemptiveCrashAlert + // Explicitly prepare ContentBlockingUpdating instance before Tabs are created + _ = ContentBlockingUpdating.shared + + // Can be removed after a couple of versions + cleanUpMacPromoExperiment2() + cleanUpIncrementalRolloutPixelTest() + + APIRequest.Headers.setUserAgent(DefaultUserAgentManager.duckDuckGoUserAgent) + + if isDebugBuild, let privacyConfigCustomURL, let url = URL(string: privacyConfigCustomURL) { + Configuration.setURLProvider(CustomConfigurationURLProvider(customPrivacyConfigurationURL: url)) + } else { + Configuration.setURLProvider(AppConfigurationURLProvider()) + } + + crashCollection.startAttachingCrashLogMessages { pixelParameters, payloads, sendReport in + pixelParameters.forEach { params in + Pixel.fire(pixel: .dbCrashDetected, withAdditionalParameters: params, includedParameters: []) + + // Each crash comes with an `appVersion` parameter representing the version that the crash occurred on. + // This is to disambiguate the situation where a crash occurs, but isn't sent until the next update. + // If for some reason the parameter can't be found, fall back to the current version. + if let crashAppVersion = params[PixelParameters.appVersion] { + let dailyParameters = [PixelParameters.appVersion: crashAppVersion] + DailyPixel.fireDaily(.dbCrashDetectedDaily, withAdditionalParameters: dailyParameters) + } else { + DailyPixel.fireDaily(.dbCrashDetectedDaily) + } + } + + // Async dispatch because rootViewController may otherwise be nil here + DispatchQueue.main.async { + guard let viewController = self.window?.rootViewController else { return } + + let crashReportUploaderOnboarding = CrashCollectionOnboarding(appSettings: AppDependencyProvider.shared.appSettings) + crashReportUploaderOnboarding.presentOnboardingIfNeeded(for: payloads, from: viewController, sendReport: sendReport) + self.crashReportUploaderOnboarding = crashReportUploaderOnboarding + } + } + + clearTmp() + + _ = DefaultUserAgentManager.shared + testing = ProcessInfo().arguments.contains("testing") + if testing { + Pixel.isDryRun = true + _ = DefaultUserAgentManager.shared + Database.shared.loadStore { _, _ in } + _ = BookmarksDatabaseSetup().loadStoreAndMigrate(bookmarksDatabase: bookmarksDatabase) + + window = UIWindow(frame: UIScreen.main.bounds) + window?.rootViewController = UIStoryboard.init(name: "LaunchScreen", bundle: nil).instantiateInitialViewController() + + let blockingDelegate = BlockingNavigationDelegate() + let webView = blockingDelegate.prepareWebView() + window?.rootViewController?.view.addSubview(webView) + window?.rootViewController?.view.backgroundColor = .red + webView.frame = CGRect(x: 10, y: 10, width: 300, height: 300) + + let request = URLRequest(url: URL(string: "about:blank")!) + webView.load(request) + + return true + } + + removeEmailWaitlistState() + + var shouldPresentInsufficientDiskSpaceAlertAndCrash = false + Database.shared.loadStore { context, error in + guard let context = context else { + + let parameters = [PixelParameters.applicationState: "\(application.applicationState.rawValue)", + PixelParameters.dataAvailability: "\(application.isProtectedDataAvailable)"] + + switch error { + case .none: + fatalError("Could not create database stack: Unknown Error") + case .some(CoreDataDatabase.Error.containerLocationCouldNotBePrepared(let underlyingError)): + Pixel.fire(pixel: .dbContainerInitializationError, + error: underlyingError, + withAdditionalParameters: parameters) + Thread.sleep(forTimeInterval: 1) + fatalError("Could not create database stack: \(underlyingError.localizedDescription)") + case .some(let error): + Pixel.fire(pixel: .dbInitializationError, + error: error, + withAdditionalParameters: parameters) + if error.isDiskFull { + shouldPresentInsufficientDiskSpaceAlertAndCrash = true + return + } else { + Thread.sleep(forTimeInterval: 1) + fatalError("Could not create database stack: \(error.localizedDescription)") + } + } + } + DatabaseMigration.migrate(to: context) + } + + switch BookmarksDatabaseSetup().loadStoreAndMigrate(bookmarksDatabase: bookmarksDatabase) { + case .success: + break + case .failure(let error): + Pixel.fire(pixel: .bookmarksCouldNotLoadDatabase, + error: error) + if error.isDiskFull { + shouldPresentInsufficientDiskSpaceAlertAndCrash = true + } else { + Thread.sleep(forTimeInterval: 1) + fatalError("Could not create database stack: \(error.localizedDescription)") + } + } + + WidgetCenter.shared.reloadAllTimelines() + + Favicons.shared.migrateFavicons(to: Favicons.Constants.maxFaviconSize) { + WidgetCenter.shared.reloadAllTimelines() + } + + PrivacyFeatures.httpsUpgrade.loadDataAsync() + + let variantManager = DefaultVariantManager() + let daxDialogs = DaxDialogs.shared + + // assign it here, because "did become active" is already too late and "viewWillAppear" + // has already been called on the HomeViewController so won't show the home row CTA + cleanUpATBAndAssignVariant(variantManager: variantManager, daxDialogs: daxDialogs) + + // MARK: Sync initialisation +#if DEBUG + let defaultEnvironment = ServerEnvironment.development +#else + let defaultEnvironment = ServerEnvironment.production +#endif + + let environment = ServerEnvironment( + UserDefaultsWrapper( + key: .syncEnvironment, + defaultValue: defaultEnvironment.description + ).wrappedValue + ) ?? defaultEnvironment + + var dryRun = false +#if DEBUG + dryRun = true +#endif + let isPhone = UIDevice.current.userInterfaceIdiom == .phone + let source = isPhone ? PixelKit.Source.iOS : PixelKit.Source.iPadOS + PixelKit.setUp(dryRun: dryRun, + appVersion: AppVersion.shared.versionNumber, + source: source.rawValue, + defaultHeaders: [:], + defaults: UserDefaults(suiteName: "\(Global.groupIdPrefix).app-configuration") ?? UserDefaults()) { (pixelName: String, headers: [String: String], parameters: [String: String], _, _, onComplete: @escaping PixelKit.CompletionBlock) in + + let url = URL.pixelUrl(forPixelNamed: pixelName) + let apiHeaders = APIRequestV2.HeadersV2(additionalHeaders: headers) + guard let request = APIRequestV2(url: url, method: .get, queryItems: parameters, headers: apiHeaders) else { + onComplete(false, nil) + return + } + Task { + do { + _ = try await DefaultAPIService().fetch(request: request) + onComplete(true, nil) + } catch { + onComplete(false, error) + } + } + } + PixelKit.configureExperimentKit(featureFlagger: AppDependencyProvider.shared.featureFlagger, + eventTracker: ExperimentEventTracker(store: UserDefaults(suiteName: "\(Global.groupIdPrefix).app-configuration") ?? UserDefaults())) + + let syncErrorHandler = SyncErrorHandler() + + syncDataProviders = SyncDataProviders( + bookmarksDatabase: bookmarksDatabase, + secureVaultErrorReporter: SecureVaultReporter(), + settingHandlers: [FavoritesDisplayModeSyncHandler()], + favoritesDisplayModeStorage: FavoritesDisplayModeStorage(), + syncErrorHandler: syncErrorHandler, + faviconStoring: Favicons.shared, + tld: AppDependencyProvider.shared.storageCache.tld + ) + + let syncService = DDGSync( + dataProvidersSource: syncDataProviders, + errorEvents: SyncErrorHandler(), + privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager, + environment: environment + ) + syncService.initializeIfNeeded() + self.syncService = syncService + + let fireproofing = UserDefaultsFireproofing.xshared + privacyProDataReporter = PrivacyProDataReporter(fireproofing: fireproofing) + + isSyncInProgressCancellable = syncService.isSyncInProgressPublisher + .filter { $0 } + .sink { [weak syncService] _ in + DailyPixel.fire(pixel: .syncDaily, includedParameters: [.appVersion]) + syncService?.syncDailyStats.sendStatsIfNeeded(handler: { params in + Pixel.fire(pixel: .syncSuccessRateDaily, + withAdditionalParameters: params, + includedParameters: [.appVersion]) + }) + } + + remoteMessagingClient = RemoteMessagingClient( + bookmarksDatabase: bookmarksDatabase, + appSettings: AppDependencyProvider.shared.appSettings, + internalUserDecider: AppDependencyProvider.shared.internalUserDecider, + configurationStore: AppDependencyProvider.shared.configurationStore, + database: Database.shared, + errorEvents: RemoteMessagingStoreErrorHandling(), + remoteMessagingAvailabilityProvider: PrivacyConfigurationRemoteMessagingAvailabilityProvider( + privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager + ), + duckPlayerStorage: DefaultDuckPlayerStorage() + ) + remoteMessagingClient.registerBackgroundRefreshTaskHandler() + + subscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability( + privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager, + purchasePlatform: .appStore) + + subscriptionCookieManager = makeSubscriptionCookieManager() + + homePageConfiguration = HomePageConfiguration(variantManager: AppDependencyProvider.shared.variantManager, + remoteMessagingClient: remoteMessagingClient, + privacyProDataReporter: privacyProDataReporter!) + + let previewsSource = TabPreviewsSource() + let historyManager = makeHistoryManager() + let tabsModel = prepareTabsModel(previewsSource: previewsSource) + + privacyProDataReporter?.injectTabsModel(tabsModel) + + if shouldPresentInsufficientDiskSpaceAlertAndCrash { + + window = UIWindow(frame: UIScreen.main.bounds) + window?.rootViewController = BlankSnapshotViewController(addressBarPosition: AppDependencyProvider.shared.appSettings.currentAddressBarPosition, + voiceSearchHelper: voiceSearchHelper) + window?.makeKeyAndVisible() + + presentInsufficientDiskSpaceAlert() + } else { + let daxDialogsFactory = ExperimentContextualDaxDialogsFactory(contextualOnboardingLogic: daxDialogs, contextualOnboardingPixelReporter: onboardingPixelReporter) + let contextualOnboardingPresenter = ContextualOnboardingPresenter(variantManager: variantManager, daxDialogsFactory: daxDialogsFactory) + let main = MainViewController(bookmarksDatabase: bookmarksDatabase, + bookmarksDatabaseCleaner: syncDataProviders.bookmarksAdapter.databaseCleaner, + historyManager: historyManager, + homePageConfiguration: homePageConfiguration, + syncService: syncService, + syncDataProviders: syncDataProviders, + appSettings: AppDependencyProvider.shared.appSettings, + previewsSource: previewsSource, + tabsModel: tabsModel, + syncPausedStateManager: syncErrorHandler, + privacyProDataReporter: privacyProDataReporter!, + variantManager: variantManager, + contextualOnboardingPresenter: contextualOnboardingPresenter, + contextualOnboardingLogic: daxDialogs, + contextualOnboardingPixelReporter: onboardingPixelReporter, + subscriptionFeatureAvailability: subscriptionFeatureAvailability, + voiceSearchHelper: voiceSearchHelper, + featureFlagger: AppDependencyProvider.shared.featureFlagger, + fireproofing: fireproofing, + subscriptionCookieManager: subscriptionCookieManager, + textZoomCoordinator: makeTextZoomCoordinator(), + websiteDataManager: makeWebsiteDataManager(fireproofing: fireproofing), + appDidFinishLaunchingStartTime: didFinishLaunchingStartTime) + + main.loadViewIfNeeded() + syncErrorHandler.alertPresenter = main + + window = UIWindow(frame: UIScreen.main.bounds) + window?.rootViewController = main + window?.makeKeyAndVisible() + + autoClear = AutoClear(worker: main) + let applicationState = application.applicationState + Task { + await autoClear?.clearDataIfEnabled(applicationState: .init(with: applicationState)) + await vpnWorkaround.installRedditSessionWorkaround() + } + } + + self.voiceSearchHelper.migrateSettingsFlagIfNecessary() + + Task { + await AppDependencyProvider.shared.subscriptionManager.loadInitialData() + } + + // Task handler registration needs to happen before the end of `didFinishLaunching`, otherwise submitting a task can throw an exception. + // Having both in `didBecomeActive` can sometimes cause the exception when running on a physical device, so registration happens here. + AppConfigurationFetch.registerBackgroundRefreshTaskHandler() + + tokenBackgroundRefreshTask = TokenBackgroundRefreshTask(subscriptionManager: AppDependencyProvider.shared.subscriptionManager) + tokenBackgroundRefreshTask?.registerBackgroundRefreshTaskHandler() + + UNUserNotificationCenter.current().delegate = self + + window?.windowScene?.screenshotService?.delegate = self + ThemeManager.shared.updateUserInterfaceStyle(window: window) + + appIsLaunching = true + + // Temporary logic for rollout of Autofill as on by default for new installs only + if AppDependencyProvider.shared.appSettings.autofillIsNewInstallForOnByDefault == nil { + AppDependencyProvider.shared.appSettings.setAutofillIsNewInstallForOnByDefault() + } + + NewTabPageIntroMessageSetup().perform() + + widgetRefreshModel.beginObservingVPNStatus() + + setUpAutofillPixelReporter() + + if didCrashDuringCrashHandlersSetUp { + Pixel.fire(pixel: .crashOnCrashHandlersSetUp) + didCrashDuringCrashHandlersSetUp = false + } + + tipKitAppEventsHandler.appDidFinishLaunching() + + return true + } + + private func makeWebsiteDataManager(fireproofing: Fireproofing, + dataStoreIDManager: DataStoreIDManaging = DataStoreIDManager.shared) -> WebsiteDataManaging { + return WebCacheManager(cookieStorage: MigratableCookieStorage(), + fireproofing: fireproofing, + dataStoreIDManager: dataStoreIDManager) + } + + private func makeTextZoomCoordinator() -> TextZoomCoordinator { + let provider = AppDependencyProvider.shared + let storage = TextZoomStorage() + + return TextZoomCoordinator(appSettings: provider.appSettings, + storage: storage, + featureFlagger: provider.featureFlagger) + } + + private func makeSubscriptionCookieManager() -> SubscriptionCookieManaging { + let subscriptionCookieManager = SubscriptionCookieManager(subscriptionManager: AppDependencyProvider.shared.subscriptionManager, + currentCookieStore: { [weak self] in + guard self?.mainViewController?.tabManager.model.hasActiveTabs ?? false else { + // We shouldn't interact with WebKit's cookie store unless we have a WebView, + // eventually the subscription cookie will be refreshed on opening the first tab + return nil + } + + return WKHTTPCookieStoreWrapper(store: WKWebsiteDataStore.current().httpCookieStore) + }, eventMapping: SubscriptionCookieManageEventPixelMapping()) + + + let privacyConfigurationManager = ContentBlocking.shared.privacyConfigurationManager + + // Enable subscriptionCookieManager if feature flag is present + if privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.setAccessTokenCookieForSubscriptionDomains) { + subscriptionCookieManager.enableSettingSubscriptionCookie() + } + + // Keep track of feature flag changes + subscriptionCookieManagerFeatureFlagCancellable = privacyConfigurationManager.updatesPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self, weak privacyConfigurationManager] in + guard let self, !self.appIsLaunching, let privacyConfigurationManager else { return } + + let isEnabled = privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.setAccessTokenCookieForSubscriptionDomains) + + Task { @MainActor [weak self] in + if isEnabled { + self?.subscriptionCookieManager.enableSettingSubscriptionCookie() + } else { + await self?.subscriptionCookieManager.disableSettingSubscriptionCookie() + } + } + } + + return subscriptionCookieManager + } + + private func makeHistoryManager() -> HistoryManaging { + + let provider = AppDependencyProvider.shared + + switch HistoryManager.make(isAutocompleteEnabledByUser: provider.appSettings.autocomplete, + isRecentlyVisitedSitesEnabledByUser: provider.appSettings.recentlyVisitedSites, + privacyConfigManager: ContentBlocking.shared.privacyConfigurationManager, + tld: provider.storageCache.tld) { + + case .failure(let error): + Pixel.fire(pixel: .historyStoreLoadFailed, error: error) + if error.isDiskFull { + self.presentInsufficientDiskSpaceAlert() + } else { + self.presentPreemptiveCrashAlert() + } + return NullHistoryManager() + + case .success(let historyManager): + return historyManager + } + } + + private func prepareTabsModel(previewsSource: TabPreviewsSource = TabPreviewsSource(), + appSettings: AppSettings = AppDependencyProvider.shared.appSettings, + isDesktop: Bool = UIDevice.current.userInterfaceIdiom == .pad) -> TabsModel { + let isPadDevice = UIDevice.current.userInterfaceIdiom == .pad + let tabsModel: TabsModel + if AutoClearSettingsModel(settings: appSettings) != nil { + tabsModel = TabsModel(desktop: isPadDevice) + tabsModel.save() + previewsSource.removeAllPreviews() + } else { + if let storedModel = TabsModel.get() { + // Save new model in case of migration + storedModel.save() + tabsModel = storedModel + } else { + tabsModel = TabsModel(desktop: isPadDevice) + } + } + return tabsModel + } + + private func presentPreemptiveCrashAlert() { + Task { @MainActor in + let alertController = CriticalAlerts.makePreemptiveCrashAlert() + window?.rootViewController?.present(alertController, animated: true, completion: nil) + } + } + + private func presentInsufficientDiskSpaceAlert() { + let alertController = CriticalAlerts.makeInsufficientDiskSpaceAlert() + window?.rootViewController?.present(alertController, animated: true, completion: nil) + } + + private func presentExpiredEntitlementAlert() { + let alertController = CriticalAlerts.makeExpiredEntitlementAlert { [weak self] in + self?.mainViewController?.segueToPrivacyPro() + } + window?.rootViewController?.present(alertController, animated: true) { [weak self] in + self?.tunnelDefaults.showEntitlementAlert = false + } + } + + private func presentExpiredEntitlementNotificationIfNeeded() { + let presenter = NetworkProtectionNotificationsPresenterTogglableDecorator( + settings: AppDependencyProvider.shared.vpnSettings, + defaults: .networkProtectionGroupDefaults, + wrappee: NetworkProtectionUNNotificationPresenter() + ) + presenter.showEntitlementNotification() + } + + private func cleanUpMacPromoExperiment2() { + UserDefaults.standard.removeObject(forKey: "com.duckduckgo.ios.macPromoMay23.exp2.cohort") + } + + private func cleanUpIncrementalRolloutPixelTest() { + UserDefaults.standard.removeObject(forKey: "network-protection.incremental-feature-flag-test.has-sent-pixel") + } + + private func clearTmp() { + let tmp = FileManager.default.temporaryDirectory + do { + try FileManager.default.removeItem(at: tmp) + } catch { + Logger.general.error("Failed to delete tmp dir") + } + } + + private func reportAdAttribution() { + Task.detached(priority: .background) { + await AdAttributionPixelReporter.shared.reportAttributionIfNeeded() + } + } + + func applicationDidBecomeActive(_ application: UIApplication) { + guard !testing else { return } + + defer { + if let didFinishLaunchingStartTime { + let launchTime = CFAbsoluteTimeGetCurrent() - didFinishLaunchingStartTime + Pixel.fire(pixel: .appDidBecomeActiveTime(time: Pixel.Event.BucketAggregation(number: launchTime)), + withAdditionalParameters: [PixelParameters.time: String(launchTime)]) + } + } + + syncService.initializeIfNeeded() + syncDataProviders.setUpDatabaseCleanersIfNeeded(syncService: syncService) + + if !(overlayWindow?.rootViewController is AuthenticationViewController) { + removeOverlay() + } + + StatisticsLoader.shared.load { + StatisticsLoader.shared.refreshAppRetentionAtb() + self.fireAppLaunchPixel() + self.reportAdAttribution() + self.onboardingPixelReporter.fireEnqueuedPixelsIfNeeded() + } + + if appIsLaunching { + appIsLaunching = false + onApplicationLaunch(application) + } + + mainViewController?.showBars() + mainViewController?.didReturnFromBackground() + + if !privacyStore.authenticationEnabled { + showKeyboardOnLaunch() + } + + if AppConfigurationFetch.shouldScheduleRulesCompilationOnAppLaunch { + ContentBlocking.shared.contentBlockingManager.scheduleCompilation() + AppConfigurationFetch.shouldScheduleRulesCompilationOnAppLaunch = false + } + AppDependencyProvider.shared.configurationManager.loadPrivacyConfigFromDiskIfNeeded() + + AppConfigurationFetch().start { result in + self.sendAppLaunchPostback() + if case .assetsUpdated(let protectionsUpdated) = result, protectionsUpdated { + ContentBlocking.shared.contentBlockingManager.scheduleCompilation() + } + } + + syncService.scheduler.notifyAppLifecycleEvent() + + privacyProDataReporter?.injectSyncService(syncService) + + fireFailedCompilationsPixelIfNeeded() + + widgetRefreshModel.refreshVPNWidget() + + if tunnelDefaults.showEntitlementAlert { + presentExpiredEntitlementAlert() + } + + presentExpiredEntitlementNotificationIfNeeded() + + Task { + await stopAndRemoveVPNIfNotAuthenticated() + await refreshShortcuts() + await vpnWorkaround.installRedditSessionWorkaround() + + if #available(iOS 17.0, *) { + await VPNSnoozeLiveActivityManager().endSnoozeActivityIfNecessary() + } + } + +// AppDependencyProvider.shared.subscriptionManager.refreshCachedSubscription { isSubscriptionActive in +// if isSubscriptionActive { +// DailyPixel.fire(pixel: .privacyProSubscriptionActive) +// } +// } + + Task { + await subscriptionCookieManager.refreshSubscriptionCookie() + } + + let importPasswordsStatusHandler = ImportPasswordsStatusHandler(syncService: syncService) + importPasswordsStatusHandler.checkSyncSuccessStatus() + + Task { + await privacyProDataReporter?.saveWidgetAdded() + } + + AppDependencyProvider.shared.persistentPixel.sendQueuedPixels { _ in } + } + + private func stopAndRemoveVPNIfNotAuthenticated() async { + // Only remove the VPN if the user is not authenticated, and it's installed: + guard !AppDependencyProvider.shared.subscriptionManager.isUserAuthenticated, + await AppDependencyProvider.shared.networkProtectionTunnelController.isInstalled else { + return + } + + await AppDependencyProvider.shared.networkProtectionTunnelController.stop() + await AppDependencyProvider.shared.networkProtectionTunnelController.removeVPN(reason: .didBecomeActiveCheck) + } + + func applicationWillResignActive(_ application: UIApplication) { + Task { @MainActor in + await refreshShortcuts() + await vpnWorkaround.removeRedditSessionWorkaround() + } + } + + private func fireAppLaunchPixel() { + + WidgetCenter.shared.getCurrentConfigurations { result in + let paramKeys: [WidgetFamily: String] = [ + .systemSmall: PixelParameters.widgetSmall, + .systemMedium: PixelParameters.widgetMedium, + .systemLarge: PixelParameters.widgetLarge + ] + + switch result { + case .failure(let error): + Pixel.fire(pixel: .appLaunch, withAdditionalParameters: [ + PixelParameters.widgetError: "1", + PixelParameters.widgetErrorCode: "\((error as NSError).code)", + PixelParameters.widgetErrorDomain: (error as NSError).domain + ], includedParameters: [.appVersion, .atb]) + + case .success(let widgetInfo): + let params = widgetInfo.reduce([String: String]()) { + var result = $0 + if let key = paramKeys[$1.family] { + result[key] = "1" + } + return result + } + Pixel.fire(pixel: .appLaunch, withAdditionalParameters: params, includedParameters: [.appVersion, .atb]) + } + + } + } + + private func fireFailedCompilationsPixelIfNeeded() { + let store = FailedCompilationsStore() + if store.hasAnyFailures { + DailyPixel.fire(pixel: .compilationFailed, withAdditionalParameters: store.summary) { error in + guard error != nil else { return } + store.cleanup() + } + } + } + + private func shouldShowKeyboardOnLaunch() -> Bool { + guard let date = lastBackgroundDate else { return true } + return Date().timeIntervalSince(date) > AppDelegate.ShowKeyboardOnLaunchThreshold + } + + private func showKeyboardOnLaunch() { + guard KeyboardSettings().onAppLaunch && showKeyboardIfSettingOn && shouldShowKeyboardOnLaunch() else { return } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.mainViewController?.enterSearch() + } + showKeyboardIfSettingOn = false + } + + private func onApplicationLaunch(_ application: UIApplication) { + Task { @MainActor in + await beginAuthentication() + initialiseBackgroundFetch(application) + applyAppearanceChanges() + refreshRemoteMessages() + } + } + + private func applyAppearanceChanges() { + UILabel.appearance(whenContainedInInstancesOf: [UIAlertController.self]).numberOfLines = 0 + } + + /// It's public in order to allow refreshing on demand via Debug menu. Otherwise it shouldn't be called from outside. + func refreshRemoteMessages() { + Task { + try? await remoteMessagingClient.fetchAndProcess(using: remoteMessagingClient.store) + } + } + + func applicationWillEnterForeground(_ application: UIApplication) { + ThemeManager.shared.updateUserInterfaceStyle() + + Task { @MainActor in + await beginAuthentication() + await autoClear?.clearDataIfEnabledAndTimeExpired(applicationState: .active) + showKeyboardIfSettingOn = true + syncService.scheduler.resumeSyncQueue() + } + } + + func applicationDidEnterBackground(_ application: UIApplication) { + displayBlankSnapshotWindow() + autoClear?.startClearingTimer() + lastBackgroundDate = Date() + AppDependencyProvider.shared.autofillLoginSession.endSession() + suspendSync() + syncDataProviders.bookmarksAdapter.cancelFaviconsFetching(application) + privacyProDataReporter?.saveApplicationLastSessionEnded() + tokenBackgroundRefreshTask?.scheduleTask() + resetAppStartTime() + } + + private func resetAppStartTime() { + didFinishLaunchingStartTime = nil + mainViewController?.appDidFinishLaunchingStartTime = nil + } + + private func suspendSync() { + if syncService.isSyncInProgress { + Logger.sync.debug("Sync is in progress. Starting background task to allow it to gracefully complete.") + + var taskID: UIBackgroundTaskIdentifier! + taskID = UIApplication.shared.beginBackgroundTask(withName: "Cancelled Sync Completion Task") { + Logger.sync.debug("Forcing background task completion") + UIApplication.shared.endBackgroundTask(taskID) + } + syncDidFinishCancellable?.cancel() + syncDidFinishCancellable = syncService.isSyncInProgressPublisher.filter { !$0 } + .prefix(1) + .receive(on: DispatchQueue.main) + .sink { _ in + Logger.sync.debug("Ending background task") + UIApplication.shared.endBackgroundTask(taskID) + } + } + + syncService.scheduler.cancelSyncAndSuspendSyncQueue() + } + + func application(_ application: UIApplication, + performActionFor shortcutItem: UIApplicationShortcutItem, + completionHandler: @escaping (Bool) -> Void) { + handleShortCutItem(shortcutItem) + } + + func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + Logger.sync.debug("App launched with url \(url.absoluteString)") + + // If showing the onboarding intro ignore deeplinks + guard mainViewController?.needsToShowOnboardingIntro() == false else { + return false + } + + if handleEmailSignUpDeepLink(url) { + return true + } + + NotificationCenter.default.post(name: AutofillLoginListAuthenticator.Notifications.invalidateContext, object: nil) + + // The openVPN action handles the navigation stack on its own and does not need it to be cleared + if url != AppDeepLinkSchemes.openVPN.url { + mainViewController?.clearNavigationStack() + } + + Task { @MainActor in + // Autoclear should have happened by now + showKeyboardIfSettingOn = false + + if !handleAppDeepLink(app, mainViewController, url) { + mainViewController?.loadUrlInNewTab(url, reuseExisting: true, inheritedAttribution: nil, fromExternalLink: true) + } + } + + return true + } + + func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + + Logger.lifecycle.debug(#function) + + AppConfigurationFetch().start(isBackgroundFetch: true) { result in + switch result { + case .noData: + completionHandler(.noData) + case .assetsUpdated: + completionHandler(.newData) + } + } + } + + func application(_ application: UIApplication, willContinueUserActivityWithType userActivityType: String) -> Bool { + return true + } + + // MARK: private + + private func sendAppLaunchPostback() { + // Attribution support + let privacyConfigurationManager = ContentBlocking.shared.privacyConfigurationManager + if privacyConfigurationManager.privacyConfig.isEnabled(featureKey: .marketplaceAdPostback) { + marketplaceAdPostbackManager.sendAppLaunchPostback() + } + } + + private func cleanUpATBAndAssignVariant(variantManager: VariantManager, daxDialogs: DaxDialogs) { + let historyMessageManager = HistoryMessageManager() + + AtbAndVariantCleanup.cleanup() + variantManager.assignVariantIfNeeded { _ in + // MARK: perform first time launch logic here + // If it's running UI Tests check if the onboarding should be in a completed state. + if launchOptionsHandler.isUITesting && launchOptionsHandler.isOnboardingCompleted { + daxDialogs.dismiss() + } else { + daxDialogs.primeForUse() + } + + // New users don't see the message + historyMessageManager.dismiss() + + // Setup storage for marketplace postback + marketplaceAdPostbackManager.updateReturningUserValue() + } + } + + private func initialiseBackgroundFetch(_ application: UIApplication) { + guard UIApplication.shared.backgroundRefreshStatus == .available else { + return + } + + // BackgroundTasks will automatically replace an existing task in the queue if one with the same identifier is queued, so we should only + // schedule a task if there are none pending in order to avoid the config task getting perpetually replaced. + BGTaskScheduler.shared.getPendingTaskRequests { tasks in + let hasConfigurationTask = tasks.contains { $0.identifier == AppConfigurationFetch.Constants.backgroundProcessingTaskIdentifier } + if !hasConfigurationTask { + AppConfigurationFetch.scheduleBackgroundRefreshTask() + } + + let hasRemoteMessageFetchTask = tasks.contains { $0.identifier == RemoteMessagingClient.Constants.backgroundRefreshTaskIdentifier } + if !hasRemoteMessageFetchTask { + RemoteMessagingClient.scheduleBackgroundRefreshTask() + } + } + } + + private func displayAuthenticationWindow() { + guard overlayWindow == nil, let frame = window?.frame else { return } + overlayWindow = UIWindow(frame: frame) + overlayWindow?.windowLevel = UIWindow.Level.alert + overlayWindow?.rootViewController = AuthenticationViewController.loadFromStoryboard() + overlayWindow?.makeKeyAndVisible() + window?.isHidden = true + } + + private func displayBlankSnapshotWindow() { + guard overlayWindow == nil, let frame = window?.frame else { return } + guard autoClear?.isClearingEnabled ?? false || privacyStore.authenticationEnabled else { return } + + overlayWindow = UIWindow(frame: frame) + overlayWindow?.windowLevel = UIWindow.Level.alert + + let overlay = BlankSnapshotViewController(addressBarPosition: AppDependencyProvider.shared.appSettings.currentAddressBarPosition, + voiceSearchHelper: voiceSearchHelper) + overlay.delegate = self + + overlayWindow?.rootViewController = overlay + overlayWindow?.makeKeyAndVisible() + window?.isHidden = true + } + + private func beginAuthentication() async { + + guard privacyStore.authenticationEnabled else { return } + + removeOverlay() + displayAuthenticationWindow() + + guard let controller = overlayWindow?.rootViewController as? AuthenticationViewController else { + removeOverlay() + return + } + + await controller.beginAuthentication { [weak self] in + self?.removeOverlay() + self?.showKeyboardOnLaunch() + } + } + + private func tryToObtainOverlayWindow() { + for window in UIApplication.shared.foregroundSceneWindows where window.rootViewController is BlankSnapshotViewController { + overlayWindow = window + return + } + } + + private func removeOverlay() { + if overlayWindow == nil { + tryToObtainOverlayWindow() + } + + if let overlay = overlayWindow { + overlay.isHidden = true + overlayWindow = nil + window?.makeKeyAndVisible() + } + } + + private func handleShortCutItem(_ shortcutItem: UIApplicationShortcutItem) { + Logger.general.debug("Handling shortcut item: \(shortcutItem.type)") + + Task { @MainActor in + + if appIsLaunching { + await autoClear?.clearDataIfEnabled() + } else { + await autoClear?.clearDataIfEnabledAndTimeExpired(applicationState: .active) + } + + if shortcutItem.type == AppDelegate.ShortcutKey.clipboard, let query = UIPasteboard.general.string { + mainViewController?.clearNavigationStack() + mainViewController?.loadQueryInNewTab(query) + return + } + + if shortcutItem.type == AppDelegate.ShortcutKey.passwords { + mainViewController?.clearNavigationStack() + // Give the `clearNavigationStack` call time to complete. + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5) { [weak self] in + self?.mainViewController?.launchAutofillLogins(openSearch: true, source: .appIconShortcut) + } + Pixel.fire(pixel: .autofillLoginsLaunchAppShortcut) + return + } + + if shortcutItem.type == AppDelegate.ShortcutKey.openVPNSettings { + presentNetworkProtectionStatusSettingsModal() + } + + } + } + + private func removeEmailWaitlistState() { + EmailWaitlist.removeEmailState() + + let autofillStorage = EmailKeychainManager() + try? autofillStorage.deleteWaitlistState() + + // Remove the authentication state if this is a fresh install. + if !Database.shared.isDatabaseFileInitialized { + try? autofillStorage.deleteAuthenticationState() + } + } + + private func handleEmailSignUpDeepLink(_ url: URL) -> Bool { + guard url.absoluteString.starts(with: URL.emailProtection.absoluteString), + let navViewController = mainViewController?.presentedViewController as? UINavigationController, + let emailSignUpViewController = navViewController.topViewController as? EmailSignupViewController else { + return false + } + emailSignUpViewController.loadUrl(url) + return true + } + + private var mainViewController: MainViewController? { + return window?.rootViewController as? MainViewController + } + + private func setUpAutofillPixelReporter() { + autofillPixelReporter = AutofillPixelReporter( + userDefaults: .standard, + autofillEnabled: AppDependencyProvider.shared.appSettings.autofillCredentialsEnabled, + eventMapping: EventMapping {[weak self] event, _, params, _ in + switch event { + case .autofillActiveUser: + Pixel.fire(pixel: .autofillActiveUser) + case .autofillEnabledUser: + Pixel.fire(pixel: .autofillEnabledUser) + case .autofillOnboardedUser: + Pixel.fire(pixel: .autofillOnboardedUser) + case .autofillToggledOn: + Pixel.fire(pixel: .autofillToggledOn, withAdditionalParameters: params ?? [:]) + if let autofillExtensionToggled = self?.autofillUsageMonitor.autofillExtensionEnabled { + Pixel.fire(pixel: autofillExtensionToggled ? .autofillExtensionToggledOn : .autofillExtensionToggledOff, + withAdditionalParameters: params ?? [:]) + } + case .autofillToggledOff: + Pixel.fire(pixel: .autofillToggledOff, withAdditionalParameters: params ?? [:]) + if let autofillExtensionToggled = self?.autofillUsageMonitor.autofillExtensionEnabled { + Pixel.fire(pixel: autofillExtensionToggled ? .autofillExtensionToggledOn : .autofillExtensionToggledOff, + withAdditionalParameters: params ?? [:]) + } + case .autofillLoginsStacked: + Pixel.fire(pixel: .autofillLoginsStacked, withAdditionalParameters: params ?? [:]) + default: + break + } + }, + installDate: StatisticsUserDefaults().installDate ?? Date()) + + _ = NotificationCenter.default.addObserver(forName: AppUserDefaults.Notifications.autofillEnabledChange, + object: nil, + queue: nil) { [weak self] _ in + self?.autofillPixelReporter?.updateAutofillEnabledStatus(AppDependencyProvider.shared.appSettings.autofillCredentialsEnabled) + } + } + + @MainActor + func refreshShortcuts() async { + guard AppDependencyProvider.shared.vpnFeatureVisibility.shouldShowVPNShortcut() else { + UIApplication.shared.shortcutItems = nil + return + } + + if await AppDependencyProvider.shared.subscriptionManager.isFeatureAvailableForUser(.networkProtection) { + let items = [ + UIApplicationShortcutItem(type: AppDelegate.ShortcutKey.openVPNSettings, + localizedTitle: UserText.netPOpenVPNQuickAction, + localizedSubtitle: nil, + icon: UIApplicationShortcutIcon(templateImageName: "VPN-16"), + userInfo: nil) + ] + + UIApplication.shared.shortcutItems = items + } else { + UIApplication.shared.shortcutItems = nil + } + } +} + + +extension OldAppDelegate: BlankSnapshotViewRecoveringDelegate { + + func recoverFromPresenting(controller: BlankSnapshotViewController) { + if overlayWindow == nil { + tryToObtainOverlayWindow() + } + + overlayWindow?.isHidden = true + overlayWindow = nil + window?.makeKeyAndVisible() + } + +} + +extension OldAppDelegate: UIScreenshotServiceDelegate { + func screenshotService(_ screenshotService: UIScreenshotService, + generatePDFRepresentationWithCompletion completionHandler: @escaping (Data?, Int, CGRect) -> Void) { + guard let webView = mainViewController?.currentTab?.webView else { + completionHandler(nil, 0, .zero) + return + } + + let zoomScale = webView.scrollView.zoomScale + + // The PDF's coordinate space has its origin at the bottom left, so the view's origin.y needs to be converted + let visibleBounds = CGRect( + x: webView.scrollView.contentOffset.x / zoomScale, + y: (webView.scrollView.contentSize.height - webView.scrollView.contentOffset.y - webView.bounds.height) / zoomScale, + width: webView.bounds.width / zoomScale, + height: webView.bounds.height / zoomScale + ) + + webView.createPDF { result in + let data = try? result.get() + completionHandler(data, 0, visibleBounds) + } + } +} + +extension OldAppDelegate: UNUserNotificationCenterDelegate { + + func userNotificationCenter(_ center: UNUserNotificationCenter, + willPresent notification: UNNotification, + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + completionHandler(.banner) + } + + func userNotificationCenter(_ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void) { + if response.actionIdentifier == UNNotificationDefaultActionIdentifier { + let identifier = response.notification.request.identifier + + if NetworkProtectionNotificationIdentifier(rawValue: identifier) != nil { + presentNetworkProtectionStatusSettingsModal() + } + } + + completionHandler() + } + + func presentNetworkProtectionStatusSettingsModal() { + Task { + if await AppDependencyProvider.shared.subscriptionManager.isFeatureAvailableForUser(.networkProtection) { + Task { @MainActor in + (window?.rootViewController as? MainViewController)?.segueToVPN() + } + } else { + Task { @MainActor in + (window?.rootViewController as? MainViewController)?.segueToPrivacyPro() + } + } + } + } + + private func presentSettings(with viewController: UIViewController) { + guard let window = window, let rootViewController = window.rootViewController as? MainViewController else { return } + + if let navigationController = rootViewController.presentedViewController as? UINavigationController { + if let lastViewController = navigationController.viewControllers.last, lastViewController.isKind(of: type(of: viewController)) { + // Avoid presenting dismissing and re-presenting the view controller if it's already visible: + return + } else { + // Otherwise, replace existing view controllers with the presented one: + navigationController.popToRootViewController(animated: false) + navigationController.pushViewController(viewController, animated: false) + return + } + } + + // If the previous checks failed, make sure the nav stack is reset and present the view controller from scratch: + rootViewController.clearNavigationStack() + + // Give the `clearNavigationStack` call time to complete. + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5) { + rootViewController.segueToSettings() + let navigationController = rootViewController.presentedViewController as? UINavigationController + navigationController?.popToRootViewController(animated: false) + navigationController?.pushViewController(viewController, animated: false) + } + } +} diff --git a/DuckDuckGo/RemoteMessagingConfigMatcherProvider.swift b/DuckDuckGo/RemoteMessagingConfigMatcherProvider.swift index a10a8f9bbf..9295cfa728 100644 --- a/DuckDuckGo/RemoteMessagingConfigMatcherProvider.swift +++ b/DuckDuckGo/RemoteMessagingConfigMatcherProvider.swift @@ -62,7 +62,7 @@ final class RemoteMessagingConfigMatcherProvider: RemoteMessagingConfigMatcherPr let variantManager = DefaultVariantManager() let subscriptionManager = AppDependencyProvider.shared.subscriptionManager - let isPrivacyProSubscriber = subscriptionManager.accountManager.isUserAuthenticated + let isPrivacyProSubscriber = subscriptionManager.isUserAuthenticated let isPrivacyProEligibleUser = subscriptionManager.canPurchase let activationDateStore = DefaultVPNActivationDateStore() @@ -84,42 +84,29 @@ final class RemoteMessagingConfigMatcherProvider: RemoteMessagingConfigMatcherPr let surveyActionMapper: DefaultRemoteMessagingSurveyURLBuilder - if let accessToken = subscriptionManager.accountManager.accessToken { - let subscriptionResult = await subscriptionManager.subscriptionEndpointService.getSubscription( - accessToken: accessToken - ) - - if case let .success(subscription) = subscriptionResult { - privacyProDaysSinceSubscribed = Calendar.current.numberOfDaysBetween(subscription.startedAt, and: Date()) ?? -1 - privacyProDaysUntilExpiry = Calendar.current.numberOfDaysBetween(Date(), and: subscription.expiresOrRenewsAt) ?? -1 - privacyProPurchasePlatform = subscription.platform.rawValue - - switch subscription.status { - case .autoRenewable, .gracePeriod: - isPrivacyProSubscriptionActive = true - case .notAutoRenewable: - isPrivacyProSubscriptionExpiring = true - case .expired, .inactive: - isPrivacyProSubscriptionExpired = true - case .unknown: - break // Not supported in RMF - } - - surveyActionMapper = DefaultRemoteMessagingSurveyURLBuilder( - statisticsStore: statisticsStore, - vpnActivationDateStore: DefaultVPNActivationDateStore(), - subscription: subscription) - } else { - surveyActionMapper = DefaultRemoteMessagingSurveyURLBuilder( - statisticsStore: statisticsStore, - vpnActivationDateStore: DefaultVPNActivationDateStore(), - subscription: nil) + if let subscription = try? await subscriptionManager.getSubscription(cachePolicy: .returnCacheDataElseLoad) { + privacyProDaysSinceSubscribed = Calendar.current.numberOfDaysBetween(subscription.startedAt, and: Date()) ?? -1 + privacyProDaysUntilExpiry = Calendar.current.numberOfDaysBetween(Date(), and: subscription.expiresOrRenewsAt) ?? -1 + privacyProPurchasePlatform = subscription.platform.rawValue + + switch subscription.status { + case .autoRenewable, .gracePeriod: + isPrivacyProSubscriptionActive = true + case .notAutoRenewable: + isPrivacyProSubscriptionExpiring = true + case .expired, .inactive: + isPrivacyProSubscriptionExpired = true + case .unknown: + break // Not supported in RMF } + + surveyActionMapper = DefaultRemoteMessagingSurveyURLBuilder(statisticsStore: statisticsStore, + vpnActivationDateStore: DefaultVPNActivationDateStore(), + subscription: subscription) } else { - surveyActionMapper = DefaultRemoteMessagingSurveyURLBuilder( - statisticsStore: statisticsStore, - vpnActivationDateStore: DefaultVPNActivationDateStore(), - subscription: nil) + surveyActionMapper = DefaultRemoteMessagingSurveyURLBuilder(statisticsStore: statisticsStore, + vpnActivationDateStore: DefaultVPNActivationDateStore(), + subscription: nil) } let dismissedMessageIds = store.fetchDismissedRemoteMessageIDs() diff --git a/DuckDuckGo/SettingsState.swift b/DuckDuckGo/SettingsState.swift index c4a4b8144b..d9a11ae56b 100644 --- a/DuckDuckGo/SettingsState.swift +++ b/DuckDuckGo/SettingsState.swift @@ -19,6 +19,7 @@ import BrowserServicesKit import Subscription +import Networking struct SettingsState { @@ -36,15 +37,15 @@ struct SettingsState { var level: TextZoomLevel } - struct Subscription: Codable { + struct SubscriptionState: Codable { var canPurchase: Bool - var isSignedIn: Bool + var subscriptionExist: Bool var hasActiveSubscription: Bool var isRestoring: Bool var shouldDisplayRestoreSubscriptionError: Bool - var subscriptionFeatures: [Entitlement.ProductName] - var entitlements: [Entitlement.ProductName] - var platform: DDGSubscription.Platform + var entitlements: [SubscriptionEntitlement] + var subscriptionFeatures: [SubscriptionEntitlement] + var platform: PrivacyProSubscription.Platform var isShowingStripeView: Bool } @@ -96,7 +97,7 @@ struct SettingsState { var networkProtectionConnected: Bool // Subscriptions Properties - var subscription: Subscription + var subscription: SubscriptionState // Sync Properties var sync: SyncSettings @@ -135,15 +136,15 @@ struct SettingsState { speechRecognitionAvailable: false, loginsEnabled: false, networkProtectionConnected: false, - subscription: Subscription(canPurchase: false, - isSignedIn: false, - hasActiveSubscription: false, - isRestoring: false, - shouldDisplayRestoreSubscriptionError: false, - subscriptionFeatures: [], - entitlements: [], - platform: .unknown, - isShowingStripeView: false), + subscription: SubscriptionState(canPurchase: false, + subscriptionExist: false, + hasActiveSubscription: false, + isRestoring: false, + shouldDisplayRestoreSubscriptionError: false, + entitlements: [], + subscriptionFeatures: [], + platform: .unknown, + isShowingStripeView: false), sync: SyncSettings(enabled: false, title: ""), syncSource: nil, duckPlayerEnabled: false, diff --git a/DuckDuckGo/SettingsSubscriptionView.swift b/DuckDuckGo/SettingsSubscriptionView.swift index d5e777ef44..025fdcc74d 100644 --- a/DuckDuckGo/SettingsSubscriptionView.swift +++ b/DuckDuckGo/SettingsSubscriptionView.swift @@ -232,7 +232,8 @@ struct SettingsSubscriptionView: View { Group { if isShowingPrivacyPro { - let isSignedIn = settingsViewModel.state.subscription.isSignedIn +// let isSignedIn = settingsViewModel.state.subscription.isSignedIn + let subscriptionExists = settingsViewModel.state.subscription.subscriptionExist let hasActiveSubscription = settingsViewModel.state.subscription.hasActiveSubscription let hasNoEntitlements = settingsViewModel.state.subscription.entitlements.isEmpty @@ -241,24 +242,24 @@ struct SettingsSubscriptionView: View { .daxFootnoteRegular().accentColor(Color.init(designSystemColor: .accent)) Section(header: Text(UserText.settingsPProSection), - footer: !isSignedIn ? footerLink : nil + footer: !subscriptionExists ? footerLink : nil ) { - switch (isSignedIn, hasActiveSubscription, hasNoEntitlements) { + switch (subscriptionExists, hasActiveSubscription, hasNoEntitlements) { - // Signed In, Subscription Expired + // Subscription exist, Subscription Expired case (true, false, _): subscriptionExpiredView - // Signed in, Subscription Active, Valid entitlements + // Subscription exist, Subscription Active, Valid entitlements case (true, true, false): subscriptionDetailsView // View for valid subscription details - // Signed in, Subscription Active, Empty Entitlements + // Subscription exist, Subscription Active, Empty Entitlements case (true, true, true): noEntitlementsAvailableView // View for no entitlements - // Signed out + // Subscription do not exist case (false, _, _): purchaseSubscriptionView // View for signing up or purchasing a subscription } @@ -272,7 +273,7 @@ struct SettingsSubscriptionView: View { } } .onReceive(settingsViewModel.$state) { state in - isShowingPrivacyPro = (state.subscription.isSignedIn || state.subscription.canPurchase) + isShowingPrivacyPro = state.subscription.subscriptionExist || state.subscription.canPurchase } } } diff --git a/DuckDuckGo/SettingsViewModel.swift b/DuckDuckGo/SettingsViewModel.swift index a35de32255..e4de818ea6 100644 --- a/DuckDuckGo/SettingsViewModel.swift +++ b/DuckDuckGo/SettingsViewModel.swift @@ -25,10 +25,11 @@ import Common import Combine import SyncUI import DuckPlayer +import Networking import Crashes - import Subscription import NetworkProtection +import os.log import AIChat final class SettingsViewModel: ObservableObject { @@ -60,7 +61,7 @@ final class SettingsViewModel: ObservableObject { case subscriptionState = "com.duckduckgo.ios.subscription.state" } // Used to cache the lasts subscription state for up to a week - private let subscriptionStateCache = UserDefaultsCache(key: UserDefaultsCacheKey.subscriptionState, + private let subscriptionStateCache = UserDefaultsCache(key: UserDefaultsCacheKey.subscriptionState, settings: UserDefaultsCacheSettings(defaultExpirationInterval: .days(7))) // Properties private lazy var isPad = UIDevice.current.userInterfaceIdiom == .pad @@ -401,7 +402,7 @@ final class SettingsViewModel: ObservableObject { } var usesUnifiedFeedbackForm: Bool { - subscriptionManager.accountManager.isUserAuthenticated && subscriptionFeatureAvailability.usesUnifiedFeedbackForm + subscriptionManager.isUserAuthenticated && subscriptionFeatureAvailability.usesUnifiedFeedbackForm } // MARK: Default Init @@ -749,6 +750,7 @@ extension SettingsViewModel { @MainActor private func setupSubscriptionEnvironment() async { + Logger.subscription.log("Setting up Subscription Environment") // If there's cached data use it by default if let cachedSubscription = subscriptionStateCache.get() { state.subscription = cachedSubscription @@ -760,48 +762,43 @@ extension SettingsViewModel { // Update if can purchase based on App Store product availability state.subscription.canPurchase = subscriptionManager.canPurchase - // Update if user is signed in based on the presence of token - state.subscription.isSignedIn = subscriptionManager.accountManager.isUserAuthenticated - - // Active subscription check - guard let token = subscriptionManager.accountManager.accessToken else { - // Reset state in case cache was outdated - state.subscription.hasActiveSubscription = false - state.subscription.entitlements = [] - state.subscription.platform = .unknown + // Fetch subscription details using a stored access token + do { + guard subscriptionManager.isUserAuthenticated == true else { + throw SubscriptionEndpointServiceError.noData + } - subscriptionStateCache.set(state.subscription) // Sync cache - return - } - - let subscriptionResult = await subscriptionManager.subscriptionEndpointService.getSubscription(accessToken: token) - switch subscriptionResult { - - case .success(let subscription): + let subscription = try await subscriptionManager.getSubscription(cachePolicy: .reloadIgnoringLocalCacheData) + Logger.subscription.log("Subscription loaded: \(subscription.debugDescription, privacy: .public)") + state.subscription.subscriptionExist = true state.subscription.platform = subscription.platform state.subscription.hasActiveSubscription = subscription.isActive - - // Check entitlements and update state - var currentEntitlements: [Entitlement.ProductName] = [] - let entitlementsToCheck: [Entitlement.ProductName] = [.networkProtection, .dataBrokerProtection, .identityTheftRestoration, .identityTheftRestorationGlobal] - - for entitlement in entitlementsToCheck { - if case .success(true) = await subscriptionManager.accountManager.hasEntitlement(forProductName: entitlement) { - currentEntitlements.append(entitlement) + let features = await subscriptionManager.currentSubscriptionFeatures(forceRefresh: false) + state.subscription.entitlements = features.compactMap({ feature in + if feature.availableForUser { + return feature.entitlement + } else { + return nil } - } - - self.state.subscription.entitlements = currentEntitlements - self.state.subscription.subscriptionFeatures = await subscriptionManager.currentSubscriptionFeatures() + }) + state.subscription.subscriptionFeatures = features.map({ feature in feature.entitlement }) + } catch SubscriptionEndpointServiceError.noData, OAuthClientError.missingTokens { + // Auth successful but no Subscription is available + Logger.subscription.log("Subscription not present") - case .failure: - break + state.subscription.subscriptionExist = false + state.subscription.platform = .unknown + state.subscription.hasActiveSubscription = false + state.subscription.entitlements = [] + } catch { + // Generic error, we don't update the cached data + Logger.subscription.debug("Failed to load Subscription: \(error, privacy: .public)") } - + // Sync Cache subscriptionStateCache.set(state.subscription) } - + private func setupNotificationObservers() { subscriptionSignOutObserver = NotificationCenter.default.addObserver(forName: .accountDidSignOut, object: nil, @@ -831,10 +828,9 @@ extension SettingsViewModel { func restoreAccountPurchase() async { DispatchQueue.main.async { self.state.subscription.isRestoring = true } - let appStoreRestoreFlow = DefaultAppStoreRestoreFlow(accountManager: subscriptionManager.accountManager, - storePurchaseManager: subscriptionManager.storePurchaseManager(), - subscriptionEndpointService: subscriptionManager.subscriptionEndpointService, - authEndpointService: subscriptionManager.authEndpointService) + + let appStoreRestoreFlow = DefaultAppStoreRestoreFlow(subscriptionManager: subscriptionManager, + storePurchaseManager: subscriptionManager.storePurchaseManager()) let result = await appStoreRestoreFlow.restoreAccountFromPastPurchase() switch result { case .success: diff --git a/DuckDuckGo/Subscription/DefaultSubscriptionManager+AccountManagerKeychainAccessDelegate.swift b/DuckDuckGo/Subscription/DefaultSubscriptionManager+AccountManagerKeychainAccessDelegate.swift deleted file mode 100644 index 9b7a86bf77..0000000000 --- a/DuckDuckGo/Subscription/DefaultSubscriptionManager+AccountManagerKeychainAccessDelegate.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// DefaultSubscriptionManager+AccountManagerKeychainAccessDelegate.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 Core -import Subscription - -extension DefaultSubscriptionManager: AccountManagerKeychainAccessDelegate { - - public func accountManagerKeychainAccessFailed(accessType: AccountKeychainAccessType, error: AccountKeychainAccessError) { - let parameters = [ - PixelParameters.privacyProKeychainAccessType: accessType.rawValue, - PixelParameters.privacyProKeychainError: error.errorDescription, - PixelParameters.source: "browser" - ] - - DailyPixel.fireDailyAndCount(pixel: .privacyProKeychainAccessError, - pixelNameSuffixes: DailyPixel.Constant.legacyDailyPixelSuffixes, - withAdditionalParameters: parameters) - } -} diff --git a/DuckDuckGo/Subscription/Feedback/UnifiedFeedbackFormViewModel.swift b/DuckDuckGo/Subscription/Feedback/UnifiedFeedbackFormViewModel.swift index 6038d19dc2..eeb0bc4266 100644 --- a/DuckDuckGo/Subscription/Feedback/UnifiedFeedbackFormViewModel.swift +++ b/DuckDuckGo/Subscription/Feedback/UnifiedFeedbackFormViewModel.swift @@ -67,6 +67,7 @@ final class UnifiedFeedbackFormViewModel: ObservableObject { enum Error: String, Swift.Error { case missingAccessToken case invalidResponse + case invalidRequest } struct Payload: Codable { @@ -134,7 +135,7 @@ final class UnifiedFeedbackFormViewModel: ObservableObject { } } - private let accountManager: any AccountManager + private let subscriptionManager: any SubscriptionManager private let apiService: any Networking.APIService private let vpnMetadataCollector: any UnifiedMetadataCollector private let defaultMetadataCollector: any UnifiedMetadataCollector @@ -151,26 +152,32 @@ final class UnifiedFeedbackFormViewModel: ObservableObject { feedbackSender: any UnifiedFeedbackSender = DefaultFeedbackSender(), source: Source = .unknown) { self.viewState = .feedbackPending - - self.accountManager = subscriptionManager.accountManager + self.subscriptionManager = subscriptionManager self.apiService = apiService self.vpnMetadataCollector = vpnMetadataCollector self.defaultMetadataCollector = defaultMetadatCollector self.feedbackSender = feedbackSender self.source = source.rawValue + } - Task { - let features = await subscriptionManager.currentSubscriptionFeatures() - - if features.contains(.networkProtection) { - availableCategories.append(.vpn) - } - if features.contains(.dataBrokerProtection) { - availableCategories.append(.pir) - } - if features.contains(.identityTheftRestoration) || features.contains(.identityTheftRestorationGlobal) { - availableCategories.append(.itr) - } + @MainActor + func updateCategories() async { + let features = await subscriptionManager.currentSubscriptionFeatures(forceRefresh: false) + let vpnFeature = features.first { $0.entitlement == .networkProtection } + let dbpFeature = features.first { $0.entitlement == .dataBrokerProtection } + let itrFeature = features.first { $0.entitlement == .identityTheftRestoration } + let itrgFeature = features.first { $0.entitlement == .identityTheftRestorationGlobal } + + if vpnFeature?.availableForUser ?? false { + availableCategories.append(.vpn) + } + if dbpFeature?.availableForUser ?? false { + availableCategories.append(.pir) + } + let idpEnabled = itrFeature?.availableForUser ?? false + let idpgEnabled = itrgFeature?.availableForUser ?? false + if idpEnabled || idpgEnabled { + availableCategories.append(.itr) } } @@ -284,7 +291,7 @@ final class UnifiedFeedbackFormViewModel: ObservableObject { private func submitIssue(metadata: UnifiedFeedbackMetadata?) async throws { guard !userEmail.isEmpty, let selectedCategory else { return } - guard let accessToken = accountManager.accessToken else { + guard let accessToken = try? await subscriptionManager.getTokenContainer(policy: .localValid) else { throw Error.missingAccessToken } @@ -296,7 +303,9 @@ final class UnifiedFeedbackFormViewModel: ObservableObject { problemSubCategory: selectedSubcategory ?? "", customMetadata: metadata?.toString() ?? "") let headers = APIRequestV2.HeadersV2(additionalHeaders: [HTTPHeaderKey.authorization: "Bearer \(accessToken)"]) - let request = APIRequestV2(url: Self.feedbackEndpoint, method: .post, headers: headers, body: payload.toData()) + guard let request = APIRequestV2(url: Self.feedbackEndpoint, method: .post, headers: headers, body: payload.toData()) else { + throw Error.invalidRequest + } let response: Response = try await apiService.fetch(request: request).decodeBody() if let error = response.error, !error.isEmpty { diff --git a/DuckDuckGo/Subscription/Feedback/UnifiedFeedbackRootView.swift b/DuckDuckGo/Subscription/Feedback/UnifiedFeedbackRootView.swift index bd11bbcbdf..10eafd44ba 100644 --- a/DuckDuckGo/Subscription/Feedback/UnifiedFeedbackRootView.swift +++ b/DuckDuckGo/Subscription/Feedback/UnifiedFeedbackRootView.swift @@ -46,6 +46,7 @@ struct UnifiedFeedbackRootView: View { } .onFirstAppear { Task { + await viewModel.updateCategories() await viewModel.process(action: .reportActions) } } diff --git a/DuckDuckGo/Subscription/TokenBackgroundRefreshTask.swift b/DuckDuckGo/Subscription/TokenBackgroundRefreshTask.swift new file mode 100644 index 0000000000..ecf09478eb --- /dev/null +++ b/DuckDuckGo/Subscription/TokenBackgroundRefreshTask.swift @@ -0,0 +1,104 @@ +// +// TokenBackgroundRefreshTask.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 BackgroundTasks +import Subscription +import Core + +class TokenBackgroundRefreshTask { + + private let taskName = "Refresh authentication token" + private let taskIdentifier = "com.duckduckgo.app.backgroundTokenRefresh" + private let minimumConfigurationRefreshInterval: TimeInterval = TimeInterval.days(7) + private let subscriptionManager: SubscriptionManager + + init(subscriptionManager: SubscriptionManager) { + self.subscriptionManager = subscriptionManager + } + + func registerBackgroundRefreshTaskHandler() { + BGTaskScheduler.shared.register(forTaskWithIdentifier: taskIdentifier, using: nil) { [weak self] task in + + guard let self else { + Logger.subscription.fault("Failed to refresh token, self is nil") + task.setTaskCompleted(success: false) + return + } + + guard self.subscriptionManager.isUserAuthenticated else { + task.setTaskCompleted(success: true) + self.scheduleTask() + return + } + + self.handle(task: task) + } + } + + func handle(task: BGTask) { + Logger.subscription.log("Token background refresh task started") + + scheduleTask() + + let refreshStartDate = Date() + task.expirationHandler = { + Logger.subscription.error("Background refresh task expired") + task.setTaskCompleted(success: false) + } + + Task { [weak self] in + guard let self else { + Logger.subscription.fault("Failed to refresh token, self is nil") + task.setTaskCompleted(success: false) + return + } + do { + try await self.subscriptionManager.getTokenContainer(policy: .localForceRefresh) + Logger.subscription.log("Token background refresh task completed successfully in \(Date().timeIntervalSince(refreshStartDate)) seconds") + task.setTaskCompleted(success: true) + } catch { + Logger.subscription.error("Failed to refresh token: \(error)") + task.setTaskCompleted(success: false) + } + } + } + + func scheduleTask() { + let task = BGProcessingTaskRequest(identifier: taskIdentifier) + task.requiresNetworkConnectivity = true + task.earliestBeginDate = Date(timeIntervalSinceNow: minimumConfigurationRefreshInterval) + + // Background tasks can be debugged by breaking on the `submit` call, stepping over, then running the following LLDB command, before resuming: + // e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.duckduckgo.app.backgroundTokenRefresh"] + // + // Task expiration can be simulated similarly: + // e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"com.duckduckgo.app.backgroundTokenRefresh"] + + #if !targetEnvironment(simulator) + do { + try BGTaskScheduler.shared.submit(task) + Logger.subscription.debug("Token background refresh task scheduled") + } catch { + Logger.subscription.error("Failed to schedule token background refresh task: \(error)") + Pixel.fire(pixel: .backgroundTaskSubmissionFailed, error: error) + } + #endif + } +} diff --git a/DuckDuckGo/Subscription/UserScripts/IdentityTheftRestorationPagesFeature.swift b/DuckDuckGo/Subscription/UserScripts/IdentityTheftRestorationPagesFeature.swift index aef3903dbb..903b8e7c68 100644 --- a/DuckDuckGo/Subscription/UserScripts/IdentityTheftRestorationPagesFeature.swift +++ b/DuckDuckGo/Subscription/UserScripts/IdentityTheftRestorationPagesFeature.swift @@ -42,10 +42,10 @@ final class IdentityTheftRestorationPagesFeature: Subfeature, ObservableObject { static let getAccessToken = "getAccessToken" } - private let accountManager: AccountManager + private let subscriptionManager: SubscriptionManager - init(accountManager: AccountManager) { - self.accountManager = accountManager + init(subscriptionManager: SubscriptionManager) { + self.subscriptionManager = subscriptionManager } weak var broker: UserScriptMessageBroker? @@ -71,9 +71,11 @@ final class IdentityTheftRestorationPagesFeature: Subfeature, ObservableObject { } func getAccessToken(params: Any, original: WKScriptMessage) async throws -> Encodable? { - if let accessToken = accountManager.accessToken { + do { + let accessToken = try await subscriptionManager.getTokenContainer(policy: .localValid).accessToken return [Constants.token: accessToken] - } else { + } catch { + Logger.subscription.debug("No access token available: \(error)") return [String: String]() } } diff --git a/DuckDuckGo/Subscription/UserScripts/IdentityTheftRestorationPagesUserScript.swift b/DuckDuckGo/Subscription/UserScripts/IdentityTheftRestorationPagesUserScript.swift index d68b4fe067..0865486925 100644 --- a/DuckDuckGo/Subscription/UserScripts/IdentityTheftRestorationPagesUserScript.swift +++ b/DuckDuckGo/Subscription/UserScripts/IdentityTheftRestorationPagesUserScript.swift @@ -64,6 +64,6 @@ extension IdentityTheftRestorationPagesUserScript: WKScriptMessageHandlerWithRep extension IdentityTheftRestorationPagesUserScript: WKScriptMessageHandler { public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { // unsupported - Logger.subscription.debug("Unsupported function: \(#function)") + Logger.subscription.log("Unsupported function: \(#function)") } } diff --git a/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift b/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift index 4bd355a5b5..24e7d8d779 100644 --- a/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift +++ b/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift @@ -26,6 +26,7 @@ import Combine import Subscription import Core import os.log +import Networking enum SubscriptionTransactionStatus: String { case idle, purchasing, restoring, polling @@ -80,11 +81,9 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec private let subscriptionAttributionOrigin: String? private let subscriptionManager: SubscriptionManager - private let subscriptionFeatureAvailability: SubscriptionFeatureAvailability - private var accountManager: AccountManager { subscriptionManager.accountManager } private let appStorePurchaseFlow: AppStorePurchaseFlow private let appStoreRestoreFlow: AppStoreRestoreFlow - private let appStoreAccountManagementFlow: AppStoreAccountManagementFlow + private let subscriptionFeatureAvailability: SubscriptionFeatureAvailability private let privacyProDataReporter: PrivacyProDataReporting? private let freeTrialsExperiment: any FreeTrialsFeatureFlagExperimenting @@ -93,14 +92,12 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec subscriptionAttributionOrigin: String?, appStorePurchaseFlow: AppStorePurchaseFlow, appStoreRestoreFlow: AppStoreRestoreFlow, - appStoreAccountManagementFlow: AppStoreAccountManagementFlow, privacyProDataReporter: PrivacyProDataReporting? = nil, freeTrialsExperiment: any FreeTrialsFeatureFlagExperimenting = FreeTrialsFeatureFlagExperiment()) { self.subscriptionManager = subscriptionManager self.subscriptionFeatureAvailability = subscriptionFeatureAvailability self.appStorePurchaseFlow = appStorePurchaseFlow self.appStoreRestoreFlow = appStoreRestoreFlow - self.appStoreAccountManagementFlow = appStoreAccountManagementFlow self.subscriptionAttributionOrigin = subscriptionAttributionOrigin self.privacyProDataReporter = subscriptionAttributionOrigin != nil ? privacyProDataReporter : nil self.freeTrialsExperiment = freeTrialsExperiment @@ -113,11 +110,11 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec // Subscription Activation Actions var onSetSubscription: (() -> Void)? var onBackToSettings: (() -> Void)? - var onFeatureSelected: ((Entitlement.ProductName) -> Void)? + var onFeatureSelected: ((SubscriptionEntitlement) -> Void)? var onActivateSubscription: (() -> Void)? struct FeatureSelection: Codable { - let productFeature: Entitlement.ProductName + let productFeature: SubscriptionEntitlement } weak var broker: UserScriptMessageBroker? @@ -136,8 +133,8 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec } func handler(forMethodNamed methodName: String) -> Subfeature.Handler? { - Logger.subscription.debug("WebView handler: \(methodName)") + switch methodName { case Handlers.getSubscription: return getSubscription case Handlers.setSubscription: return setSubscription @@ -179,20 +176,20 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec private func setTransactionStatus(_ status: SubscriptionTransactionStatus) { if status != transactionStatus { - Logger.subscription.debug("Transaction state updated: \(status.rawValue)") + Logger.subscription.log("Transaction state updated: \(status.rawValue)") transactionStatus = status } } // MARK: Broker Methods (Called from WebView via UserScripts) - - func getSubscription(params: Any, original: WKScriptMessage) async -> Encodable? { - guard accountManager.isUserAuthenticated else { return [Constants.token: Constants.empty] } - switch await appStoreAccountManagementFlow.refreshAuthTokenIfNeeded() { - case .success(let currentAuthToken): - return [Constants.token: currentAuthToken] - case .failure: + /// Returns the auth token + func getSubscription(params: Any, original: WKScriptMessage) async -> Encodable? { + do { + let accessToken = try await subscriptionManager.getTokenContainer(policy: .localValid).accessToken + return [Constants.token: accessToken] + } catch { + Logger.subscription.debug("No subscription available: \(error)") return [Constants.token: Constants.empty] } } @@ -212,9 +209,11 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec } if let subscriptionOptions { + Logger.subscription.debug("Subscription options retrieved: \(String(describing: subscriptionOptions), privacy: .public)") if subscriptionFeatureAvailability.isSubscriptionPurchaseAllowed { return subscriptionOptions } else { + Logger.subscription.log("Subscription purchase not allowed") return subscriptionOptions.withoutPurchaseOptions() } } else { @@ -237,7 +236,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec } let message = original - guard let subscriptionSelection: SubscriptionSelection = DecodableHelper.decode(from: params) else { + guard let subscriptionSelection: SubscriptionSelection = CodableHelper.decode(from: params) else { assertionFailure("SubscriptionPagesUserScript: expected JSON representation of SubscriptionSelection") Logger.subscription.error("SubscriptionPagesUserScript: expected JSON representation of SubscriptionSelection") setTransactionStatus(.idle) @@ -246,20 +245,18 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec // Check for active subscriptions if await subscriptionManager.storePurchaseManager().hasActiveSubscription() { - Logger.subscription.debug("Subscription already active") + Logger.subscription.log("Subscription already active") setTransactionError(.hasActiveSubscription) Pixel.fire(pixel: .privacyProRestoreAfterPurchaseAttempt) setTransactionStatus(.idle) return nil } - let emailAccessToken = try? EmailManager().getToken() let purchaseTransactionJWS: String - switch await appStorePurchaseFlow.purchaseSubscription(with: subscriptionSelection.id, - emailAccessToken: emailAccessToken) { + switch await appStorePurchaseFlow.purchaseSubscription(with: subscriptionSelection.id) { case .success(let transactionJWS): - Logger.subscription.debug("Subscription purchased successfully") + Logger.subscription.log("Subscription purchased successfully") purchaseTransactionJWS = transactionJWS case .failure(let error): @@ -268,7 +265,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec switch error { case .cancelledByUser: setTransactionError(.cancelledByUser) - await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: PurchaseUpdate(type: "canceled")) + await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: PurchaseUpdate.canceled) return nil case .accountCreationFailed: setTransactionError(.accountCreationFailed) @@ -283,61 +280,74 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec setTransactionStatus(.polling) + guard purchaseTransactionJWS.isEmpty == false else { + Logger.subscription.fault("Purchase transaction JWS is empty") + assertionFailure("Purchase transaction JWS is empty") + setTransactionStatus(.idle) + return nil + } + // Free Trials Experiment Pixels fireFreeTrialSubscriptionPurchasePixelIfApplicable(for: subscriptionSelection.id) - switch await appStorePurchaseFlow.completeSubscriptionPurchase(with: purchaseTransactionJWS, additionalParams: completeSubscriptionFreeTrialParameters) { - case .success(let purchaseUpdate): - Logger.subscription.debug("Subscription purchase completed successfully") + switch await appStorePurchaseFlow.completeSubscriptionPurchase(with: purchaseTransactionJWS, + additionalParams: completeSubscriptionFreeTrialParameters) { + case .success: + Logger.subscription.log("Subscription purchase completed successfully") DailyPixel.fireDailyAndCount(pixel: .privacyProPurchaseSuccess, pixelNameSuffixes: DailyPixel.Constant.legacyDailyPixelSuffixes) UniquePixel.fire(pixel: .privacyProSubscriptionActivated) Pixel.fireAttribution(pixel: .privacyProSuccessfulSubscriptionAttribution, origin: subscriptionAttributionOrigin, privacyProDataReporter: privacyProDataReporter) + setTransactionStatus(.idle) - await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: purchaseUpdate) + await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: PurchaseUpdate.completed) case .failure(let error): - Logger.subscription.error("App store complete subscription purchase error: \(error.localizedDescription)") + Logger.subscription.error("App store complete subscription purchase error: \(error, privacy: .public)") + + await subscriptionManager.signOut(notifyUI: true) + setTransactionStatus(.idle) setTransactionError(.missingEntitlements) - await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: PurchaseUpdate(type: "completed")) + await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: PurchaseUpdate.completed) } return nil } func setSubscription(params: Any, original: WKScriptMessage) async -> Encodable? { - guard let subscriptionValues: SubscriptionValues = DecodableHelper.decode(from: params) else { + // Note: This is called by the web FE when a subscription is retrieved, `params` contains an auth token V1 that will need to be exchanged for a V2. This is a temporary workaround until the FE fully supports v2 auth. + + guard let subscriptionValues: SubscriptionValues = CodableHelper.decode(from: params) else { + Logger.subscription.fault("SubscriptionPagesUserScript: expected JSON representation of SubscriptionValues") assertionFailure("SubscriptionPagesUserScript: expected JSON representation of SubscriptionValues") - Logger.subscription.error("SubscriptionPagesUserScript: expected JSON representation of SubscriptionValues") setTransactionError(.generalError) return nil } // Clear subscription Cache - subscriptionManager.subscriptionEndpointService.signOut() + await subscriptionManager.signOut(notifyUI: false) let authToken = subscriptionValues.token - if case let .success(accessToken) = await accountManager.exchangeAuthTokenToAccessToken(authToken), - case let .success(accountDetails) = await accountManager.fetchAccountDetails(with: accessToken) { - accountManager.storeAuthToken(token: authToken) - accountManager.storeAccount(token: accessToken, email: accountDetails.email, externalID: accountDetails.externalID) + do { + _ = try await subscriptionManager.exchange(tokenV1: authToken) + Logger.subscription.log("v1 token exchanged for v2") + onSetSubscription?() - - } else { - Logger.subscription.error("Failed to obtain subscription options") + } catch { + Logger.subscription.error("Failed to exchange v1 token for v2") setTransactionError(.failedToSetSubscription) } - return nil } func activateSubscription(params: Any, original: WKScriptMessage) async -> Encodable? { + Logger.subscription.log("Activating Subscription") Pixel.fire(pixel: .privacyProRestorePurchaseOfferPageEntry, debounce: 2) onActivateSubscription?() return nil } func featureSelected(params: Any, original: WKScriptMessage) async -> Encodable? { - guard let featureSelection: FeatureSelection = DecodableHelper.decode(from: params) else { + guard let featureSelection: FeatureSelection = CodableHelper.decode(from: params) else { assertionFailure("SubscriptionPagesUserScript: expected JSON representation of FeatureSelection") Logger.subscription.error("SubscriptionPagesUserScript: expected JSON representation of FeatureSelection") return nil @@ -349,33 +359,39 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec } func backToSettings(params: Any, original: WKScriptMessage) async -> Encodable? { - guard let accessToken = accountManager.accessToken else { - Logger.subscription.error("Missing access token") - return nil - } - - switch await accountManager.fetchAccountDetails(with: accessToken) { - case .success(let accountDetails): - switch await subscriptionManager.subscriptionEndpointService.getSubscription(accessToken: accessToken) { - case .success: - accountManager.storeAccount(token: accessToken, - email: accountDetails.email, - externalID: accountDetails.externalID) - onBackToSettings?() - case .failure(let error): - Logger.subscription.error("Error retrieving subscription details: \(error.localizedDescription)") - } - case .failure(let error): - Logger.subscription.error("Could not get account Details: \(error.localizedDescription)") - setTransactionError(.generalError) - } + Logger.subscription.log("Back to settings") +// guard let accessToken = accountManager.accessToken else { +// Logger.subscription.error("Missing access token") +// return nil +// } +// +// switch await accountManager.fetchAccountDetails(with: accessToken) { +// case .success(let accountDetails): +// switch await subscriptionManager.subscriptionEndpointService.getSubscription(accessToken: accessToken) { +// case .success: +// accountManager.storeAccount(token: accessToken, +// email: accountDetails.email, +// externalID: accountDetails.externalID) +// onBackToSettings?() +// case .failure(let error): +// Logger.subscription.error("Error retrieving subscription details: \(error.localizedDescription)") +// } +// case .failure(let error): +// Logger.subscription.error("Could not get account Details: \(error.localizedDescription)") +// setTransactionError(.generalError) +// } +// return nil + _ = try? await subscriptionManager.getTokenContainer(policy: .localForceRefresh) + onBackToSettings?() return nil } func getAccessToken(params: Any, original: WKScriptMessage) async throws -> Encodable? { - if let accessToken = subscriptionManager.accountManager.accessToken { + do { + let accessToken = try await subscriptionManager.getTokenContainer(policy: .localValid).accessToken return [Constants.token: accessToken] - } else { + } catch { + Logger.subscription.debug("No access token available: \(error)") return [String: String]() } } @@ -383,7 +399,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec // MARK: Pixel related actions func subscriptionsMonthlyPriceClicked(params: Any, original: WKScriptMessage) async -> Encodable? { - Logger.subscription.debug("Web function called: \(#function)") + Logger.subscription.log("Web function called: \(#function)") Pixel.fire(pixel: .privacyProOfferMonthlyPriceClick) if userIsEnrolledInFreeTrialsExperiment { @@ -394,7 +410,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec } func subscriptionsYearlyPriceClicked(params: Any, original: WKScriptMessage) async -> Encodable? { - Logger.subscription.debug("Web function called: \(#function)") + Logger.subscription.log("Web function called: \(#function)") Pixel.fire(pixel: .privacyProOfferYearlyPriceClick) if userIsEnrolledInFreeTrialsExperiment { @@ -406,18 +422,18 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec func subscriptionsUnknownPriceClicked(params: Any, original: WKScriptMessage) async -> Encodable? { // Not used - Logger.subscription.debug("Web function called: \(#function)") + Logger.subscription.log("Web function called: \(#function)") return nil } func subscriptionsAddEmailSuccess(params: Any, original: WKScriptMessage) async -> Encodable? { - Logger.subscription.debug("Web function called: \(#function)") + Logger.subscription.log("Web function called: \(#function)") UniquePixel.fire(pixel: .privacyProAddEmailSuccess) return nil } func subscriptionsWelcomeFaqClicked(params: Any, original: WKScriptMessage) async -> Encodable? { - Logger.subscription.debug("Web function called: \(#function)") + Logger.subscription.log("Web function called: \(#function)") UniquePixel.fire(pixel: .privacyProWelcomeFAQClick) return nil } @@ -445,16 +461,24 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec func restoreAccountFromAppStorePurchase() async throws { setTransactionStatus(.restoring) let result = await appStoreRestoreFlow.restoreAccountFromPastPurchase() + + setTransactionStatus(.idle) switch result { case .success: - setTransactionStatus(.idle) + Logger.subscription.log("Subscription restored successfully from App Store purchase") case .failure(let error): + Logger.subscription.error("Failed to restore subscription from App Store purchase: \(error.localizedDescription)") let mappedError = mapAppStoreRestoreErrorToTransactionError(error) - setTransactionStatus(.idle) throw mappedError } } - + + func cancelRestoreAccountFromAppStorePurchase() async throws { + setTransactionStatus(.idle) + await subscriptionManager.signOut(notifyUI: false) + } + + // MARK: Utility Methods func mapAppStoreRestoreErrorToTransactionError(_ error: AppStoreRestoreFlowError) -> UseSubscriptionError { @@ -529,7 +553,7 @@ private extension SubscriptionPagesUseSubscriptionFeature { /// - Returns: A `FreeTrialsFeatureFlagExperiment.Cohort` if the user is part of a cohort, otherwise `nil`. func freeTrialCohortIfApplicable() -> FreeTrialsFeatureFlagExperiment.Cohort? { // Check if the user is authenticated; free trials are not applicable for authenticated users - guard !subscriptionManager.accountManager.isUserAuthenticated else { return nil } + guard !subscriptionManager.isUserAuthenticated else { return nil } // Ensure that the user can make purchases guard subscriptionManager.canPurchase else { return nil } diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift index a059bcae64..a8a0ccbe18 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift @@ -69,7 +69,7 @@ final class SubscriptionEmailViewModel: ObservableObject { } private var cancellables = Set() - var accountManager: AccountManager { subscriptionManager.accountManager } +// var accountManager: AccountManager { subscriptionManager.accountManager } private var isWelcomePageOrSuccessPage: Bool { let subscriptionActivateSuccessURL = subscriptionManager.url(for: .activateSuccess) @@ -131,12 +131,12 @@ final class SubscriptionEmailViewModel: ObservableObject { func onAppear() { state.shouldDismissView = false // If the user is Authenticated & not in the Welcome page - if accountManager.isUserAuthenticated && !isWelcomePageOrSuccessPage { + if subscriptionManager.isUserAuthenticated && !isWelcomePageOrSuccessPage { // If user is authenticated, we want to "Add or manage email" instead of activating let addEmailToSubscriptionURL = subscriptionManager.url(for: .addEmail) let manageSubscriptionEmailURL = subscriptionManager.url(for: .manageEmail) - emailURL = accountManager.email == nil ? addEmailToSubscriptionURL : manageSubscriptionEmailURL - state.viewTitle = accountManager.email == nil ? UserText.subscriptionRestoreAddEmailTitle : UserText.subscriptionEditEmailTitle + emailURL = subscriptionManager.userEmail == nil ? addEmailToSubscriptionURL : manageSubscriptionEmailURL + state.viewTitle = subscriptionManager.userEmail == nil ? UserText.subscriptionRestoreAddEmailTitle : UserText.subscriptionEditEmailTitle // Also we assume subscription requires managing, and not activation state.managingSubscriptionEmail = true diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift index fd9ae97268..10b2bc333d 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift @@ -339,7 +339,21 @@ final class SubscriptionFlowViewModel: ObservableObject { } } } - + + @MainActor + func cancelAppstoreTransaction() { + clearTransactionError() + Task { + do { + try await subFeature.cancelRestoreAccountFromAppStorePurchase() + } catch let error { + if let specificError = error as? SubscriptionPagesUseSubscriptionFeature.UseSubscriptionError { + handleTransactionError(error: specificError) + } + } + } + } + @MainActor func navigateBack() async { await webViewModel.navigationCoordinator.goBack() diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionITPViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionITPViewModel.swift index 3e9692f30b..9e67cc8b86 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionITPViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionITPViewModel.swift @@ -64,7 +64,7 @@ final class SubscriptionITPViewModel: ObservableObject { self.itpURL = subscriptionManager.url(for: .identityTheftRestoration) self.manageITPURL = self.itpURL self.userScript = IdentityTheftRestorationPagesUserScript() - self.subFeature = IdentityTheftRestorationPagesFeature(accountManager: subscriptionManager.accountManager) + self.subFeature = IdentityTheftRestorationPagesFeature(subscriptionManager: subscriptionManager) let webViewSettings = AsyncHeadlessWebViewSettings(bounces: false, allowedDomains: Self.allowedDomains, diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift index 749a650c14..fac452ab2a 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift @@ -28,7 +28,6 @@ final class SubscriptionRestoreViewModel: ObservableObject { let userScript: SubscriptionPagesUserScript let subFeature: SubscriptionPagesUseSubscriptionFeature let subscriptionManager: SubscriptionManager - var accountManager: AccountManager { subscriptionManager.accountManager } private var cancellables = Set() diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionSettingsViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionSettingsViewModel.swift index a8a3c3a875..88795ed4de 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionSettingsViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionSettingsViewModel.swift @@ -24,6 +24,7 @@ import Subscription import Core import os.log import BrowserServicesKit +import Networking final class SubscriptionSettingsViewModel: ObservableObject { @@ -40,7 +41,7 @@ final class SubscriptionSettingsViewModel: ObservableObject { var isShowingGoogleView: Bool = false var isShowingFAQView: Bool = false var isShowingLearnMoreView: Bool = false - var subscriptionInfo: Subscription? + var subscriptionInfo: PrivacyProSubscription? var isLoadingSubscriptionInfo: Bool = false var isLoadingEmailInfo: Bool = false @@ -74,18 +75,21 @@ final class SubscriptionSettingsViewModel: ObservableObject { let subscriptionFAQURL = subscriptionManager.url(for: .faq) let learnMoreURL = subscriptionFAQURL.appendingPathComponent("adding-email") self.state = State(faqURL: subscriptionFAQURL, learnMoreURL: learnMoreURL) - self.usesUnifiedFeedbackForm = subscriptionManager.accountManager.isUserAuthenticated - + self.usesUnifiedFeedbackForm = subscriptionManager.isUserAuthenticated setupNotificationObservers() } - private var dateFormatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateStyle = .long - formatter.timeStyle = .none - return formatter + private var dateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .long +#if DEBUG + dateFormatter.timeStyle = .medium +#else + dateFormatter.timeStyle = .none +#endif + return dateFormatter }() - + func onFirstAppear() { Task { // Load initial state from the cache @@ -109,15 +113,15 @@ final class SubscriptionSettingsViewModel: ObservableObject { } } - private func fetchAndUpdateSubscriptionDetails(cachePolicy: APICachePolicy, loadingIndicator: Bool) async -> Bool { - Logger.subscription.debug("\(#function)") - guard let token = self.subscriptionManager.accountManager.accessToken else { return false } + private func fetchAndUpdateSubscriptionDetails(cachePolicy: SubscriptionCachePolicy, loadingIndicator: Bool) async -> Bool { + Logger.subscription.log("Fetch and update subscription details") - if loadingIndicator { displaySubscriptionLoader(true) } - let subscriptionResult = await self.subscriptionManager.subscriptionEndpointService.getSubscription(accessToken: token, - cachePolicy: cachePolicy) - switch subscriptionResult { - case .success(let subscription): + DispatchQueue.main.async { + if loadingIndicator { self.displaySubscriptionLoader(true) } + } + + do { + let subscription = try await self.subscriptionManager.getSubscription(cachePolicy: cachePolicy) DispatchQueue.main.async { self.state.subscriptionInfo = subscription if loadingIndicator { self.displaySubscriptionLoader(false) } @@ -127,7 +131,7 @@ final class SubscriptionSettingsViewModel: ObservableObject { product: subscription.productId, billingPeriod: subscription.billingPeriod) return true - case .failure(let error): + } catch { Logger.subscription.error("\(#function) error: \(error.localizedDescription)") DispatchQueue.main.async { if loadingIndicator { self.displaySubscriptionLoader(true) } @@ -136,42 +140,26 @@ final class SubscriptionSettingsViewModel: ObservableObject { } } - func fetchAndUpdateAccountEmail(cachePolicy: APICachePolicy = .returnCacheDataElseLoad, loadingIndicator: Bool) async -> Bool { - Logger.subscription.debug("\(#function)") - guard let token = self.subscriptionManager.accountManager.accessToken else { return false } - + func fetchAndUpdateAccountEmail(cachePolicy: SubscriptionCachePolicy = .returnCacheDataElseLoad, loadingIndicator: Bool) async -> Bool { + Logger.subscription.log("Fetch and update account email") + var tokensPolicy: AuthTokensCachePolicy = .local switch cachePolicy { - case .returnCacheDataDontLoad, .returnCacheDataElseLoad: - DispatchQueue.main.async { - self.state.subscriptionEmail = self.subscriptionManager.accountManager.email - } - return true case .reloadIgnoringLocalCacheData: - break + tokensPolicy = .localForceRefresh + case .returnCacheDataElseLoad: + tokensPolicy = .localValid + case .returnCacheDataDontLoad: + tokensPolicy = .local } - if loadingIndicator { displayEmailLoader(true) } - switch await self.subscriptionManager.accountManager.fetchAccountDetails(with: token) { - case .success(let details): - Logger.subscription.debug("Account details fetched successfully") - DispatchQueue.main.async { - self.state.subscriptionEmail = details.email - if loadingIndicator { self.displayEmailLoader(false) } - } - - // If fetched email is different then update accountManager - if details.email != subscriptionManager.accountManager.email { - let externalID = subscriptionManager.accountManager.externalID - subscriptionManager.accountManager.storeAccount(token: token, email: details.email, externalID: externalID) - } - return true - case .failure(let error): - Logger.subscription.error("\(#function) error: \(error.localizedDescription)") - DispatchQueue.main.async { + if let tokenContainer = try? await subscriptionManager.getTokenContainer(policy: tokensPolicy) { + Task { @MainActor in + self.state.subscriptionEmail = tokenContainer.decodedAccessToken.email if loadingIndicator { self.displayEmailLoader(true) } } - return false + return true } + return false } private func displaySubscriptionLoader(_ show: Bool) { @@ -187,7 +175,7 @@ final class SubscriptionSettingsViewModel: ObservableObject { } func manageSubscription() { - Logger.subscription.debug("User action: \(#function)") + Logger.subscription.log("User action: \(#function)") switch state.subscriptionInfo?.platform { case .apple: Task { await manageAppleSubscription() } @@ -196,6 +184,7 @@ final class SubscriptionSettingsViewModel: ObservableObject { case .stripe: Task { await manageStripeSubscription() } default: + assertionFailure("Invalid subscription platform") return } } @@ -211,7 +200,9 @@ final class SubscriptionSettingsViewModel: ObservableObject { } @MainActor - private func updateSubscriptionsStatusMessage(status: Subscription.Status, date: Date, product: String, billingPeriod: Subscription.BillingPeriod) { + private func updateSubscriptionsStatusMessage(status: PrivacyProSubscription.Status, date: Date, product: String, billingPeriod: PrivacyProSubscription.BillingPeriod) { + Logger.subscription.log("Update subscription status: \(status.rawValue)") +// let billingPeriod = billingPeriod == .monthly ? UserText.subscriptionMonthlyBillingPeriod : UserText.subscriptionAnnualBillingPeriod let date = dateFormatter.string(from: date) switch status { @@ -225,19 +216,25 @@ final class SubscriptionSettingsViewModel: ObservableObject { } func removeSubscription() { - subscriptionManager.accountManager.signOut() - _ = ActionMessageView() - ActionMessageView.present(message: UserText.subscriptionRemovalConfirmation, - presentationLocation: .withoutBottomBar) + Logger.subscription.log("Remove subscription") + + Task { + await subscriptionManager.signOut(notifyUI: true) + _ = await ActionMessageView() + await ActionMessageView.present(message: UserText.subscriptionRemovalConfirmation, + presentationLocation: .withoutBottomBar) + } } func displayGoogleView(_ value: Bool) { + Logger.subscription.log("Show google") if value != state.isShowingGoogleView { state.isShowingGoogleView = value } } func displayStripeView(_ value: Bool) { + Logger.subscription.log("Show stripe") if value != state.isShowingStripeView { state.isShowingStripeView = value } @@ -250,12 +247,14 @@ final class SubscriptionSettingsViewModel: ObservableObject { } func displayFAQView(_ value: Bool) { + Logger.subscription.log("Show faq") if value != state.isShowingFAQView { state.isShowingFAQView = value } } func displayLearnMoreView(_ value: Bool) { + Logger.subscription.log("Show learn more") if value != state.isShowingLearnMoreView { state.isShowingLearnMoreView = value } @@ -271,12 +270,14 @@ final class SubscriptionSettingsViewModel: ObservableObject { @MainActor func showTermsOfService() { + Logger.subscription.log("Show terms of service") self.openURL(SettingsSubscriptionView.ViewConstants.privacyPolicyURL) } // MARK: - @MainActor private func manageAppleSubscription() async { + Logger.subscription.log("Managing Apple Subscription") if state.subscriptionInfo?.isActive ?? false { let url = subscriptionManager.url(for: .manageSubscriptionsInAppStore) if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene { @@ -292,13 +293,10 @@ final class SubscriptionSettingsViewModel: ObservableObject { } private func manageStripeSubscription() async { - guard let token = subscriptionManager.accountManager.accessToken, - let externalID = subscriptionManager.accountManager.externalID else { return } - let serviceResponse = await subscriptionManager.subscriptionEndpointService.getCustomerPortalURL(accessToken: token, externalID: externalID) - - // Get Stripe Customer Portal URL and update the model - if case .success(let response) = serviceResponse { - guard let url = URL(string: response.customerPortalUrl) else { return } + Logger.subscription.log("Managing Stripe Subscription") + do { + // Get Stripe Customer Portal URL and update the model + let url = try await subscriptionManager.getCustomerPortalURL() if let existingModel = state.stripeViewModel { existingModel.url = url } else { @@ -307,9 +305,11 @@ final class SubscriptionSettingsViewModel: ObservableObject { self.state.stripeViewModel = model } } - } - DispatchQueue.main.async { - self.displayStripeView(true) + DispatchQueue.main.async { + self.displayStripeView(true) + } + } catch { + Logger.subscription.error("\(error.localizedDescription)") } } diff --git a/DuckDuckGo/Subscription/Views/SubscriptionContainerViewFactory.swift b/DuckDuckGo/Subscription/Views/SubscriptionContainerViewFactory.swift index 80b1ea8b53..ef688c4765 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionContainerViewFactory.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionContainerViewFactory.swift @@ -19,6 +19,8 @@ import SwiftUI import Subscription +import Networking +import os.log import BrowserServicesKit enum SubscriptionContainerViewFactory { @@ -28,18 +30,12 @@ enum SubscriptionContainerViewFactory { subscriptionManager: SubscriptionManager, subscriptionFeatureAvailability: SubscriptionFeatureAvailability, privacyProDataReporter: PrivacyProDataReporting?) -> some View { - let appStoreRestoreFlow = DefaultAppStoreRestoreFlow(accountManager: subscriptionManager.accountManager, - storePurchaseManager: subscriptionManager.storePurchaseManager(), - subscriptionEndpointService: subscriptionManager.subscriptionEndpointService, - authEndpointService: subscriptionManager.authEndpointService) - let appStorePurchaseFlow = DefaultAppStorePurchaseFlow(subscriptionEndpointService: subscriptionManager.subscriptionEndpointService, + + let appStoreRestoreFlow = DefaultAppStoreRestoreFlow(subscriptionManager: subscriptionManager, + storePurchaseManager: subscriptionManager.storePurchaseManager()) + let appStorePurchaseFlow = DefaultAppStorePurchaseFlow(subscriptionManager: subscriptionManager, storePurchaseManager: subscriptionManager.storePurchaseManager(), - accountManager: subscriptionManager.accountManager, - appStoreRestoreFlow: appStoreRestoreFlow, - authEndpointService: subscriptionManager.authEndpointService) - let appStoreAccountManagementFlow = DefaultAppStoreAccountManagementFlow(authEndpointService: subscriptionManager.authEndpointService, - storePurchaseManager: subscriptionManager.storePurchaseManager(), - accountManager: subscriptionManager.accountManager) + appStoreRestoreFlow: appStoreRestoreFlow) let viewModel = SubscriptionContainerViewModel( subscriptionManager: subscriptionManager, @@ -50,7 +46,6 @@ enum SubscriptionContainerViewFactory { subscriptionAttributionOrigin: origin, appStorePurchaseFlow: appStorePurchaseFlow, appStoreRestoreFlow: appStoreRestoreFlow, - appStoreAccountManagementFlow: appStoreAccountManagementFlow, privacyProDataReporter: privacyProDataReporter) ) return SubscriptionContainerView(currentView: .subscribe, viewModel: viewModel) @@ -60,30 +55,20 @@ enum SubscriptionContainerViewFactory { static func makeRestoreFlow(navigationCoordinator: SubscriptionNavigationCoordinator, subscriptionManager: SubscriptionManager, subscriptionFeatureAvailability: SubscriptionFeatureAvailability) -> some View { - let appStoreRestoreFlow = DefaultAppStoreRestoreFlow(accountManager: subscriptionManager.accountManager, - storePurchaseManager: subscriptionManager.storePurchaseManager(), - subscriptionEndpointService: subscriptionManager.subscriptionEndpointService, - authEndpointService: subscriptionManager.authEndpointService) - let appStorePurchaseFlow = DefaultAppStorePurchaseFlow(subscriptionEndpointService: subscriptionManager.subscriptionEndpointService, + let appStoreRestoreFlow = DefaultAppStoreRestoreFlow(subscriptionManager: subscriptionManager, + storePurchaseManager: subscriptionManager.storePurchaseManager()) + let appStorePurchaseFlow = DefaultAppStorePurchaseFlow(subscriptionManager: subscriptionManager, storePurchaseManager: subscriptionManager.storePurchaseManager(), - accountManager: subscriptionManager.accountManager, - appStoreRestoreFlow: appStoreRestoreFlow, - authEndpointService: subscriptionManager.authEndpointService) - let appStoreAccountManagementFlow = DefaultAppStoreAccountManagementFlow(authEndpointService: subscriptionManager.authEndpointService, - storePurchaseManager: subscriptionManager.storePurchaseManager(), - accountManager: subscriptionManager.accountManager) - - let viewModel = SubscriptionContainerViewModel( - subscriptionManager: subscriptionManager, - origin: nil, - userScript: SubscriptionPagesUserScript(), - subFeature: SubscriptionPagesUseSubscriptionFeature(subscriptionManager: subscriptionManager, - subscriptionFeatureAvailability: subscriptionFeatureAvailability, - subscriptionAttributionOrigin: nil, - appStorePurchaseFlow: appStorePurchaseFlow, - appStoreRestoreFlow: appStoreRestoreFlow, - appStoreAccountManagementFlow: appStoreAccountManagementFlow) - ) + appStoreRestoreFlow: appStoreRestoreFlow) + let subscriptionPagesUseSubscriptionFeature = SubscriptionPagesUseSubscriptionFeature(subscriptionManager: subscriptionManager, + subscriptionFeatureAvailability: subscriptionFeatureAvailability, + subscriptionAttributionOrigin: nil, + appStorePurchaseFlow: appStorePurchaseFlow, + appStoreRestoreFlow: appStoreRestoreFlow) + let viewModel = SubscriptionContainerViewModel(subscriptionManager: subscriptionManager, + origin: nil, + userScript: SubscriptionPagesUserScript(), + subFeature: subscriptionPagesUseSubscriptionFeature) return SubscriptionContainerView(currentView: .restore, viewModel: viewModel) .environmentObject(navigationCoordinator) } @@ -92,18 +77,11 @@ enum SubscriptionContainerViewFactory { subscriptionManager: SubscriptionManager, subscriptionFeatureAvailability: SubscriptionFeatureAvailability, onDisappear: @escaping () -> Void) -> some View { - let appStoreRestoreFlow = DefaultAppStoreRestoreFlow(accountManager: subscriptionManager.accountManager, - storePurchaseManager: subscriptionManager.storePurchaseManager(), - subscriptionEndpointService: subscriptionManager.subscriptionEndpointService, - authEndpointService: subscriptionManager.authEndpointService) - let appStorePurchaseFlow = DefaultAppStorePurchaseFlow(subscriptionEndpointService: subscriptionManager.subscriptionEndpointService, + let appStoreRestoreFlow = DefaultAppStoreRestoreFlow(subscriptionManager: subscriptionManager, + storePurchaseManager: subscriptionManager.storePurchaseManager()) + let appStorePurchaseFlow = DefaultAppStorePurchaseFlow(subscriptionManager: subscriptionManager, storePurchaseManager: subscriptionManager.storePurchaseManager(), - accountManager: subscriptionManager.accountManager, - appStoreRestoreFlow: appStoreRestoreFlow, - authEndpointService: subscriptionManager.authEndpointService) - let appStoreAccountManagementFlow = DefaultAppStoreAccountManagementFlow(authEndpointService: subscriptionManager.authEndpointService, - storePurchaseManager: subscriptionManager.storePurchaseManager(), - accountManager: subscriptionManager.accountManager) + appStoreRestoreFlow: appStoreRestoreFlow) let viewModel = SubscriptionContainerViewModel( subscriptionManager: subscriptionManager, origin: nil, @@ -112,8 +90,7 @@ enum SubscriptionContainerViewFactory { subscriptionFeatureAvailability: subscriptionFeatureAvailability, subscriptionAttributionOrigin: nil, appStorePurchaseFlow: appStorePurchaseFlow, - appStoreRestoreFlow: appStoreRestoreFlow, - appStoreAccountManagementFlow: appStoreAccountManagementFlow) + appStoreRestoreFlow: appStoreRestoreFlow) ) return SubscriptionContainerView(currentView: .email, viewModel: viewModel) .environmentObject(navigationCoordinator) diff --git a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift index 430475bbe4..b01dc7c5ad 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift @@ -189,8 +189,8 @@ struct SubscriptionFlowView: View { title: Text(UserText.subscriptionFoundTitle), message: Text(UserText.subscriptionFoundText), primaryButton: .cancel(Text(UserText.subscriptionFoundCancel)) { - viewModel.clearTransactionError() - dismiss() + viewModel.cancelAppstoreTransaction() + dismiss() }, secondaryButton: .default(Text(UserText.subscriptionFoundRestore)) { viewModel.restoreAppstoreTransaction() diff --git a/DuckDuckGo/Subscription/Views/SubscriptionGoogleView.swift b/DuckDuckGo/Subscription/Views/SubscriptionGoogleView.swift index 06f81ecda7..53b5776158 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionGoogleView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionGoogleView.swift @@ -49,8 +49,6 @@ struct SubscriptionGoogleView: View { } - -#if DEBUG struct SubscriptionGoogleView_Previews: PreviewProvider { static var previews: some View { NavigationView { @@ -58,4 +56,3 @@ struct SubscriptionGoogleView_Previews: PreviewProvider { } } } -#endif diff --git a/DuckDuckGo/SubscriptionDebugViewController.swift b/DuckDuckGo/SubscriptionDebugViewController.swift index 2609faaf88..6f4e3a1b27 100644 --- a/DuckDuckGo/SubscriptionDebugViewController.swift +++ b/DuckDuckGo/SubscriptionDebugViewController.swift @@ -22,6 +22,7 @@ import UIKit import Subscription import Core import NetworkProtection +import Networking import StoreKit import BrowserServicesKit @@ -317,18 +318,25 @@ final class SubscriptionDebugViewController: UITableViewController { } private func clearAuthData() { - subscriptionManager.accountManager.signOut() - showAlert(title: "Data cleared!") + Task { + await subscriptionManager.signOut(notifyUI: true) + showAlert(title: "Data cleared!") + } } private func showAccountDetails() { - let title = subscriptionManager.accountManager.isUserAuthenticated ? "Authenticated" : "Not Authenticated" - let message = subscriptionManager.accountManager.isUserAuthenticated ? - ["Service Environment: \(subscriptionManager.currentEnvironment.serviceEnvironment.description)", - "AuthToken: \(subscriptionManager.accountManager.authToken ?? "")", - "AccessToken: \(subscriptionManager.accountManager.accessToken ?? "")", - "Email: \(subscriptionManager.accountManager.email ?? "")"].joined(separator: "\n") : nil - showAlert(title: title, message: message) + Task { + let tokenContainer = try? await subscriptionManager.getTokenContainer(policy: .local) + let authenticated = tokenContainer != nil + let title = authenticated ? "Authenticated" : "Not Authenticated" + let message = authenticated ? + ["Service Environment: \(subscriptionManager.currentEnvironment.serviceEnvironment)", + "AuthToken: \(tokenContainer?.accessToken ?? "")", + "Email: \(tokenContainer?.decodedAccessToken.email ?? "")"].joined(separator: "\n") : nil + DispatchQueue.main.async { + self.showAlert(title: title, message: message) + } + } } private func showRandomizedParamters() { @@ -376,14 +384,12 @@ final class SubscriptionDebugViewController: UITableViewController { private func validateToken() { Task { - guard let token = subscriptionManager.accountManager.accessToken else { + do { + let tokenContainer = try await subscriptionManager.getTokenContainer(policy: .localValid) + showAlert(title: "Token details", message: "\(tokenContainer.debugDescription)") + } catch OAuthClientError.missingTokens { showAlert(title: "Not authenticated", message: "No authenticated user found! - Token not available") - return - } - switch await subscriptionManager.authEndpointService.validateToken(accessToken: token) { - case .success(let response): - showAlert(title: "Token details", message: "\(response)") - case .failure(let error): + } catch { showAlert(title: "Error Validating Token", message: "\(error)") } } @@ -391,37 +397,28 @@ final class SubscriptionDebugViewController: UITableViewController { private func getSubscriptionDetails() { Task { - guard let token = subscriptionManager.accountManager.accessToken else { - showAlert(title: "Not authenticated", message: "No authenticated user found! - Subscription not available") - return - } - switch await subscriptionManager.subscriptionEndpointService.getSubscription(accessToken: token, - cachePolicy: .reloadIgnoringLocalCacheData) { - case .success(let response): - showAlert(title: "Subscription info", message: "\(response)") - case .failure(let error): - showAlert(title: "Subscription Error", message: "\(error)") + do { + let subscription = try await subscriptionManager.getSubscription(cachePolicy: .reloadIgnoringLocalCacheData) + showAlert(title: "Subscription info", message: subscription.debugDescription) + } catch { + showAlert(title: "Subscription info", message: "\(error)") } } } private func checkEntitlements() { Task { - var results: [String] = [] - guard subscriptionManager.accountManager.accessToken != nil else { - showAlert(title: "Not authenticated", message: "No authenticated user found! - Subscription not available") - return - } - let entitlements: [Entitlement.ProductName] = [.networkProtection, .dataBrokerProtection, .identityTheftRestoration] - for entitlement in entitlements { - if case let .success(result) = await subscriptionManager.accountManager.hasEntitlement(forProductName: entitlement, - cachePolicy: .reloadIgnoringLocalCacheData) { - let resultSummary = "Entitlement check for \(entitlement.rawValue): \(result)" - results.append(resultSummary) - print(resultSummary) - } + do { + let tokenContainer = try await subscriptionManager.getTokenContainer(policy: .localValid) + let entitlementsDescription = tokenContainer.decodedAccessToken.subscriptionEntitlements.map { entitlement in + return entitlement.rawValue + }.joined(separator: "\n") + showAlert(title: "Available Entitlements", message: entitlementsDescription) + } catch OAuthClientError.missingTokens { + showAlert(title: "Not authenticated", message: "No authenticated user found! - Token not available") + } catch { + showAlert(title: "Error retrieving entitlements", message: "\(error)") } - showAlert(title: "Available Entitlements", message: results.joined(separator: "\n")) } } @@ -433,20 +430,22 @@ final class SubscriptionDebugViewController: UITableViewController { newSubscriptionEnvironment.serviceEnvironment = environment if newSubscriptionEnvironment.serviceEnvironment != currentSubscriptionEnvironment.serviceEnvironment { - subscriptionManager.accountManager.signOut() - - // Save Subscription environment - DefaultSubscriptionManager.save(subscriptionEnvironment: newSubscriptionEnvironment, userDefaults: subscriptionUserDefaults) - - // The VPN environment is forced to match the subscription environment - let settings = AppDependencyProvider.shared.vpnSettings - switch newSubscriptionEnvironment.serviceEnvironment { - case .production: - settings.selectedEnvironment = .production - case .staging: - settings.selectedEnvironment = .staging + Task { + await subscriptionManager.signOut(notifyUI: true) + + // Save Subscription environment + DefaultSubscriptionManager.save(subscriptionEnvironment: newSubscriptionEnvironment, userDefaults: subscriptionUserDefaults) + + // The VPN environment is forced to match the subscription environment + let settings = AppDependencyProvider.shared.vpnSettings + switch newSubscriptionEnvironment.serviceEnvironment { + case .production: + settings.selectedEnvironment = .production + case .staging: + settings.selectedEnvironment = .staging + } + NetworkProtectionLocationListCompositeRepository.clearCache() } - NetworkProtectionLocationListCompositeRepository.clearCache() } } diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index 34931e4d01..0e6ecc0ca6 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -1201,7 +1201,7 @@ But if you *do* want a peek under the hood, you can find more information about public static let subscriptionSubscribed = NSLocalizedString("subscription.subscribed", value: "Subscribed", comment: "Subtitle in header when subscribed") public static let subscriptionCloseButton = NSLocalizedString("subscription.close", value: "Close", comment: "Navigation Button for closing subscription view") - static func renewingSubscriptionInfo(billingPeriod: Subscription.BillingPeriod, renewalDate: String) -> String { + static func renewingSubscriptionInfo(billingPeriod: PrivacyProSubscription.BillingPeriod, renewalDate: String) -> String { let localized: String switch billingPeriod { @@ -1222,7 +1222,7 @@ But if you *do* want a peek under the hood, you can find more information about return String(format: localized, renewalDate) } - static func expiringSubscriptionInfo(billingPeriod: Subscription.BillingPeriod, expiryDate: String) -> String { + static func expiringSubscriptionInfo(billingPeriod: PrivacyProSubscription.BillingPeriod, expiryDate: String) -> String { let localized: String switch billingPeriod { diff --git a/DuckDuckGo/VPNRedditSessionWorkaround.swift b/DuckDuckGo/VPNRedditSessionWorkaround.swift index dd1839a9ea..59cb95491f 100644 --- a/DuckDuckGo/VPNRedditSessionWorkaround.swift +++ b/DuckDuckGo/VPNRedditSessionWorkaround.swift @@ -28,11 +28,11 @@ final class VPNRedditSessionWorkaround { @UserDefaultsWrapper(key: .vpnRedditWorkaroundInstalled, defaultValue: false) var vpnWorkaroundInstalled: Bool - private let accountManager: AccountManager + private let subscriptionManager: SubscriptionManager private let tunnelController: TunnelController - init(accountManager: AccountManager, tunnelController: TunnelController) { - self.accountManager = accountManager + init(subscriptionManager: SubscriptionManager, tunnelController: TunnelController) { + self.subscriptionManager = subscriptionManager self.tunnelController = tunnelController } @@ -50,7 +50,7 @@ final class VPNRedditSessionWorkaround { @MainActor func installRedditSessionWorkaround(to cookieStore: WKHTTPCookieStore) async { - guard accountManager.isUserAuthenticated, + guard subscriptionManager.isUserAuthenticated, await tunnelController.isConnected, let redditSessionCookie = HTTPCookie.emptyRedditSession else { return diff --git a/DuckDuckGoTests/AdAttributionFetcherTests.swift b/DuckDuckGoTests/AdAttributionFetcherTests.swift index 28b9c94541..a3296e29d5 100644 --- a/DuckDuckGoTests/AdAttributionFetcherTests.swift +++ b/DuckDuckGoTests/AdAttributionFetcherTests.swift @@ -20,7 +20,7 @@ import XCTest @testable import DuckDuckGo -@testable import TestUtils +import NetworkingTestingUtils final class AdAttributionFetcherTests: XCTestCase { diff --git a/DuckDuckGoTests/AutocompleteSuggestionsDataSourceTests.swift b/DuckDuckGoTests/AutocompleteSuggestionsDataSourceTests.swift index 29ea8e5636..e6b632ed1b 100644 --- a/DuckDuckGoTests/AutocompleteSuggestionsDataSourceTests.swift +++ b/DuckDuckGoTests/AutocompleteSuggestionsDataSourceTests.swift @@ -29,7 +29,7 @@ import History @testable import Core @testable import DuckDuckGo -@testable import TestUtils +import PersistenceTestingUtils final class AutocompleteSuggestionsDataSourceTests: XCTestCase { diff --git a/DuckDuckGoTests/AutofillLoginListViewModelTests.swift b/DuckDuckGoTests/AutofillLoginListViewModelTests.swift index cdc7c8dabd..7364f80ee7 100644 --- a/DuckDuckGoTests/AutofillLoginListViewModelTests.swift +++ b/DuckDuckGoTests/AutofillLoginListViewModelTests.swift @@ -25,7 +25,7 @@ import Combine @testable import Core @testable import BrowserServicesKit @testable import Common -@testable import TestUtils +import PersistenceTestingUtils class AutofillLoginListViewModelTests: XCTestCase { diff --git a/DuckDuckGoTests/AutofillVaultKeychainMigratorTests.swift b/DuckDuckGoTests/AutofillVaultKeychainMigratorTests.swift index 7d5083ad09..88b8089878 100644 --- a/DuckDuckGoTests/AutofillVaultKeychainMigratorTests.swift +++ b/DuckDuckGoTests/AutofillVaultKeychainMigratorTests.swift @@ -20,7 +20,7 @@ import XCTest @testable import Core import BrowserServicesKit -import TestUtils +import PersistenceTestingUtils final class AutofillVaultKeychainMigratorTests: XCTestCase { diff --git a/DuckDuckGoTests/BookmarkStateRepairTests.swift b/DuckDuckGoTests/BookmarkStateRepairTests.swift index 421f4a3f7a..3ae7ff9357 100644 --- a/DuckDuckGoTests/BookmarkStateRepairTests.swift +++ b/DuckDuckGoTests/BookmarkStateRepairTests.swift @@ -20,7 +20,7 @@ import XCTest import CoreData import Bookmarks -import TestUtils +import PersistenceTestingUtils @testable import Core @testable import DuckDuckGo diff --git a/DuckDuckGoTests/BookmarksDatabaseSetupTests.swift b/DuckDuckGoTests/BookmarksDatabaseSetupTests.swift index b7ef842a95..afe1524d75 100644 --- a/DuckDuckGoTests/BookmarksDatabaseSetupTests.swift +++ b/DuckDuckGoTests/BookmarksDatabaseSetupTests.swift @@ -25,7 +25,7 @@ import CoreData import Bookmarks @testable import DuckDuckGo @testable import Core -import TestUtils +import PersistenceTestingUtils class DummyCoreDataStoreMock: CoreDataStoring { diff --git a/DuckDuckGoTests/BookmarksStateValidationTests.swift b/DuckDuckGoTests/BookmarksStateValidationTests.swift index 75d3ff5365..21b8196842 100644 --- a/DuckDuckGoTests/BookmarksStateValidationTests.swift +++ b/DuckDuckGoTests/BookmarksStateValidationTests.swift @@ -20,7 +20,7 @@ import XCTest import CoreData import Bookmarks -import TestUtils +import PersistenceTestingUtils @testable import Core @testable import DuckDuckGo diff --git a/DuckDuckGoTests/BrokenSiteReportingTests.swift b/DuckDuckGoTests/BrokenSiteReportingTests.swift index 708c5d59d8..7029f0989f 100644 --- a/DuckDuckGoTests/BrokenSiteReportingTests.swift +++ b/DuckDuckGoTests/BrokenSiteReportingTests.swift @@ -25,7 +25,7 @@ import OHHTTPStubsSwift @testable import Core import PrivacyDashboard @testable import DuckDuckGo -import TestUtils +import PersistenceTestingUtils final class BrokenSiteReportingTests: XCTestCase { private let data = JsonTestDataLoader() diff --git a/DuckDuckGoTests/DailyPixelTests.swift b/DuckDuckGoTests/DailyPixelTests.swift index 61dd1849e6..56ea3cab0a 100644 --- a/DuckDuckGoTests/DailyPixelTests.swift +++ b/DuckDuckGoTests/DailyPixelTests.swift @@ -19,7 +19,7 @@ import XCTest import Networking -import TestUtils +import PersistenceTestingUtils import Persistence @testable import Core diff --git a/DuckDuckGoTests/FireButtonReferenceTests.swift b/DuckDuckGoTests/FireButtonReferenceTests.swift index 66d1c32688..1129fa41b5 100644 --- a/DuckDuckGoTests/FireButtonReferenceTests.swift +++ b/DuckDuckGoTests/FireButtonReferenceTests.swift @@ -21,7 +21,7 @@ import XCTest import os.log import WebKit @testable import Core -import TestUtils +import PersistenceTestingUtils final class FireButtonReferenceTests: XCTestCase { diff --git a/DuckDuckGoTests/NetworkProtectionFeatureVisibilityTests.swift b/DuckDuckGoTests/NetworkProtectionFeatureVisibilityTests.swift deleted file mode 100644 index 04b77fcfa8..0000000000 --- a/DuckDuckGoTests/NetworkProtectionFeatureVisibilityTests.swift +++ /dev/null @@ -1,93 +0,0 @@ -// -// NetworkProtectionFeatureVisibilityTests.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 XCTest -@testable import DuckDuckGo -import Subscription -import SubscriptionTestingUtilities -import Common - -/// Test all permutations according to https://app.asana.com/0/0/1206812323779606/f -final class NetworkProtectionFeatureVisibilityTests: XCTestCase { - - func testPrivacyProNotYetLaunched() { - // Waitlist beta OFF, not current waitlist user -> Show nothing, use nothing - let mockWithNothing = NetworkProtectionFeatureVisibilityMocks(with: []) - XCTAssertFalse(mockWithNothing.shouldMonitorEntitlement()) - XCTAssertFalse(mockWithNothing.shouldShowVPNShortcut()) - } - - func testPrivacyProLaunched() { - // Waitlist beta OFF, not current waitlist user -> Enforce entitlement check, nothing else - let mockWithNothingElse = NetworkProtectionFeatureVisibilityMocks(with: [.isPrivacyProLaunched]) - XCTAssertTrue(mockWithNothingElse.shouldMonitorEntitlement()) - XCTAssertFalse(mockWithNothingElse.shouldShowVPNShortcut()) - } -} - -struct NetworkProtectionFeatureVisibilityMocks: NetworkProtectionFeatureVisibility { - - let accountManager: AccountManager - - func shouldShowVPNShortcut() -> Bool { - if isPrivacyProLaunched() { - return accountManager.isUserAuthenticated - } else { - return false - } - } - - struct Options: OptionSet { - let rawValue: Int - - static let isPrivacyProLaunched = Options(rawValue: 1 << 0) - } - - let options: Options - - init(with options: Options) { - self.options = options - - let subscriptionAppGroup = "NetworkProtectionFeatureVisibilityTests" - let subscriptionUserDefaults = UserDefaults(suiteName: subscriptionAppGroup)! - let subscriptionEnvironment = DefaultSubscriptionManager.getSavedOrDefaultEnvironment(userDefaults: subscriptionUserDefaults) - let entitlementsCache = UserDefaultsCache<[Entitlement]>(userDefaults: subscriptionUserDefaults, - key: UserDefaultsCacheKey.subscriptionEntitlements, - settings: UserDefaultsCacheSettings(defaultExpirationInterval: .minutes(20))) - let accessTokenStorage = SubscriptionTokenKeychainStorage(keychainType: .dataProtection(.named(subscriptionAppGroup))) - let subscriptionService = DefaultSubscriptionEndpointService(currentServiceEnvironment: subscriptionEnvironment.serviceEnvironment) - let authService = DefaultAuthEndpointService(currentServiceEnvironment: subscriptionEnvironment.serviceEnvironment) - accountManager = DefaultAccountManager(accessTokenStorage: accessTokenStorage, - entitlementsCache: entitlementsCache, - subscriptionEndpointService: subscriptionService, - authEndpointService: authService) - } - - func adding(_ additionalOptions: Options) -> NetworkProtectionFeatureVisibilityMocks { - NetworkProtectionFeatureVisibilityMocks(with: options.union(additionalOptions)) - } - - func isPrivacyProLaunched() -> Bool { - options.contains(.isPrivacyProLaunched) - } - - func shouldMonitorEntitlement() -> Bool { - isPrivacyProLaunched() - } -} diff --git a/DuckDuckGoTests/NetworkProtectionStatusViewModelTests.swift b/DuckDuckGoTests/NetworkProtectionStatusViewModelTests.swift index 231ad293f4..33099f489e 100644 --- a/DuckDuckGoTests/NetworkProtectionStatusViewModelTests.swift +++ b/DuckDuckGoTests/NetworkProtectionStatusViewModelTests.swift @@ -42,13 +42,7 @@ final class NetworkProtectionStatusViewModelTests: XCTestCase { tunnelController = MockTunnelController() statusObserver = MockConnectionStatusObserver() serverInfoObserver = MockConnectionServerInfoObserver() - subscriptionManager = SubscriptionManagerMock(accountManager: AccountManagerMock(), - subscriptionEndpointService: SubscriptionEndpointServiceMock(), - authEndpointService: AuthEndpointServiceMock(), - storePurchaseManager: StorePurchaseManagerMock(), - currentEnvironment: SubscriptionEnvironment(serviceEnvironment: .production, purchasePlatform: .appStore), - canPurchase: true, - subscriptionFeatureMappingCache: SubscriptionFeatureMappingCacheMock()) + subscriptionManager = SubscriptionManagerMock() viewModel = NetworkProtectionStatusViewModel(tunnelController: tunnelController, settings: VPNSettings(defaults: .networkProtectionGroupDefaults), statusObserver: statusObserver, diff --git a/DuckDuckGoTests/PersistentPixelTests.swift b/DuckDuckGoTests/PersistentPixelTests.swift index 1112aa0afc..c0afd90fb4 100644 --- a/DuckDuckGoTests/PersistentPixelTests.swift +++ b/DuckDuckGoTests/PersistentPixelTests.swift @@ -21,7 +21,7 @@ import Foundation import XCTest import Networking import Persistence -import TestUtils +import PersistenceTestingUtils @testable import Core final class PersistentPixelTests: XCTestCase { diff --git a/DuckDuckGoTests/Subscription/FreeTrialsExperiment/SubscriptionPagesUseSubscriptionFeatureFreeTrialsTests.swift b/DuckDuckGoTests/Subscription/FreeTrialsExperiment/SubscriptionPagesUseSubscriptionFeatureFreeTrialsTests.swift index eb85275588..35cc41101a 100644 --- a/DuckDuckGoTests/Subscription/FreeTrialsExperiment/SubscriptionPagesUseSubscriptionFeatureFreeTrialsTests.swift +++ b/DuckDuckGoTests/Subscription/FreeTrialsExperiment/SubscriptionPagesUseSubscriptionFeatureFreeTrialsTests.swift @@ -21,6 +21,8 @@ import XCTest import BrowserServicesKit import SubscriptionTestingUtilities @testable import Subscription +@testable import Networking +import NetworkingTestingUtils @testable import DuckDuckGo @@ -29,21 +31,16 @@ final class SubscriptionPagesUseSubscriptionFeatureFreeTrialsTests: XCTestCase { private var sut: SubscriptionPagesUseSubscriptionFeature! private var mockSubscriptionManager: SubscriptionManagerMock! - private var mockAccountManager: AccountManagerMock! private var mockStorePurchaseManager: StorePurchaseManagerMock! private var mockFreeTrialsFeatureFlagExperiment: MockFreeTrialsFeatureFlagExperiment! private var mockAppStorePurchaseFlow: AppStorePurchaseFlowMock! + let mostRecentTransactionJWS = "dGhpcyBpcyBub3QgYSByZWFsIEFw(...)cCBTdG9yZSB0cmFuc2FjdGlvbiBKV1M=" + override func setUpWithError() throws { - mockAccountManager = AccountManagerMock() mockStorePurchaseManager = StorePurchaseManagerMock() - mockSubscriptionManager = SubscriptionManagerMock(accountManager: mockAccountManager, - subscriptionEndpointService: SubscriptionEndpointServiceMock(), - authEndpointService: AuthEndpointServiceMock(), - storePurchaseManager: mockStorePurchaseManager, - currentEnvironment: SubscriptionEnvironment(serviceEnvironment: .production, purchasePlatform: .appStore), - canPurchase: true, - subscriptionFeatureMappingCache: SubscriptionFeatureMappingCacheMock()) + mockSubscriptionManager = SubscriptionManagerMock() + mockSubscriptionManager.resultStorePurchaseManager = mockStorePurchaseManager mockAppStorePurchaseFlow = AppStorePurchaseFlowMock() mockFreeTrialsFeatureFlagExperiment = MockFreeTrialsFeatureFlagExperiment() @@ -53,13 +50,11 @@ final class SubscriptionPagesUseSubscriptionFeatureFreeTrialsTests: XCTestCase { subscriptionAttributionOrigin: nil, appStorePurchaseFlow: mockAppStorePurchaseFlow, appStoreRestoreFlow: AppStoreRestoreFlowMock(), - appStoreAccountManagementFlow: AppStoreAccountManagementFlowMock(), freeTrialsExperiment: mockFreeTrialsFeatureFlagExperiment) } func testWhenFreeTrialsCohortIsControl_thenStandardSubscriptionOptionsAreReturned() async throws { // Given - mockAccountManager.accessToken = nil mockSubscriptionManager.canPurchase = true mockFreeTrialsFeatureFlagExperiment.cohortToReturn = FreeTrialsFeatureFlagExperiment.Cohort.control mockStorePurchaseManager.subscriptionOptionsResult = .mockStandard @@ -75,7 +70,6 @@ final class SubscriptionPagesUseSubscriptionFeatureFreeTrialsTests: XCTestCase { func testWhenFreeTrialsCohortIsTreatment_thenFreeTrialSubscriptionOptionsAreReturned() async throws { // Given - mockAccountManager.accessToken = nil mockSubscriptionManager.canPurchase = true mockFreeTrialsFeatureFlagExperiment.cohortToReturn = FreeTrialsFeatureFlagExperiment.Cohort.treatment mockStorePurchaseManager.freeTrialSubscriptionOptionsResult = .mockFreeTrial @@ -91,8 +85,8 @@ final class SubscriptionPagesUseSubscriptionFeatureFreeTrialsTests: XCTestCase { func testWhenUserIsAuthenticated_thenStandardSubscriptionOptionsAreReturned() async throws { // Given - mockAccountManager.accessToken = "token" mockSubscriptionManager.canPurchase = true + mockSubscriptionManager.resultTokenContainer = OAuthTokensFactory.makeValidTokenContainerWithEntitlements() mockStorePurchaseManager.subscriptionOptionsResult = .mockStandard // When @@ -106,7 +100,6 @@ final class SubscriptionPagesUseSubscriptionFeatureFreeTrialsTests: XCTestCase { func testWhenUserCannotPurchase_thenStandardSubscriptionOptionsAreReturned() async throws { // Given - mockAccountManager.accessToken = nil mockSubscriptionManager.canPurchase = false mockStorePurchaseManager.subscriptionOptionsResult = .mockStandard @@ -121,7 +114,6 @@ final class SubscriptionPagesUseSubscriptionFeatureFreeTrialsTests: XCTestCase { func testWhenFailedToFetchSubscriptionOptions_thenEmptyOptionsAreReturned() async throws { // Given - mockAccountManager.accessToken = nil mockSubscriptionManager.canPurchase = true mockFreeTrialsFeatureFlagExperiment.cohortToReturn = FreeTrialsFeatureFlagExperiment.Cohort.control mockStorePurchaseManager.subscriptionOptionsResult = nil @@ -136,7 +128,6 @@ final class SubscriptionPagesUseSubscriptionFeatureFreeTrialsTests: XCTestCase { func testWhenFreeTrialsCohortIsTreatmentAndFreeTrialOptionsAreNil_thenFallbackToStandardOptions() async throws { // Given - mockAccountManager.accessToken = nil mockSubscriptionManager.canPurchase = true mockFreeTrialsFeatureFlagExperiment.cohortToReturn = FreeTrialsFeatureFlagExperiment.Cohort.treatment mockStorePurchaseManager.freeTrialSubscriptionOptionsResult = nil @@ -153,10 +144,10 @@ final class SubscriptionPagesUseSubscriptionFeatureFreeTrialsTests: XCTestCase { func testWhenMonthlySubscribeSucceedsForTreatment_thenSubscriptionPurchasedMonthlyPixelFired() async throws { // Given - mockAccountManager.accessToken = nil + mockStorePurchaseManager.purchaseSubscriptionResult = .success(mostRecentTransactionJWS) + mockAppStorePurchaseFlow.purchaseSubscriptionResult = .success(mostRecentTransactionJWS) mockSubscriptionManager.canPurchase = true mockFreeTrialsFeatureFlagExperiment.cohortToReturn = FreeTrialsFeatureFlagExperiment.Cohort.treatment - mockAppStorePurchaseFlow.purchaseSubscriptionResult = .success("") mockAppStorePurchaseFlow.completeSubscriptionPurchaseResult = .success(.completed) let params: [String: Any] = ["id": "monthly-free-trial"] @@ -171,10 +162,9 @@ final class SubscriptionPagesUseSubscriptionFeatureFreeTrialsTests: XCTestCase { func testWhenMonthlySubscribeSucceedsForTreatment_thenSubscriptionPurchasedYearlyPixelFired() async throws { // Given - mockAccountManager.accessToken = nil mockSubscriptionManager.canPurchase = true mockFreeTrialsFeatureFlagExperiment.cohortToReturn = FreeTrialsFeatureFlagExperiment.Cohort.treatment - mockAppStorePurchaseFlow.purchaseSubscriptionResult = .success("") + mockAppStorePurchaseFlow.purchaseSubscriptionResult = .success(mostRecentTransactionJWS) mockAppStorePurchaseFlow.completeSubscriptionPurchaseResult = .success(.completed) let params: [String: Any] = ["id": "yearly-free-trial"] @@ -189,10 +179,9 @@ final class SubscriptionPagesUseSubscriptionFeatureFreeTrialsTests: XCTestCase { func testWhenMonthlySubscribeSucceedsForControl_thenSubscriptionPurchasedMonthlyPixelFired() async throws { // Given - mockAccountManager.accessToken = nil mockSubscriptionManager.canPurchase = true mockFreeTrialsFeatureFlagExperiment.cohortToReturn = FreeTrialsFeatureFlagExperiment.Cohort.control - mockAppStorePurchaseFlow.purchaseSubscriptionResult = .success("") + mockAppStorePurchaseFlow.purchaseSubscriptionResult = .success(mostRecentTransactionJWS) mockAppStorePurchaseFlow.completeSubscriptionPurchaseResult = .success(.completed) let params: [String: Any] = ["id": "monthly-free-trial"] @@ -207,10 +196,9 @@ final class SubscriptionPagesUseSubscriptionFeatureFreeTrialsTests: XCTestCase { func testWhenMonthlySubscribeSucceedsForControl_thenSubscriptionPurchasedYearlyPixelFired() async throws { // Given - mockAccountManager.accessToken = nil mockSubscriptionManager.canPurchase = true mockFreeTrialsFeatureFlagExperiment.cohortToReturn = FreeTrialsFeatureFlagExperiment.Cohort.control - mockAppStorePurchaseFlow.purchaseSubscriptionResult = .success("") + mockAppStorePurchaseFlow.purchaseSubscriptionResult = .success(mostRecentTransactionJWS) mockAppStorePurchaseFlow.completeSubscriptionPurchaseResult = .success(.completed) let params: [String: Any] = ["id": "yearly-free-trial"] @@ -226,28 +214,28 @@ final class SubscriptionPagesUseSubscriptionFeatureFreeTrialsTests: XCTestCase { private extension SubscriptionOptions { static let mockStandard = SubscriptionOptions(platform: .ios, + options: [ + SubscriptionOption(id: "1", + cost: SubscriptionOptionCost(displayPrice: "9", recurrence: "monthly")), + SubscriptionOption(id: "2", + cost: SubscriptionOptionCost(displayPrice: "99", recurrence: "yearly")) + ], + availableEntitlements: [ + SubscriptionEntitlement.networkProtection, + SubscriptionEntitlement.dataBrokerProtection, + SubscriptionEntitlement.identityTheftRestoration + ]) + + static let mockFreeTrial = SubscriptionOptions(platform: .ios, options: [ - SubscriptionOption(id: "1", - cost: SubscriptionOptionCost(displayPrice: "9", recurrence: "monthly")), - SubscriptionOption(id: "2", - cost: SubscriptionOptionCost(displayPrice: "99", recurrence: "yearly")) + SubscriptionOption(id: "3", + cost: SubscriptionOptionCost(displayPrice: "0", recurrence: "monthly-free-trial"), offer: .init(type: .freeTrial, id: "1", durationInDays: 4, isUserEligible: true)), + SubscriptionOption(id: "4", + cost: SubscriptionOptionCost(displayPrice: "0", recurrence: "yearly-free-trial"), offer: .init(type: .freeTrial, id: "1", durationInDays: 4, isUserEligible: true)) ], - features: [ - SubscriptionFeature(name: .networkProtection), - SubscriptionFeature(name: .dataBrokerProtection), - SubscriptionFeature(name: .identityTheftRestoration) + availableEntitlements: [ + SubscriptionEntitlement.networkProtection ]) - - static let mockFreeTrial = SubscriptionOptions(platform: .ios, - options: [ - SubscriptionOption(id: "3", - cost: SubscriptionOptionCost(displayPrice: "0", recurrence: "monthly-free-trial"), offer: .init(type: .freeTrial, id: "1", durationInDays: 4, isUserEligible: true)), - SubscriptionOption(id: "4", - cost: SubscriptionOptionCost(displayPrice: "0", recurrence: "yearly-free-trial"), offer: .init(type: .freeTrial, id: "1", durationInDays: 4, isUserEligible: true)) - ], - features: [ - SubscriptionFeature(name: .networkProtection) - ]) } private final class MockFreeTrialsFeatureFlagExperiment: FreeTrialsFeatureFlagExperimenting { diff --git a/DuckDuckGoTests/Subscription/SubscriptionContainerViewModelTests.swift b/DuckDuckGoTests/Subscription/SubscriptionContainerViewModelTests.swift index 7efea415d4..0af78b1874 100644 --- a/DuckDuckGoTests/Subscription/SubscriptionContainerViewModelTests.swift +++ b/DuckDuckGoTests/Subscription/SubscriptionContainerViewModelTests.swift @@ -24,23 +24,7 @@ import SubscriptionTestingUtilities final class SubscriptionContainerViewModelTests: XCTestCase { var sut: SubscriptionContainerViewModel! - - let subscriptionManager: SubscriptionManager = { - let accountManager = AccountManagerMock() - let subscriptionService = SubscriptionEndpointServiceMock() - let authService = AuthEndpointServiceMock() - let storePurchaseManager = StorePurchaseManagerMock() - let subscriptionFeatureMappingCache = SubscriptionFeatureMappingCacheMock() - return SubscriptionManagerMock(accountManager: accountManager, - subscriptionEndpointService: subscriptionService, - authEndpointService: authService, - storePurchaseManager: storePurchaseManager, - currentEnvironment: SubscriptionEnvironment(serviceEnvironment: .production, - purchasePlatform: .appStore), - canPurchase: true, - subscriptionFeatureMappingCache: subscriptionFeatureMappingCache) - }() - + let subscriptionManager = SubscriptionManagerMock() let subscriptionFeatureAvailability = SubscriptionFeatureAvailabilityMock.enabled func testWhenInitWithOriginThenSubscriptionFlowPurchaseURLHasOriginSet() { @@ -48,19 +32,13 @@ final class SubscriptionContainerViewModelTests: XCTestCase { let origin = "test_origin" let queryParameter = URLQueryItem(name: "origin", value: "test_origin") let expectedURL = SubscriptionURL.purchase.subscriptionURL(environment: .production).appending(percentEncodedQueryItem: queryParameter) - let appStoreRestoreFlow = DefaultAppStoreRestoreFlow(accountManager: subscriptionManager.accountManager, - storePurchaseManager: subscriptionManager.storePurchaseManager(), - subscriptionEndpointService: subscriptionManager.subscriptionEndpointService, - authEndpointService: subscriptionManager.authEndpointService) - let appStorePurchaseFlow = DefaultAppStorePurchaseFlow(subscriptionEndpointService: subscriptionManager.subscriptionEndpointService, - storePurchaseManager: subscriptionManager.storePurchaseManager(), - accountManager: subscriptionManager.accountManager, - appStoreRestoreFlow: appStoreRestoreFlow, - authEndpointService: subscriptionManager.authEndpointService) - let appStoreAccountManagementFlow = DefaultAppStoreAccountManagementFlow(authEndpointService: subscriptionManager.authEndpointService, - storePurchaseManager: subscriptionManager.storePurchaseManager(), - accountManager: subscriptionManager.accountManager) - + let storePurchaseManager = DefaultStorePurchaseManager(subscriptionFeatureMappingCache: SubscriptionFeatureMappingCacheMock()) + let appStoreRestoreFlow = DefaultAppStoreRestoreFlow(subscriptionManager: subscriptionManager, + storePurchaseManager: storePurchaseManager) + let appStorePurchaseFlow = DefaultAppStorePurchaseFlow(subscriptionManager: subscriptionManager, + storePurchaseManager: storePurchaseManager, + appStoreRestoreFlow: appStoreRestoreFlow) + subscriptionManager.resultURL = SubscriptionURL.purchase.subscriptionURL(environment: .production) // URL(string: "https://duckduckgo.com") // WHEN sut = .init(subscriptionManager: subscriptionManager, origin: origin, @@ -69,27 +47,20 @@ final class SubscriptionContainerViewModelTests: XCTestCase { subscriptionFeatureAvailability: subscriptionFeatureAvailability, subscriptionAttributionOrigin: nil, appStorePurchaseFlow: appStorePurchaseFlow, - appStoreRestoreFlow: appStoreRestoreFlow, - appStoreAccountManagementFlow: appStoreAccountManagementFlow)) + appStoreRestoreFlow: appStoreRestoreFlow)) // THEN XCTAssertEqual(sut.flow.purchaseURL, expectedURL) } func testWhenInitWithoutOriginThenSubscriptionFlowPurchaseURLDoesNotHaveOriginSet() { - let appStoreRestoreFlow = DefaultAppStoreRestoreFlow(accountManager: subscriptionManager.accountManager, - storePurchaseManager: subscriptionManager.storePurchaseManager(), - subscriptionEndpointService: subscriptionManager.subscriptionEndpointService, - authEndpointService: subscriptionManager.authEndpointService) - let appStorePurchaseFlow = DefaultAppStorePurchaseFlow(subscriptionEndpointService: subscriptionManager.subscriptionEndpointService, - storePurchaseManager: subscriptionManager.storePurchaseManager(), - accountManager: subscriptionManager.accountManager, - appStoreRestoreFlow: appStoreRestoreFlow, - authEndpointService: subscriptionManager.authEndpointService) - let appStoreAccountManagementFlow = DefaultAppStoreAccountManagementFlow(authEndpointService: subscriptionManager.authEndpointService, - storePurchaseManager: subscriptionManager.storePurchaseManager(), - accountManager: subscriptionManager.accountManager) - + let storePurchaseManager = DefaultStorePurchaseManager(subscriptionFeatureMappingCache: SubscriptionFeatureMappingCacheMock()) + let appStoreRestoreFlow = DefaultAppStoreRestoreFlow(subscriptionManager: subscriptionManager, + storePurchaseManager: storePurchaseManager) + let appStorePurchaseFlow = DefaultAppStorePurchaseFlow(subscriptionManager: subscriptionManager, + storePurchaseManager: storePurchaseManager, + appStoreRestoreFlow: appStoreRestoreFlow) + subscriptionManager.resultURL = SubscriptionURL.purchase.subscriptionURL(environment: .production) // WHEN sut = .init(subscriptionManager: subscriptionManager, origin: nil, @@ -98,8 +69,7 @@ final class SubscriptionContainerViewModelTests: XCTestCase { subscriptionFeatureAvailability: subscriptionFeatureAvailability, subscriptionAttributionOrigin: nil, appStorePurchaseFlow: appStorePurchaseFlow, - appStoreRestoreFlow: appStoreRestoreFlow, - appStoreAccountManagementFlow: appStoreAccountManagementFlow)) + appStoreRestoreFlow: appStoreRestoreFlow)) // THEN XCTAssertEqual(sut.flow.purchaseURL, SubscriptionURL.purchase.subscriptionURL(environment: .production)) diff --git a/DuckDuckGoTests/Subscription/SubscriptionFlowViewModelTests.swift b/DuckDuckGoTests/Subscription/SubscriptionFlowViewModelTests.swift index cc114e794b..e4a0959f43 100644 --- a/DuckDuckGoTests/Subscription/SubscriptionFlowViewModelTests.swift +++ b/DuckDuckGoTests/Subscription/SubscriptionFlowViewModelTests.swift @@ -24,23 +24,8 @@ import SubscriptionTestingUtilities final class SubscriptionFlowViewModelTests: XCTestCase { private var sut: SubscriptionFlowViewModel! - - let subscriptionManager: SubscriptionManager = { - let accountManager = AccountManagerMock() - let subscriptionService = DefaultSubscriptionEndpointService(currentServiceEnvironment: .production) - let authService = DefaultAuthEndpointService(currentServiceEnvironment: .production) - let subscriptionFeatureMappingCache = SubscriptionFeatureMappingCacheMock() - let storePurchaseManager = DefaultStorePurchaseManager(subscriptionFeatureMappingCache: subscriptionFeatureMappingCache) - return SubscriptionManagerMock(accountManager: accountManager, - subscriptionEndpointService: subscriptionService, - authEndpointService: authService, - storePurchaseManager: storePurchaseManager, - currentEnvironment: SubscriptionEnvironment(serviceEnvironment: .production, - purchasePlatform: .appStore), - canPurchase: true, - subscriptionFeatureMappingCache: subscriptionFeatureMappingCache) - }() + let subscriptionManager = SubscriptionManagerMock() let subscriptionFeatureAvailability = SubscriptionFeatureAvailabilityMock.enabled func testWhenInitWithOriginThenSubscriptionFlowPurchaseURLHasOriginSet() { @@ -48,26 +33,19 @@ final class SubscriptionFlowViewModelTests: XCTestCase { let origin = "test_origin" let queryParameter = URLQueryItem(name: "origin", value: "test_origin") let expectedURL = SubscriptionURL.purchase.subscriptionURL(environment: .production).appending(percentEncodedQueryItem: queryParameter) - let appStoreRestoreFlow = DefaultAppStoreRestoreFlow(accountManager: subscriptionManager.accountManager, - storePurchaseManager: subscriptionManager.storePurchaseManager(), - subscriptionEndpointService: subscriptionManager.subscriptionEndpointService, - authEndpointService: subscriptionManager.authEndpointService) - let appStorePurchaseFlow = DefaultAppStorePurchaseFlow(subscriptionEndpointService: subscriptionManager.subscriptionEndpointService, - storePurchaseManager: subscriptionManager.storePurchaseManager(), - accountManager: subscriptionManager.accountManager, - appStoreRestoreFlow: appStoreRestoreFlow, - authEndpointService: subscriptionManager.authEndpointService) - let appStoreAccountManagementFlow = DefaultAppStoreAccountManagementFlow(authEndpointService: subscriptionManager.authEndpointService, - storePurchaseManager: subscriptionManager.storePurchaseManager(), - accountManager: subscriptionManager.accountManager) - + let storePurchaseManager = DefaultStorePurchaseManager(subscriptionFeatureMappingCache: SubscriptionFeatureMappingCacheMock()) + let appStoreRestoreFlow = DefaultAppStoreRestoreFlow(subscriptionManager: subscriptionManager, + storePurchaseManager: storePurchaseManager) + let appStorePurchaseFlow = DefaultAppStorePurchaseFlow(subscriptionManager: subscriptionManager, + storePurchaseManager: storePurchaseManager, + appStoreRestoreFlow: appStoreRestoreFlow) + subscriptionManager.resultURL = SubscriptionURL.purchase.subscriptionURL(environment: .production) // WHEN sut = .init(origin: origin, userScript: .init(), subFeature: .init(subscriptionManager: subscriptionManager, subscriptionFeatureAvailability: subscriptionFeatureAvailability, subscriptionAttributionOrigin: nil, appStorePurchaseFlow: appStorePurchaseFlow, - appStoreRestoreFlow: appStoreRestoreFlow, - appStoreAccountManagementFlow: appStoreAccountManagementFlow), + appStoreRestoreFlow: appStoreRestoreFlow), subscriptionManager: subscriptionManager) // THEN @@ -75,26 +53,19 @@ final class SubscriptionFlowViewModelTests: XCTestCase { } func testWhenInitWithoutOriginThenSubscriptionFlowPurchaseURLDoesNotHaveOriginSet() { - let appStoreRestoreFlow = DefaultAppStoreRestoreFlow(accountManager: subscriptionManager.accountManager, - storePurchaseManager: subscriptionManager.storePurchaseManager(), - subscriptionEndpointService: subscriptionManager.subscriptionEndpointService, - authEndpointService: subscriptionManager.authEndpointService) - let appStorePurchaseFlow = DefaultAppStorePurchaseFlow(subscriptionEndpointService: subscriptionManager.subscriptionEndpointService, - storePurchaseManager: subscriptionManager.storePurchaseManager(), - accountManager: subscriptionManager.accountManager, - appStoreRestoreFlow: appStoreRestoreFlow, - authEndpointService: subscriptionManager.authEndpointService) - let appStoreAccountManagementFlow = DefaultAppStoreAccountManagementFlow(authEndpointService: subscriptionManager.authEndpointService, - storePurchaseManager: subscriptionManager.storePurchaseManager(), - accountManager: subscriptionManager.accountManager) - + let storePurchaseManager = DefaultStorePurchaseManager(subscriptionFeatureMappingCache: SubscriptionFeatureMappingCacheMock()) + let appStoreRestoreFlow = DefaultAppStoreRestoreFlow(subscriptionManager: subscriptionManager, + storePurchaseManager: storePurchaseManager) + let appStorePurchaseFlow = DefaultAppStorePurchaseFlow(subscriptionManager: subscriptionManager, + storePurchaseManager: storePurchaseManager, + appStoreRestoreFlow: appStoreRestoreFlow) + subscriptionManager.resultURL = SubscriptionURL.purchase.subscriptionURL(environment: .production) // WHEN sut = .init(origin: nil, userScript: .init(), subFeature: .init(subscriptionManager: subscriptionManager, subscriptionFeatureAvailability: subscriptionFeatureAvailability, subscriptionAttributionOrigin: nil, appStorePurchaseFlow: appStorePurchaseFlow, - appStoreRestoreFlow: appStoreRestoreFlow, - appStoreAccountManagementFlow: appStoreAccountManagementFlow), + appStoreRestoreFlow: appStoreRestoreFlow), subscriptionManager: subscriptionManager) // THEN diff --git a/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift b/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift index 434821cb4a..04a3e0bd9f 100644 --- a/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift +++ b/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift @@ -28,6 +28,9 @@ import BrowserServicesKit import OHHTTPStubs import OHHTTPStubsSwift import os.log +import Networking +import NetworkingTestingUtils +import PersistenceTestingUtils final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { @@ -40,9 +43,9 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { static let email = "dax@duck.com" - static let entitlements = [Entitlement(product: .dataBrokerProtection), - Entitlement(product: .identityTheftRestoration), - Entitlement(product: .networkProtection)] + static let entitlements: [SubscriptionEntitlement] = [.dataBrokerProtection, + .identityTheftRestoration, + .networkProtection] static let mostRecentTransactionJWS = "dGhpcyBpcyBub3QgYSByZWFsIEFw(...)cCBTdG9yZSB0cmFuc2FjdGlvbiBKV1M=" @@ -53,42 +56,25 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { SubscriptionOption(id: "2", cost: SubscriptionOptionCost(displayPrice: "99 USD", recurrence: "yearly")) ], - features: [ - SubscriptionFeature(name: .networkProtection), - SubscriptionFeature(name: .dataBrokerProtection), - SubscriptionFeature(name: .identityTheftRestoration) - ]) - - static let validateTokenResponse = ValidateTokenResponse(account: ValidateTokenResponse.Account(email: Constants.email, - entitlements: Constants.entitlements, - externalID: Constants.externalID)) + availableEntitlements: [.networkProtection, .dataBrokerProtection, .identityTheftRestoration]) static let mockParams: [String: String] = [:] @MainActor static let mockScriptMessage = MockWKScriptMessage(name: "", body: "", webView: WKWebView() ) - - static let invalidTokenError = APIServiceError.serverError(statusCode: 401, error: "invalid_token") } var userDefaults: UserDefaults! - var accountStorage: AccountKeychainStorageMock! - var accessTokenStorage: SubscriptionTokenKeychainStorageMock! - var entitlementsCache: UserDefaultsCache<[Entitlement]>! - - var subscriptionService: SubscriptionEndpointServiceMock! - var authService: AuthEndpointServiceMock! +// var subscriptionService: SubscriptionEndpointServiceMock! var storePurchaseManager: StorePurchaseManagerMock! var subscriptionEnvironment: SubscriptionEnvironment! var subscriptionFeatureMappingCache: SubscriptionFeatureMappingCacheMock! + var subscriptionFeatureFlagger: FeatureFlaggerMapping! var appStorePurchaseFlow: AppStorePurchaseFlow! var appStoreRestoreFlow: AppStoreRestoreFlow! - var appStoreAccountManagementFlow: AppStoreAccountManagementFlow! - - var accountManager: AccountManager! - var subscriptionManager: SubscriptionManager! + var subscriptionManager: SubscriptionManagerMock! var subscriptionFeatureAvailability = SubscriptionFeatureAvailabilityMock.enabled var feature: SubscriptionPagesUseSubscriptionFeature! @@ -116,89 +102,36 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { } // Mocks - subscriptionService = SubscriptionEndpointServiceMock() - authService = AuthEndpointServiceMock() - storePurchaseManager = StorePurchaseManagerMock() subscriptionEnvironment = SubscriptionEnvironment(serviceEnvironment: .production, purchasePlatform: .appStore) - accountStorage = AccountKeychainStorageMock() - accessTokenStorage = SubscriptionTokenKeychainStorageMock() - userDefaults = UserDefaults(suiteName: Constants.userDefaultsSuiteName)! userDefaults.removePersistentDomain(forName: Constants.userDefaultsSuiteName) - entitlementsCache = UserDefaultsCache<[Entitlement]>(userDefaults: userDefaults, - key: UserDefaultsCacheKey.subscriptionEntitlements, - settings: UserDefaultsCacheSettings(defaultExpirationInterval: .minutes(20))) - - subscriptionFeatureMappingCache = SubscriptionFeatureMappingCacheMock() - - // Real AccountManager - accountManager = DefaultAccountManager(storage: accountStorage, - accessTokenStorage: accessTokenStorage, - entitlementsCache: entitlementsCache, - subscriptionEndpointService: subscriptionService, - authEndpointService: authService) - + subscriptionManager = SubscriptionManagerMock() // Real Flows - appStoreRestoreFlow = DefaultAppStoreRestoreFlow(accountManager: accountManager, - storePurchaseManager: storePurchaseManager, - subscriptionEndpointService: subscriptionService, - authEndpointService: authService) - - appStorePurchaseFlow = DefaultAppStorePurchaseFlow(subscriptionEndpointService: subscriptionService, + appStoreRestoreFlow = DefaultAppStoreRestoreFlow(subscriptionManager: subscriptionManager, + storePurchaseManager: storePurchaseManager) + appStorePurchaseFlow = DefaultAppStorePurchaseFlow(subscriptionManager: subscriptionManager, storePurchaseManager: storePurchaseManager, - accountManager: accountManager, - appStoreRestoreFlow: appStoreRestoreFlow, - authEndpointService: authService) - - appStoreAccountManagementFlow = DefaultAppStoreAccountManagementFlow(authEndpointService: authService, - storePurchaseManager: storePurchaseManager, - accountManager: accountManager) - // Real SubscriptionManager - subscriptionManager = DefaultSubscriptionManager(storePurchaseManager: storePurchaseManager, - accountManager: accountManager, - subscriptionEndpointService: subscriptionService, - authEndpointService: authService, - subscriptionFeatureMappingCache: subscriptionFeatureMappingCache, - subscriptionEnvironment: subscriptionEnvironment) - + appStoreRestoreFlow: appStoreRestoreFlow) feature = SubscriptionPagesUseSubscriptionFeature(subscriptionManager: subscriptionManager, subscriptionFeatureAvailability: subscriptionFeatureAvailability, subscriptionAttributionOrigin: nil, appStorePurchaseFlow: appStorePurchaseFlow, - appStoreRestoreFlow: appStoreRestoreFlow, - appStoreAccountManagementFlow: appStoreAccountManagementFlow) + appStoreRestoreFlow: appStoreRestoreFlow) } override func tearDownWithError() throws { Pixel.isDryRun = true pixelsFired.removeAll() HTTPStubs.removeAllStubs() - - subscriptionService = nil - authService = nil storePurchaseManager = nil subscriptionEnvironment = nil - userDefaults = nil - - accountStorage = nil - accessTokenStorage = nil - - entitlementsCache?.reset() - entitlementsCache = nil - - accountManager = nil - - // Real Flows appStorePurchaseFlow = nil appStoreRestoreFlow = nil - appStoreAccountManagementFlow = nil - subscriptionManager = nil - feature = nil } @@ -207,45 +140,16 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { func testGetSubscriptionSuccessRefreshingAuthToken() async throws { // Given ensureUserAuthenticatedState() + subscriptionManager.resultTokenContainer = OAuthTokensFactory.makeValidTokenContainerWithEntitlements() - let newAuthToken = UUID().uuidString - - authService.validateTokenResult = .failure(Constants.invalidTokenError) storePurchaseManager.mostRecentTransactionResult = Constants.mostRecentTransactionJWS - authService.storeLoginResult = .success(StoreLoginResponse(authToken: newAuthToken, - email: Constants.email, - externalID: Constants.externalID, - id: 1, status: "authenticated")) - // When let result = await feature.getSubscription(params: Constants.mockParams, original: Constants.mockScriptMessage) // Then let resultDictionary = try XCTUnwrap(result as? [String: String]) - XCTAssertEqual(resultDictionary[SubscriptionPagesUseSubscriptionFeature.Constants.token], newAuthToken) - XCTAssertEqual(accountManager.authToken, newAuthToken) - - XCTAssertEqual(feature.transactionStatus, .idle) - XCTAssertEqual(feature.transactionError, nil) - - await XCTAssertPrivacyPixelsFired([]) - } - - func testGetSubscriptionSuccessWithoutRefreshingAuthToken() async throws { - // Given - ensureUserAuthenticatedState() - - authService.validateTokenResult = .success(Constants.validateTokenResponse) - - // When - let result = await feature.getSubscription(params: Constants.mockParams, original: Constants.mockScriptMessage) - - // Then - let resultDictionary = try XCTUnwrap(result as? [String: String]) - - XCTAssertEqual(resultDictionary[SubscriptionPagesUseSubscriptionFeature.Constants.token], Constants.authToken) - XCTAssertEqual(accountManager.authToken, Constants.authToken) + XCTAssertEqual(resultDictionary[SubscriptionPagesUseSubscriptionFeature.Constants.token], subscriptionManager.resultTokenContainer?.accessToken) XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, nil) @@ -256,10 +160,8 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { func testGetSubscriptionSuccessErrorWhenUnauthenticated() async throws { // Given ensureUserUnauthenticatedState() - - authService.validateTokenResult = .failure(Constants.invalidTokenError) storePurchaseManager.mostRecentTransactionResult = nil - + subscriptionManager.resultTokenContainer = nil // When let result = await feature.getSubscription(params: Constants.mockParams, original: Constants.mockScriptMessage) @@ -267,7 +169,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { let resultDictionary = try XCTUnwrap(result as? [String: String]) XCTAssertEqual(resultDictionary[SubscriptionPagesUseSubscriptionFeature.Constants.token], SubscriptionPagesUseSubscriptionFeature.Constants.empty) - XCTAssertFalse(accountManager.isUserAuthenticated) + XCTAssertFalse(subscriptionManager.isUserAuthenticated) XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, nil) @@ -323,8 +225,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { subscriptionFeatureAvailability: subscriptionFeatureAvailabilityWithoutPurchaseAllowed, subscriptionAttributionOrigin: nil, appStorePurchaseFlow: appStorePurchaseFlow, - appStoreRestoreFlow: appStoreRestoreFlow, - appStoreAccountManagementFlow: appStoreAccountManagementFlow) + appStoreRestoreFlow: appStoreRestoreFlow) storePurchaseManager.subscriptionOptionsResult = Constants.subscriptionOptions @@ -347,20 +248,21 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { // Given ensureUserUnauthenticatedState() - XCTAssertFalse(accountManager.isUserAuthenticated) + XCTAssertFalse(subscriptionManager.isUserAuthenticated) storePurchaseManager.hasActiveSubscriptionResult = false storePurchaseManager.mostRecentTransactionResult = nil - authService.createAccountResult = .success(CreateAccountResponse(authToken: Constants.authToken, - externalID: Constants.externalID, - status: "created")) - authService.getAccessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) - authService.validateTokenResult = .success(Constants.validateTokenResponse) +// authService.createAccountResult = .success(CreateAccountResponse(authToken: Constants.authToken, +// externalID: Constants.externalID, +// status: "created")) +// authService.getAccessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) +// authService.validateTokenResult = .success(Constants.validateTokenResponse) storePurchaseManager.purchaseSubscriptionResult = .success(Constants.mostRecentTransactionJWS) - subscriptionService.confirmPurchaseResult = .success(ConfirmPurchaseResponse(email: Constants.email, - entitlements: Constants.entitlements, - subscription: SubscriptionMockFactory.subscription)) +// subscriptionService.confirmPurchaseResult = .success(ConfirmPurchaseResponse(email: Constants.email, +// subscription: SubscriptionMockFactory.subscription)) + subscriptionManager.resultSubscription = SubscriptionMockFactory.subscription + subscriptionManager.resultTokenContainer = OAuthTokensFactory.makeValidTokenContainerWithEntitlements() // When let subscriptionSelectedParams = ["id": "some-subscription-id"] @@ -384,30 +286,32 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { // Given ensureUserAuthenticatedState() - XCTAssertTrue(accountManager.isUserAuthenticated) + XCTAssertTrue(subscriptionManager.isUserAuthenticated) storePurchaseManager.hasActiveSubscriptionResult = false storePurchaseManager.mostRecentTransactionResult = Constants.mostRecentTransactionJWS - subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredSubscription) - - authService.storeLoginResult = .success(StoreLoginResponse(authToken: Constants.authToken, - email: Constants.email, - externalID: Constants.externalID, - id: 1, - status: "authenticated")) - authService.getAccessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) - authService.validateTokenResult = .success(Constants.validateTokenResponse) +// subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredSubscription) + subscriptionManager.resultSubscription = SubscriptionMockFactory.expiredSubscription + +// authService.storeLoginResult = .success(StoreLoginResponse(authToken: Constants.authToken, +// email: Constants.email, +// externalID: Constants.externalID, +// id: 1, +// status: "authenticated")) +// authService.getAccessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) +// authService.validateTokenResult = .success(Constants.validateTokenResponse) + subscriptionManager.resultTokenContainer = OAuthTokensFactory.makeValidTokenContainerWithEntitlements() + storePurchaseManager.purchaseSubscriptionResult = .success(Constants.mostRecentTransactionJWS) - subscriptionService.confirmPurchaseResult = .success(ConfirmPurchaseResponse(email: Constants.email, - entitlements: Constants.entitlements, - subscription: SubscriptionMockFactory.subscription)) +// subscriptionService.confirmPurchaseResult = .success(ConfirmPurchaseResponse(email: Constants.email, +// subscription: SubscriptionMockFactory.subscription)) // When let subscriptionSelectedParams = ["id": "some-subscription-id"] let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) // Then - XCTAssertFalse(authService.createAccountCalled) +// XCTAssertFalse(authService.createAccountCalled) XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) XCTAssertNil(result) @@ -427,21 +331,21 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { // Given ensureUserAuthenticatedState() - XCTAssertTrue(accountManager.isUserAuthenticated) + XCTAssertTrue(subscriptionManager.isUserAuthenticated) storePurchaseManager.hasActiveSubscriptionResult = false - subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) +// subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) + subscriptionManager.resultSubscription = SubscriptionMockFactory.expiredStripeSubscription storePurchaseManager.purchaseSubscriptionResult = .success(Constants.mostRecentTransactionJWS) - subscriptionService.confirmPurchaseResult = .success(ConfirmPurchaseResponse(email: Constants.email, - entitlements: Constants.entitlements, - subscription: SubscriptionMockFactory.subscription)) +// subscriptionService.confirmPurchaseResult = .success(ConfirmPurchaseResponse(email: Constants.email, +// subscription: SubscriptionMockFactory.subscription)) // When let subscriptionSelectedParams = ["id": "some-subscription-id"] let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) // Then - XCTAssertFalse(authService.createAccountCalled) +// XCTAssertFalse(authService.createAccountCalled) XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) XCTAssertNil(result) @@ -509,7 +413,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { storePurchaseManager.hasActiveSubscriptionResult = false storePurchaseManager.mostRecentTransactionResult = nil - authService.createAccountResult = .failure(Constants.invalidTokenError) +// authService.createAccountResult = .failure(Constants.invalidTokenError) // When let subscriptionSelectedParams = ["id": "some-subscription-id"] @@ -532,7 +436,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { ensureUserAuthenticatedState() storePurchaseManager.hasActiveSubscriptionResult = false - subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) +// subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.purchaseCancelledByUser) // When @@ -556,7 +460,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { ensureUserAuthenticatedState() storePurchaseManager.hasActiveSubscriptionResult = false - subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) +// subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.productNotFound) // When @@ -580,7 +484,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { ensureUserAuthenticatedState() storePurchaseManager.hasActiveSubscriptionResult = false - subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) +// subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.externalIDisNotAValidUUID) // When @@ -604,7 +508,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { ensureUserAuthenticatedState() storePurchaseManager.hasActiveSubscriptionResult = false - subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) +// subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.purchaseFailed) // When @@ -628,7 +532,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { ensureUserAuthenticatedState() storePurchaseManager.hasActiveSubscriptionResult = false - subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) +// subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.transactionCannotBeVerified) // When @@ -652,7 +556,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { ensureUserAuthenticatedState() storePurchaseManager.hasActiveSubscriptionResult = false - subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) +// subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.transactionPendingAuthentication) // When @@ -676,7 +580,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { ensureUserAuthenticatedState() storePurchaseManager.hasActiveSubscriptionResult = false - subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) +// subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.unknownError) // When @@ -701,8 +605,10 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { // Given ensureUserUnauthenticatedState() - authService.getAccessTokenResult = .success(.init(accessToken: Constants.accessToken)) - authService.validateTokenResult = .success(Constants.validateTokenResponse) +// authService.getAccessTokenResult = .success(.init(accessToken: Constants.accessToken)) +// authService.validateTokenResult = .success(Constants.validateTokenResponse) + subscriptionManager.resultSubscription = SubscriptionMockFactory.subscription + subscriptionManager.resultExchangeTokenContainer = OAuthTokensFactory.makeValidTokenContainerWithEntitlements() let onSetSubscriptionCalled = expectation(description: "onSetSubscription") feature.onSetSubscription = { @@ -713,11 +619,9 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { let setSubscriptionParams = ["token": Constants.authToken] let result = await feature.setSubscription(params: setSubscriptionParams, original: Constants.mockScriptMessage) + let tokens = try await subscriptionManager.getTokenContainer(policy: .local) // Then - XCTAssertEqual(accountManager.authToken, Constants.authToken) - XCTAssertEqual(accountManager.accessToken, Constants.accessToken) - XCTAssertEqual(accountManager.email, Constants.email) - XCTAssertEqual(accountManager.externalID, Constants.externalID) + XCTAssertEqual(tokens, subscriptionManager.resultExchangeTokenContainer) await fulfillment(of: [onSetSubscriptionCalled], timeout: 0.5) XCTAssertNil(result) @@ -732,37 +636,8 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { // Given ensureUserUnauthenticatedState() - authService.getAccessTokenResult = .failure(Constants.invalidTokenError) - - let onSetSubscriptionCalled = expectation(description: "onSetSubscription") - onSetSubscriptionCalled.isInverted = true - feature.onSetSubscription = { - onSetSubscriptionCalled.fulfill() - } - - // When - let setSubscriptionParams = ["token": Constants.authToken] - let result = await feature.setSubscription(params: setSubscriptionParams, original: Constants.mockScriptMessage) - - // Then - XCTAssertNil(accountManager.authToken) - XCTAssertFalse(accountManager.isUserAuthenticated) - - await fulfillment(of: [onSetSubscriptionCalled], timeout: 0.5) - XCTAssertNil(result) - - XCTAssertEqual(feature.transactionStatus, .idle) - XCTAssertEqual(feature.transactionError, .failedToSetSubscription) - - await XCTAssertPrivacyPixelsFired([]) - } - - func testSetSubscriptionErrorWhenFailedToFetchAccountDetails() async throws { - // Given - ensureUserUnauthenticatedState() - - authService.getAccessTokenResult = .success(.init(accessToken: Constants.accessToken)) - authService.validateTokenResult = .failure(Constants.invalidTokenError) +// subscriptionManager.resultSubscription = SubscriptionMockFactory.subscription + subscriptionManager.resultExchangeTokenContainer = nil let onSetSubscriptionCalled = expectation(description: "onSetSubscription") onSetSubscriptionCalled.isInverted = true @@ -775,8 +650,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { let result = await feature.setSubscription(params: setSubscriptionParams, original: Constants.mockScriptMessage) // Then - XCTAssertNil(accountManager.authToken) - XCTAssertFalse(accountManager.isUserAuthenticated) + XCTAssertFalse(subscriptionManager.isUserAuthenticated) await fulfillment(of: [onSetSubscriptionCalled], timeout: 0.5) XCTAssertNil(result) @@ -821,7 +695,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { } // When - let featureSelectionParams = ["productFeature": Entitlement.ProductName.identityTheftRestoration.rawValue] + let featureSelectionParams = ["productFeature": SubscriptionEntitlement.identityTheftRestoration.rawValue] let result = await feature.featureSelected(params: featureSelectionParams, original: Constants.mockScriptMessage) // Then @@ -836,17 +710,16 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { func testBackToSettingsSuccess() async throws { // Given ensureUserAuthenticatedState() - accountStorage.email = nil - XCTAssertNil(accountManager.email) + XCTAssertNil(subscriptionManager.userEmail) let onBackToSettingsCalled = expectation(description: "onBackToSettings") feature.onBackToSettings = { onBackToSettingsCalled.fulfill() } - authService.validateTokenResult = .success(Constants.validateTokenResponse) - subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.subscription) +// subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.subscription) + subscriptionManager.resultSubscription = SubscriptionMockFactory.subscription // When let result = await feature.backToSettings(params: Constants.mockParams, original: Constants.mockScriptMessage) @@ -854,7 +727,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { // Then await fulfillment(of: [onBackToSettingsCalled], timeout: 0.5) - XCTAssertEqual(accountManager.email, Constants.email) + XCTAssertEqual(subscriptionManager.userEmail, Constants.email) XCTAssertNil(result) await XCTAssertPrivacyPixelsFired([]) @@ -870,7 +743,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { onBackToSettingsCalled.fulfill() } - authService.validateTokenResult = .failure(Constants.invalidTokenError) +// authService.validateTokenResult = .failure(Constants.invalidTokenError) // When let result = await feature.backToSettings(params: Constants.mockParams, original: Constants.mockScriptMessage) @@ -905,7 +778,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { func testGetAccessTokenEmptyOnMissingToken() async throws { // Given ensureUserUnauthenticatedState() - XCTAssertNil(accountManager.accessToken) + XCTAssertFalse(subscriptionManager.isUserAuthenticated) // When let result = try await feature.getAccessToken(params: Constants.mockParams, original: Constants.mockScriptMessage) @@ -924,19 +797,20 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { ensureUserUnauthenticatedState() storePurchaseManager.mostRecentTransactionResult = Constants.mostRecentTransactionJWS - authService.storeLoginResult = .success(StoreLoginResponse(authToken: Constants.authToken, - email: Constants.email, - externalID: Constants.externalID, - id: 1, status: "authenticated")) - authService.getAccessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) - authService.validateTokenResult = .success(Constants.validateTokenResponse) - subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.subscription) - +// authService.storeLoginResult = .success(StoreLoginResponse(authToken: Constants.authToken, +// email: Constants.email, +// externalID: Constants.externalID, +// id: 1, status: "authenticated")) +// authService.getAccessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) +// authService.validateTokenResult = .success(Constants.validateTokenResponse) +// subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.subscription) + subscriptionManager.resultSubscription = SubscriptionMockFactory.subscription + subscriptionManager.resultExchangeTokenContainer = OAuthTokensFactory.makeValidTokenContainerWithEntitlements() // When try await feature.restoreAccountFromAppStorePurchase() // Then - XCTAssertTrue(accountManager.isUserAuthenticated) + XCTAssertTrue(subscriptionManager.isUserAuthenticated) XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, nil) @@ -949,14 +823,15 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { ensureUserUnauthenticatedState() storePurchaseManager.mostRecentTransactionResult = Constants.mostRecentTransactionJWS - authService.storeLoginResult = .success(StoreLoginResponse(authToken: Constants.authToken, - email: Constants.email, - externalID: Constants.externalID, - id: 1, status: "authenticated")) - authService.getAccessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) - authService.validateTokenResult = .success(Constants.validateTokenResponse) - subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredSubscription) - +// authService.storeLoginResult = .success(StoreLoginResponse(authToken: Constants.authToken, +// email: Constants.email, +// externalID: Constants.externalID, +// id: 1, status: "authenticated")) +// authService.getAccessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) +// authService.validateTokenResult = .success(Constants.validateTokenResponse) +// subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredSubscription) + subscriptionManager.resultSubscription = SubscriptionMockFactory.expiredSubscription + subscriptionManager.resultExchangeTokenContainer = OAuthTokensFactory.makeValidTokenContainerWithEntitlements() do { // When @@ -970,7 +845,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { } XCTAssertEqual(error, .subscriptionExpired) - XCTAssertFalse(accountManager.isUserAuthenticated) + XCTAssertFalse(subscriptionManager.isUserAuthenticated) XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, nil) @@ -997,7 +872,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { } XCTAssertEqual(error, .subscriptionNotFound) - XCTAssertFalse(accountManager.isUserAuthenticated) + XCTAssertFalse(subscriptionManager.isUserAuthenticated) XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, nil) @@ -1011,7 +886,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { ensureUserUnauthenticatedState() storePurchaseManager.mostRecentTransactionResult = Constants.mostRecentTransactionJWS - authService.storeLoginResult = .failure(Constants.invalidTokenError) +// authService.storeLoginResult = .failure(Constants.invalidTokenError) do { // When @@ -1025,7 +900,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { } XCTAssertEqual(error, .failedToRestorePastPurchase) - XCTAssertFalse(accountManager.isUserAuthenticated) + XCTAssertFalse(subscriptionManager.isUserAuthenticated) XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, nil) @@ -1038,19 +913,15 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { extension SubscriptionPagesUseSubscriptionFeatureTests { func ensureUserAuthenticatedState() { - accountStorage.authToken = Constants.authToken - accountStorage.email = Constants.email - accountStorage.externalID = Constants.externalID - accessTokenStorage.accessToken = Constants.accessToken + subscriptionManager.resultExchangeTokenContainer = OAuthTokensFactory.makeValidTokenContainerWithEntitlements() } func ensureUserUnauthenticatedState() { - try? accessTokenStorage.removeAccessToken() - try? accountStorage.clearAuthenticationState() + subscriptionManager.resultExchangeTokenContainer = nil } public func XCTAssertPrivacyPixelsFired(_ pixels: [String], file: StaticString = #file, line: UInt = #line) async { - try? await Task.sleep(seconds: 0.1) + try? await Task.sleep(interval: 0.1) let pixelsFired = Set(pixelsFired) let expectedPixels = Set(pixels) diff --git a/DuckDuckGoTests/UsageSegmentationStorageTests.swift b/DuckDuckGoTests/UsageSegmentationStorageTests.swift index 5fd41ae6c8..b857764a1b 100644 --- a/DuckDuckGoTests/UsageSegmentationStorageTests.swift +++ b/DuckDuckGoTests/UsageSegmentationStorageTests.swift @@ -20,7 +20,7 @@ import Foundation import XCTest @testable import Core -@testable import TestUtils +import PersistenceTestingUtils @testable import Persistence final class UsageSegmentationStorageTests: XCTestCase { diff --git a/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift b/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift index 7f75a8b758..07107024cc 100644 --- a/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift +++ b/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift @@ -31,13 +31,27 @@ import WidgetKit import WireGuard import BrowserServicesKit + +public protocol SubscriptionSomething { + func isUserAuthenticated() -> Bool + var subscriptionAuthToken: String? { get } + +} + +extension SubscriptionSomething { + var accessToken: String? { + guard let subscriptionAuthToken else { return nil } + return "ddg:"+subscriptionAuthToken + } +} + // Initial implementation for initial Network Protection tests. Will be fleshed out with https://app.asana.com/0/1203137811378537/1204630829332227/f final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { private static var vpnLogger = VPNLogger() private static let persistentPixel: PersistentPixelFiring = PersistentPixel() private var cancellables = Set() - private let accountManager: AccountManager + private let subscriptionManager: any SubscriptionManager private let configurationStore = ConfigurationStore() private let configurationManager: ConfigurationManager @@ -438,28 +452,60 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { } // MARK: - Configure Subscription - let entitlementsCache = UserDefaultsCache<[Entitlement]>(userDefaults: UserDefaults.standard, - key: UserDefaultsCacheKey.subscriptionEntitlements, - settings: UserDefaultsCacheSettings(defaultExpirationInterval: .minutes(20))) - + let configuration = URLSessionConfiguration.default + configuration.httpCookieStorage = nil + configuration.requestCachePolicy = .reloadIgnoringLocalCacheData + let urlSession = URLSession(configuration: configuration, + delegate: SessionDelegate(), + delegateQueue: nil) + let apiService = DefaultAPIService(urlSession: urlSession) + let authEnvironment: OAuthEnvironment = subscriptionEnvironment.serviceEnvironment == .production ? .production : .staging + + let authService = DefaultOAuthService(baseURL: authEnvironment.url, apiService: apiService) + + // keychain storage let subscriptionAppGroup = Bundle.main.appGroup(bundle: .subs) - let accessTokenStorage = SubscriptionTokenKeychainStorage(keychainType: .dataProtection(.named(subscriptionAppGroup))) - let subscriptionService = DefaultSubscriptionEndpointService(currentServiceEnvironment: subscriptionEnvironment.serviceEnvironment) - let authService = DefaultAuthEndpointService(currentServiceEnvironment: subscriptionEnvironment.serviceEnvironment) - let accountManager = DefaultAccountManager(accessTokenStorage: accessTokenStorage, - entitlementsCache: entitlementsCache, - subscriptionEndpointService: subscriptionService, - authEndpointService: authService) - self.accountManager = accountManager - let featureVisibility = NetworkProtectionVisibilityForTunnelProvider(accountManager: accountManager) - let accessTokenProvider: () -> String? = { - if featureVisibility.shouldMonitorEntitlement() { - return { accountManager.accessToken } + let tokenStorage = SubscriptionTokenKeychainStorageV2(keychainType: .dataProtection(.named(subscriptionAppGroup))) { keychainType, error in + Pixel.fire(.privacyProKeychainAccessError, withAdditionalParameters: ["type": keychainType.rawValue, "error": error.errorDescription]) + } + let legacyAccountStorage = SubscriptionTokenKeychainStorage(keychainType: .dataProtection(.named(subscriptionAppGroup))) + + let authClient = DefaultOAuthClient(tokensStorage: tokenStorage, + legacyTokenStorage: legacyAccountStorage, + authService: authService) + + apiService.authorizationRefresherCallback = { _ in + guard let tokenContainer = tokenStorage.tokenContainer else { + throw OAuthClientError.internalError("Missing refresh token") } - return { nil } - }() - let tokenStore = NetworkProtectionKeychainTokenStore(accessTokenProvider: accessTokenProvider) + if tokenContainer.decodedAccessToken.isExpired() { + Logger.OAuth.debug("Refreshing tokens") + let tokens = try await authClient.getTokens(policy: .localForceRefresh) + return tokens.accessToken + } else { + Logger.general.debug("Trying to refresh valid token, using the old one") + return tokenContainer.accessToken + } + } + let subscriptionEndpointService = DefaultSubscriptionEndpointService(apiService: apiService, + baseURL: subscriptionEnvironment.serviceEnvironment.url) + let storePurchaseManager = DefaultStorePurchaseManager(subscriptionFeatureMappingCache: subscriptionEndpointService) + + let pixelHandler: SubscriptionManager.PixelHandler = { type in + switch type { + case .deadToken: + Pixel.fire(pixel: .privacyProDeadTokenDetected) + case .subscriptionIsActive, .v1MigrationFailed, .v1MigrationSuccessful: // handled by the main app only + break + } + } + let subscriptionManager = DefaultSubscriptionManager(storePurchaseManager: storePurchaseManager, + oAuthClient: authClient, + subscriptionEndpointService: subscriptionEndpointService, + subscriptionEnvironment: subscriptionEnvironment, + pixelHandler: pixelHandler) + self.subscriptionManager = subscriptionManager let errorStore = NetworkProtectionTunnelErrorStore() let notificationsPresenter = NetworkProtectionUNNotificationPresenter() @@ -469,20 +515,27 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { wrappee: notificationsPresenter ) notificationsPresenter.requestAuthorization() + + let entitlementsCheck: (() async -> Result) = { + Logger.networkProtection.log("Subscription Entitlements check...") + let isNetworkProtectionEnabled = await subscriptionManager.isFeatureAvailableForUser(.networkProtection) + Logger.networkProtection.log("NetworkProtectionEnabled if: \( isNetworkProtectionEnabled ? "Enabled" : "Disabled", privacy: .public)") + return .success(isNetworkProtectionEnabled) + } + super.init(notificationsPresenter: notificationsPresenterDecorator, tunnelHealthStore: NetworkProtectionTunnelHealthStore(), controllerErrorStore: errorStore, snoozeTimingStore: NetworkProtectionSnoozeTimingStore(userDefaults: .networkProtectionGroupDefaults), wireGuardInterface: DefaultWireGuardInterface(), keychainType: .dataProtection(.unspecified), - tokenStore: tokenStore, + subscriptionManager: subscriptionManager, debugEvents: Self.networkProtectionDebugEvents(controllerErrorStore: errorStore), providerEvents: Self.packetTunnelProviderEvents, settings: settings, defaults: .networkProtectionGroupDefaults, - entitlementCheck: { return await Self.entitlementCheck(accountManager: accountManager) }) + entitlementCheck: entitlementsCheck) - accountManager.delegate = self startMonitoringMemoryPressureEvents() observeServerChanges() APIRequest.Headers.setUserAgent(DefaultUserAgentManager.duckDuckGoUserAgent) @@ -535,21 +588,6 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { VPNReloadStatusWidgets() } - - private static func entitlementCheck(accountManager: AccountManager) async -> Result { - - guard NetworkProtectionVisibilityForTunnelProvider(accountManager: accountManager).shouldMonitorEntitlement() else { - return .success(true) - } - - let result = await accountManager.hasEntitlement(forProductName: .networkProtection) - switch result { - case .success(let hasEntitlement): - return .success(hasEntitlement) - case .failure(let error): - return .failure(error) - } - } } final class DefaultWireGuardInterface: WireGuardInterface { @@ -581,18 +619,3 @@ final class DefaultWireGuardInterface: WireGuardInterface { wgSetLogger(context, logFunction) } } - -extension NetworkProtectionPacketTunnelProvider: AccountManagerKeychainAccessDelegate { - - public func accountManagerKeychainAccessFailed(accessType: AccountKeychainAccessType, error: AccountKeychainAccessError) { - let parameters = [ - PixelParameters.privacyProKeychainAccessType: accessType.rawValue, - PixelParameters.privacyProKeychainError: error.errorDescription, - PixelParameters.source: "vpn" - ] - - DailyPixel.fireDailyAndCount(pixel: .privacyProKeychainAccessError, - pixelNameSuffixes: DailyPixel.Constant.legacyDailyPixelSuffixes, - withAdditionalParameters: parameters) - } -} diff --git a/WebViewUnitTests/CookieStorageTests.swift b/WebViewUnitTests/CookieStorageTests.swift index 62d56e30aa..d0e04ebb63 100644 --- a/WebViewUnitTests/CookieStorageTests.swift +++ b/WebViewUnitTests/CookieStorageTests.swift @@ -21,7 +21,7 @@ import XCTest @testable import Core import WebKit import Persistence -import TestUtils +import PersistenceTestingUtils public class CookieStorageTests: XCTestCase { diff --git a/WebViewUnitTests/DataStoreIDManagerTests.swift b/WebViewUnitTests/DataStoreIDManagerTests.swift index ea32e99b48..f71422262a 100644 --- a/WebViewUnitTests/DataStoreIDManagerTests.swift +++ b/WebViewUnitTests/DataStoreIDManagerTests.swift @@ -22,7 +22,7 @@ import Foundation import XCTest @testable import Core import WebKit -import TestUtils +import PersistenceTestingUtils class DataStoreIDManagerTests: XCTestCase { diff --git a/WebViewUnitTests/WebCacheManagerTests.swift b/WebViewUnitTests/WebCacheManagerTests.swift index 6fa7be86c1..10c9018f18 100644 --- a/WebViewUnitTests/WebCacheManagerTests.swift +++ b/WebViewUnitTests/WebCacheManagerTests.swift @@ -20,7 +20,7 @@ import XCTest @testable import Core import WebKit -import TestUtils +import PersistenceTestingUtils extension HTTPCookie {