From e581911a00d3889f7510a0f74ae4c141bfa5d018 Mon Sep 17 00:00:00 2001 From: Federico Cappelli Date: Fri, 5 Jul 2024 11:05:21 +0100 Subject: [PATCH] Subscription refactoring #5 (#2930) Task/Issue URL: https://app.asana.com/0/1205842942115003/1206805455884775/f Tech Design URL: https://app.asana.com/0/1205842942115003/1207147511614062/f **Description**: This PR updates BSK from https://github.com/duckduckgo/BrowserServicesKit/pull/874 and contains all the needed refactoring for the Subscription classes inits. It also contains new unit test classes with example tests: - SubscriptionErrorReporterTests - SubscriptionAppStoreRestorerTests - SubscriptionRedirectManager Complete tests will be implemented in follow-up tasks **Steps to test this PR**: Subscription must work as usual, manual smoke test script available in this [README](https://github.com/duckduckgo/BrowserServicesKit/blob/main/Sources/SubscriptionTestingUtilities/README.md) --- DuckDuckGo.xcodeproj/project.pbxproj | 78 +-- .../xcshareddata/swiftpm/Package.resolved | 6 +- .../Extensions/TimeIntervalExtension.swift | 39 -- .../View/PreferencesRootView.swift | 29 +- .../View/PreferencesViewController.swift | 5 +- .../SubscriptionRedirectManager.swift | 2 +- .../SubscriptionAppStoreRestorer.swift | 22 +- .../SubscriptionErrorReporter.swift | 6 +- ...scriptionPagesUseSubscriptionFeature.swift | 497 ++++++++++++++++++ .../SubscriptionPagesUserScript.swift | 463 +--------------- DuckDuckGo/Tab/UserScripts/UserScripts.swift | 6 +- .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- .../DebugMenu/SubscriptionDebugMenu.swift | 16 +- .../PreferencesSubscriptionModel.swift | 9 +- UnitTests/Menus/MoreOptionsMenuTests.swift | 2 +- .../SubscriptionFeatureAvailabilityMock.swift | 0 .../SubscriptionUIHandlerMock.swift | 8 +- .../SubscriptionAppStoreRestorerTests.swift | 89 ++++ .../SubscriptionErrorReporterTests.swift | 65 +++ 21 files changed, 779 insertions(+), 569 deletions(-) delete mode 100644 DuckDuckGo/Common/Extensions/TimeIntervalExtension.swift create mode 100644 DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUseSubscriptionFeature.swift rename UnitTests/Subscription/{ => Mocks}/SubscriptionFeatureAvailabilityMock.swift (100%) rename UnitTests/Subscription/{ => Mocks}/SubscriptionUIHandlerMock.swift (87%) create mode 100644 UnitTests/Subscription/SubscriptionAppStoreRestorerTests.swift create mode 100644 UnitTests/Subscription/SubscriptionErrorReporterTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index f9f73e8ff9..7c5522665f 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -920,7 +920,6 @@ 3707C722294B5D2900682A9F /* WKWebViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA92127625ADA07900600CD4 /* WKWebViewExtension.swift */; }; 3707C723294B5D2900682A9F /* URLSessionExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6DB3AEE278D5C370024C5C4 /* URLSessionExtension.swift */; }; 3707C724294B5D2900682A9F /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA8EDF2624923EC70071C2E8 /* StringExtension.swift */; }; - 3707C726294B5D2900682A9F /* TimeIntervalExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAADFD05264AA282001555EA /* TimeIntervalExtension.swift */; }; 3707C727294B5D2900682A9F /* WKWebView+SessionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B63D466825BEB6C200874977 /* WKWebView+SessionState.swift */; }; 3707C728294B5D2900682A9F /* WKWebViewConfigurationExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68458CC25C7EB9000DC17B6 /* WKWebViewConfigurationExtensions.swift */; }; 3707C72A294B5D2900682A9F /* URLExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA8EDF2324923E980071C2E8 /* URLExtension.swift */; }; @@ -1967,7 +1966,6 @@ AAA892EA250A4CEF005B37B2 /* WindowControllersManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA892E9250A4CEF005B37B2 /* WindowControllersManager.swift */; }; AAAB9114288EB1D600A057A9 /* CleanThisHistoryMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAAB9113288EB1D600A057A9 /* CleanThisHistoryMenuItem.swift */; }; AAAB9116288EB46B00A057A9 /* VisitMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAAB9115288EB46B00A057A9 /* VisitMenuItem.swift */; }; - AAADFD06264AA282001555EA /* TimeIntervalExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAADFD05264AA282001555EA /* TimeIntervalExtension.swift */; }; AAB549DF25DAB8F80058460B /* BookmarkViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAB549DE25DAB8F80058460B /* BookmarkViewModel.swift */; }; AAB7320726DD0C37002FACF9 /* Fire.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AAB7320626DD0C37002FACF9 /* Fire.storyboard */; }; AAB7320926DD0CD9002FACF9 /* FireViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAB7320826DD0CD9002FACF9 /* FireViewController.swift */; }; @@ -2623,6 +2621,8 @@ F18826912BC0105800D9AC4F /* PixelDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6DA44012616B28300DD1EC2 /* PixelDataStore.swift */; }; F18826922BC0105900D9AC4F /* PixelDataRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68C92C32750EF76002AC6B0 /* PixelDataRecord.swift */; }; F18826932BC0105900D9AC4F /* PixelDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6DA44012616B28300DD1EC2 /* PixelDataStore.swift */; }; + F19547A92C33FA4C0041ACC9 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = F19547A82C33FA4C0041ACC9 /* Subscription */; }; + F19547AB2C33FA650041ACC9 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = F19547AA2C33FA650041ACC9 /* Subscription */; }; F198C7122BD18A28000BF24D /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = F198C7112BD18A28000BF24D /* PixelKit */; }; F198C7142BD18A30000BF24D /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = F198C7132BD18A30000BF24D /* PixelKit */; }; F198C7162BD18A44000BF24D /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = F198C7152BD18A44000BF24D /* PixelKit */; }; @@ -2631,14 +2631,16 @@ F198C71C2BD18A61000BF24D /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = F198C71B2BD18A61000BF24D /* PixelKit */; }; F198C71E2BD18D88000BF24D /* SwiftLintTool in Frameworks */ = {isa = PBXBuildFile; productRef = F198C71D2BD18D88000BF24D /* SwiftLintTool */; }; F198C7202BD18D92000BF24D /* SwiftLintTool in Frameworks */ = {isa = PBXBuildFile; productRef = F198C71F2BD18D92000BF24D /* SwiftLintTool */; }; + F1AFDBD22C231B7A00710F2C /* SubscriptionAppStoreRestorerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1AFDBD12C231B7A00710F2C /* SubscriptionAppStoreRestorerTests.swift */; }; + F1AFDBD42C231B9700710F2C /* SubscriptionErrorReporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1AFDBD32C231B9700710F2C /* SubscriptionErrorReporterTests.swift */; }; + F1AFDBD72C23221700710F2C /* SubscriptionErrorReporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1AFDBD32C231B9700710F2C /* SubscriptionErrorReporterTests.swift */; }; + F1AFDBD92C23221700710F2C /* SubscriptionAppStoreRestorerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1AFDBD12C231B7A00710F2C /* SubscriptionAppStoreRestorerTests.swift */; }; F1B33DF22BAD929D001128B3 /* SubscriptionAppStoreRestorer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B33DF12BAD929D001128B3 /* SubscriptionAppStoreRestorer.swift */; }; F1B33DF32BAD929D001128B3 /* SubscriptionAppStoreRestorer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B33DF12BAD929D001128B3 /* SubscriptionAppStoreRestorer.swift */; }; F1B33DF62BAD970E001128B3 /* SubscriptionErrorReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B33DF52BAD970E001128B3 /* SubscriptionErrorReporter.swift */; }; F1B33DF72BAD970E001128B3 /* SubscriptionErrorReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B33DF52BAD970E001128B3 /* SubscriptionErrorReporter.swift */; }; F1B8EC7A2C29957A00D395F5 /* SubscriptionFeatureAvailabilityMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B8EC792C29957A00D395F5 /* SubscriptionFeatureAvailabilityMock.swift */; }; F1B8EC7B2C29957A00D395F5 /* SubscriptionFeatureAvailabilityMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B8EC792C29957A00D395F5 /* SubscriptionFeatureAvailabilityMock.swift */; }; - F1B8EC7C2C29958900D395F5 /* SubscriptionFeatureAvailabilityMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B8EC792C29957A00D395F5 /* SubscriptionFeatureAvailabilityMock.swift */; }; - F1B8EC7D2C29958A00D395F5 /* SubscriptionFeatureAvailabilityMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B8EC792C29957A00D395F5 /* SubscriptionFeatureAvailabilityMock.swift */; }; F1C5763E2BFF972900C78647 /* SubscriptionUIHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1C5763D2BFF972900C78647 /* SubscriptionUIHandling.swift */; }; F1C5763F2BFF972900C78647 /* SubscriptionUIHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1C5763D2BFF972900C78647 /* SubscriptionUIHandling.swift */; }; F1C70D792BFF50A400599292 /* DataBrokerProtectionLoginItemInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1C70D782BFF50A400599292 /* DataBrokerProtectionLoginItemInterface.swift */; }; @@ -2685,18 +2687,16 @@ F1DA51972BF6083A00CF29FA /* PrivacyProPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F188267F2BBEB58100D9AC4F /* PrivacyProPixel.swift */; }; F1DA51982BF6083B00CF29FA /* PrivacyProPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F188267F2BBEB58100D9AC4F /* PrivacyProPixel.swift */; }; F1DA51992BF6083B00CF29FA /* PrivacyProPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F188267F2BBEB58100D9AC4F /* PrivacyProPixel.swift */; }; - F1DA51A32BF6114200CF29FA /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = F1DA51A22BF6114200CF29FA /* Subscription */; }; F1DA51A52BF6114200CF29FA /* SubscriptionTestingUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = F1DA51A42BF6114200CF29FA /* SubscriptionTestingUtilities */; }; - F1DA51A72BF6114B00CF29FA /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = F1DA51A62BF6114B00CF29FA /* Subscription */; }; F1DA51A92BF6114C00CF29FA /* SubscriptionTestingUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = F1DA51A82BF6114C00CF29FA /* SubscriptionTestingUtilities */; }; F1DF95E32BD1807C0045E591 /* Crashes in Frameworks */ = {isa = PBXBuildFile; productRef = 08D4923DC968236E22E373E2 /* Crashes */; }; F1DF95E42BD1807C0045E591 /* Crashes in Frameworks */ = {isa = PBXBuildFile; productRef = 537FC71EA5115A983FAF3170 /* Crashes */; }; F1DF95E52BD1807C0045E591 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = DC3F73D49B2D44464AFEFCD8 /* Subscription */; }; F1DF95E72BD188B60045E591 /* LoginItems in Frameworks */ = {isa = PBXBuildFile; productRef = F1DF95E62BD188B60045E591 /* LoginItems */; }; F1F861152C1B25D4005DB446 /* SubscriptionUIHandlerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1F861142C1B25D4005DB446 /* SubscriptionUIHandlerMock.swift */; }; - F1F861162C1B25D4005DB446 /* SubscriptionUIHandlerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1F861142C1B25D4005DB446 /* SubscriptionUIHandlerMock.swift */; }; F1F861172C1B25D4005DB446 /* SubscriptionUIHandlerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1F861142C1B25D4005DB446 /* SubscriptionUIHandlerMock.swift */; }; - F1F861182C1B25D4005DB446 /* SubscriptionUIHandlerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1F861142C1B25D4005DB446 /* SubscriptionUIHandlerMock.swift */; }; + F1FD5B672C2B0AAA0040FA0D /* SubscriptionPagesUseSubscriptionFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FD5B662C2B0AAA0040FA0D /* SubscriptionPagesUseSubscriptionFeature.swift */; }; + F1FD5B682C2B0AAA0040FA0D /* SubscriptionPagesUseSubscriptionFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FD5B662C2B0AAA0040FA0D /* SubscriptionPagesUseSubscriptionFeature.swift */; }; F1FDC9382BF51F41006B1435 /* VPNSettings+Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9372BF51F41006B1435 /* VPNSettings+Environment.swift */; }; F1FDC9392BF51F41006B1435 /* VPNSettings+Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9372BF51F41006B1435 /* VPNSettings+Environment.swift */; }; F1FDC93A2BF51F41006B1435 /* VPNSettings+Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9372BF51F41006B1435 /* VPNSettings+Environment.swift */; }; @@ -3766,7 +3766,6 @@ AAA892E9250A4CEF005B37B2 /* WindowControllersManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowControllersManager.swift; sourceTree = ""; }; AAAB9113288EB1D600A057A9 /* CleanThisHistoryMenuItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CleanThisHistoryMenuItem.swift; sourceTree = ""; }; AAAB9115288EB46B00A057A9 /* VisitMenuItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisitMenuItem.swift; sourceTree = ""; }; - AAADFD05264AA282001555EA /* TimeIntervalExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeIntervalExtension.swift; sourceTree = ""; }; AAB549DE25DAB8F80058460B /* BookmarkViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkViewModel.swift; sourceTree = ""; }; AAB7320626DD0C37002FACF9 /* Fire.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Fire.storyboard; sourceTree = ""; }; AAB7320826DD0CD9002FACF9 /* FireViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FireViewController.swift; sourceTree = ""; }; @@ -4189,6 +4188,8 @@ F188267B2BBEB3AA00D9AC4F /* GeneralPixel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralPixel.swift; sourceTree = ""; }; F188267F2BBEB58100D9AC4F /* PrivacyProPixel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyProPixel.swift; sourceTree = ""; }; F18826832BBEE31700D9AC4F /* PixelKit+Assertion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "PixelKit+Assertion.swift"; sourceTree = ""; }; + F1AFDBD12C231B7A00710F2C /* SubscriptionAppStoreRestorerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionAppStoreRestorerTests.swift; sourceTree = ""; }; + F1AFDBD32C231B9700710F2C /* SubscriptionErrorReporterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionErrorReporterTests.swift; sourceTree = ""; }; F1B33DF12BAD929D001128B3 /* SubscriptionAppStoreRestorer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionAppStoreRestorer.swift; sourceTree = ""; }; F1B33DF52BAD970E001128B3 /* SubscriptionErrorReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionErrorReporter.swift; sourceTree = ""; }; F1B8EC792C29957A00D395F5 /* SubscriptionFeatureAvailabilityMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionFeatureAvailabilityMock.swift; sourceTree = ""; }; @@ -4201,6 +4202,7 @@ F1DA51842BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionAttributionPixelHandler.swift; sourceTree = ""; }; F1DA51852BF607D200CF29FA /* SubscriptionRedirectManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionRedirectManager.swift; sourceTree = ""; }; F1F861142C1B25D4005DB446 /* SubscriptionUIHandlerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionUIHandlerMock.swift; sourceTree = ""; }; + F1FD5B662C2B0AAA0040FA0D /* SubscriptionPagesUseSubscriptionFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionPagesUseSubscriptionFeature.swift; sourceTree = ""; }; F1FDC9372BF51F41006B1435 /* VPNSettings+Environment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "VPNSettings+Environment.swift"; sourceTree = ""; }; F41D174025CB131900472416 /* NSColorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSColorExtension.swift; sourceTree = ""; }; F44C130125C2DA0400426E3E /* NSAppearanceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSAppearanceExtension.swift; sourceTree = ""; }; @@ -4254,10 +4256,10 @@ buildActionMask = 2147483647; files = ( 3706FE88293F661700E42796 /* OHHTTPStubs in Frameworks */, + F19547AB2C33FA650041ACC9 /* Subscription in Frameworks */, F116A7C72BD1925500F3FCF7 /* PixelKitTestingUtilities in Frameworks */, B65CD8CF2B316E0200A595BB /* SnapshotTesting in Frameworks */, F1DA51A52BF6114200CF29FA /* SubscriptionTestingUtilities in Frameworks */, - F1DA51A32BF6114200CF29FA /* Subscription in Frameworks */, 3706FE89293F661700E42796 /* OHHTTPStubsSwift in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4456,10 +4458,10 @@ buildActionMask = 2147483647; files = ( B6DA44172616C13800DD1EC2 /* OHHTTPStubs in Frameworks */, + F19547A92C33FA4C0041ACC9 /* Subscription in Frameworks */, F116A7C32BD1924B00F3FCF7 /* PixelKitTestingUtilities in Frameworks */, B65CD8CB2B316DF100A595BB /* SnapshotTesting in Frameworks */, F1DA51A92BF6114C00CF29FA /* SubscriptionTestingUtilities in Frameworks */, - F1DA51A72BF6114B00CF29FA /* Subscription in Frameworks */, B6DA44192616C13800DD1EC2 /* OHHTTPStubsSwift in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -6482,10 +6484,11 @@ 9F64346E2BECB9FB00D2D8A0 /* Subscription */ = { isa = PBXGroup; children = ( - F1B8EC792C29957A00D395F5 /* SubscriptionFeatureAvailabilityMock.swift */, + F16F92812C2AC0080007C93E /* Mocks */, 9F64346F2BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift */, 9F0660722BECC71200B8EEF1 /* SubscriptionAttributionPixelHandlerTests.swift */, - F1F861142C1B25D4005DB446 /* SubscriptionUIHandlerMock.swift */, + F1AFDBD12C231B7A00710F2C /* SubscriptionAppStoreRestorerTests.swift */, + F1AFDBD32C231B9700710F2C /* SubscriptionErrorReporterTests.swift */, ); path = Subscription; sourceTree = ""; @@ -7611,7 +7614,6 @@ B65783E625F8AAFB00D8DB33 /* String+Punycode.swift */, AA8EDF2624923EC70071C2E8 /* StringExtension.swift */, B6BCC5492AFDF24B002C5499 /* TaskWithProgress.swift */, - AAADFD05264AA282001555EA /* TimeIntervalExtension.swift */, AA8EDF2324923E980071C2E8 /* URLExtension.swift */, AA88D14A252A557100980B4E /* URLRequestExtension.swift */, B6DB3AEE278D5C370024C5C4 /* URLSessionExtension.swift */, @@ -8393,10 +8395,20 @@ path = Subscription; sourceTree = ""; }; + F16F92812C2AC0080007C93E /* Mocks */ = { + isa = PBXGroup; + children = ( + F1B8EC792C29957A00D395F5 /* SubscriptionFeatureAvailabilityMock.swift */, + F1F861142C1B25D4005DB446 /* SubscriptionUIHandlerMock.swift */, + ); + path = Mocks; + sourceTree = ""; + }; F1B33DF92BAD9C83001128B3 /* Subscription */ = { isa = PBXGroup; children = ( 1E0C72052ABC63BD00802009 /* SubscriptionPagesUserScript.swift */, + F1FD5B662C2B0AAA0040FA0D /* SubscriptionPagesUseSubscriptionFeature.swift */, F1C5763D2BFF972900C78647 /* SubscriptionUIHandling.swift */, F1B33DF12BAD929D001128B3 /* SubscriptionAppStoreRestorer.swift */, F1B33DF52BAD970E001128B3 /* SubscriptionErrorReporter.swift */, @@ -8485,8 +8497,8 @@ 3706FDD8293F661700E42796 /* OHHTTPStubsSwift */, B65CD8CE2B316E0200A595BB /* SnapshotTesting */, F116A7C62BD1925500F3FCF7 /* PixelKitTestingUtilities */, - F1DA51A22BF6114200CF29FA /* Subscription */, F1DA51A42BF6114200CF29FA /* SubscriptionTestingUtilities */, + F19547AA2C33FA650041ACC9 /* Subscription */, ); productName = DuckDuckGoTests; productReference = 3706FE99293F661700E42796 /* Unit Tests App Store.xctest */; @@ -8888,8 +8900,8 @@ B6DA44182616C13800DD1EC2 /* OHHTTPStubsSwift */, B65CD8CA2B316DF100A595BB /* SnapshotTesting */, F116A7C22BD1924B00F3FCF7 /* PixelKitTestingUtilities */, - F1DA51A62BF6114B00CF29FA /* Subscription */, F1DA51A82BF6114C00CF29FA /* SubscriptionTestingUtilities */, + F19547A82C33FA4C0041ACC9 /* Subscription */, ); productName = DuckDuckGoTests; productReference = AA585D90248FD31400E9A3E2 /* Unit Tests.xctest */; @@ -9953,7 +9965,6 @@ 3706FB59293F65D500E42796 /* TemporaryFileHandler.swift in Sources */, 37197EA62942443D00394917 /* WebViewSnapshotView.swift in Sources */, 3706FB5A293F65D500E42796 /* PrivacyFeatures.swift in Sources */, - 3707C726294B5D2900682A9F /* TimeIntervalExtension.swift in Sources */, 3706FB5C293F65D500E42796 /* AVCaptureDevice+SwizzledAuthState.swift in Sources */, 3706FB5D293F65D500E42796 /* VisitMenuItem.swift in Sources */, 3706FB5E293F65D500E42796 /* EncryptionKeyStore.swift in Sources */, @@ -10400,6 +10411,7 @@ 3706FC7F293F65D500E42796 /* Tab.swift in Sources */, 3706FC81293F65D500E42796 /* DispatchQueueExtensions.swift in Sources */, C13909F02B85FD4E001626ED /* AutofillActionExecutor.swift in Sources */, + F1FD5B682C2B0AAA0040FA0D /* SubscriptionPagesUseSubscriptionFeature.swift in Sources */, 3706FC82293F65D500E42796 /* PermissionAuthorizationPopover.swift in Sources */, 4BCBE4552BA7E16600FC75A1 /* NetworkProtectionSubscriptionEventHandler.swift in Sources */, 3706FC83293F65D500E42796 /* PopoverMessageViewController.swift in Sources */, @@ -10702,6 +10714,7 @@ 3706FE6B293F661700E42796 /* AppKitPrivateMethodsAvailabilityTests.swift in Sources */, 3706FE6D293F661700E42796 /* ChromiumBookmarksReaderTests.swift in Sources */, C1E961F42B87B276001760E1 /* MockAutofillActionPresenter.swift in Sources */, + F1AFDBD72C23221700710F2C /* SubscriptionErrorReporterTests.swift in Sources */, 3706FE6E293F661700E42796 /* FirefoxBookmarksReaderTests.swift in Sources */, 4B9DB05B2A983B55000927DB /* MockWaitlistRequest.swift in Sources */, 1D9FDEBE2B9B5F0F0040B78C /* CookiePopupProtectionPreferencesTests.swift in Sources */, @@ -10718,6 +10731,7 @@ 56D145EF29E6DAD900E3488A /* DataImportProviderTests.swift in Sources */, 569277C529DEE09D00B633EF /* ContinueSetUpModelTests.swift in Sources */, 3706FE76293F661700E42796 /* MockSecureVault.swift in Sources */, + F1AFDBD92C23221700710F2C /* SubscriptionAppStoreRestorerTests.swift in Sources */, C1E961F32B87B273001760E1 /* MockAutofillActionExecutor.swift in Sources */, 376E2D2729428353001CD31B /* BrokenSiteReportingReferenceTests.swift in Sources */, 3707C72F294B5D4F00682A9F /* WebViewTests.swift in Sources */, @@ -10773,7 +10787,6 @@ B603973529BEF86200902A34 /* HTTPSUpgradeIntegrationTests.swift in Sources */, B644B44029D57299003FA9AB /* SuggestionLoadingMock.swift in Sources */, 1D8B7D6B2A38BF060045C6F6 /* FireproofDomainsStoreMock.swift in Sources */, - F1F861182C1B25D4005DB446 /* SubscriptionUIHandlerMock.swift in Sources */, B630E80229C887ED00363609 /* NSErrorAdditionalInfo.swift in Sources */, 3706FEA3293F662100E42796 /* CoreDataEncryptionTests.swift in Sources */, B60C6F8929B1CAB7007BFAA8 /* TestRunHelperInitializer.m in Sources */, @@ -10790,7 +10803,6 @@ B603972D29BEDF2100902A34 /* ExpectedNavigationExtension.swift in Sources */, 3706FEA6293F662100E42796 /* EncryptionKeyStoreTests.swift in Sources */, B6F5656A299A414300A04298 /* WKWebViewMockingExtension.swift in Sources */, - F1B8EC7D2C29958A00D395F5 /* SubscriptionFeatureAvailabilityMock.swift in Sources */, B693766F2B6B5F27005BD9D4 /* ErrorPageTests.swift in Sources */, 56A054362C205820007D8FAB /* OnboardingPageTests.swift in Sources */, ); @@ -10818,7 +10830,6 @@ B603973429BEF86200902A34 /* HTTPSUpgradeIntegrationTests.swift in Sources */, 4B1AD91725FC46FB00261379 /* CoreDataEncryptionTests.swift in Sources */, B644B43F29D57298003FA9AB /* SuggestionLoadingMock.swift in Sources */, - F1F861162C1B25D4005DB446 /* SubscriptionUIHandlerMock.swift in Sources */, 1D8B7D6A2A38BF050045C6F6 /* FireproofDomainsStoreMock.swift in Sources */, B630E7FF29C887ED00363609 /* NSErrorAdditionalInfo.swift in Sources */, 7BA4727D26F01BC400EAA165 /* CoreDataTestUtilities.swift in Sources */, @@ -10835,7 +10846,6 @@ B603972C29BEDF2100902A34 /* ExpectedNavigationExtension.swift in Sources */, 4B1AD8D525FC38DD00261379 /* EncryptionKeyStoreTests.swift in Sources */, B6F56568299A414300A04298 /* WKWebViewMockingExtension.swift in Sources */, - F1B8EC7C2C29958900D395F5 /* SubscriptionFeatureAvailabilityMock.swift in Sources */, B693766E2B6B5F27005BD9D4 /* ErrorPageTests.swift in Sources */, 56A054352C20581F007D8FAB /* OnboardingPageTests.swift in Sources */, ); @@ -11406,11 +11416,11 @@ AA5FA69D275F945C00DCE9C9 /* FaviconStore.swift in Sources */, 4B9DB0352A983B24000927DB /* WaitlistTermsAndConditionsView.swift in Sources */, AAB8203C26B2DE0D00788AC3 /* SuggestionListCharacteristics.swift in Sources */, - AAADFD06264AA282001555EA /* TimeIntervalExtension.swift in Sources */, 4B6785472AA8DE68008A5004 /* VPNUninstaller.swift in Sources */, 4B0526642B1D55D80054955A /* VPNFeedbackCategory.swift in Sources */, 4B9292D42667123700AD2C21 /* BookmarkListViewController.swift in Sources */, 9F56CFB12B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift in Sources */, + F1FD5B672C2B0AAA0040FA0D /* SubscriptionPagesUseSubscriptionFeature.swift in Sources */, 4B723E0D26B0006100E14D75 /* SecureVaultLoginImporter.swift in Sources */, B645D8F629FA95440024461F /* WKProcessPoolExtension.swift in Sources */, 9D9AE86B2AA76CF90026E7DC /* LoginItemsManager.swift in Sources */, @@ -11925,6 +11935,7 @@ B69B50462726C5C200758A2B /* AtbAndVariantCleanupTests.swift in Sources */, 567DA94529E95C3F008AC5EE /* YoutubeOverlayUserScriptTests.swift in Sources */, 4B59024C26B38BB800489384 /* ChromiumLoginReaderTests.swift in Sources */, + F1AFDBD42C231B9700710F2C /* SubscriptionErrorReporterTests.swift in Sources */, 1D8C2FEA2B70F5A7005E4BBD /* MockWebViewSnapshotRenderer.swift in Sources */, AAC9C01724CAFBDC00AD1325 /* TabCollectionTests.swift in Sources */, 378205F8283BC6A600D1D4AA /* StartupPreferencesTests.swift in Sources */, @@ -12096,6 +12107,7 @@ B603971029B9D67E00902A34 /* PublishersExtensions.swift in Sources */, 9F26060E2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift in Sources */, 4B9292C32667103100AD2C21 /* PasteboardBookmarkTests.swift in Sources */, + F1AFDBD22C231B7A00710F2C /* SubscriptionAppStoreRestorerTests.swift in Sources */, B610F2E427A8F37A00FCEBE9 /* CBRCompileTimeReporterTests.swift in Sources */, AABAF59C260A7D130085060C /* FaviconManagerMock.swift in Sources */, 1DFAB5222A8983DE00A0F7F6 /* SetExtensionTests.swift in Sources */, @@ -13166,7 +13178,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 164.3.0; + version = 165.0.0; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { @@ -13793,6 +13805,16 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = PixelKitTestingUtilities; }; + F19547A82C33FA4C0041ACC9 /* Subscription */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Subscription; + }; + F19547AA2C33FA650041ACC9 /* Subscription */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Subscription; + }; F198C7112BD18A28000BF24D /* PixelKit */ = { isa = XCSwiftPackageProductDependency; package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; @@ -13853,21 +13875,11 @@ package = F1D43AF12B98E47800BAB743 /* XCRemoteSwiftPackageReference "BareBonesBrowser" */; productName = BareBonesBrowserKit; }; - F1DA51A22BF6114200CF29FA /* Subscription */ = { - isa = XCSwiftPackageProductDependency; - package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; - productName = Subscription; - }; F1DA51A42BF6114200CF29FA /* SubscriptionTestingUtilities */ = { isa = XCSwiftPackageProductDependency; package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = SubscriptionTestingUtilities; }; - F1DA51A62BF6114B00CF29FA /* Subscription */ = { - isa = XCSwiftPackageProductDependency; - package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; - productName = Subscription; - }; F1DA51A82BF6114C00CF29FA /* SubscriptionTestingUtilities */ = { isa = XCSwiftPackageProductDependency; package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index aba1b5ff7d..85931bed6c 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" : "28dd48c5aca37c46402e2a14f7c47aad3877b3aa", - "version" : "164.3.0" + "revision" : "777e5ae1ab890d9ec22e069bc5dc0f0ada4b35af", + "version" : "165.0.0" } }, { @@ -75,7 +75,7 @@ { "identity" : "lottie-spm", "kind" : "remoteSourceControl", - "location" : "https://github.com/airbnb/lottie-spm", + "location" : "https://github.com/airbnb/lottie-spm.git", "state" : { "revision" : "1d29eccc24cc8b75bff9f6804155112c0ffc9605", "version" : "4.4.3" diff --git a/DuckDuckGo/Common/Extensions/TimeIntervalExtension.swift b/DuckDuckGo/Common/Extensions/TimeIntervalExtension.swift deleted file mode 100644 index 0e9faa4710..0000000000 --- a/DuckDuckGo/Common/Extensions/TimeIntervalExtension.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// TimeIntervalExtension.swift -// -// Copyright © 2021 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 - -extension TimeInterval { - static let day = days(1) - - static func seconds(_ amount: Int) -> TimeInterval { - TimeInterval(amount) - } - - static func minutes(_ amount: Int) -> TimeInterval { - .seconds(60) * TimeInterval(amount) - } - - static func hours(_ amount: Int) -> TimeInterval { - .minutes(60) * TimeInterval(amount) - } - - static func days(_ amount: Int) -> TimeInterval { - .hours(24) * TimeInterval(amount) - } -} diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index 330d94f1b1..4658eb92b5 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -47,9 +47,13 @@ enum Preferences { @ObservedObject var model: PreferencesSidebarModel var subscriptionModel: PreferencesSubscriptionModel? + let subscriptionManager: SubscriptionManager + let subscriptionUIHandler: SubscriptionUIHandling - init(model: PreferencesSidebarModel) { + init(model: PreferencesSidebarModel, subscriptionManager: SubscriptionManager, subscriptionUIHandler: SubscriptionUIHandling) { self.model = model + self.subscriptionManager = subscriptionManager + self.subscriptionUIHandler = subscriptionUIHandler self.subscriptionModel = makeSubscriptionViewModel() } @@ -138,7 +142,7 @@ enum Preferences { WindowControllersManager.shared.showTab(with: .dataBrokerProtection) case .openITR: PixelKit.fire(PrivacyProPixel.privacyProIdentityRestorationSettings) - let url = Application.appDelegate.subscriptionManager.url(for: .identityTheftRestoration) + let url = subscriptionManager.url(for: .identityTheftRestoration) WindowControllersManager.shared.showTab(with: .identityTheftRestoration(url)) case .iHaveASubscriptionClick: PixelKit.fire(PrivacyProPixel.privacyProRestorePurchaseClick) @@ -162,25 +166,28 @@ enum Preferences { let sheetActionHandler = SubscriptionAccessActionHandlers( openActivateViaEmailURL: { - let url = Application.appDelegate.subscriptionManager.url(for: .activateViaEmail) + let url = subscriptionManager.url(for: .activateViaEmail) WindowControllersManager.shared.showTab(with: .subscription(url)) - }, - restorePurchases: { + }, restorePurchases: { if #available(macOS 12.0, *) { Task { - let subscriptionAppStoreRestorer = SubscriptionAppStoreRestorer(subscriptionManager: Application.appDelegate.subscriptionManager, - uiHandler: Application.appDelegate.subscriptionUIHandler) + let appStoreRestoreFlow = DefaultAppStoreRestoreFlow(accountManager: subscriptionManager.accountManager, + storePurchaseManager: subscriptionManager.storePurchaseManager(), + subscriptionEndpointService: subscriptionManager.subscriptionEndpointService, + authEndpointService: subscriptionManager.authEndpointService) + let subscriptionAppStoreRestorer = DefaultSubscriptionAppStoreRestorer( + subscriptionManager: subscriptionManager, + appStoreRestoreFlow: appStoreRestoreFlow, + uiHandler: subscriptionUIHandler) await subscriptionAppStoreRestorer.restoreAppStoreSubscription() } } - }, - uiActionHandler: handleUIEvent - ) + }, uiActionHandler: handleUIEvent) return PreferencesSubscriptionModel(openURLHandler: openURL, userEventHandler: handleUIEvent, sheetActionHandler: sheetActionHandler, - subscriptionManager: Application.appDelegate.subscriptionManager) + subscriptionManager: subscriptionManager) } } } diff --git a/DuckDuckGo/Preferences/View/PreferencesViewController.swift b/DuckDuckGo/Preferences/View/PreferencesViewController.swift index 7520ff043a..3f46451624 100644 --- a/DuckDuckGo/Preferences/View/PreferencesViewController.swift +++ b/DuckDuckGo/Preferences/View/PreferencesViewController.swift @@ -51,7 +51,10 @@ final class PreferencesViewController: NSViewController { override func viewDidLoad() { super.viewDidLoad() - let host = NSHostingView(rootView: Preferences.RootView(model: model)) + let prefRootView = Preferences.RootView(model: model, + subscriptionManager: Application.appDelegate.subscriptionManager, + subscriptionUIHandler: Application.appDelegate.subscriptionUIHandler) + let host = NSHostingView(rootView: prefRootView) view.addAndLayout(host) } diff --git a/DuckDuckGo/Subscription/SubscriptionRedirectManager.swift b/DuckDuckGo/Subscription/SubscriptionRedirectManager.swift index 2e9c3d1f62..2a5bdfb4e4 100644 --- a/DuckDuckGo/Subscription/SubscriptionRedirectManager.swift +++ b/DuckDuckGo/Subscription/SubscriptionRedirectManager.swift @@ -56,7 +56,7 @@ final class PrivacyProSubscriptionRedirectManager: SubscriptionRedirectManager { } } -private extension URL { +fileprivate extension URL { func addingQueryItems(from url: URL) -> URL { // If the origin value is of type "do+something" appending the percentEncodedQueryItem crashes the browser as + is replaced by a space. diff --git a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionAppStoreRestorer.swift b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionAppStoreRestorer.swift index abd215ec5e..12a157cd95 100644 --- a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionAppStoreRestorer.swift +++ b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionAppStoreRestorer.swift @@ -23,16 +23,25 @@ import enum StoreKit.StoreKitError import PixelKit @available(macOS 12.0, *) -struct SubscriptionAppStoreRestorer { +protocol SubscriptionAppStoreRestorer { + var uiHandler: SubscriptionUIHandling { get } + func restoreAppStoreSubscription() async +} +@available(macOS 12.0, *) +struct DefaultSubscriptionAppStoreRestorer: SubscriptionAppStoreRestorer { private let subscriptionManager: SubscriptionManager - @MainActor var window: NSWindow? { WindowControllersManager.shared.lastKeyMainWindowController?.window } - let subscriptionErrorReporter = SubscriptionErrorReporter() + private let subscriptionErrorReporter: SubscriptionErrorReporter + private let appStoreRestoreFlow: AppStoreRestoreFlow let uiHandler: SubscriptionUIHandling public init(subscriptionManager: SubscriptionManager, + subscriptionErrorReporter: SubscriptionErrorReporter = DefaultSubscriptionErrorReporter(), + appStoreRestoreFlow: AppStoreRestoreFlow, uiHandler: SubscriptionUIHandling) { self.subscriptionManager = subscriptionManager + self.subscriptionErrorReporter = subscriptionErrorReporter + self.appStoreRestoreFlow = appStoreRestoreFlow self.uiHandler = uiHandler } @@ -59,7 +68,6 @@ struct SubscriptionAppStoreRestorer { } private func continueRestore() async { - let appStoreRestoreFlow = DefaultAppStoreRestoreFlow(subscriptionManager: subscriptionManager) let result = await appStoreRestoreFlow.restoreAccountFromPastPurchase() await uiHandler.dismissProgressViewController() switch result { @@ -87,12 +95,12 @@ struct SubscriptionAppStoreRestorer { // MARK: - UI interactions - func showSomethingWentWrongAlert() async { + private func showSomethingWentWrongAlert() async { PixelKit.fire(PrivacyProPixel.privacyProPurchaseFailure, frequency: .dailyAndCount) await uiHandler.show(alertType: .somethingWentWrong) } - func showSubscriptionNotFoundAlert() async { + private func showSubscriptionNotFoundAlert() async { switch await uiHandler.show(alertType: .subscriptionNotFound) { case .alertFirstButtonReturn: let url = subscriptionManager.url(for: .purchase) @@ -102,7 +110,7 @@ struct SubscriptionAppStoreRestorer { } } - func showSubscriptionInactiveAlert() async { + private func showSubscriptionInactiveAlert() async { switch await uiHandler.show(alertType: .subscriptionInactive) { case .alertFirstButtonReturn: let url = subscriptionManager.url(for: .purchase) diff --git a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionErrorReporter.swift b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionErrorReporter.swift index 874e5ab9cb..27a88fec6a 100644 --- a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionErrorReporter.swift +++ b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionErrorReporter.swift @@ -37,7 +37,11 @@ enum SubscriptionError: Error { generalError } -struct SubscriptionErrorReporter { +protocol SubscriptionErrorReporter { + func report(subscriptionActivationError: SubscriptionError) +} + +struct DefaultSubscriptionErrorReporter: SubscriptionErrorReporter { // swiftlint:disable:next cyclomatic_complexity func report(subscriptionActivationError: SubscriptionError) { diff --git a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUseSubscriptionFeature.swift b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUseSubscriptionFeature.swift new file mode 100644 index 0000000000..812e03026b --- /dev/null +++ b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUseSubscriptionFeature.swift @@ -0,0 +1,497 @@ +// +// SubscriptionPagesUseSubscriptionFeature.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import BrowserServicesKit +import Common +import Combine +import WebKit +import UserScript +import Subscription +import PixelKit + +/// Use Subscription sub-feature +final class SubscriptionPagesUseSubscriptionFeature: Subfeature { + weak var broker: UserScriptMessageBroker? + var featureName = "useSubscription" + var messageOriginPolicy: MessageOriginPolicy = .only(rules: [ + .exact(hostname: "duckduckgo.com"), + .exact(hostname: "abrown.duckduckgo.com") + ]) + let subscriptionManager: SubscriptionManager + var accountManager: AccountManager { subscriptionManager.accountManager } + var subscriptionPlatform: SubscriptionEnvironment.PurchasePlatform { subscriptionManager.currentEnvironment.purchasePlatform } + + let stripePurchaseFlow: StripePurchaseFlow + let subscriptionErrorReporter = DefaultSubscriptionErrorReporter() + let subscriptionSuccessPixelHandler: SubscriptionAttributionPixelHandler + let uiHandler: SubscriptionUIHandling + + public init(subscriptionManager: SubscriptionManager, + subscriptionSuccessPixelHandler: SubscriptionAttributionPixelHandler = PrivacyProSubscriptionAttributionPixelHandler(), + stripePurchaseFlow: StripePurchaseFlow, + uiHandler: SubscriptionUIHandling) { + self.subscriptionManager = subscriptionManager + self.stripePurchaseFlow = stripePurchaseFlow + self.subscriptionSuccessPixelHandler = subscriptionSuccessPixelHandler + self.uiHandler = uiHandler + } + + func with(broker: UserScriptMessageBroker) { + self.broker = broker + } + + struct Handlers { + static let getSubscription = "getSubscription" + static let setSubscription = "setSubscription" + static let backToSettings = "backToSettings" + static let getSubscriptionOptions = "getSubscriptionOptions" + static let subscriptionSelected = "subscriptionSelected" + static let activateSubscription = "activateSubscription" + static let featureSelected = "featureSelected" + static let completeStripePayment = "completeStripePayment" + // Pixels related events + static let subscriptionsMonthlyPriceClicked = "subscriptionsMonthlyPriceClicked" + static let subscriptionsYearlyPriceClicked = "subscriptionsYearlyPriceClicked" + static let subscriptionsUnknownPriceClicked = "subscriptionsUnknownPriceClicked" + static let subscriptionsAddEmailSuccess = "subscriptionsAddEmailSuccess" + static let subscriptionsWelcomeFaqClicked = "subscriptionsWelcomeFaqClicked" + static let getAccessToken = "getAccessToken" + } + + // swiftlint:disable:next cyclomatic_complexity + func handler(forMethodNamed methodName: String) -> Subfeature.Handler? { + switch methodName { + case Handlers.getSubscription: return getSubscription + case Handlers.setSubscription: return setSubscription + case Handlers.backToSettings: return backToSettings + case Handlers.getSubscriptionOptions: return getSubscriptionOptions + case Handlers.subscriptionSelected: return subscriptionSelected + case Handlers.activateSubscription: return activateSubscription + case Handlers.featureSelected: return featureSelected + case Handlers.completeStripePayment: return completeStripePayment + // Pixel related events + case Handlers.subscriptionsMonthlyPriceClicked: return subscriptionsMonthlyPriceClicked + case Handlers.subscriptionsYearlyPriceClicked: return subscriptionsYearlyPriceClicked + case Handlers.subscriptionsUnknownPriceClicked: return subscriptionsUnknownPriceClicked + case Handlers.subscriptionsAddEmailSuccess: return subscriptionsAddEmailSuccess + case Handlers.subscriptionsWelcomeFaqClicked: return subscriptionsWelcomeFaqClicked + case Handlers.getAccessToken: return getAccessToken + default: + return nil + } + } + + struct Subscription: Encodable { + let token: String + } + + /// Values that the Frontend can use to determine the current state. + struct SubscriptionValues: Codable { + enum CodingKeys: String, CodingKey { + case token + } + let token: String + } + + func getSubscription(params: Any, original: WKScriptMessage) async throws -> Encodable? { + let authToken = accountManager.authToken ?? "" + return Subscription(token: authToken) + } + + func setSubscription(params: Any, original: WKScriptMessage) async throws -> Encodable? { + + PixelKit.fire(PrivacyProPixel.privacyProRestorePurchaseEmailSuccess, frequency: .dailyAndCount) + + guard let subscriptionValues: SubscriptionValues = DecodableHelper.decode(from: params) else { + assertionFailure("SubscriptionPagesUserScript: expected JSON representation of SubscriptionValues") + return nil + } + + 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) + } + + return nil + } + + func backToSettings(params: Any, original: WKScriptMessage) async throws -> Encodable? { + if let accessToken = accountManager.accessToken, + case let .success(accountDetails) = await accountManager.fetchAccountDetails(with: accessToken) { + accountManager.storeAccount(token: accessToken, email: accountDetails.email, externalID: accountDetails.externalID) + } + + DispatchQueue.main.async { + NotificationCenter.default.post(name: .subscriptionPageCloseAndOpenPreferences, object: self) + } + + return nil + } + + func getSubscriptionOptions(params: Any, original: WKScriptMessage) async throws -> Encodable? { + guard DefaultSubscriptionFeatureAvailability().isSubscriptionPurchaseAllowed else { return SubscriptionOptions.empty } + + switch subscriptionPlatform { + case .appStore: + if #available(macOS 12.0, *) { + return await subscriptionManager.storePurchaseManager().subscriptionOptions() + } + case .stripe: + switch await stripePurchaseFlow.subscriptionOptions() { + case .success(let subscriptionOptions): + return subscriptionOptions + case .failure: + break + } + } + + return SubscriptionOptions.empty + } + + // swiftlint:disable:next function_body_length cyclomatic_complexity + func subscriptionSelected(params: Any, original: WKScriptMessage) async throws -> Encodable? { + PixelKit.fire(PrivacyProPixel.privacyProPurchaseAttempt, frequency: .dailyAndCount) + struct SubscriptionSelection: Decodable { + let id: String + } + + let message = original + + // Extract the origin from the webview URL to use for attribution pixel. + subscriptionSuccessPixelHandler.origin = await originFrom(originalMessage: message) + if subscriptionManager.currentEnvironment.purchasePlatform == .appStore { + if #available(macOS 12.0, *) { + guard let subscriptionSelection: SubscriptionSelection = DecodableHelper.decode(from: params) else { + assertionFailure("SubscriptionPagesUserScript: expected JSON representation of SubscriptionSelection") + subscriptionErrorReporter.report(subscriptionActivationError: .generalError) + await uiHandler.dismissProgressViewController() + return nil + } + + os_log(.info, log: .subscription, "[Purchase] Starting purchase for: %{public}s", subscriptionSelection.id) + + await uiHandler.presentProgressViewController(withTitle: UserText.purchasingSubscriptionTitle) + + // Check for active subscriptions + if await subscriptionManager.storePurchaseManager().hasActiveSubscription() { + PixelKit.fire(PrivacyProPixel.privacyProRestoreAfterPurchaseAttempt) + os_log(.info, log: .subscription, "[Purchase] Found active subscription during purchase") + subscriptionErrorReporter.report(subscriptionActivationError: .hasActiveSubscription) + await showSubscriptionFoundAlert(originalMessage: message) + await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: PurchaseUpdate(type: "canceled")) + return nil + } + + let emailAccessToken = try? EmailManager().getToken() + let purchaseTransactionJWS: String + 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) + + os_log(.info, log: .subscription, "[Purchase] Purchasing") + switch await appStorePurchaseFlow.purchaseSubscription(with: subscriptionSelection.id, emailAccessToken: emailAccessToken) { + case .success(let transactionJWS): + purchaseTransactionJWS = transactionJWS + case .failure(let error): + switch error { + case .noProductsFound: + subscriptionErrorReporter.report(subscriptionActivationError: .subscriptionNotFound) + case .activeSubscriptionAlreadyPresent: + subscriptionErrorReporter.report(subscriptionActivationError: .activeSubscriptionAlreadyPresent) + case .authenticatingWithTransactionFailed: + subscriptionErrorReporter.report(subscriptionActivationError: .generalError) + case .accountCreationFailed: + subscriptionErrorReporter.report(subscriptionActivationError: .accountCreationFailed) + case .purchaseFailed: + subscriptionErrorReporter.report(subscriptionActivationError: .purchaseFailed) + case .cancelledByUser: + subscriptionErrorReporter.report(subscriptionActivationError: .cancelledByUser) + case .missingEntitlements: + subscriptionErrorReporter.report(subscriptionActivationError: .missingEntitlements) + case .internalError: + assertionFailure("Internal error") + } + + if error != .cancelledByUser { + await showSomethingWentWrongAlert() + } else { + await uiHandler.dismissProgressViewController() + } + await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: PurchaseUpdate(type: "canceled")) + return nil + } + + await uiHandler.updateProgressViewController(title: UserText.completingPurchaseTitle) + + os_log(.info, log: .subscription, "[Purchase] Completing purchase") + let completePurchaseResult = await appStorePurchaseFlow.completeSubscriptionPurchase(with: purchaseTransactionJWS) + switch completePurchaseResult { + case .success(let purchaseUpdate): + os_log(.info, log: .subscription, "[Purchase] Purchase complete") + PixelKit.fire(PrivacyProPixel.privacyProPurchaseSuccess, frequency: .dailyAndCount) + PixelKit.fire(PrivacyProPixel.privacyProSubscriptionActivated, frequency: .unique) + subscriptionSuccessPixelHandler.fireSuccessfulSubscriptionAttributionPixel() + await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: purchaseUpdate) + case .failure(let error): + switch error { + case .noProductsFound: + subscriptionErrorReporter.report(subscriptionActivationError: .subscriptionNotFound) + case .activeSubscriptionAlreadyPresent: + subscriptionErrorReporter.report(subscriptionActivationError: .activeSubscriptionAlreadyPresent) + case .authenticatingWithTransactionFailed: + subscriptionErrorReporter.report(subscriptionActivationError: .generalError) + case .accountCreationFailed: + subscriptionErrorReporter.report(subscriptionActivationError: .accountCreationFailed) + case .purchaseFailed: + subscriptionErrorReporter.report(subscriptionActivationError: .purchaseFailed) + case .cancelledByUser: + subscriptionErrorReporter.report(subscriptionActivationError: .cancelledByUser) + case .missingEntitlements: + subscriptionErrorReporter.report(subscriptionActivationError: .missingEntitlements) + DispatchQueue.main.async { + NotificationCenter.default.post(name: .subscriptionPageCloseAndOpenPreferences, object: self) + } + await uiHandler.dismissProgressViewController() + return nil + case .internalError: + assertionFailure("Internal error") + } + + await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: PurchaseUpdate(type: "completed")) + } + } + } else if subscriptionPlatform == .stripe { + let emailAccessToken = try? EmailManager().getToken() + let result = await stripePurchaseFlow.prepareSubscriptionPurchase(emailAccessToken: emailAccessToken) + switch result { + case .success(let success): + await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: success) + case .failure(let error): + await showSomethingWentWrongAlert() + switch error { + case .noProductsFound: + subscriptionErrorReporter.report(subscriptionActivationError: .subscriptionNotFound) + case .accountCreationFailed: + subscriptionErrorReporter.report(subscriptionActivationError: .accountCreationFailed) + } + await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: PurchaseUpdate(type: "canceled")) + } + } + + await uiHandler.dismissProgressViewController() + return nil + } + + // MARK: functions used in SubscriptionAccessActionHandlers + + func activateSubscription(params: Any, original: WKScriptMessage) async throws -> Encodable? { + PixelKit.fire(PrivacyProPixel.privacyProRestorePurchaseOfferPageEntry) + Task { @MainActor in + uiHandler.presentSubscriptionAccessViewController(handler: self, message: original) + } + return nil + } + + func featureSelected(params: Any, original: WKScriptMessage) async throws -> Encodable? { + struct FeatureSelection: Codable { + let feature: String + } + + guard let featureSelection: FeatureSelection = DecodableHelper.decode(from: params) else { + assertionFailure("SubscriptionPagesUserScript: expected JSON representation of FeatureSelection") + return nil + } + + guard let subscriptionFeatureName = SubscriptionFeatureName(rawValue: featureSelection.feature) else { + assertionFailure("SubscriptionPagesUserScript: feature name does not matches mapping") + return nil + } + + switch subscriptionFeatureName { + case .privateBrowsing: + NotificationCenter.default.post(name: .openPrivateBrowsing, object: self, userInfo: nil) + case .privateSearch: + NotificationCenter.default.post(name: .openPrivateSearch, object: self, userInfo: nil) + case .emailProtection: + NotificationCenter.default.post(name: .openEmailProtection, object: self, userInfo: nil) + case .appTrackingProtection: + NotificationCenter.default.post(name: .openAppTrackingProtection, object: self, userInfo: nil) + case .vpn: + PixelKit.fire(PrivacyProPixel.privacyProWelcomeVPN, frequency: .unique) + NotificationCenter.default.post(name: .ToggleNetworkProtectionInMainWindow, object: self, userInfo: nil) + case .personalInformationRemoval: + PixelKit.fire(PrivacyProPixel.privacyProWelcomePersonalInformationRemoval, frequency: .unique) + NotificationCenter.default.post(name: .openPersonalInformationRemoval, object: self, userInfo: nil) + await uiHandler.showTab(with: .dataBrokerProtection) + case .identityTheftRestoration: + PixelKit.fire(PrivacyProPixel.privacyProWelcomeIdentityRestoration, frequency: .unique) + let url = subscriptionManager.url(for: .identityTheftRestoration) + await uiHandler.showTab(with: .identityTheftRestoration(url)) + } + + return nil + } + + func completeStripePayment(params: Any, original: WKScriptMessage) async throws -> Encodable? { + await uiHandler.presentProgressViewController(withTitle: UserText.completingPurchaseTitle) + await stripePurchaseFlow.completeSubscriptionPurchase() + await uiHandler.dismissProgressViewController() + + PixelKit.fire(PrivacyProPixel.privacyProPurchaseStripeSuccess, frequency: .dailyAndCount) + subscriptionSuccessPixelHandler.fireSuccessfulSubscriptionAttributionPixel() + return [String: String]() // cannot be nil, the web app expect something back before redirecting the user to the final page + } + + // MARK: Pixel related actions + + func subscriptionsMonthlyPriceClicked(params: Any, original: WKScriptMessage) async -> Encodable? { + PixelKit.fire(PrivacyProPixel.privacyProOfferMonthlyPriceClick) + return nil + } + + func subscriptionsYearlyPriceClicked(params: Any, original: WKScriptMessage) async -> Encodable? { + PixelKit.fire(PrivacyProPixel.privacyProOfferYearlyPriceClick) + return nil + } + + func subscriptionsUnknownPriceClicked(params: Any, original: WKScriptMessage) async -> Encodable? { + // Not used + return nil + } + + func subscriptionsAddEmailSuccess(params: Any, original: WKScriptMessage) async -> Encodable? { + PixelKit.fire(PrivacyProPixel.privacyProAddEmailSuccess, frequency: .unique) + return nil + } + + func subscriptionsWelcomeFaqClicked(params: Any, original: WKScriptMessage) async -> Encodable? { + PixelKit.fire(PrivacyProPixel.privacyProWelcomeFAQClick, frequency: .unique) + return nil + } + + func getAccessToken(params: Any, original: WKScriptMessage) async throws -> Encodable? { + if let accessToken = accountManager.accessToken { + return ["token": accessToken] + } else { + return [String: String]() + } + } + + // MARK: Push actions + + enum SubscribeActionName: String { + case onPurchaseUpdate + } + + @MainActor + func pushPurchaseUpdate(originalMessage: WKScriptMessage, purchaseUpdate: PurchaseUpdate) { + pushAction(method: .onPurchaseUpdate, webView: originalMessage.webView!, params: purchaseUpdate) + } + + func pushAction(method: SubscribeActionName, webView: WKWebView, params: Encodable) { + guard let broker else { + assertionFailure("Cannot continue without broker instance") + return + } + + broker.push(method: method.rawValue, params: params, for: self, into: webView) + } + + @MainActor + private func originFrom(originalMessage: WKScriptMessage) -> String? { + let url = originalMessage.webView?.url + return url?.getParameter(named: AttributionParameter.origin) + } + + // MARK: - UI interactions + + func showSomethingWentWrongAlert() async { + PixelKit.fire(PrivacyProPixel.privacyProPurchaseFailure, frequency: .dailyAndCount) + switch await uiHandler.dismissProgressViewAndShow(alertType: .somethingWentWrong, text: nil) { + case .alertFirstButtonReturn: + let url = subscriptionManager.url(for: .purchase) + await uiHandler.showTab(with: .subscription(url)) + PixelKit.fire(PrivacyProPixel.privacyProOfferScreenImpression) + default: return + } + } + + func showSubscriptionFoundAlert(originalMessage: WKScriptMessage) async { + + switch await uiHandler.dismissProgressViewAndShow(alertType: .subscriptionFound, text: nil) { + case .alertFirstButtonReturn: + if #available(macOS 12.0, *) { + let appStoreRestoreFlow = DefaultAppStoreRestoreFlow(accountManager: subscriptionManager.accountManager, + storePurchaseManager: subscriptionManager.storePurchaseManager(), + subscriptionEndpointService: subscriptionManager.subscriptionEndpointService, + authEndpointService: subscriptionManager.authEndpointService) + let result = await appStoreRestoreFlow.restoreAccountFromPastPurchase() + switch result { + case .success: PixelKit.fire(PrivacyProPixel.privacyProRestorePurchaseStoreSuccess, frequency: .dailyAndCount) + case .failure: break + } + Task { @MainActor in + originalMessage.webView?.reload() + } + } + default: return + } + } +} + +extension SubscriptionPagesUseSubscriptionFeature: SubscriptionAccessActionHandling { + + func subscriptionAccessActionRestorePurchases(message: WKScriptMessage) { + if #available(macOS 12.0, *) { + Task { @MainActor in + let appStoreRestoreFlow = DefaultAppStoreRestoreFlow(accountManager: subscriptionManager.accountManager, + storePurchaseManager: subscriptionManager.storePurchaseManager(), + subscriptionEndpointService: subscriptionManager.subscriptionEndpointService, + authEndpointService: subscriptionManager.authEndpointService) + let subscriptionAppStoreRestorer = DefaultSubscriptionAppStoreRestorer(subscriptionManager: self.subscriptionManager, + appStoreRestoreFlow: appStoreRestoreFlow, + uiHandler: self.uiHandler) + await subscriptionAppStoreRestorer.restoreAppStoreSubscription() + message.webView?.reload() + } + } + } + + func subscriptionAccessActionOpenURLHandler(url: URL) { + Task { + await self.uiHandler.showTab(with: .subscription(url)) + } + } + + func subscriptionAccessActionHandleAction(event: SubscriptionAccessActionHandlingEvent) { + switch event { + case .activateAddEmailClick: + PixelKit.fire(PrivacyProPixel.privacyProRestorePurchaseEmailStart, frequency: .dailyAndCount) + default: break + } + } +} diff --git a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUserScript.swift index da91ad1ef7..3e48b083b8 100644 --- a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUserScript.swift @@ -33,16 +33,11 @@ public extension Notification.Name { /// The user script that will be the broker for all subscription features public final class SubscriptionPagesUserScript: NSObject, UserScript, UserScriptMessaging { public var source: String = "" - public static let context = "subscriptionPages" // special pages messaging cannot be isolated as we'll want regular page-scripts to be able to communicate public let broker = UserScriptMessageBroker(context: SubscriptionPagesUserScript.context, requiresRunInPageContentWorld: true ) - - public let messageNames: [String] = [ - SubscriptionPagesUserScript.context - ] - + public let messageNames: [String] = [SubscriptionPagesUserScript.context] public let injectionTime: WKUserScriptInjectionTime = .atDocumentStart public let forMainFrameOnly = true public let requiresRunInPageContentWorld = true @@ -69,459 +64,3 @@ extension SubscriptionPagesUserScript: WKScriptMessageHandler { // unsupported } } - -/// Use Subscription sub-feature -final class SubscriptionPagesUseSubscriptionFeature: Subfeature { - weak var broker: UserScriptMessageBroker? - var featureName = "useSubscription" - var messageOriginPolicy: MessageOriginPolicy = .only(rules: [ - .exact(hostname: "duckduckgo.com"), - .exact(hostname: "abrown.duckduckgo.com") - ]) - let subscriptionManager: SubscriptionManager - var accountManager: AccountManager { subscriptionManager.accountManager } - var subscriptionPlatform: SubscriptionEnvironment.PurchasePlatform { subscriptionManager.currentEnvironment.purchasePlatform } - - let stripePurchaseFlow: StripePurchaseFlow - let subscriptionErrorReporter = SubscriptionErrorReporter() - let subscriptionSuccessPixelHandler: SubscriptionAttributionPixelHandler - let uiHandler: SubscriptionUIHandling - - public init(subscriptionManager: SubscriptionManager, - subscriptionSuccessPixelHandler: SubscriptionAttributionPixelHandler = PrivacyProSubscriptionAttributionPixelHandler(), - stripePurchaseFlow: StripePurchaseFlow, - uiHandler: SubscriptionUIHandling) { - self.subscriptionManager = subscriptionManager - self.stripePurchaseFlow = stripePurchaseFlow - self.subscriptionSuccessPixelHandler = subscriptionSuccessPixelHandler - self.uiHandler = uiHandler - } - - func with(broker: UserScriptMessageBroker) { - self.broker = broker - } - - struct Handlers { - static let getSubscription = "getSubscription" - static let setSubscription = "setSubscription" - static let backToSettings = "backToSettings" - static let getSubscriptionOptions = "getSubscriptionOptions" - static let subscriptionSelected = "subscriptionSelected" - static let activateSubscription = "activateSubscription" - static let featureSelected = "featureSelected" - static let completeStripePayment = "completeStripePayment" - // Pixels related events - static let subscriptionsMonthlyPriceClicked = "subscriptionsMonthlyPriceClicked" - static let subscriptionsYearlyPriceClicked = "subscriptionsYearlyPriceClicked" - static let subscriptionsUnknownPriceClicked = "subscriptionsUnknownPriceClicked" - static let subscriptionsAddEmailSuccess = "subscriptionsAddEmailSuccess" - static let subscriptionsWelcomeFaqClicked = "subscriptionsWelcomeFaqClicked" - static let getAccessToken = "getAccessToken" - } - - // swiftlint:disable:next cyclomatic_complexity - func handler(forMethodNamed methodName: String) -> Subfeature.Handler? { - switch methodName { - case Handlers.getSubscription: return getSubscription - case Handlers.setSubscription: return setSubscription - case Handlers.backToSettings: return backToSettings - case Handlers.getSubscriptionOptions: return getSubscriptionOptions - case Handlers.subscriptionSelected: return subscriptionSelected - case Handlers.activateSubscription: return activateSubscription - case Handlers.featureSelected: return featureSelected - case Handlers.completeStripePayment: return completeStripePayment - // Pixel related events - case Handlers.subscriptionsMonthlyPriceClicked: return subscriptionsMonthlyPriceClicked - case Handlers.subscriptionsYearlyPriceClicked: return subscriptionsYearlyPriceClicked - case Handlers.subscriptionsUnknownPriceClicked: return subscriptionsUnknownPriceClicked - case Handlers.subscriptionsAddEmailSuccess: return subscriptionsAddEmailSuccess - case Handlers.subscriptionsWelcomeFaqClicked: return subscriptionsWelcomeFaqClicked - case Handlers.getAccessToken: return getAccessToken - default: - return nil - } - } - - struct Subscription: Encodable { - let token: String - } - - /// Values that the Frontend can use to determine the current state. - struct SubscriptionValues: Codable { - enum CodingKeys: String, CodingKey { - case token - } - let token: String - } - - func getSubscription(params: Any, original: WKScriptMessage) async throws -> Encodable? { - let authToken = accountManager.authToken ?? "" - return Subscription(token: authToken) - } - - func setSubscription(params: Any, original: WKScriptMessage) async throws -> Encodable? { - - PixelKit.fire(PrivacyProPixel.privacyProRestorePurchaseEmailSuccess, frequency: .dailyAndCount) - - guard let subscriptionValues: SubscriptionValues = DecodableHelper.decode(from: params) else { - assertionFailure("SubscriptionPagesUserScript: expected JSON representation of SubscriptionValues") - return nil - } - - 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) - } - - return nil - } - - func backToSettings(params: Any, original: WKScriptMessage) async throws -> Encodable? { - if let accessToken = accountManager.accessToken, - case let .success(accountDetails) = await accountManager.fetchAccountDetails(with: accessToken) { - accountManager.storeAccount(token: accessToken, email: accountDetails.email, externalID: accountDetails.externalID) - } - - DispatchQueue.main.async { - NotificationCenter.default.post(name: .subscriptionPageCloseAndOpenPreferences, object: self) - } - - return nil - } - - func getSubscriptionOptions(params: Any, original: WKScriptMessage) async throws -> Encodable? { - guard DefaultSubscriptionFeatureAvailability().isSubscriptionPurchaseAllowed else { return SubscriptionOptions.empty } - - switch subscriptionPlatform { - case .appStore: - if #available(macOS 12.0, *) { - return await subscriptionManager.storePurchaseManager().subscriptionOptions() - } - case .stripe: - switch await stripePurchaseFlow.subscriptionOptions() { - case .success(let subscriptionOptions): - return subscriptionOptions - case .failure: - break - } - } - - return SubscriptionOptions.empty - } - - // swiftlint:disable:next function_body_length cyclomatic_complexity - func subscriptionSelected(params: Any, original: WKScriptMessage) async throws -> Encodable? { - PixelKit.fire(PrivacyProPixel.privacyProPurchaseAttempt, frequency: .dailyAndCount) - struct SubscriptionSelection: Decodable { - let id: String - } - - let message = original - - // Extract the origin from the webview URL to use for attribution pixel. - subscriptionSuccessPixelHandler.origin = await originFrom(originalMessage: message) - if subscriptionManager.currentEnvironment.purchasePlatform == .appStore { - if #available(macOS 12.0, *) { - guard let subscriptionSelection: SubscriptionSelection = DecodableHelper.decode(from: params) else { - assertionFailure("SubscriptionPagesUserScript: expected JSON representation of SubscriptionSelection") - subscriptionErrorReporter.report(subscriptionActivationError: .generalError) - await uiHandler.dismissProgressViewController() - return nil - } - - os_log(.info, log: .subscription, "[Purchase] Starting purchase for: %{public}s", subscriptionSelection.id) - - await uiHandler.presentProgressViewController(withTitle: UserText.purchasingSubscriptionTitle) - - // Check for active subscriptions - if await subscriptionManager.storePurchaseManager().hasActiveSubscription() { - PixelKit.fire(PrivacyProPixel.privacyProRestoreAfterPurchaseAttempt) - os_log(.info, log: .subscription, "[Purchase] Found active subscription during purchase") - subscriptionErrorReporter.report(subscriptionActivationError: .hasActiveSubscription) - await showSubscriptionFoundAlert(originalMessage: message) - await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: PurchaseUpdate(type: "canceled")) - return nil - } - - let emailAccessToken = try? EmailManager().getToken() - let purchaseTransactionJWS: String - let appStorePurchaseFlow = DefaultAppStorePurchaseFlow(subscriptionManager: subscriptionManager, - appStoreRestoreFlow: DefaultAppStoreRestoreFlow(subscriptionManager: subscriptionManager)) - - os_log(.info, log: .subscription, "[Purchase] Purchasing") - switch await appStorePurchaseFlow.purchaseSubscription(with: subscriptionSelection.id, emailAccessToken: emailAccessToken) { - case .success(let transactionJWS): - purchaseTransactionJWS = transactionJWS - case .failure(let error): - switch error { - case .noProductsFound: - subscriptionErrorReporter.report(subscriptionActivationError: .subscriptionNotFound) - case .activeSubscriptionAlreadyPresent: - subscriptionErrorReporter.report(subscriptionActivationError: .activeSubscriptionAlreadyPresent) - case .authenticatingWithTransactionFailed: - subscriptionErrorReporter.report(subscriptionActivationError: .generalError) - case .accountCreationFailed: - subscriptionErrorReporter.report(subscriptionActivationError: .accountCreationFailed) - case .purchaseFailed: - subscriptionErrorReporter.report(subscriptionActivationError: .purchaseFailed) - case .cancelledByUser: - subscriptionErrorReporter.report(subscriptionActivationError: .cancelledByUser) - case .missingEntitlements: - subscriptionErrorReporter.report(subscriptionActivationError: .missingEntitlements) - case .internalError: - assertionFailure("Internal error") - } - - if error != .cancelledByUser { - await showSomethingWentWrongAlert() - } else { - await uiHandler.dismissProgressViewController() - } - await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: PurchaseUpdate(type: "canceled")) - return nil - } - - await uiHandler.updateProgressViewController(title: UserText.completingPurchaseTitle) - - os_log(.info, log: .subscription, "[Purchase] Completing purchase") - let completePurchaseResult = await appStorePurchaseFlow.completeSubscriptionPurchase(with: purchaseTransactionJWS) - switch completePurchaseResult { - case .success(let purchaseUpdate): - os_log(.info, log: .subscription, "[Purchase] Purchase complete") - PixelKit.fire(PrivacyProPixel.privacyProPurchaseSuccess, frequency: .dailyAndCount) - PixelKit.fire(PrivacyProPixel.privacyProSubscriptionActivated, frequency: .unique) - subscriptionSuccessPixelHandler.fireSuccessfulSubscriptionAttributionPixel() - await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: purchaseUpdate) - case .failure(let error): - switch error { - case .noProductsFound: - subscriptionErrorReporter.report(subscriptionActivationError: .subscriptionNotFound) - case .activeSubscriptionAlreadyPresent: - subscriptionErrorReporter.report(subscriptionActivationError: .activeSubscriptionAlreadyPresent) - case .authenticatingWithTransactionFailed: - subscriptionErrorReporter.report(subscriptionActivationError: .generalError) - case .accountCreationFailed: - subscriptionErrorReporter.report(subscriptionActivationError: .accountCreationFailed) - case .purchaseFailed: - subscriptionErrorReporter.report(subscriptionActivationError: .purchaseFailed) - case .cancelledByUser: - subscriptionErrorReporter.report(subscriptionActivationError: .cancelledByUser) - case .missingEntitlements: - subscriptionErrorReporter.report(subscriptionActivationError: .missingEntitlements) - DispatchQueue.main.async { - NotificationCenter.default.post(name: .subscriptionPageCloseAndOpenPreferences, object: self) - } - await uiHandler.dismissProgressViewController() - return nil - case .internalError: - assertionFailure("Internal error") - } - - await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: PurchaseUpdate(type: "completed")) - } - } - } else if subscriptionPlatform == .stripe { - let emailAccessToken = try? EmailManager().getToken() - let result = await stripePurchaseFlow.prepareSubscriptionPurchase(emailAccessToken: emailAccessToken) - switch result { - case .success(let success): - await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: success) - case .failure(let error): - await showSomethingWentWrongAlert() - switch error { - case .noProductsFound: - subscriptionErrorReporter.report(subscriptionActivationError: .subscriptionNotFound) - case .accountCreationFailed: - subscriptionErrorReporter.report(subscriptionActivationError: .accountCreationFailed) - } - await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: PurchaseUpdate(type: "canceled")) - } - } - - await uiHandler.dismissProgressViewController() - return nil - } - - // MARK: functions used in SubscriptionAccessActionHandlers - - func activateSubscription(params: Any, original: WKScriptMessage) async throws -> Encodable? { - PixelKit.fire(PrivacyProPixel.privacyProRestorePurchaseOfferPageEntry) - Task { @MainActor in - uiHandler.presentSubscriptionAccessViewController(handler: self, message: original) - } - return nil - } - - func featureSelected(params: Any, original: WKScriptMessage) async throws -> Encodable? { - struct FeatureSelection: Codable { - let feature: String - } - - guard let featureSelection: FeatureSelection = DecodableHelper.decode(from: params) else { - assertionFailure("SubscriptionPagesUserScript: expected JSON representation of FeatureSelection") - return nil - } - - guard let subscriptionFeatureName = SubscriptionFeatureName(rawValue: featureSelection.feature) else { - assertionFailure("SubscriptionPagesUserScript: feature name does not matches mapping") - return nil - } - - switch subscriptionFeatureName { - case .privateBrowsing: - NotificationCenter.default.post(name: .openPrivateBrowsing, object: self, userInfo: nil) - case .privateSearch: - NotificationCenter.default.post(name: .openPrivateSearch, object: self, userInfo: nil) - case .emailProtection: - NotificationCenter.default.post(name: .openEmailProtection, object: self, userInfo: nil) - case .appTrackingProtection: - NotificationCenter.default.post(name: .openAppTrackingProtection, object: self, userInfo: nil) - case .vpn: - PixelKit.fire(PrivacyProPixel.privacyProWelcomeVPN, frequency: .unique) - NotificationCenter.default.post(name: .ToggleNetworkProtectionInMainWindow, object: self, userInfo: nil) - case .personalInformationRemoval: - PixelKit.fire(PrivacyProPixel.privacyProWelcomePersonalInformationRemoval, frequency: .unique) - NotificationCenter.default.post(name: .openPersonalInformationRemoval, object: self, userInfo: nil) - await uiHandler.showTab(with: .dataBrokerProtection) - case .identityTheftRestoration: - PixelKit.fire(PrivacyProPixel.privacyProWelcomeIdentityRestoration, frequency: .unique) - let url = subscriptionManager.url(for: .identityTheftRestoration) - await uiHandler.showTab(with: .identityTheftRestoration(url)) - } - - return nil - } - - func completeStripePayment(params: Any, original: WKScriptMessage) async throws -> Encodable? { - await uiHandler.presentProgressViewController(withTitle: UserText.completingPurchaseTitle) - await stripePurchaseFlow.completeSubscriptionPurchase() - await uiHandler.dismissProgressViewController() - - PixelKit.fire(PrivacyProPixel.privacyProPurchaseStripeSuccess, frequency: .dailyAndCount) - subscriptionSuccessPixelHandler.fireSuccessfulSubscriptionAttributionPixel() - return [String: String]() // cannot be nil, the web app expect something back before redirecting the user to the final page - } - - // MARK: Pixel related actions - - func subscriptionsMonthlyPriceClicked(params: Any, original: WKScriptMessage) async -> Encodable? { - PixelKit.fire(PrivacyProPixel.privacyProOfferMonthlyPriceClick) - return nil - } - - func subscriptionsYearlyPriceClicked(params: Any, original: WKScriptMessage) async -> Encodable? { - PixelKit.fire(PrivacyProPixel.privacyProOfferYearlyPriceClick) - return nil - } - - func subscriptionsUnknownPriceClicked(params: Any, original: WKScriptMessage) async -> Encodable? { - // Not used - return nil - } - - func subscriptionsAddEmailSuccess(params: Any, original: WKScriptMessage) async -> Encodable? { - PixelKit.fire(PrivacyProPixel.privacyProAddEmailSuccess, frequency: .unique) - return nil - } - - func subscriptionsWelcomeFaqClicked(params: Any, original: WKScriptMessage) async -> Encodable? { - PixelKit.fire(PrivacyProPixel.privacyProWelcomeFAQClick, frequency: .unique) - return nil - } - - func getAccessToken(params: Any, original: WKScriptMessage) async throws -> Encodable? { - if let accessToken = accountManager.accessToken { - return ["token": accessToken] - } else { - return [String: String]() - } - } - - // MARK: Push actions - - enum SubscribeActionName: String { - case onPurchaseUpdate - } - - @MainActor - func pushPurchaseUpdate(originalMessage: WKScriptMessage, purchaseUpdate: PurchaseUpdate) { - pushAction(method: .onPurchaseUpdate, webView: originalMessage.webView!, params: purchaseUpdate) - } - - func pushAction(method: SubscribeActionName, webView: WKWebView, params: Encodable) { - guard let broker else { - assertionFailure("Cannot continue without broker instance") - return - } - - broker.push(method: method.rawValue, params: params, for: self, into: webView) - } - - @MainActor - private func originFrom(originalMessage: WKScriptMessage) -> String? { - let url = originalMessage.webView?.url - return url?.getParameter(named: AttributionParameter.origin) - } - - // MARK: - UI interactions - - func showSomethingWentWrongAlert() async { - PixelKit.fire(PrivacyProPixel.privacyProPurchaseFailure, frequency: .dailyAndCount) - switch await uiHandler.dismissProgressViewAndShow(alertType: .somethingWentWrong, text: nil) { - case .alertFirstButtonReturn: - let url = subscriptionManager.url(for: .purchase) - await uiHandler.showTab(with: .subscription(url)) - PixelKit.fire(PrivacyProPixel.privacyProOfferScreenImpression) - default: return - } - } - - func showSubscriptionFoundAlert(originalMessage: WKScriptMessage) async { - - switch await uiHandler.dismissProgressViewAndShow(alertType: .subscriptionFound, text: nil) { - case .alertFirstButtonReturn: - if #available(macOS 12.0, *) { - let appStoreRestoreFlow = DefaultAppStoreRestoreFlow(subscriptionManager: self.subscriptionManager) - let result = await appStoreRestoreFlow.restoreAccountFromPastPurchase() - switch result { - case .success: PixelKit.fire(PrivacyProPixel.privacyProRestorePurchaseStoreSuccess, frequency: .dailyAndCount) - case .failure: break - } - Task { @MainActor in - originalMessage.webView?.reload() - } - } - default: return - } - } -} - -extension SubscriptionPagesUseSubscriptionFeature: SubscriptionAccessActionHandling { - - func subscriptionAccessActionRestorePurchases(message: WKScriptMessage) { - if #available(macOS 12.0, *) { - Task { @MainActor in - let subscriptionAppStoreRestorer = SubscriptionAppStoreRestorer(subscriptionManager: self.subscriptionManager, - uiHandler: self.uiHandler) - await subscriptionAppStoreRestorer.restoreAppStoreSubscription() - message.webView?.reload() - } - } - } - - func subscriptionAccessActionOpenURLHandler(url: URL) { - Task { - await self.uiHandler.showTab(with: .subscription(url)) - } - } - - func subscriptionAccessActionHandleAction(event: SubscriptionAccessActionHandlingEvent) { - switch event { - case .activateAddEmailClick: - PixelKit.fire(PrivacyProPixel.privacyProRestorePurchaseEmailStart, frequency: .dailyAndCount) - default: break - } - } -} diff --git a/DuckDuckGo/Tab/UserScripts/UserScripts.swift b/DuckDuckGo/Tab/UserScripts/UserScripts.swift index 924fbe4cc8..664c08c210 100644 --- a/DuckDuckGo/Tab/UserScripts/UserScripts.swift +++ b/DuckDuckGo/Tab/UserScripts/UserScripts.swift @@ -47,6 +47,7 @@ final class UserScripts: UserScriptsProvider { let sslErrorPageUserScript: SSLErrorPageUserScript? let onboardingUserScript: OnboardingUserScript? + // swiftlint:disable:next function_body_length init(with sourceProvider: ScriptSourceProviding) { clickToLoadScript = ClickToLoadUserScript() contentBlockerRulesScript = ContentBlockerRulesUserScript(configuration: sourceProvider.contentBlockerRulesConfig!) @@ -102,8 +103,11 @@ final class UserScripts: UserScriptsProvider { if DefaultSubscriptionFeatureAvailability().isFeatureAvailable { let subscriptionManager = Application.appDelegate.subscriptionManager + let stripePurchaseFlow = DefaultStripePurchaseFlow(subscriptionEndpointService: subscriptionManager.subscriptionEndpointService, + authEndpointService: subscriptionManager.authEndpointService, + accountManager: subscriptionManager.accountManager) let delegate = SubscriptionPagesUseSubscriptionFeature(subscriptionManager: subscriptionManager, - stripePurchaseFlow: DefaultStripePurchaseFlow(subscriptionManager: subscriptionManager), + stripePurchaseFlow: stripePurchaseFlow, uiHandler: Application.appDelegate.subscriptionUIHandler) subscriptionPagesUserScript.registerSubfeature(delegate: delegate) userScripts.append(subscriptionPagesUserScript) diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 9082fc5d60..c2895aabfb 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "164.3.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "165.0.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index afc73ff1a0..c6e528be95 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -32,7 +32,7 @@ let package = Package( .library(name: "VPNAppLauncher", targets: ["VPNAppLauncher"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "164.3.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "165.0.0"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.3"), .package(path: "../AppLauncher"), .package(path: "../UDSHelper"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 5fa29213b3..7110af005f 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "164.3.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "165.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift index 5b51c92043..f672fa995d 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift @@ -243,8 +243,15 @@ public final class SubscriptionDebugMenu: NSMenuItem { @IBAction func showPurchaseView(_ sender: Any?) { if #available(macOS 12.0, *) { let storePurchaseManager = DefaultStorePurchaseManager() - let appStorePurchaseFlow = DefaultAppStorePurchaseFlow(subscriptionManager: subscriptionManager, - appStoreRestoreFlow: DefaultAppStoreRestoreFlow(subscriptionManager: subscriptionManager)) + 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 vc = DebugPurchaseViewController(storePurchaseManager: storePurchaseManager, appStorePurchaseFlow: appStorePurchaseFlow) currentViewController()?.presentAsSheet(vc) } @@ -314,7 +321,10 @@ public final class SubscriptionDebugMenu: NSMenuItem { func restorePurchases(_ sender: Any?) { if #available(macOS 12.0, *) { Task { - let appStoreRestoreFlow = DefaultAppStoreRestoreFlow(subscriptionManager: subscriptionManager) + let appStoreRestoreFlow = DefaultAppStoreRestoreFlow(accountManager: subscriptionManager.accountManager, + storePurchaseManager: subscriptionManager.storePurchaseManager(), + subscriptionEndpointService: subscriptionManager.subscriptionEndpointService, + authEndpointService: subscriptionManager.authEndpointService) await appStoreRestoreFlow.restoreAccountFromPastPurchase() } } diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift index 35b39ea62b..bfacdc587e 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift @@ -257,7 +257,9 @@ public final class PreferencesSubscriptionModel: ObservableObject { Task { if subscriptionManager.currentEnvironment.purchasePlatform == .appStore { if #available(macOS 12.0, iOS 15.0, *) { - let appStoreAccountManagementFlow = DefaultAppStoreAccountManagementFlow(subscriptionManager: subscriptionManager) + let appStoreAccountManagementFlow = DefaultAppStoreAccountManagementFlow(authEndpointService: subscriptionManager.authEndpointService, + storePurchaseManager: subscriptionManager.storePurchaseManager(), + accountManager: subscriptionManager.accountManager) await appStoreAccountManagementFlow.refreshAuthTokenIfNeeded() } } @@ -285,7 +287,10 @@ public final class PreferencesSubscriptionModel: ObservableObject { if subscriptionManager.currentEnvironment.purchasePlatform == .appStore { if #available(macOS 12.0, *) { Task { - let appStoreRestoreFlow = DefaultAppStoreRestoreFlow(subscriptionManager: subscriptionManager) + let appStoreRestoreFlow = DefaultAppStoreRestoreFlow(accountManager: subscriptionManager.accountManager, + storePurchaseManager: subscriptionManager.storePurchaseManager(), + subscriptionEndpointService: subscriptionManager.subscriptionEndpointService, + authEndpointService: subscriptionManager.authEndpointService) await appStoreRestoreFlow.restoreAccountFromPastPurchase() fetchAndUpdateSubscriptionDetails() } diff --git a/UnitTests/Menus/MoreOptionsMenuTests.swift b/UnitTests/Menus/MoreOptionsMenuTests.swift index 5cc5668373..863ac2c823 100644 --- a/UnitTests/Menus/MoreOptionsMenuTests.swift +++ b/UnitTests/Menus/MoreOptionsMenuTests.swift @@ -57,7 +57,7 @@ final class MoreOptionsMenuTests: XCTestCase { subscriptionManager = SubscriptionManagerMock(accountManager: AccountManagerMock(), subscriptionEndpointService: SubscriptionEndpointServiceMock(), - authEndpointService: AuthEndpointServiceMock(), + authEndpointService: SubscriptionMockFactory.authEndpointService, storePurchaseManager: storePurchaseManager, currentEnvironment: SubscriptionEnvironment(serviceEnvironment: .production, purchasePlatform: .appStore), diff --git a/UnitTests/Subscription/SubscriptionFeatureAvailabilityMock.swift b/UnitTests/Subscription/Mocks/SubscriptionFeatureAvailabilityMock.swift similarity index 100% rename from UnitTests/Subscription/SubscriptionFeatureAvailabilityMock.swift rename to UnitTests/Subscription/Mocks/SubscriptionFeatureAvailabilityMock.swift diff --git a/UnitTests/Subscription/SubscriptionUIHandlerMock.swift b/UnitTests/Subscription/Mocks/SubscriptionUIHandlerMock.swift similarity index 87% rename from UnitTests/Subscription/SubscriptionUIHandlerMock.swift rename to UnitTests/Subscription/Mocks/SubscriptionUIHandlerMock.swift index f318353224..925bb767ba 100644 --- a/UnitTests/Subscription/SubscriptionUIHandlerMock.swift +++ b/UnitTests/Subscription/Mocks/SubscriptionUIHandlerMock.swift @@ -32,10 +32,16 @@ public struct SubscriptionUIHandlerMock: SubscriptionUIHandling { let didPerformActionCallback: (_ action: UIHandlerMockPerformedAction) -> Void + public init(alertResponse: NSApplication.ModalResponse? = nil, + didPerformActionCallback: @escaping (UIHandlerMockPerformedAction) -> Void) { + self.didPerformActionCallback = didPerformActionCallback + self.alertResponse = alertResponse + } + public var alertResponse: NSApplication.ModalResponse? public func presentProgressViewController(withTitle: String) { - didPerformActionCallback(.didDismissProgressViewController) + didPerformActionCallback(.didPresentProgressViewController) } public func dismissProgressViewController() { diff --git a/UnitTests/Subscription/SubscriptionAppStoreRestorerTests.swift b/UnitTests/Subscription/SubscriptionAppStoreRestorerTests.swift new file mode 100644 index 0000000000..480648894b --- /dev/null +++ b/UnitTests/Subscription/SubscriptionAppStoreRestorerTests.swift @@ -0,0 +1,89 @@ +// +// SubscriptionAppStoreRestorerTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import Subscription +import SubscriptionTestingUtilities +@testable import DuckDuckGo_Privacy_Browser +@testable import PixelKit +import PixelKitTestingUtilities +import Common + +@available(macOS 12.0, *) +final class SubscriptionAppStoreRestorerTests: XCTestCase { + + var pixelKit: PixelKit? + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + PixelKit.tearDown() + pixelKit?.clearFrequencyHistoryForAllPixels() + } + + let testUserDefault = UserDefaults(suiteName: #function)! + + func testBaseSuccessfulPurchase() async throws { + let pixelExpectation = expectation(description: "Pixel fired") + pixelExpectation.expectedFulfillmentCount = 2 // All pixels are .dailyAndCount + let expectedPixel = PrivacyProPixel.privacyProRestorePurchaseStoreSuccess + let pixelKit = PixelKit(dryRun: false, + appVersion: "1.0.0", + defaultHeaders: [:], + defaults: testUserDefault) { pixelName, _, _, _, _, _ in + if pixelName.hasPrefix(expectedPixel.name) { + pixelExpectation.fulfill() + } else { + XCTFail("Wrong pixel fired: \(pixelName)") + } + } + PixelKit.setSharedForTesting(pixelKit: pixelKit) + + let progressViewPresentedExpectation = expectation(description: "Progress view presented") + let progressViewDismissedExpectation = expectation(description: "Progress view dismissed") + + let uiHandler = await SubscriptionUIHandlerMock { action in + switch action { + case .didPresentProgressViewController: + progressViewPresentedExpectation.fulfill() + case .didDismissProgressViewController: + progressViewDismissedExpectation.fulfill() + case .didUpdateProgressViewController: + break + case .didPresentSubscriptionAccessViewController: + break + case .didShowAlert: + break + case .didShowTab: + break + } + } + + let subscriptionAppStoreRestorer = DefaultSubscriptionAppStoreRestorer( + subscriptionManager: SubscriptionMockFactory.subscriptionManager, + appStoreRestoreFlow: SubscriptionMockFactory.appStoreRestoreFlow, + uiHandler: uiHandler) + await subscriptionAppStoreRestorer.restoreAppStoreSubscription() + + await fulfillment(of: [progressViewDismissedExpectation, + progressViewPresentedExpectation, + pixelExpectation], timeout: 3.0) + } +} diff --git a/UnitTests/Subscription/SubscriptionErrorReporterTests.swift b/UnitTests/Subscription/SubscriptionErrorReporterTests.swift new file mode 100644 index 0000000000..afa0f125f3 --- /dev/null +++ b/UnitTests/Subscription/SubscriptionErrorReporterTests.swift @@ -0,0 +1,65 @@ +// +// SubscriptionErrorReporterTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import Subscription +@testable import DuckDuckGo_Privacy_Browser +@testable import PixelKit +import PixelKitTestingUtilities + +final class SubscriptionErrorReporterTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + let testUserDefault = UserDefaults(suiteName: #function)! + let reporter = DefaultSubscriptionErrorReporter() + + func handle(error: SubscriptionError, expectedPixel: PrivacyProPixel) async throws { + let pixelExpectation = expectation(description: "Pixel fired") + pixelExpectation.expectedFulfillmentCount = 2 // All pixels are .dailyAndCount + let pixelKit = PixelKit(dryRun: false, + appVersion: "1.0.0", + defaultHeaders: [:], + defaults: testUserDefault) { pixelName, _, _, _, _, _ in + if pixelName.hasPrefix(expectedPixel.name) { + pixelExpectation.fulfill() + } else { + XCTFail("Wrong pixel fired: \(pixelName)") + } + } + PixelKit.setSharedForTesting(pixelKit: pixelKit) + reporter.report(subscriptionActivationError: error) + await fulfillment(of: [pixelExpectation], timeout: 1.0) + PixelKit.tearDown() + pixelKit.clearFrequencyHistoryForAllPixels() + } + + func testErrorHandling() async throws { + try await handle(error: .purchaseFailed, expectedPixel: .privacyProPurchaseFailureStoreError) + try await handle(error: .missingEntitlements, expectedPixel: .privacyProPurchaseFailureBackendError) + try await handle(error: .failedToGetSubscriptionOptions, expectedPixel: .privacyProPurchaseFailureStoreError) + // ... TBC + } + +}