diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 2248bd73f8..87a40966e1 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -40,7 +40,7 @@ jobs: name: Unit Tests runs-on: macos-14-xlarge - timeout-minutes: 20 + timeout-minutes: 15 outputs: commit_author: ${{ steps.fetch_commit_author.outputs.commit_author }} 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 b0796afe64..60c075daa4 100644 --- a/Core/Pixel.swift +++ b/Core/Pixel.swift @@ -235,7 +235,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) @@ -266,7 +266,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 969bf6f637..3564088ab8 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -730,6 +730,7 @@ extension Pixel { case privacyProSubscriptionCookieRefreshedWithAccessToken case privacyProSubscriptionCookieRefreshedWithEmptyValue case privacyProSubscriptionCookieFailedToSetSubscriptionCookie + case privacyProDeadTokenDetected // MARK: Pixel Experiment case pixelExperimentEnrollment @@ -1572,6 +1573,7 @@ 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" // MARK: Pixel Experiment case .pixelExperimentEnrollment: return "pixel_experiment_enrollment" diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index e73881c3aa..446b113a15 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 */; }; @@ -390,7 +389,7 @@ 838306E320C733010045E854 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 838306E120C733010045E854 /* InfoPlist.strings */; }; 8390446F20BDCE10006461CD /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8390446E20BDCE10006461CD /* ShareViewController.swift */; }; 8390447220BDCE10006461CD /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8390447020BDCE10006461CD /* MainInterface.storyboard */; }; - 8390447620BDCE10006461CD /* ShareExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 8390446C20BDCE10006461CD /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 8390447620BDCE10006461CD /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 8390446C20BDCE10006461CD /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 83BE9BC3215D69C1009844D9 /* AppConfigurationFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83BE9BC2215D69C1009844D9 /* AppConfigurationFetch.swift */; }; 83E2D2B2253CC16B005605F5 /* httpsMobileV2Bloom.bin in Resources */ = {isa = PBXBuildFile; fileRef = 83E2D2AF253CC16B005605F5 /* httpsMobileV2Bloom.bin */; }; 83E2D2B3253CC16B005605F5 /* httpsMobileV2FalsePositives.json in Resources */ = {isa = PBXBuildFile; fileRef = 83E2D2B0253CC16B005605F5 /* httpsMobileV2FalsePositives.json */; }; @@ -423,7 +422,7 @@ 8512EA5124ED30D20073EE19 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8512EA5024ED30D20073EE19 /* SwiftUI.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; 8512EA5424ED30D20073EE19 /* Widgets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8512EA5324ED30D20073EE19 /* Widgets.swift */; }; 8512EA5724ED30D30073EE19 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8512EA5624ED30D30073EE19 /* Assets.xcassets */; }; - 8512EA5D24ED30D30073EE19 /* WidgetsExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 8512EA4D24ED30D20073EE19 /* WidgetsExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 8512EA5D24ED30D30073EE19 /* WidgetsExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 8512EA4D24ED30D20073EE19 /* WidgetsExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 8512EA9D24EEA6820073EE19 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F143C2B11E49D78C00CFDE3A /* Assets.xcassets */; }; 851481882A600EFC00ABC65F /* RemoteMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = 851481872A600EFC00ABC65F /* RemoteMessaging */; }; 851624C22B95F8BD002D5CD7 /* HistoryCapture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851624C12B95F8BD002D5CD7 /* HistoryCapture.swift */; }; @@ -481,7 +480,7 @@ 8546A54A2A672959003929BF /* MainViewController+Email.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8546A5492A672959003929BF /* MainViewController+Email.swift */; }; 85482D8D2462DCD100EDEDD1 /* ActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85482D8C2462DCD100EDEDD1 /* ActionViewController.swift */; }; 85482D902462DCD100EDEDD1 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 85482D8E2462DCD100EDEDD1 /* MainInterface.storyboard */; }; - 85482D942462DCD100EDEDD1 /* OpenAction.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 85482D882462DCD100EDEDD1 /* OpenAction.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 85482D942462DCD100EDEDD1 /* OpenAction.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 85482D882462DCD100EDEDD1 /* OpenAction.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 85482D992462F1C600EDEDD1 /* ActionIcons.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 85482D982462F1C600EDEDD1 /* ActionIcons.xcassets */; }; 854858E32937BC550063610B /* CollectionExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EE411F22857C4A30003FE64 /* CollectionExtension.swift */; }; 8548D95E25262B1B005AAE49 /* ViewHighlighter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8548D95D25262B1B005AAE49 /* ViewHighlighter.swift */; }; @@ -846,7 +845,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 */; }; @@ -866,10 +864,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 */; }; C111B26927F579EF006558B1 /* BookmarkOrFolderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C111B26827F579EF006558B1 /* BookmarkOrFolderTests.swift */; }; C12324C32C4697C900FBB26B /* AutofillBreakageReportTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1836CE42C35A0EA0016D057 /* AutofillBreakageReportTableViewCell.swift */; }; @@ -946,7 +942,6 @@ CB5516D0286500290079B175 /* TrackerRadarIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85519124247468580010FDD0 /* TrackerRadarIntegrationTests.swift */; }; CB5516D1286500290079B175 /* ContentBlockingRulesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02CA904C24FD2DB000D41DDF /* ContentBlockingRulesTests.swift */; }; CB5516D2286500290079B175 /* AtbServerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F21DBD21121147002631A6 /* AtbServerTests.swift */; }; - CB6CC7E42CD2529000320907 /* BrokenSitePrompt in Frameworks */ = {isa = PBXBuildFile; productRef = CB6CC7E32CD2529000320907 /* BrokenSitePrompt */; }; CB6D8E982C80A9B100D0E772 /* SpecialErrorPages in Frameworks */ = {isa = PBXBuildFile; productRef = CB6D8E972C80A9B100D0E772 /* SpecialErrorPages */; }; CB825C922C071B1400BCC586 /* AlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB825C912C071B1400BCC586 /* AlertView.swift */; }; CB825C962C071C9300BCC586 /* AlertViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB825C952C071C9300BCC586 /* AlertViewPresenter.swift */; }; @@ -968,7 +963,6 @@ CBD4F140279EBFB300B20FD7 /* SwiftUICollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB1AEFB02799AA940031AE3D /* SwiftUICollectionViewCell.swift */; }; CBDD5DDF29A6736A00832877 /* APIHeadersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBDD5DDE29A6736A00832877 /* APIHeadersTests.swift */; }; CBDD5DE129A6741300832877 /* MockBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBDD5DE029A6741300832877 /* MockBundle.swift */; }; - CBECDB6F2CD3DFBE005B8B87 /* PageRefreshMonitor in Frameworks */ = {isa = PBXBuildFile; productRef = CBECDB6E2CD3DFBE005B8B87 /* PageRefreshMonitor */; }; CBECDB7A2CD981CE005B8B87 /* AppPageRefreshMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBECDB792CD981C6005B8B87 /* AppPageRefreshMonitor.swift */; }; CBEFB9142AE0844700DEDE7B /* CriticalAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBEFB9102ADFFE7900DEDE7B /* CriticalAlerts.swift */; }; CBFCB30E2B2CD47800253E9E /* ConfigurationURLDebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBFCB30D2B2CD47800253E9E /* ConfigurationURLDebugViewController.swift */; }; @@ -1104,6 +1098,8 @@ F143C3271E4A9A0E00CFDE3A /* Logger+Multiple.swift in Sources */ = {isa = PBXBuildFile; fileRef = F143C3231E4A9A0E00CFDE3A /* Logger+Multiple.swift */; }; F143C3281E4A9A0E00CFDE3A /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F143C3241E4A9A0E00CFDE3A /* StringExtension.swift */; }; F143C3291E4A9A0E00CFDE3A /* URLExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F143C3251E4A9A0E00CFDE3A /* URLExtension.swift */; }; + F1449EB12CE612A5002536E4 /* PageRefreshMonitor in Frameworks */ = {isa = PBXBuildFile; productRef = F1449EB02CE612A5002536E4 /* PageRefreshMonitor */; }; + F1449EB32CE612C5002536E4 /* BrokenSitePrompt in Frameworks */ = {isa = PBXBuildFile; productRef = F1449EB22CE612C5002536E4 /* BrokenSitePrompt */; }; F14E491F1E391CE900DC037C /* URLExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F14E491E1E391CE900DC037C /* URLExtensionTests.swift */; }; F15531902BF215ED0029ED04 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = F155318F2BF215ED0029ED04 /* Subscription */; }; F15531922BF215ED0029ED04 /* SubscriptionTestingUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = F15531912BF215ED0029ED04 /* SubscriptionTestingUtilities */; }; @@ -1113,6 +1109,7 @@ F1617C131E572E0300DEDCAF /* TabSwitcherViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1617C121E572E0300DEDCAF /* TabSwitcherViewController.swift */; }; F1617C151E57336D00DEDCAF /* TabManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1617C141E57336D00DEDCAF /* TabManager.swift */; }; F1617C191E573EA800DEDCAF /* TabSwitcherDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1617C181E573EA800DEDCAF /* TabSwitcherDelegate.swift */; }; + F1619A1C2CDA32D100D6D4C9 /* TokenBackgroundRefreshTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1619A1B2CDA32D100D6D4C9 /* TokenBackgroundRefreshTask.swift */; }; F16393FF1ECCB9CC00DDD653 /* FileLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = F16393FE1ECCB9CC00DDD653 /* FileLoader.swift */; }; F1668BCE1E798081008CBA04 /* BookmarksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1668BCD1E798081008CBA04 /* BookmarksViewController.swift */; }; F176699F1E40BC86003D3222 /* Settings.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F176699D1E40BC86003D3222 /* Settings.storyboard */; }; @@ -1131,13 +1128,13 @@ F1AE54E81F0425FC00D9A700 /* AuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1AE54E71F0425FC00D9A700 /* AuthenticationViewController.swift */; }; 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 */; }; F1BDDC022C340DDF00459306 /* SyncManagementViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1BDDC002C340DDF00459306 /* SyncManagementViewModelTests.swift */; }; F1BE54581E69DE1000FCF649 /* TutorialSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1BE54571E69DE1000FCF649 /* TutorialSettings.swift */; }; F1C4A70E1E57725800A6CA1B /* OmniBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1C4A70D1E57725800A6CA1B /* OmniBar.swift */; }; F1CA3C371F045878005FADB3 /* PrivacyStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1CA3C361F045878005FADB3 /* PrivacyStore.swift */; }; F1CA3C391F045885005FADB3 /* PrivacyUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1CA3C381F045885005FADB3 /* PrivacyUserDefaults.swift */; }; F1CA3C3B1F045B65005FADB3 /* Authenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1CA3C3A1F045B65005FADB3 /* Authenticator.swift */; }; + F1D07FB92CD277F60016F7FD /* Logger+Pixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D07FB82CD277F00016F7FD /* Logger+Pixel.swift */; }; F1D43AFA2B99C1D300BAB743 /* BareBonesBrowserKit in Frameworks */ = {isa = PBXBuildFile; productRef = F1D43AF92B99C1D300BAB743 /* BareBonesBrowserKit */; }; F1D43AFC2B99C56000BAB743 /* RootDebugViewController+VanillaBrowser.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D43AFB2B99C56000BAB743 /* RootDebugViewController+VanillaBrowser.swift */; }; F1D477C61F2126CC0031ED49 /* OmniBarState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D477C51F2126CC0031ED49 /* OmniBarState.swift */; }; @@ -1289,17 +1286,17 @@ /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ - 83E282AC20BC1840005FBE88 /* Embed App Extensions */ = { + 83E282AC20BC1840005FBE88 /* Embed Foundation Extensions */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 13; files = ( - 85482D942462DCD100EDEDD1 /* OpenAction.appex in Embed App Extensions */, - 8512EA5D24ED30D30073EE19 /* WidgetsExtension.appex in Embed App Extensions */, - 8390447620BDCE10006461CD /* ShareExtension.appex in Embed App Extensions */, + 85482D942462DCD100EDEDD1 /* OpenAction.appex in Embed Foundation Extensions */, + 8512EA5D24ED30D30073EE19 /* WidgetsExtension.appex in Embed Foundation Extensions */, + 8390447620BDCE10006461CD /* ShareExtension.appex in Embed Foundation Extensions */, ); - name = "Embed App Extensions"; + name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; }; F10307651E7D5B2C0059FEC7 /* Copy Frameworks */ = { @@ -1395,7 +1392,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 = ""; }; 1E61BC2927074BED00B2854D /* TextSizeUserScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextSizeUserScript.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 = ""; }; @@ -2651,7 +2647,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 = ""; }; @@ -2671,8 +2666,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 = ""; }; C111B26827F579EF006558B1 /* BookmarkOrFolderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkOrFolderTests.swift; sourceTree = ""; }; C12726ED2A5FF88C00215B02 /* EmailSignupPromptView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailSignupPromptView.swift; sourceTree = ""; }; @@ -2950,6 +2943,7 @@ F1617C121E572E0300DEDCAF /* TabSwitcherViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabSwitcherViewController.swift; sourceTree = ""; }; F1617C141E57336D00DEDCAF /* TabManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabManager.swift; sourceTree = ""; }; F1617C181E573EA800DEDCAF /* TabSwitcherDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabSwitcherDelegate.swift; sourceTree = ""; }; + F1619A1B2CDA32D100D6D4C9 /* TokenBackgroundRefreshTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenBackgroundRefreshTask.swift; sourceTree = ""; }; F16393F41ECCA85900DDD653 /* DomainsProtectionUserDefaultsStoreTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DomainsProtectionUserDefaultsStoreTests.swift; sourceTree = ""; }; F16393FE1ECCB9CC00DDD653 /* FileLoader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileLoader.swift; sourceTree = ""; }; F1668BCD1E798081008CBA04 /* BookmarksViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarksViewController.swift; sourceTree = ""; }; @@ -2972,7 +2966,6 @@ F1B745211E549D550072547E /* UIColorExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = UIColorExtension.swift; path = ../Core/UIColorExtension.swift; sourceTree = ""; }; F1BDDBF92C340D9C00459306 /* SubscriptionContainerViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionContainerViewModelTests.swift; sourceTree = ""; }; F1BDDBFA2C340D9C00459306 /* SubscriptionFlowViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionFlowViewModelTests.swift; sourceTree = ""; }; - F1BDDBFB2C340D9C00459306 /* SubscriptionPagesUseSubscriptionFeatureTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionPagesUseSubscriptionFeatureTests.swift; sourceTree = ""; }; F1BDDC002C340DDF00459306 /* SyncManagementViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncManagementViewModelTests.swift; sourceTree = ""; }; F1BE54571E69DE1000FCF649 /* TutorialSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TutorialSettings.swift; sourceTree = ""; }; F1C4A70D1E57725800A6CA1B /* OmniBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OmniBar.swift; sourceTree = ""; }; @@ -2980,6 +2973,7 @@ F1CA3C381F045885005FADB3 /* PrivacyUserDefaults.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivacyUserDefaults.swift; sourceTree = ""; }; F1CA3C3A1F045B65005FADB3 /* Authenticator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Authenticator.swift; sourceTree = ""; }; F1CB8EA21F26B39000A7171B /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + F1D07FB82CD277F00016F7FD /* Logger+Pixel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+Pixel.swift"; sourceTree = ""; }; F1D43AFB2B99C56000BAB743 /* RootDebugViewController+VanillaBrowser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RootDebugViewController+VanillaBrowser.swift"; sourceTree = ""; }; 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 = ""; }; @@ -3154,11 +3148,12 @@ files = ( 4B948E2629DCCDB9002531FA /* Persistence in Frameworks */, 98A50962294B48A400D10880 /* Bookmarks in Frameworks */, + F1449EB32CE612C5002536E4 /* BrokenSitePrompt in Frameworks */, 1E60989B290009C700A508F9 /* Common in Frameworks */, 1E60989D290011E600A508F9 /* ContentBlocking in Frameworks */, - CB6CC7E42CD2529000320907 /* BrokenSitePrompt in Frameworks */, F486D33425069BBB002D07D7 /* Kingfisher in Frameworks */, EE8E568A2A56BCE400F11DCA /* NetworkProtection in Frameworks */, + F1449EB12CE612A5002536E4 /* PageRefreshMonitor in Frameworks */, D6BC8ACB2C5AA3860025375B /* DuckPlayer in Frameworks */, CBC83E3429B631780008E19C /* Configuration in Frameworks */, D61CDA182B7CF78300A0FBB9 /* ZIPFoundation in Frameworks */, @@ -3169,7 +3164,6 @@ 851481882A600EFC00ABC65F /* RemoteMessaging in Frameworks */, 37DF000C29F9CA80002B7D3E /* SyncDataProviders in Frameworks */, 1E6098A1290011E600A508F9 /* UserScript in Frameworks */, - CBECDB6F2CD3DFBE005B8B87 /* PageRefreshMonitor in Frameworks */, D61CDA162B7CF77300A0FBB9 /* Subscription in Frameworks */, 858D009D2B9799FC004E5B4C /* History in Frameworks */, C14882ED27F211A000D59F0C /* SwiftSoup in Frameworks */, @@ -5095,7 +5089,6 @@ children = ( BDFF03192BA39C5A00F324C9 /* NetworkProtectionFeatureVisibility.swift */, BDFF031C2BA3D2BD00F324C9 /* DefaultNetworkProtectionVisibility.swift */, - BDFF03202BA3D3CF00F324C9 /* NetworkProtectionVisibilityForTunnelProvider.swift */, ); name = "Feature Visibility"; sourceTree = ""; @@ -5371,7 +5364,6 @@ BDE91CD42C6292BF0005CB74 /* Feedback */, F1FDC92F2BF4E0B3006B1435 /* SubscriptionEnvironment+Default.swift */, D60170BB2BA32DD6001911B5 /* Subscription.swift */, - 1E53508E2C7C9A1F00818DAA /* DefaultSubscriptionManager+AccountManagerKeychainAccessDelegate.swift */, D6D95CE42B6DA3F200960317 /* AsyncHeadlessWebview */, D664C7932B289AA000CBFA76 /* ViewModel */, D664C7AC2B289AA000CBFA76 /* Views */, @@ -5380,6 +5372,7 @@ D65CEA6F2B6AC6C9008A759B /* Subscription.xcassets */, BDE219E52C406D19005D5884 /* PrivacyProDataReporting.swift */, 1E39BEAF2CC9477200496FBA /* SubscriptionCookieManageEventPixelMapping.swift */, + F1619A1B2CDA32D100D6D4C9 /* TokenBackgroundRefreshTask.swift */, ); path = Subscription; sourceTree = ""; @@ -5402,7 +5395,6 @@ D664C7962B289AA000CBFA76 /* Extensions */ = { isa = PBXGroup; children = ( - BD10B8A92C7629740033115D /* Logger+Subscription.swift */, F1FDC9342BF51E41006B1435 /* VPNSettings+Environment.swift */, D664C7982B289AA000CBFA76 /* WKUserContentController+Handler.swift */, ); @@ -5574,7 +5566,6 @@ children = ( EEFE9C722A603CE9005B0A26 /* NetworkProtectionStatusViewModelTests.swift */, EEC02C152B065BE00045CE11 /* NetworkProtectionVPNLocationViewModelTests.swift */, - BDFF03242BA3D92E00F324C9 /* NetworkProtectionFeatureVisibilityTests.swift */, ); name = NetworkProtection; sourceTree = ""; @@ -5642,6 +5633,7 @@ CB2A7EF028410DF700885F67 /* PixelEvent.swift */, BDC234F62B27F51100D3C798 /* UniquePixel.swift */, 853A717520F62FE800FE60BC /* Pixel.swift */, + F1D07FB82CD277F00016F7FD /* Logger+Pixel.swift */, 6F03CB062C32F173004179A8 /* PixelFiring.swift */, 6F03CB082C32F331004179A8 /* PixelFiringAsync.swift */, 6F7FB8E22C660BF300867DA7 /* DailyPixelFiring.swift */, @@ -6173,7 +6165,6 @@ BDE219E92C457B46005D5884 /* PrivacyProDataReporterTests.swift */, F1BDDBF92C340D9C00459306 /* SubscriptionContainerViewModelTests.swift */, F1BDDBFA2C340D9C00459306 /* SubscriptionFlowViewModelTests.swift */, - F1BDDBFB2C340D9C00459306 /* SubscriptionPagesUseSubscriptionFeatureTests.swift */, 1E4E6C562C78B8540059C0FA /* StorePurchaseManagerTests.swift */, ); path = Subscription; @@ -6565,7 +6556,7 @@ F143C2F01E4A4CD400CFDE3A /* Embed Frameworks */, 37B4F3D329D2C84400758752 /* Copy GRDB framework */, F10307651E7D5B2C0059FEC7 /* Copy Frameworks */, - 83E282AC20BC1840005FBE88 /* Embed App Extensions */, + 83E282AC20BC1840005FBE88 /* Embed Foundation Extensions */, EE9286812A812BD2002B7818 /* Embed PacketTunnelProvider */, ); buildRules = ( @@ -6797,8 +6788,8 @@ 851F74252B9A1BFD00747C42 /* Suggestions */, D6BC8ACA2C5AA3860025375B /* DuckPlayer */, CB6D8E972C80A9B100D0E772 /* SpecialErrorPages */, - CB6CC7E32CD2529000320907 /* BrokenSitePrompt */, - CBECDB6E2CD3DFBE005B8B87 /* PageRefreshMonitor */, + F1449EB02CE612A5002536E4 /* PageRefreshMonitor */, + F1449EB22CE612C5002536E4 /* BrokenSitePrompt */, ); productName = Core; productReference = F143C2E41E4A4CD400CFDE3A /* Core.framework */; @@ -6810,8 +6801,9 @@ 84E3418A1E2F7EFB00BDBA6F /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1420; - LastUpgradeCheck = 1500; + LastUpgradeCheck = 1600; ORGANIZATIONNAME = DuckDuckGo; TargetAttributes = { 02025661298818B100E694E7 = { @@ -7343,7 +7335,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 */, 7BC571212BDBB977003B0CCE /* VPNActivationDateStore.swift in Sources */, ); @@ -7812,7 +7803,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 */, @@ -7899,6 +7889,7 @@ 6FD1BAE62B87A107000C475C /* AdAttributionFetcher.swift in Sources */, EE01EB402AFBD0000096AAC9 /* NetworkProtectionVPNSettingsViewModel.swift in Sources */, EE72CA852A862D000043B5B3 /* NetworkProtectionDebugViewController.swift in Sources */, + F1619A1C2CDA32D100D6D4C9 /* TokenBackgroundRefreshTask.swift in Sources */, C18ED43A2AB6F77600BF3805 /* AutofillSettingsEnableFooterView.swift in Sources */, CB84C7BD29A3EF530088A5B8 /* AppConfigurationURLProvider.swift in Sources */, AA3D854723D9E88E00788410 /* AppIconSettingsCell.swift in Sources */, @@ -7933,7 +7924,6 @@ D664C7B92B289AA200CBFA76 /* WKUserContentController+Handler.swift in Sources */, 1E8AD1D727C2E24E00ABA377 /* DownloadsListRowViewModel.swift in Sources */, 9FEA222E2C324ECD006B03BF /* ViewVisibility.swift in Sources */, - BD10B8AA2C7629740033115D /* Logger+Subscription.swift in Sources */, 1E865AF0272042DB001C74F3 /* TextSizeSettingsViewController.swift in Sources */, D6E0C1892B7A2E0D00D5E1E9 /* DesktopDownloadViewModel.swift in Sources */, 8524CC9A246DA81700E59D45 /* FullscreenDaxDialogViewController.swift in Sources */, @@ -8055,7 +8045,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 */, D62EC3BA2C246A7000FC9D04 /* YoutublePlayerNavigationHandlerTests.swift in Sources */, 1EAABE712C99FC75003F5137 /* SubscriptionFeatureAvailabilityMock.swift in Sources */, @@ -8150,7 +8139,6 @@ F1134ED61F40F29F00B73467 /* StatisticsUserDefaultsTests.swift in Sources */, 98629D342C21BE37001E6031 /* BookmarksStateValidationTests.swift in Sources */, C1FFBD462C761BE20073622B /* SyncPromoManagerTests.swift in Sources */, - F1BDDBFF2C340D9C00459306 /* SubscriptionPagesUseSubscriptionFeatureTests.swift in Sources */, 98EA2C3C218B9AAD0023E1DC /* ThemeManagerTests.swift in Sources */, 569437292BDD487600C0881B /* SyncCredentialsAdapterTests.swift in Sources */, 6AC98419288055C1005FA9CA /* BarsAnimatorTests.swift in Sources */, @@ -8370,6 +8358,7 @@ F1134EA61F3E2AF400B73467 /* StatisticsStore.swift in Sources */, F17D723C1E8BB374003E8B0E /* AppDeepLinkSchemes.swift in Sources */, 1E8AD1DB27C51AE000ABA377 /* TimeIntervalExtension.swift in Sources */, + F1D07FB92CD277F60016F7FD /* Logger+Pixel.swift in Sources */, 9FCFCD812C6B020D006EB7A0 /* LaunchOptionsHandler.swift in Sources */, B652DF0D287C2A6300C12A9C /* PrivacyFeatures.swift in Sources */, F10E522D1E946F8800CE1253 /* NSAttributedStringExtension.swift in Sources */, @@ -9212,6 +9201,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 2; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -9249,6 +9239,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 2; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9339,6 +9330,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 2; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9366,6 +9358,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 2; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9419,7 +9412,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; @@ -9509,7 +9502,6 @@ 84E341BB1E2F7EFC00BDBA6F /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; @@ -9534,12 +9526,12 @@ 84E341BC1E2F7EFC00BDBA6F /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CURRENT_PROJECT_VERSION = 2; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGo/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9557,7 +9549,6 @@ 84E341BE1E2F7EFC00BDBA6F /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGoTests/Info.plist; @@ -9577,7 +9568,6 @@ 84E341BF1E2F7EFC00BDBA6F /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGoTests/Info.plist; @@ -9609,7 +9599,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 2; - DEAD_CODE_STRIPPING = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9643,7 +9633,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 2; - DEAD_CODE_STRIPPING = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9676,6 +9666,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 2; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9706,6 +9697,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 2; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9896,6 +9888,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CODE_SIGN_STYLE = Automatic; + DEAD_CODE_STRIPPING = YES; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Instruments/Packages"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -9909,6 +9902,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CODE_SIGN_STYLE = Automatic; + DEAD_CODE_STRIPPING = YES; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Instruments/Packages"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -9921,6 +9915,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = B6DFE6D92BC7E61B00A9CE59 /* SwiftLintToolBundleConfiguration.xcconfig */; buildSettings = { + DEAD_CODE_STRIPPING = YES; }; name = Debug; }; @@ -9928,6 +9923,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = B6DFE6D92BC7E61B00A9CE59 /* SwiftLintToolBundleConfiguration.xcconfig */; buildSettings = { + DEAD_CODE_STRIPPING = YES; }; name = "Alpha Debug"; }; @@ -9935,6 +9931,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = B6DFE6D92BC7E61B00A9CE59 /* SwiftLintToolBundleConfiguration.xcconfig */; buildSettings = { + DEAD_CODE_STRIPPING = YES; }; name = Alpha; }; @@ -9942,6 +9939,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = B6DFE6D92BC7E61B00A9CE59 /* SwiftLintToolBundleConfiguration.xcconfig */; buildSettings = { + DEAD_CODE_STRIPPING = YES; }; name = Release; }; @@ -9979,7 +9977,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; @@ -10010,7 +10008,6 @@ D664C7DF2B28A0FD00CBFA76 /* Alpha Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = "DDG-AppIcon-Alpha"; CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; @@ -10047,6 +10044,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 2; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -10075,6 +10073,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 2; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -10108,7 +10107,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 2; - DEAD_CODE_STRIPPING = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -10138,6 +10137,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 2; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -10176,6 +10176,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 2; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( @@ -10183,6 +10184,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu99 gnu++11"; PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.mobile.ios.Core; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -10201,6 +10203,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CODE_SIGN_STYLE = Automatic; + DEAD_CODE_STRIPPING = YES; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Instruments/Packages"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -10238,7 +10241,6 @@ D664C7E72B28A0FD00CBFA76 /* Alpha Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGoTests/Info.plist; @@ -10374,7 +10376,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; @@ -10402,7 +10404,6 @@ EE5A7C472A82BBB700387C84 /* Alpha */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = "DDG-AppIcon-Alpha"; CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; @@ -10435,6 +10436,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 2; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10467,6 +10469,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 2; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10504,7 +10507,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 2; - DEAD_CODE_STRIPPING = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10539,6 +10542,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 2; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10579,6 +10583,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 2; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( @@ -10586,6 +10591,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu99 gnu++11"; PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.mobile.ios.Core; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -10604,6 +10610,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CODE_SIGN_STYLE = Automatic; + DEAD_CODE_STRIPPING = YES; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Instruments/Packages"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -10641,7 +10648,6 @@ EE5A7C4F2A82BBB700387C84 /* Alpha */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGoTests/Info.plist; @@ -10756,6 +10762,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 2; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( @@ -10763,6 +10770,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu99 gnu++11"; PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.mobile.ios.Core; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -10784,10 +10792,12 @@ CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 2; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 2; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( @@ -10795,6 +10805,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu99 gnu++11"; PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.mobile.ios.Core; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -10995,8 +11006,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { - kind = exactVersion; - version = 209.1.0; + branch = fcappelli/subscription_oauth_api_v2; + kind = branch; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { @@ -11221,10 +11232,6 @@ package = C14882EB27F211A000D59F0C /* XCRemoteSwiftPackageReference "SwiftSoup" */; productName = SwiftSoup; }; - CB6CC7E32CD2529000320907 /* BrokenSitePrompt */ = { - isa = XCSwiftPackageProductDependency; - productName = BrokenSitePrompt; - }; CB6D8E972C80A9B100D0E772 /* SpecialErrorPages */ = { isa = XCSwiftPackageProductDependency; package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; @@ -11240,10 +11247,6 @@ package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = Configuration; }; - CBECDB6E2CD3DFBE005B8B87 /* PageRefreshMonitor */ = { - isa = XCSwiftPackageProductDependency; - productName = PageRefreshMonitor; - }; D61CDA152B7CF77300A0FBB9 /* Subscription */ = { isa = XCSwiftPackageProductDependency; package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; @@ -11274,6 +11277,16 @@ package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = TestUtils; }; + F1449EB02CE612A5002536E4 /* PageRefreshMonitor */ = { + isa = XCSwiftPackageProductDependency; + package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = PageRefreshMonitor; + }; + F1449EB22CE612C5002536E4 /* BrokenSitePrompt */ = { + isa = XCSwiftPackageProductDependency; + package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = BrokenSitePrompt; + }; F155318F2BF215ED0029ED04 /* Subscription */ = { isa = XCSwiftPackageProductDependency; package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 16e6bef221..c3ff5c3a9e 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" : "948420e704ea4d9412a4fc3e2c2ab0d5ea5fe5d7", - "version" : "209.1.0" + "branch" : "fcappelli/subscription_oauth_api_v2", + "revision" : "aab97954107399096a9bbe64b6aff8eb49bc648b" } }, { @@ -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" : "7faebca1ea4f9aaf0cda1cef7c43aecd2311ddf6", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto.git", + "state" : { + "revision" : "06dc63c6d8da54ee11ceb268cde1fa68161afc96", + "version" : "3.9.1" + } + }, { "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 accountManager = DefaultAccountManager(accessTokenStorage: accessTokenStorage, - entitlementsCache: entitlementsCache, - subscriptionEndpointService: subscriptionService, - authEndpointService: authService) - - let subscriptionManager = DefaultSubscriptionManager(storePurchaseManager: DefaultStorePurchaseManager(), - accountManager: accountManager, - subscriptionEndpointService: subscriptionService, - authEndpointService: authService, - subscriptionEnvironment: subscriptionEnvironment) - accountManager.delegate = subscriptionManager - + 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 tokenStorage = SubscriptionTokenKeychainStorageV2(keychainType: .dataProtection(.named(subscriptionAppGroup))) + let legacyAccountStorage = SubscriptionTokenKeychainStorage(keychainType: .dataProtection(.named(subscriptionAppGroup))) + + let authClient = DefaultOAuthClient(tokensStorage: tokenStorage, + legacyTokenStorage: legacyAccountStorage, + authService: authService) + + self.privacyProInfoProvider = authClient + + 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 storePurchaseManager = DefaultStorePurchaseManager() + + let subscriptionEndpointService = DefaultSubscriptionEndpointService(apiService: apiService, + baseURL: subscriptionEnvironment.serviceEnvironment.url) + let pixelHandler: SubscriptionManager.PixelHandler = { type in + switch type { + case .deadToken: + Pixel.fire(pixel: .privacyProDeadTokenDetected) + } + } + let subscriptionManager = DefaultSubscriptionManager(storePurchaseManager: storePurchaseManager, + 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, + networkProtectionKeychainTokenStore = NetworkProtectionKeychainTokenStore(accessTokenProvider: { + guard let token = subscriptionManager.getTokenContainerSynchronously(policy: .localValid)?.accessToken else { + Logger.networkProtection.error("NetworkProtectionKeychainTokenStore failed to provide token") + return nil + } + return token + }) + networkProtectionTunnelController = NetworkProtectionTunnelController(tokenStore: networkProtectionKeychainTokenStore, featureFlagger: featureFlagger, persistentPixel: persistentPixel, settings: vpnSettings) vpnFeatureVisibility = DefaultNetworkProtectionVisibility(userDefaults: .networkProtectionGroupDefaults, - accountManager: accountManager) + oAuthClient: authClient) } +} +extension DefaultOAuthClient: PrivacyProInfoProvider { + + var hasVPNEntitlements: Bool { + guard let tokenContainer = tokenStorage.tokenContainer else { + return false + } + return tokenContainer.decodedAccessToken.hasEntitlement(.networkProtection) + } } 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/DuckPlayer/DuckPlayer.swift b/DuckDuckGo/DuckPlayer/DuckPlayer.swift index 486688dce2..54e611bc9c 100644 --- a/DuckDuckGo/DuckPlayer/DuckPlayer.swift +++ b/DuckDuckGo/DuckPlayer/DuckPlayer.swift @@ -278,7 +278,7 @@ final class DuckPlayer: DuckPlayerControlling { /// - message: The script message containing the parameters. /// - Returns: An optional `Encodable` response. public func setUserValues(params: Any, message: WKScriptMessage) -> Encodable? { - guard let userValues: UserValues = DecodableHelper.decode(from: params) else { + guard let userValues: UserValues = CodableHelper.decode(from: params) else { assertionFailure("DuckPlayer: expected JSON representation of UserValues") return nil } @@ -369,7 +369,7 @@ final class DuckPlayer: DuckPlayerControlling { /// - message: The script message containing the parameters. @MainActor public func telemetryEvent(params: Any, message: WKScriptMessage) async -> Encodable? { - guard let event: TelemetryEvent = DecodableHelper.decode(from: params) else { + guard let event: TelemetryEvent = CodableHelper.decode(from: params) else { return nil } @@ -461,7 +461,7 @@ final class DuckPlayer: DuckPlayerControlling { @MainActor private func firePixels(message: WKScriptMessage, userValues: UserValues) { - guard let messageData: WKMessageData = DecodableHelper.decode(from: message.body) else { + guard let messageData: WKMessageData = CodableHelper.decode(from: message.body) else { assertionFailure("DuckPlayer: expected JSON representation of Message") return } diff --git a/DuckDuckGo/Feedback/VPNMetadataCollector.swift b/DuckDuckGo/Feedback/VPNMetadataCollector.swift index f77dcfddd2..ef86c45043 100644 --- a/DuckDuckGo/Feedback/VPNMetadataCollector.swift +++ b/DuckDuckGo/Feedback/VPNMetadataCollector.swift @@ -99,21 +99,26 @@ protocol VPNMetadataCollector { func collectVPNMetadata() async -> VPNMetadata } +protocol PrivacyProInfoProvider { + var isUserAuthenticated: Bool { get } + var hasVPNEntitlements: Bool { get } +} + final class DefaultVPNMetadataCollector: VPNMetadataCollector { private let statusObserver: ConnectionStatusObserver private let serverInfoObserver: ConnectionServerInfoObserver - private let accountManager: AccountManager + private let privacyProInfoProvider: PrivacyProInfoProvider private let settings: VPNSettings private let defaults: UserDefaults init(statusObserver: ConnectionStatusObserver, - serverInfoObserver: ConnectionServerInfoObserver, - accountManager: AccountManager = AppDependencyProvider.shared.subscriptionManager.accountManager, + serverInfoObserver: ConnectionServerInfoObserver,// ConnectionServerInfoObserverThroughSession(), + privacyProInfoProvider: PrivacyProInfoProvider = AppDependencyProvider.shared.privacyProInfoProvider, settings: VPNSettings = .init(defaults: .networkProtectionGroupDefaults), defaults: UserDefaults = .networkProtectionGroupDefaults) { self.statusObserver = statusObserver self.serverInfoObserver = serverInfoObserver - self.accountManager = accountManager + self.privacyProInfoProvider = privacyProInfoProvider self.settings = settings self.defaults = defaults } @@ -242,10 +247,10 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { } func collectPrivacyProInfo() async -> VPNMetadata.PrivacyProInfo { - let hasVPNEntitlement = (try? await accountManager.hasEntitlement(forProductName: .networkProtection).get()) ?? false +// let hasVPNEntitlement = (try? await accountManager.hasEntitlement(forProductName: .networkProtection).get()) ?? false return .init( - hasPrivacyProAccount: accountManager.isUserAuthenticated, - hasVPNEntitlement: hasVPNEntitlement + hasPrivacyProAccount: privacyProInfoProvider.isUserAuthenticated, + hasVPNEntitlement: privacyProInfoProvider.hasVPNEntitlements ) } diff --git a/DuckDuckGo/Info.plist b/DuckDuckGo/Info.plist index 6d3941b0ca..f0c871a752 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 75c5777782..01a0036f80 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -1598,8 +1598,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..ea3d15fd5e 100644 --- a/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift +++ b/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift @@ -67,7 +67,7 @@ extension NetworkProtectionVPNSettingsViewModel { extension NetworkProtectionLocationListCompositeRepository { - convenience init(accountManager: AccountManager) { + convenience init() { let settings = AppDependencyProvider.shared.vpnSettings self.init( environment: settings.selectedEnvironment, @@ -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..7b95b991b8 100644 --- a/DuckDuckGo/NetworkProtectionDebugViewController.swift +++ b/DuckDuckGo/NetworkProtectionDebugViewController.swift @@ -125,25 +125,21 @@ 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, tokenStore: AppDependencyProvider.shared.networkProtectionKeychainTokenStore) } override func viewWillAppear(_ animated: Bool) { @@ -686,9 +682,11 @@ shouldShowVPNShortcut: \(vpnVisibility.shouldShowVPNShortcut() ? "YES" : "NO") if let subscriptionOverrideEnabled = defaults.subscriptionOverrideEnabled { if subscriptionOverrideEnabled { defaults.subscriptionOverrideEnabled = false - accountManager.signOut() + Task { + await AppDependencyProvider.shared.subscriptionManager.signOut() + } } else { - defaults.resetsubscriptionOverrideEnabled() + defaults.resetSubscriptionOverrideEnabled() } } else { defaults.subscriptionOverrideEnabled = true diff --git a/DuckDuckGo/NetworkProtectionRootView.swift b/DuckDuckGo/NetworkProtectionRootView.swift index 974c03022c..aa445919f5 100644 --- a/DuckDuckGo/NetworkProtectionRootView.swift +++ b/DuckDuckGo/NetworkProtectionRootView.swift @@ -27,9 +27,9 @@ struct NetworkProtectionRootView: View { let statusViewModel: NetworkProtectionStatusViewModel init() { - let accountManager = AppDependencyProvider.shared.subscriptionManager.accountManager - let locationListRepository = NetworkProtectionLocationListCompositeRepository(accountManager: accountManager) - let usesUnifiedFeedbackForm = accountManager.isUserAuthenticated + let subscriptionManager = AppDependencyProvider.shared.subscriptionManager + 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/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtectionTunnelController.swift index ed22d7cd91..b93f7606ef 100644 --- a/DuckDuckGo/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtectionTunnelController.swift @@ -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(tokenStore: NetworkProtectionKeychainTokenStore, featureFlagger: FeatureFlagger, persistentPixel: PersistentPixelFiring, settings: VPNSettings) { @@ -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 = tokenStore.fetchToken() 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/RemoteMessagingConfigMatcherProvider.swift b/DuckDuckGo/RemoteMessagingConfigMatcherProvider.swift index a10a8f9bbf..48cfb7c798 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.currentSubscription(refresh: false) { + 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 299cfeba21..aa8c48682b 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 size: Int } - struct Subscription: Codable { + struct SubscriptionState: Codable { var enabled: Bool var canPurchase: Bool - var isSignedIn: Bool + var subscriptionExist: Bool var hasActiveSubscription: Bool var isRestoring: Bool var shouldDisplayRestoreSubscriptionError: Bool - var entitlements: [Entitlement.ProductName] - var platform: DDGSubscription.Platform + var entitlements: [SubscriptionEntitlement] + var platform: PrivacyProSubscription.Platform var isShowingStripeView: Bool } @@ -90,7 +91,7 @@ struct SettingsState { var networkProtectionConnected: Bool // Subscriptions Properties - var subscription: Subscription + var subscription: SubscriptionState // Sync Properties var sync: SyncSettings @@ -126,9 +127,9 @@ struct SettingsState { speechRecognitionAvailable: false, loginsEnabled: false, networkProtectionConnected: false, - subscription: Subscription(enabled: false, + subscription: SubscriptionState(enabled: false, canPurchase: false, - isSignedIn: false, + subscriptionExist: false, hasActiveSubscription: false, isRestoring: false, shouldDisplayRestoreSubscriptionError: false, diff --git a/DuckDuckGo/SettingsSubscriptionView.swift b/DuckDuckGo/SettingsSubscriptionView.swift index 6c22e97d9d..a874a0f6cc 100644 --- a/DuckDuckGo/SettingsSubscriptionView.swift +++ b/DuckDuckGo/SettingsSubscriptionView.swift @@ -199,7 +199,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 @@ -208,24 +209,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 } @@ -239,7 +240,7 @@ struct SettingsSubscriptionView: View { } } .onReceive(settingsViewModel.$state) { state in - isShowingPrivacyPro = state.subscription.enabled && (state.subscription.isSignedIn || state.subscription.canPurchase) + isShowingPrivacyPro = state.subscription.enabled && (state.subscription.subscriptionExist || state.subscription.canPurchase) } } } diff --git a/DuckDuckGo/SettingsViewModel.swift b/DuckDuckGo/SettingsViewModel.swift index e008a34ab6..f5e716c71a 100644 --- a/DuckDuckGo/SettingsViewModel.swift +++ b/DuckDuckGo/SettingsViewModel.swift @@ -25,9 +25,10 @@ import Common import Combine import SyncUI import DuckPlayer - +import Networking import Subscription import NetworkProtection +import os.log final class SettingsViewModel: ObservableObject { @@ -55,7 +56,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 @@ -355,7 +356,7 @@ final class SettingsViewModel: ObservableObject { } var usesUnifiedFeedbackForm: Bool { - subscriptionManager.accountManager.isUserAuthenticated && subscriptionFeatureAvailability.usesUnifiedFeedbackForm + subscriptionManager.isUserAuthenticated && subscriptionFeatureAvailability.usesUnifiedFeedbackForm } // MARK: Default Init @@ -697,6 +698,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 @@ -711,47 +713,35 @@ 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.currentSubscription(refresh: true) + Logger.subscription.log("Subscription loaded: \(subscription.debugDescription, privacy: .public)") + state.subscription.subscriptionExist = true state.subscription.platform = subscription.platform state.subscription.hasActiveSubscription = subscription.isActive + state.subscription.entitlements = subscriptionManager.entitlements + } catch SubscriptionEndpointServiceError.noData, OAuthClientError.missingTokens { + // Auth successful but no Subscription is available + Logger.subscription.log("Subscription not present") - // Check entitlements and update state - var currentEntitlements: [Entitlement.ProductName] = [] - let entitlementsToCheck: [Entitlement.ProductName] = [.networkProtection, .dataBrokerProtection, .identityTheftRestoration] - - for entitlement in entitlementsToCheck { - if case .success(true) = await subscriptionManager.accountManager.hasEntitlement(forProductName: entitlement) { - currentEntitlements.append(entitlement) - } - } - - self.state.subscription.entitlements = currentEntitlements - - 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, @@ -775,10 +765,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/TokenBackgroundRefreshTask.swift b/DuckDuckGo/Subscription/TokenBackgroundRefreshTask.swift new file mode 100644 index 0000000000..27ac409785 --- /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.configurationRefresh"] + // + // Task expiration can be simulated similarly: + // e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"com.duckduckgo.app.configurationRefresh"] + + #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..151c536ba4 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,8 +71,8 @@ final class IdentityTheftRestorationPagesFeature: Subfeature, ObservableObject { } func getAccessToken(params: Any, original: WKScriptMessage) async throws -> Encodable? { - if let accessToken = accountManager.accessToken { - return [Constants.token: accessToken] + if let tokenContainer = try? await subscriptionManager.getTokenContainer(policy: .localValid) { + return [Constants.token: tokenContainer.accessToken] } else { 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 178c07bd2f..bf24037b9c 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 @@ -90,11 +91,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? init(subscriptionManager: SubscriptionManager, @@ -102,13 +101,11 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec subscriptionAttributionOrigin: String?, appStorePurchaseFlow: AppStorePurchaseFlow, appStoreRestoreFlow: AppStoreRestoreFlow, - appStoreAccountManagementFlow: AppStoreAccountManagementFlow, privacyProDataReporter: PrivacyProDataReporting? = nil) { self.subscriptionManager = subscriptionManager self.subscriptionFeatureAvailability = subscriptionFeatureAvailability self.appStorePurchaseFlow = appStorePurchaseFlow self.appStoreRestoreFlow = appStoreRestoreFlow - self.appStoreAccountManagementFlow = appStoreAccountManagementFlow self.subscriptionAttributionOrigin = subscriptionAttributionOrigin self.privacyProDataReporter = subscriptionAttributionOrigin != nil ? privacyProDataReporter : nil } @@ -186,26 +183,32 @@ 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? { - await appStoreAccountManagementFlow.refreshAuthTokenIfNeeded() - let authToken = accountManager.authToken ?? Constants.empty - return [Constants.token: authToken] + /// 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] + } } func getSubscriptionOptions(params: Any, original: WKScriptMessage) async -> Encodable? { resetSubscriptionFlow() if let subscriptionOptions = await subscriptionManager.storePurchaseManager().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.empty } } else { @@ -228,7 +231,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) @@ -237,20 +240,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): @@ -259,7 +260,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) @@ -273,58 +274,72 @@ 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 + } + switch await appStorePurchaseFlow.completeSubscriptionPurchase(with: purchaseTransactionJWS) { - case .success(let purchaseUpdate): - Logger.subscription.debug("Subscription purchase completed successfully") + 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() + 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() 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 @@ -343,33 +358,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]() } } @@ -377,31 +398,31 @@ 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) return nil } 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) return nil } 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 } @@ -427,12 +448,14 @@ 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 } } diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift index 75b49918cb..59f3e6174b 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/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 3d3fafb4cc..bb81f40b68 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,8 +75,7 @@ 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() } @@ -108,15 +108,12 @@ 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): + + if let subscription = try? await self.subscriptionManager.currentSubscription(refresh: cachePolicy != SubscriptionCachePolicy.returnCacheDataDontLoad) { DispatchQueue.main.async { self.state.subscriptionInfo = subscription if loadingIndicator { self.displaySubscriptionLoader(false) } @@ -125,52 +122,30 @@ final class SubscriptionSettingsViewModel: ObservableObject { date: subscription.expiresOrRenewsAt, product: subscription.productId, billingPeriod: subscription.billingPeriod) - return true - case .failure(let error): - Logger.subscription.error("\(#function) error: \(error.localizedDescription)") - DispatchQueue.main.async { - if loadingIndicator { self.displaySubscriptionLoader(true) } - } - return false } + return true } - 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: TokensCachePolicy = .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)") + if let tokenContainer = try? await subscriptionManager.getTokenContainer(policy: tokensPolicy) { DispatchQueue.main.async { + self.state.subscriptionEmail = tokenContainer.decodedAccessToken.email if loadingIndicator { self.displayEmailLoader(true) } } - return false + return true } + return false } private func displaySubscriptionLoader(_ show: Bool) { @@ -186,7 +161,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() } @@ -210,7 +185,8 @@ 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) @@ -225,19 +201,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() + _ = 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 +232,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 +255,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 +278,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 +290,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/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 45247ddb76..ff71e1feb0 100644 --- a/DuckDuckGo/SubscriptionDebugViewController.swift +++ b/DuckDuckGo/SubscriptionDebugViewController.swift @@ -22,6 +22,7 @@ import UIKit import Subscription import Core import NetworkProtection +import Networking final class SubscriptionDebugViewController: UITableViewController { @@ -263,18 +264,25 @@ final class SubscriptionDebugViewController: UITableViewController { } private func clearAuthData() { - subscriptionManager.accountManager.signOut() - showAlert(title: "Data cleared!") + Task { + await subscriptionManager.signOut() + 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() { @@ -316,14 +324,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)") } } @@ -331,37 +337,30 @@ 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.currentSubscription(refresh: true) + showAlert(title: "Subscription info", message: "\(subscription)") + } catch OAuthClientError.missingTokens { + showAlert(title: "Not authenticated", message: "No authenticated user found! - Token not available") + } catch { + showAlert(title: "Error retrieving subscription", 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")) } } @@ -373,20 +372,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() + + // 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/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/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/Subscription/SubscriptionContainerViewModelTests.swift b/DuckDuckGoTests/Subscription/SubscriptionContainerViewModelTests.swift index 2d4979997f..e3a9ab49d8 100644 --- a/DuckDuckGoTests/Subscription/SubscriptionContainerViewModelTests.swift +++ b/DuckDuckGoTests/Subscription/SubscriptionContainerViewModelTests.swift @@ -24,21 +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() - return SubscriptionManagerMock(accountManager: accountManager, - subscriptionEndpointService: subscriptionService, - authEndpointService: authService, - storePurchaseManager: storePurchaseManager, - currentEnvironment: SubscriptionEnvironment(serviceEnvironment: .production, - purchasePlatform: .appStore), - canPurchase: true) - }() - + let subscriptionManager = SubscriptionManagerMock() let subscriptionFeatureAvailability = SubscriptionFeatureAvailabilityMock.enabled func testWhenInitWithOriginThenSubscriptionFlowPurchaseURLHasOriginSet() { @@ -46,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() + 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, @@ -67,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() + 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, @@ -96,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 67235b541e..efcd07daa7 100644 --- a/DuckDuckGoTests/Subscription/SubscriptionFlowViewModelTests.swift +++ b/DuckDuckGoTests/Subscription/SubscriptionFlowViewModelTests.swift @@ -25,19 +25,19 @@ 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 storePurchaseManager = DefaultStorePurchaseManager() - return SubscriptionManagerMock(accountManager: accountManager, - subscriptionEndpointService: subscriptionService, - authEndpointService: authService, - storePurchaseManager: storePurchaseManager, - currentEnvironment: SubscriptionEnvironment(serviceEnvironment: .production, - purchasePlatform: .appStore), - canPurchase: true) - }() + let subscriptionManager = SubscriptionManagerMock() +// let accountManager = AccountManagerMock() +// let subscriptionService = DefaultSubscriptionEndpointService(currentServiceEnvironment: .production) +// let authService = DefaultAuthEndpointService(currentServiceEnvironment: .production) +// let storePurchaseManager = DefaultStorePurchaseManager() +// return SubscriptionManagerMock(accountManager: accountManager, +// subscriptionEndpointService: subscriptionService, +// authEndpointService: authService, +// storePurchaseManager: storePurchaseManager, +// currentEnvironment: SubscriptionEnvironment(serviceEnvironment: .production, +// purchasePlatform: .appStore), +// canPurchase: true) +// }() let subscriptionFeatureAvailability = SubscriptionFeatureAvailabilityMock.enabled @@ -46,26 +46,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() + 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 @@ -73,26 +66,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() + 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 deleted file mode 100644 index 8636921470..0000000000 --- a/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift +++ /dev/null @@ -1,1068 +0,0 @@ -// -// SubscriptionPagesUseSubscriptionFeatureTests.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 -@testable import Core -@testable import Subscription -import SubscriptionTestingUtilities -import Common -import WebKit -import BrowserServicesKit -import OHHTTPStubs -import OHHTTPStubsSwift -import os.log - -final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { - - private struct Constants { - static let userDefaultsSuiteName = "SubscriptionPagesUseSubscriptionFeatureTests" - - static let authToken = UUID().uuidString - static let accessToken = UUID().uuidString - static let externalID = UUID().uuidString - - static let email = "dax@duck.com" - - static let entitlements = [Entitlement(product: .dataBrokerProtection), - Entitlement(product: .identityTheftRestoration), - Entitlement(product: .networkProtection)] - - static let mostRecentTransactionJWS = "dGhpcyBpcyBub3QgYSByZWFsIEFw(...)cCBTdG9yZSB0cmFuc2FjdGlvbiBKV1M=" - - static let subscriptionOptions = SubscriptionOptions(platform: SubscriptionPlatformName.ios.rawValue, - options: [ - SubscriptionOption(id: "1", - cost: SubscriptionOptionCost(displayPrice: "9 USD", recurrence: "monthly")), - SubscriptionOption(id: "2", - cost: SubscriptionOptionCost(displayPrice: "99 USD", recurrence: "yearly")) - ], - features: [ - SubscriptionFeature(name: "vpn"), - SubscriptionFeature(name: "personal-information-removal"), - SubscriptionFeature(name: "identity-theft-restoration") - ]) - - static let validateTokenResponse = ValidateTokenResponse(account: ValidateTokenResponse.Account(email: Constants.email, - entitlements: Constants.entitlements, - externalID: Constants.externalID)) - - 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 storePurchaseManager: StorePurchaseManagerMock! - var subscriptionEnvironment: SubscriptionEnvironment! - - var appStorePurchaseFlow: AppStorePurchaseFlow! - var appStoreRestoreFlow: AppStoreRestoreFlow! - var appStoreAccountManagementFlow: AppStoreAccountManagementFlow! - - var accountManager: AccountManager! - var subscriptionManager: SubscriptionManager! - var subscriptionFeatureAvailability = SubscriptionFeatureAvailabilityMock.enabled - - var feature: SubscriptionPagesUseSubscriptionFeature! - - var pixelsFired: [String] = [] - - override func setUpWithError() throws { - // Pixels - Pixel.isDryRun = false - stub(condition: isHost("improving.duckduckgo.com")) { request -> HTTPStubsResponse in - if let path = request.url?.path { - let pixelName = path.dropping(prefix: "/t/") - .dropping(suffix: "_ios_phone") - .dropping(suffix: "_ios_tablet") - self.pixelsFired.append(pixelName) - } - - return HTTPStubsResponse(data: Data(), statusCode: 200, headers: nil) - } - - // Reset all daily pixel storage - [Pixel.storage, DailyPixel.storage, UniquePixel.storage].forEach { storage in - storage.dictionaryRepresentation().keys.forEach(storage.removeObject(forKey:)) - } - - // 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))) - - // Real AccountManager - accountManager = DefaultAccountManager(storage: accountStorage, - accessTokenStorage: accessTokenStorage, - entitlementsCache: entitlementsCache, - subscriptionEndpointService: subscriptionService, - authEndpointService: authService) - - // Real Flows - appStoreRestoreFlow = DefaultAppStoreRestoreFlow(accountManager: accountManager, - storePurchaseManager: storePurchaseManager, - subscriptionEndpointService: subscriptionService, - authEndpointService: authService) - - appStorePurchaseFlow = DefaultAppStorePurchaseFlow(subscriptionEndpointService: subscriptionService, - 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, - subscriptionEnvironment: subscriptionEnvironment) - - feature = SubscriptionPagesUseSubscriptionFeature(subscriptionManager: subscriptionManager, - subscriptionFeatureAvailability: subscriptionFeatureAvailability, - subscriptionAttributionOrigin: nil, - appStorePurchaseFlow: appStorePurchaseFlow, - appStoreRestoreFlow: appStoreRestoreFlow, - appStoreAccountManagementFlow: appStoreAccountManagementFlow) - } - - 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 - } - - // MARK: - Tests for getSubscription - - func testGetSubscriptionSuccessRefreshingAuthToken() async throws { - // Given - ensureUserAuthenticatedState() - - 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(feature.transactionStatus, .idle) - XCTAssertEqual(feature.transactionError, nil) - - await XCTAssertPrivacyPixelsFired([]) - } - - func testGetSubscriptionSuccessErrorWhenUnauthenticated() async throws { - // Given - ensureUserUnauthenticatedState() - - authService.validateTokenResult = .failure(Constants.invalidTokenError) - storePurchaseManager.mostRecentTransactionResult = nil - - // 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], SubscriptionPagesUseSubscriptionFeature.Constants.empty) - XCTAssertFalse(accountManager.isUserAuthenticated) - - XCTAssertEqual(feature.transactionStatus, .idle) - XCTAssertEqual(feature.transactionError, nil) - - await XCTAssertPrivacyPixelsFired([]) - } - - // MARK: - Tests for getSubscriptionOptions - - func testGetSubscriptionOptionsSuccess() async throws { - // Given - storePurchaseManager.subscriptionOptionsResult = Constants.subscriptionOptions - - // When - let result = await feature.getSubscriptionOptions(params: Constants.mockParams, original: Constants.mockScriptMessage) - - // Then - let subscriptionOptionsResult = try XCTUnwrap(result as? SubscriptionOptions) - - XCTAssertEqual(subscriptionOptionsResult, Constants.subscriptionOptions) - - XCTAssertEqual(feature.transactionStatus, .idle) - XCTAssertEqual(feature.transactionError, nil) - - await XCTAssertPrivacyPixelsFired([]) - } - - func testGetSubscriptionOptionsReturnsEmptyOptionsWhenNoSubscriptionOptions() async throws { - // Given - storePurchaseManager.subscriptionOptionsResult = nil - - // When - let result = await feature.getSubscriptionOptions(params: Constants.mockParams, original: Constants.mockScriptMessage) - - // Then - let subscriptionOptionsResult = try XCTUnwrap(result as? SubscriptionOptions) - XCTAssertEqual(subscriptionOptionsResult, SubscriptionOptions.empty) - - XCTAssertEqual(feature.transactionStatus, .idle) - XCTAssertEqual(feature.transactionError, .failedToGetSubscriptionOptions) - - await XCTAssertPrivacyPixelsFired([]) - } - - func testGetSubscriptionOptionsReturnsEmptyOptionsWhenPurchaseNotAllowed() async throws { - // Given - let subscriptionFeatureAvailabilityWithoutPurchaseAllowed = SubscriptionFeatureAvailabilityMock( - isFeatureAvailable: true, - isSubscriptionPurchaseAllowed: false, - usesUnifiedFeedbackForm: true - ) - - feature = SubscriptionPagesUseSubscriptionFeature(subscriptionManager: subscriptionManager, - subscriptionFeatureAvailability: subscriptionFeatureAvailabilityWithoutPurchaseAllowed, - subscriptionAttributionOrigin: nil, - appStorePurchaseFlow: appStorePurchaseFlow, - appStoreRestoreFlow: appStoreRestoreFlow, - appStoreAccountManagementFlow: appStoreAccountManagementFlow) - - storePurchaseManager.subscriptionOptionsResult = Constants.subscriptionOptions - - // When - let result = await feature.getSubscriptionOptions(params: Constants.mockParams, original: Constants.mockScriptMessage) - - // Then - let subscriptionOptionsResult = try XCTUnwrap(result as? SubscriptionOptions) - XCTAssertEqual(subscriptionOptionsResult, SubscriptionOptions.empty) - - XCTAssertEqual(feature.transactionStatus, .idle) - XCTAssertEqual(feature.transactionError, nil) - - await XCTAssertPrivacyPixelsFired([]) - } - - // MARK: - Tests for subscriptionSelected - - func testSubscriptionSelectedSuccessWhenPurchasingFirstTime() async throws { - // Given - ensureUserUnauthenticatedState() - - XCTAssertFalse(accountManager.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) - storePurchaseManager.purchaseSubscriptionResult = .success(Constants.mostRecentTransactionJWS) - subscriptionService.confirmPurchaseResult = .success(ConfirmPurchaseResponse(email: Constants.email, - entitlements: Constants.entitlements, - subscription: SubscriptionMockFactory.subscription)) - - // When - let subscriptionSelectedParams = ["id": "some-subscription-id"] - let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) - - // Then - XCTAssertNil(result) - - XCTAssertEqual(feature.transactionStatus, .idle) - XCTAssertEqual(feature.transactionError, nil) - - await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", - Pixel.Event.privacyProPurchaseAttempt.name + "_c", - Pixel.Event.privacyProPurchaseSuccess.name + "_d", - Pixel.Event.privacyProPurchaseSuccess.name + "_c", - Pixel.Event.privacyProSubscriptionActivated.name, - Pixel.Event.privacyProSuccessfulSubscriptionAttribution.name]) - } - - func testSubscriptionSelectedSuccessWhenRepurchasingForExpiredAppleSubscription() async throws { - // Given - ensureUserAuthenticatedState() - - XCTAssertTrue(accountManager.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) - storePurchaseManager.purchaseSubscriptionResult = .success(Constants.mostRecentTransactionJWS) - subscriptionService.confirmPurchaseResult = .success(ConfirmPurchaseResponse(email: Constants.email, - entitlements: Constants.entitlements, - subscription: SubscriptionMockFactory.subscription)) - - // When - let subscriptionSelectedParams = ["id": "some-subscription-id"] - let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) - - // Then - XCTAssertFalse(authService.createAccountCalled) - XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) - - XCTAssertNil(result) - - XCTAssertEqual(feature.transactionStatus, .idle) - XCTAssertEqual(feature.transactionError, nil) - - await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", - Pixel.Event.privacyProPurchaseAttempt.name + "_c", - Pixel.Event.privacyProPurchaseSuccess.name + "_d", - Pixel.Event.privacyProPurchaseSuccess.name + "_c", - Pixel.Event.privacyProSubscriptionActivated.name, - Pixel.Event.privacyProSuccessfulSubscriptionAttribution.name]) - } - - func testSubscriptionSelectedSuccessWhenRepurchasingForExpiredStripeSubscription() async throws { - // Given - ensureUserAuthenticatedState() - - XCTAssertTrue(accountManager.isUserAuthenticated) - - storePurchaseManager.hasActiveSubscriptionResult = false - subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) - storePurchaseManager.purchaseSubscriptionResult = .success(Constants.mostRecentTransactionJWS) - subscriptionService.confirmPurchaseResult = .success(ConfirmPurchaseResponse(email: Constants.email, - entitlements: Constants.entitlements, - subscription: SubscriptionMockFactory.subscription)) - - // When - let subscriptionSelectedParams = ["id": "some-subscription-id"] - let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) - - // Then - XCTAssertFalse(authService.createAccountCalled) - XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) - - XCTAssertNil(result) - - XCTAssertEqual(feature.transactionStatus, .idle) - XCTAssertEqual(feature.transactionError, nil) - await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", - Pixel.Event.privacyProPurchaseAttempt.name + "_c", - Pixel.Event.privacyProPurchaseSuccess.name + "_d", - Pixel.Event.privacyProPurchaseSuccess.name + "_c", - Pixel.Event.privacyProSubscriptionActivated.name, - Pixel.Event.privacyProSuccessfulSubscriptionAttribution.name]) - } - - func testSubscriptionSelectedErrorWhenPurchasingWhenHavingActiveSubscription() async throws { - // Given - ensureUserAuthenticatedState() - - storePurchaseManager.hasActiveSubscriptionResult = true - - // When - let subscriptionSelectedParams = ["id": "some-subscription-id"] - let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) - - // Then - XCTAssertFalse(storePurchaseManager.purchaseSubscriptionCalled) - - XCTAssertNil(result) - - XCTAssertEqual(feature.transactionStatus, .idle) - XCTAssertEqual(feature.transactionError, .hasActiveSubscription) - - await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", - Pixel.Event.privacyProPurchaseAttempt.name + "_c", - Pixel.Event.privacyProRestoreAfterPurchaseAttempt.name]) - } - - func testSubscriptionSelectedErrorWhenPurchasingWhenUnauthenticatedAndHavingActiveSubscriptionOnAppleID() async throws { - // Given - ensureUserUnauthenticatedState() - - storePurchaseManager.hasActiveSubscriptionResult = true - - // When - let subscriptionSelectedParams = ["id": "some-subscription-id"] - let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) - - // Then - XCTAssertFalse(storePurchaseManager.purchaseSubscriptionCalled) - - XCTAssertNil(result) - - XCTAssertEqual(feature.transactionStatus, .idle) - XCTAssertEqual(feature.transactionError, .hasActiveSubscription) - - await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", - Pixel.Event.privacyProPurchaseAttempt.name + "_c", - Pixel.Event.privacyProRestoreAfterPurchaseAttempt.name]) - } - - func testSubscriptionSelectedErrorWhenUnauthenticatedAndAccountCreationFails() async throws { - // Given - ensureUserUnauthenticatedState() - - storePurchaseManager.hasActiveSubscriptionResult = false - storePurchaseManager.mostRecentTransactionResult = nil - - authService.createAccountResult = .failure(Constants.invalidTokenError) - - // When - let subscriptionSelectedParams = ["id": "some-subscription-id"] - let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) - - // Then - XCTAssertFalse(storePurchaseManager.purchaseSubscriptionCalled) - - XCTAssertNil(result) - - XCTAssertEqual(feature.transactionStatus, .idle) - XCTAssertEqual(feature.transactionError, .accountCreationFailed) - - await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", - Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) - } - - func testSubscriptionSelectedErrorWhenPurchaseCancelledByUser() async throws { - // Given - ensureUserAuthenticatedState() - - storePurchaseManager.hasActiveSubscriptionResult = false - subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) - storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.purchaseCancelledByUser) - - // When - let subscriptionSelectedParams = ["id": "some-subscription-id"] - let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) - - // Then - XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) - - XCTAssertNil(result) - - XCTAssertEqual(feature.transactionStatus, .idle) - XCTAssertEqual(feature.transactionError, .cancelledByUser) - - await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", - Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) - } - - func testSubscriptionSelectedErrorWhenProductNotFound() async throws { - // Given - ensureUserAuthenticatedState() - - storePurchaseManager.hasActiveSubscriptionResult = false - subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) - storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.productNotFound) - - // When - let subscriptionSelectedParams = ["id": "some-subscription-id"] - let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) - - // Then - XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) - - XCTAssertNil(result) - - XCTAssertEqual(feature.transactionStatus, .idle) - XCTAssertEqual(feature.transactionError, .purchaseFailed) - - await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", - Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) - } - - func testSubscriptionSelectedErrorWhenExternalIDIsNotValidUUID() async throws { - // Given - ensureUserAuthenticatedState() - - storePurchaseManager.hasActiveSubscriptionResult = false - subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) - storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.externalIDisNotAValidUUID) - - // When - let subscriptionSelectedParams = ["id": "some-subscription-id"] - let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) - - // Then - XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) - - XCTAssertNil(result) - - XCTAssertEqual(feature.transactionStatus, .idle) - XCTAssertEqual(feature.transactionError, .purchaseFailed) - - await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", - Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) - } - - func testSubscriptionSelectedErrorWhenPurchaseFailed() async throws { - // Given - ensureUserAuthenticatedState() - - storePurchaseManager.hasActiveSubscriptionResult = false - subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) - storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.purchaseFailed) - - // When - let subscriptionSelectedParams = ["id": "some-subscription-id"] - let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) - - // Then - XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) - - XCTAssertNil(result) - - XCTAssertEqual(feature.transactionStatus, .idle) - XCTAssertEqual(feature.transactionError, .purchaseFailed) - - await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", - Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) - } - - func testSubscriptionSelectedErrorWhenTransactionCannotBeVerified() async throws { - // Given - ensureUserAuthenticatedState() - - storePurchaseManager.hasActiveSubscriptionResult = false - subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) - storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.transactionCannotBeVerified) - - // When - let subscriptionSelectedParams = ["id": "some-subscription-id"] - let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) - - // Then - XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) - - XCTAssertNil(result) - - XCTAssertEqual(feature.transactionStatus, .idle) - XCTAssertEqual(feature.transactionError, .purchaseFailed) - - await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", - Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) - } - - func testSubscriptionSelectedErrorWhenTransactionPendingAuthentication() async throws { - // Given - ensureUserAuthenticatedState() - - storePurchaseManager.hasActiveSubscriptionResult = false - subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) - storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.transactionPendingAuthentication) - - // When - let subscriptionSelectedParams = ["id": "some-subscription-id"] - let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) - - // Then - XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) - - XCTAssertNil(result) - - XCTAssertEqual(feature.transactionStatus, .idle) - XCTAssertEqual(feature.transactionError, .purchaseFailed) - - await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", - Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) - } - - func testSubscriptionSelectedErrorDueToUnknownPurchaseError() async throws { - // Given - ensureUserAuthenticatedState() - - storePurchaseManager.hasActiveSubscriptionResult = false - subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) - storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.unknownError) - - // When - let subscriptionSelectedParams = ["id": "some-subscription-id"] - let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) - - // Then - XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) - - XCTAssertNil(result) - - XCTAssertEqual(feature.transactionStatus, .idle) - XCTAssertEqual(feature.transactionError, .purchaseFailed) - - await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", - Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) - } - - // MARK: - Tests for setSubscription - - func testSetSubscriptionSuccess() async throws { - // Given - ensureUserUnauthenticatedState() - - authService.getAccessTokenResult = .success(.init(accessToken: Constants.accessToken)) - authService.validateTokenResult = .success(Constants.validateTokenResponse) - - let onSetSubscriptionCalled = expectation(description: "onSetSubscription") - feature.onSetSubscription = { - onSetSubscriptionCalled.fulfill() - } - - // When - let setSubscriptionParams = ["token": Constants.authToken] - let result = await feature.setSubscription(params: setSubscriptionParams, original: Constants.mockScriptMessage) - - // Then - XCTAssertEqual(accountManager.authToken, Constants.authToken) - XCTAssertEqual(accountManager.accessToken, Constants.accessToken) - XCTAssertEqual(accountManager.email, Constants.email) - XCTAssertEqual(accountManager.externalID, Constants.externalID) - - await fulfillment(of: [onSetSubscriptionCalled], timeout: 0.5) - XCTAssertNil(result) - - XCTAssertEqual(feature.transactionStatus, .idle) - XCTAssertEqual(feature.transactionError, nil) - - await XCTAssertPrivacyPixelsFired([]) - } - - func testSetSubscriptionErrorWhenFailedToExchangeToken() async throws { - // 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) - - 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([]) - } - - // MARK: - Tests for activateSubscription - - func testActivateSubscriptionTokenSuccess() async throws { - // Given - ensureUserAuthenticatedState() - - let onActivateSubscriptionCalled = expectation(description: "onActivateSubscription") - feature.onActivateSubscription = { - onActivateSubscriptionCalled.fulfill() - } - - // When - let result = await feature.activateSubscription(params: Constants.mockParams, original: Constants.mockScriptMessage) - - // Then - await fulfillment(of: [onActivateSubscriptionCalled], timeout: 0.5) - XCTAssertNil(result) - - await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProRestorePurchaseOfferPageEntry.name]) - } - - // MARK: - Tests for featureSelected - - func testFeatureSelectedSuccess() async throws { - // Given - ensureUserAuthenticatedState() - - let onFeatureSelectedCalled = expectation(description: "onFeatureSelected") - feature.onFeatureSelected = { selection in - onFeatureSelectedCalled.fulfill() - XCTAssertEqual(selection, SubscriptionFeatureSelection.itr) - } - - // When - let featureSelectionParams = ["feature": SubscriptionFeatureName.itr] - let result = await feature.featureSelected(params: featureSelectionParams, original: Constants.mockScriptMessage) - - // Then - await fulfillment(of: [onFeatureSelectedCalled], timeout: 0.5) - XCTAssertNil(result) - - await XCTAssertPrivacyPixelsFired([]) - } - - // MARK: - Tests for backToSettings - - func testBackToSettingsSuccess() async throws { - // Given - ensureUserAuthenticatedState() - accountStorage.email = nil - - XCTAssertNil(accountManager.email) - - let onBackToSettingsCalled = expectation(description: "onBackToSettings") - feature.onBackToSettings = { - onBackToSettingsCalled.fulfill() - } - - authService.validateTokenResult = .success(Constants.validateTokenResponse) - subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.subscription) - - // When - let result = await feature.backToSettings(params: Constants.mockParams, original: Constants.mockScriptMessage) - - // Then - await fulfillment(of: [onBackToSettingsCalled], timeout: 0.5) - - XCTAssertEqual(accountManager.email, Constants.email) - XCTAssertNil(result) - - await XCTAssertPrivacyPixelsFired([]) - } - - func testBackToSettingsErrorOnFetchingAccountDetails() async throws { - // Given - ensureUserAuthenticatedState() - - let onBackToSettingsCalled = expectation(description: "onBackToSettings") - onBackToSettingsCalled.isInverted = true - feature.onBackToSettings = { - onBackToSettingsCalled.fulfill() - } - - authService.validateTokenResult = .failure(Constants.invalidTokenError) - - // When - let result = await feature.backToSettings(params: Constants.mockParams, original: Constants.mockScriptMessage) - - // Then - await fulfillment(of: [onBackToSettingsCalled], timeout: 0.5) - - XCTAssertEqual(feature.transactionError, .generalError) - XCTAssertNil(result) - - await XCTAssertPrivacyPixelsFired([]) - } - - // MARK: - Tests for getAccessToken - func testGetAccessTokenSuccess() async throws { - // Given - ensureUserAuthenticatedState() - - // When - let result = try await feature.getAccessToken(params: Constants.mockParams, original: Constants.mockScriptMessage) - - // Then - let resultDictionary = try XCTUnwrap(result as? [String: String]) - XCTAssertEqual(resultDictionary[SubscriptionPagesUseSubscriptionFeature.Constants.token], Constants.accessToken) - - XCTAssertEqual(feature.transactionStatus, .idle) - XCTAssertEqual(feature.transactionError, nil) - - await XCTAssertPrivacyPixelsFired([]) - } - - func testGetAccessTokenEmptyOnMissingToken() async throws { - // Given - ensureUserUnauthenticatedState() - XCTAssertNil(accountManager.accessToken) - - // When - let result = try await feature.getAccessToken(params: Constants.mockParams, original: Constants.mockScriptMessage) - - // Then - let resultDictionary = try XCTUnwrap(result as? [String: String]) - XCTAssertEqual(resultDictionary, [String: String]()) - - await XCTAssertPrivacyPixelsFired([]) - } - - // MARK: - Tests for restoreAccountFromAppStorePurchase - - func testRestoreAccountFromAppStorePurchaseSuccess() async throws { - // Given - 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) - - // When - try await feature.restoreAccountFromAppStorePurchase() - - // Then - XCTAssertTrue(accountManager.isUserAuthenticated) - - XCTAssertEqual(feature.transactionStatus, .idle) - XCTAssertEqual(feature.transactionError, nil) - - await XCTAssertPrivacyPixelsFired([]) - } - - func testRestoreAccountFromAppStorePurchaseErrorDueToExpiredSubscription() async throws { - // Given - 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) - - - do { - // When - try await feature.restoreAccountFromAppStorePurchase() - XCTFail("Unexpected success") - } catch let error { - // Then - guard let error = error as? SubscriptionPagesUseSubscriptionFeature.UseSubscriptionError else { - XCTFail("Unexpected error type") - return - } - - XCTAssertEqual(error, .subscriptionExpired) - XCTAssertFalse(accountManager.isUserAuthenticated) - - XCTAssertEqual(feature.transactionStatus, .idle) - XCTAssertEqual(feature.transactionError, nil) - - await XCTAssertPrivacyPixelsFired([]) - } - } - - func testRestoreAccountFromAppStorePurchaseErrorDueToNoTransaction() async throws { - // Given - ensureUserUnauthenticatedState() - - storePurchaseManager.mostRecentTransactionResult = nil - - do { - // When - try await feature.restoreAccountFromAppStorePurchase() - XCTFail("Unexpected success") - } catch let error { - // Then - guard let error = error as? SubscriptionPagesUseSubscriptionFeature.UseSubscriptionError else { - XCTFail("Unexpected error type") - return - } - - XCTAssertEqual(error, .subscriptionNotFound) - XCTAssertFalse(accountManager.isUserAuthenticated) - - XCTAssertEqual(feature.transactionStatus, .idle) - XCTAssertEqual(feature.transactionError, nil) - - await XCTAssertPrivacyPixelsFired([]) - } - } - - func testRestoreAccountFromAppStorePurchaseErrorDueToOtherError() async throws { - // Given - ensureUserUnauthenticatedState() - - storePurchaseManager.mostRecentTransactionResult = Constants.mostRecentTransactionJWS - authService.storeLoginResult = .failure(Constants.invalidTokenError) - - do { - // When - try await feature.restoreAccountFromAppStorePurchase() - XCTFail("Unexpected success") - } catch let error { - // Then - guard let error = error as? SubscriptionPagesUseSubscriptionFeature.UseSubscriptionError else { - XCTFail("Unexpected error type") - return - } - - XCTAssertEqual(error, .failedToRestorePastPurchase) - XCTAssertFalse(accountManager.isUserAuthenticated) - - XCTAssertEqual(feature.transactionStatus, .idle) - XCTAssertEqual(feature.transactionError, nil) - - await XCTAssertPrivacyPixelsFired([]) - } - } -} - -extension SubscriptionPagesUseSubscriptionFeatureTests { - - func ensureUserAuthenticatedState() { - accountStorage.authToken = Constants.authToken - accountStorage.email = Constants.email - accountStorage.externalID = Constants.externalID - accessTokenStorage.accessToken = Constants.accessToken - } - - func ensureUserUnauthenticatedState() { - try? accessTokenStorage.removeAccessToken() - try? accountStorage.clearAuthenticationState() - } - - public func XCTAssertPrivacyPixelsFired(_ pixels: [String], file: StaticString = #file, line: UInt = #line) async { - try? await Task.sleep(seconds: 0.1) - - let pixelsFired = Set(pixelsFired) - let expectedPixels = Set(pixels) - - // Assert expected pixels were fired - XCTAssertTrue(expectedPixels.isSubset(of: pixelsFired), - "Expected Privacy Pro pixels were not fired: \(expectedPixels.subtracting(pixelsFired))", - file: file, - line: line) - - // Assert no other Privacy Pro pixels were fired except the expected - let privacyProPixelPrefix = "m_privacy-pro" - let otherPixels = pixelsFired.subtracting(expectedPixels) - let otherPrivacyProPixels = otherPixels.filter { $0.hasPrefix(privacyProPixelPrefix) } - XCTAssertTrue(otherPrivacyProPixels.isEmpty, - "Unexpected Privacy Pro pixels fired: \(otherPrivacyProPixels)", - file: file, - line: line) - } -} diff --git a/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift b/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift index 4399848fc6..97398284a3 100644 --- a/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift +++ b/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift @@ -31,13 +31,29 @@ 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 networkProtectionKeychainTokenStore: NetworkProtectionKeychainTokenStore private let configurationStore = ConfigurationStore() private let configurationManager: ConfigurationManager @@ -416,28 +432,55 @@ 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))) + 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 storePurchaseManager = DefaultStorePurchaseManager() + let subscriptionEndpointService = DefaultSubscriptionEndpointService(apiService: apiService, + baseURL: subscriptionEnvironment.serviceEnvironment.url) + let pixelHandler: SubscriptionManager.PixelHandler = { type in + switch type { + case .deadToken: + Pixel.fire(pixel: .privacyProDeadTokenDetected) + } + } + let subscriptionManager = DefaultSubscriptionManager(storePurchaseManager: storePurchaseManager, + oAuthClient: authClient, + subscriptionEndpointService: subscriptionEndpointService, + subscriptionEnvironment: subscriptionEnvironment, + pixelHandler: pixelHandler) + self.subscriptionManager = subscriptionManager let errorStore = NetworkProtectionTunnelErrorStore() let notificationsPresenter = NetworkProtectionUNNotificationPresenter() @@ -447,20 +490,32 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { wrappee: notificationsPresenter ) notificationsPresenter.requestAuthorization() + + self.networkProtectionKeychainTokenStore = NetworkProtectionKeychainTokenStore(accessTokenProvider: { + guard let token = subscriptionManager.getTokenContainerSynchronously(policy: .localValid)?.accessToken else { + Logger.networkProtection.error("NetworkProtectionKeychainTokenStore failed to provide token") + return nil + } + return token + }) + super.init(notificationsPresenter: notificationsPresenterDecorator, tunnelHealthStore: NetworkProtectionTunnelHealthStore(), controllerErrorStore: errorStore, snoozeTimingStore: NetworkProtectionSnoozeTimingStore(userDefaults: .networkProtectionGroupDefaults), wireGuardInterface: DefaultWireGuardInterface(), keychainType: .dataProtection(.unspecified), - tokenStore: tokenStore, + tokenStore: self.networkProtectionKeychainTokenStore, debugEvents: Self.networkProtectionDebugEvents(controllerErrorStore: errorStore), providerEvents: Self.packetTunnelProviderEvents, settings: settings, defaults: .networkProtectionGroupDefaults, - entitlementCheck: { return await Self.entitlementCheck(accountManager: accountManager) }) + entitlementCheck: { + let hasEntitlement = subscriptionManager.entitlements.contains(.networkProtection) + return .success(hasEntitlement) + } + ) - accountManager.delegate = self startMonitoringMemoryPressureEvents() observeServerChanges() APIRequest.Headers.setUserAgent(DefaultUserAgentManager.duckDuckGoUserAgent) @@ -513,19 +568,9 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { WidgetCenter.shared.reloadTimelines(ofKind: "VPNStatusWidget") } - 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) - } + private func entitlementCheck() async -> Result { + let hasEntitlement = self.subscriptionManager.entitlements.contains(.networkProtection) + return .success(hasEntitlement) } } @@ -559,17 +604,17 @@ final class DefaultWireGuardInterface: WireGuardInterface { } } -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) - } -} +// 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) +// } +// }