diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 99377ac113..1241d55709 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -229,8 +229,8 @@ 1EC7115A2D035BC30009EB5C /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 1EC711572D035BC30009EB5C /* Localizable.xcstrings */; }; 1ED910D52B63BFB300936947 /* IdentityTheftRestorationPagesUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED910D42B63BFB300936947 /* IdentityTheftRestorationPagesUserScript.swift */; }; 1ED910D62B63BFB300936947 /* IdentityTheftRestorationPagesUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED910D42B63BFB300936947 /* IdentityTheftRestorationPagesUserScript.swift */; }; - 1EEB2D7A2C986A40000D908B /* SubscriptionPagesUseSubscriptionFeatureTestsForStripe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EEB2D792C986A40000D908B /* SubscriptionPagesUseSubscriptionFeatureTestsForStripe.swift */; }; - 1EEB2D7B2C986A40000D908B /* SubscriptionPagesUseSubscriptionFeatureTestsForStripe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EEB2D792C986A40000D908B /* SubscriptionPagesUseSubscriptionFeatureTestsForStripe.swift */; }; + 1EEB2D7A2C986A40000D908B /* SubscriptionPagesUseSubscriptionFeatureForStripeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EEB2D792C986A40000D908B /* SubscriptionPagesUseSubscriptionFeatureForStripeTests.swift */; }; + 1EEB2D7B2C986A40000D908B /* SubscriptionPagesUseSubscriptionFeatureForStripeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EEB2D792C986A40000D908B /* SubscriptionPagesUseSubscriptionFeatureForStripeTests.swift */; }; 1EFA1A072C7C7F0E0099F508 /* PrivacyProPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F188267F2BBEB58100D9AC4F /* PrivacyProPixel.swift */; }; 1EFA1A082C7C7F0F0099F508 /* PrivacyProPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F188267F2BBEB58100D9AC4F /* PrivacyProPixel.swift */; }; 31031EB72CC94C6E00684340 /* AIChatRemoteSettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31031EB62CC94C6C00684340 /* AIChatRemoteSettingsTests.swift */; }; @@ -345,10 +345,6 @@ 31E3FD732CE39C4600A392C8 /* AIChatDebugURLSettingsRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31E3FD702CE39C4600A392C8 /* AIChatDebugURLSettingsRepresentable.swift */; }; 31ECDA0E2BED317300AE679F /* BundleExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6106B9D26A565DA0013B453 /* BundleExtension.swift */; }; 31ECDA0F2BED317300AE679F /* BundleExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6106B9D26A565DA0013B453 /* BundleExtension.swift */; }; - 31ECDA112BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31ECDA102BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift */; }; - 31ECDA122BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31ECDA102BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift */; }; - 31ECDA132BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31ECDA102BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift */; }; - 31ECDA142BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31ECDA102BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift */; }; 31EF1E802B63FFA800E6DB17 /* DBPHomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3192EC872A4DCF21001E97A5 /* DBPHomeViewController.swift */; }; 31EF1E812B63FFB800E6DB17 /* DataBrokerProtectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3139A1512AA4B3C000969C7D /* DataBrokerProtectionManager.swift */; }; 31EF1E832B63FFCA00E6DB17 /* LoginItem+DataBrokerProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D8FA00B2AC5BDCE005DD0D0 /* LoginItem+DataBrokerProtection.swift */; }; @@ -917,7 +913,6 @@ 3706FE1B293F661700E42796 /* PermissionManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6106B9F26A7BE0B0013B453 /* PermissionManagerTests.swift */; }; 3706FE1C293F661700E42796 /* ConnectBitwardenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B59CC8B290083240058F2F6 /* ConnectBitwardenViewModelTests.swift */; }; 3706FE1E293F661700E42796 /* GeolocationProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6106BB426A809E60013B453 /* GeolocationProviderTests.swift */; }; - 3706FE1F293F661700E42796 /* AppStateChangePublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A5A29F25B96E8300AA7ADA /* AppStateChangePublisherTests.swift */; }; 3706FE20293F661700E42796 /* CLLocationManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B63ED0E226B3E7FA00A9DAD1 /* CLLocationManagerMock.swift */; }; 3706FE21293F661700E42796 /* DownloadsPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CD54BA27F25A4000F1F7B9 /* DownloadsPreferencesTests.swift */; }; 3706FE22293F661700E42796 /* FireproofDomainsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B02199925E063DE00ED7DEA /* FireproofDomainsTests.swift */; }; @@ -963,7 +958,6 @@ 3706FE4D293F661700E42796 /* OnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F487B4276A8F2E003CE668 /* OnboardingTests.swift */; }; 3706FE4E293F661700E42796 /* BookmarkListTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA652CCD25DD9071009059CC /* BookmarkListTests.swift */; }; 3706FE4F293F661700E42796 /* BookmarksExporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 859E7D6C274548F2009C2B69 /* BookmarksExporterTests.swift */; }; - 3706FE50293F661700E42796 /* WindowManagerStateRestorationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A5A2A725BAA35500AA7ADA /* WindowManagerStateRestorationTests.swift */; }; 3706FE51293F661700E42796 /* SafariBookmarksReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB99D0E26FE1A84001E4761 /* SafariBookmarksReaderTests.swift */; }; 3706FE52293F661700E42796 /* FileSystemDSLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBF0924283083EC00EE1418 /* FileSystemDSLTests.swift */; }; 3706FE53293F661700E42796 /* CoreDataEncryptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B11060925903EAC0039B979 /* CoreDataEncryptionTests.swift */; }; @@ -1115,8 +1109,6 @@ 37219B3B2CBFD4F300C9D7A8 /* NewTabPageSearchBoxExperiment+Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37219B392CBFD4F300C9D7A8 /* NewTabPageSearchBoxExperiment+Logger.swift */; }; 37219B3D2CC27DB700C9D7A8 /* NewTabPageSearchBoxExperimentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37219B3C2CC27DB300C9D7A8 /* NewTabPageSearchBoxExperimentTests.swift */; }; 37219B3E2CC27DB700C9D7A8 /* NewTabPageSearchBoxExperimentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37219B3C2CC27DB300C9D7A8 /* NewTabPageSearchBoxExperimentTests.swift */; }; - 372217802B3337FE00B8E9C2 /* TestUtils in Frameworks */ = {isa = PBXBuildFile; productRef = 3722177F2B3337FE00B8E9C2 /* TestUtils */; }; - 372217822B33380700B8E9C2 /* TestUtils in Frameworks */ = {isa = PBXBuildFile; productRef = 372217812B33380700B8E9C2 /* TestUtils */; }; 37269EFB2B332F9E005E8E46 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 37269EFA2B332F9E005E8E46 /* Common */; }; 37269EFD2B332FAC005E8E46 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 37269EFC2B332FAC005E8E46 /* Common */; }; 37269EFF2B332FBB005E8E46 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 37269EFE2B332FBB005E8E46 /* Common */; }; @@ -1152,7 +1144,6 @@ 374EF08429B7575B003D2E87 /* RecentlyClosedCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374EF08229B751FC003D2E87 /* RecentlyClosedCoordinatorTests.swift */; }; 374EFDEB2D01A1D800B30939 /* Utilities in Frameworks */ = {isa = PBXBuildFile; productRef = 374EFDEA2D01A1D800B30939 /* Utilities */; }; 374EFDED2D01A1DE00B30939 /* Utilities in Frameworks */ = {isa = PBXBuildFile; productRef = 374EFDEC2D01A1DE00B30939 /* Utilities */; }; - 374EFDEF2D01C70300B30939 /* Utilities in Frameworks */ = {isa = PBXBuildFile; productRef = 374EFDEE2D01C70300B30939 /* Utilities */; }; 374EFDF12D01C70A00B30939 /* Utilities in Frameworks */ = {isa = PBXBuildFile; productRef = 374EFDF02D01C70A00B30939 /* Utilities */; }; 374EFDF32D01C99E00B30939 /* ActiveRemoteMessageModel+NewTabPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374EFDF22D01C99700B30939 /* ActiveRemoteMessageModel+NewTabPage.swift */; }; 374EFDF42D01C99E00B30939 /* ActiveRemoteMessageModel+NewTabPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374EFDF22D01C99700B30939 /* ActiveRemoteMessageModel+NewTabPage.swift */; }; @@ -1720,8 +1711,6 @@ 567A23DC2C88980B0010F66C /* ContextualDaxDialogsFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567A23DA2C8894CD0010F66C /* ContextualDaxDialogsFactoryTests.swift */; }; 567A23DE2C89980A0010F66C /* OnboardingNavigationDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567A23DD2C89980A0010F66C /* OnboardingNavigationDelegateTests.swift */; }; 567A23DF2C89980A0010F66C /* OnboardingNavigationDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567A23DD2C89980A0010F66C /* OnboardingNavigationDelegateTests.swift */; }; - 567A23E12C89B1EE0010F66C /* BrowserTabViewControllerOnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567A23E02C89B1EE0010F66C /* BrowserTabViewControllerOnboardingTests.swift */; }; - 567A23E22C89B1EE0010F66C /* BrowserTabViewControllerOnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567A23E02C89B1EE0010F66C /* BrowserTabViewControllerOnboardingTests.swift */; }; 567DA93F29E8045D008AC5EE /* MockEmailStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567DA93E29E8045D008AC5EE /* MockEmailStorage.swift */; }; 567DA94029E8045D008AC5EE /* MockEmailStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567DA93E29E8045D008AC5EE /* MockEmailStorage.swift */; }; 567DA94529E95C3F008AC5EE /* YoutubeOverlayUserScriptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567DA94429E95C3F008AC5EE /* YoutubeOverlayUserScriptTests.swift */; }; @@ -1768,8 +1757,6 @@ 56A054532C2592CE007D8FAB /* OnboardingUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A054522C2592CE007D8FAB /* OnboardingUITests.swift */; }; 56A214AF2CB583BF00E5BC0E /* TrackerMessageProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A214AE2CB583BF00E5BC0E /* TrackerMessageProviderTests.swift */; }; 56A214B02CB583BF00E5BC0E /* TrackerMessageProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A214AE2CB583BF00E5BC0E /* TrackerMessageProviderTests.swift */; }; - 56AC09C72C2D7DD6002D70E0 /* BookmarksBarViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AC09C62C2D7DD6002D70E0 /* BookmarksBarViewControllerTests.swift */; }; - 56AC09C82C2D7DD6002D70E0 /* BookmarksBarViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AC09C62C2D7DD6002D70E0 /* BookmarksBarViewControllerTests.swift */; }; 56B234BF2A84EFD200F2A1CC /* NavigationBarUrlExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B234BE2A84EFD200F2A1CC /* NavigationBarUrlExtensionsTests.swift */; }; 56B234C02A84EFD800F2A1CC /* NavigationBarUrlExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B234BE2A84EFD200F2A1CC /* NavigationBarUrlExtensionsTests.swift */; }; 56BA1E752BAAF70F001CF69F /* SpecialErrorPageTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E742BAAF70F001CF69F /* SpecialErrorPageTabExtension.swift */; }; @@ -1975,8 +1962,6 @@ 84B479092CCA7A3E00F40329 /* Logger+UnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B479072CCA7A3900F40329 /* Logger+UnitTests.swift */; }; 84B49F0D2CB10F0900FF08BB /* OHHTTPStubs in Frameworks */ = {isa = PBXBuildFile; productRef = 84B49F0C2CB10F0900FF08BB /* OHHTTPStubs */; }; 84B49F0F2CB10F0900FF08BB /* OHHTTPStubsSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 84B49F0E2CB10F0900FF08BB /* OHHTTPStubsSwift */; }; - 84BBC7FF2CFA0D2F00BAE57A /* TestUtils in Frameworks */ = {isa = PBXBuildFile; productRef = 84BBC7FE2CFA0D2F00BAE57A /* TestUtils */; }; - 84BBC8012CFA0D3800BAE57A /* TestUtils in Frameworks */ = {isa = PBXBuildFile; productRef = 84BBC8002CFA0D3800BAE57A /* TestUtils */; }; 84C96E462CF9BB6400A80A01 /* malwareFilterSet.json in Resources */ = {isa = PBXBuildFile; fileRef = 84C96E442CF9BB6400A80A01 /* malwareFilterSet.json */; }; 84C96E472CF9BB6400A80A01 /* malwareHashPrefixes.json in Resources */ = {isa = PBXBuildFile; fileRef = 84C96E452CF9BB6400A80A01 /* malwareHashPrefixes.json */; }; 84C96E482CF9BB6400A80A01 /* malwareFilterSet.json in Resources */ = {isa = PBXBuildFile; fileRef = 84C96E442CF9BB6400A80A01 /* malwareFilterSet.json */; }; @@ -2719,8 +2704,6 @@ B6A5A27125B9377300AA7ADA /* StatePersistenceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A5A27025B9377300AA7ADA /* StatePersistenceService.swift */; }; B6A5A27925B93FFF00AA7ADA /* StateRestorationManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A5A27825B93FFE00AA7ADA /* StateRestorationManagerTests.swift */; }; B6A5A27E25B9403E00AA7ADA /* FileStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A5A27D25B9403E00AA7ADA /* FileStoreMock.swift */; }; - B6A5A2A025B96E8300AA7ADA /* AppStateChangePublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A5A29F25B96E8300AA7ADA /* AppStateChangePublisherTests.swift */; }; - B6A5A2A825BAA35500AA7ADA /* WindowManagerStateRestorationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A5A2A725BAA35500AA7ADA /* WindowManagerStateRestorationTests.swift */; }; B6A924D92664C72E001A28CA /* WebKitDownloadTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A924D82664C72D001A28CA /* WebKitDownloadTask.swift */; }; B6A9E46B2614618A0067D1B9 /* OperatingSystemVersionExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A9E46A2614618A0067D1B9 /* OperatingSystemVersionExtension.swift */; }; B6AA64732994B43300D99CD6 /* FutureExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6AA64722994B43300D99CD6 /* FutureExtensionTests.swift */; }; @@ -3036,8 +3019,6 @@ C1CE846A2C887CF60068913B /* FreemiumDBPScanResultPolling.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1CE84682C887CF60068913B /* FreemiumDBPScanResultPolling.swift */; }; C1CE846C2C88868D0068913B /* FreemiumDBPScanResultPollingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1CE846B2C88868D0068913B /* FreemiumDBPScanResultPollingTests.swift */; }; C1CE846D2C88868D0068913B /* FreemiumDBPScanResultPollingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1CE846B2C88868D0068913B /* FreemiumDBPScanResultPollingTests.swift */; }; - C1D8BE452C1739E70057E426 /* DataBrokerProtectionMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D8BE442C1739E70057E426 /* DataBrokerProtectionMocks.swift */; }; - C1D8BE462C1739EC0057E426 /* DataBrokerProtectionMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D8BE442C1739E70057E426 /* DataBrokerProtectionMocks.swift */; }; C1DAF3B52B9A44860059244F /* AutofillPopoverPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1DAF3B42B9A44860059244F /* AutofillPopoverPresenter.swift */; }; C1DAF3B62B9A44860059244F /* AutofillPopoverPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1DAF3B42B9A44860059244F /* AutofillPopoverPresenter.swift */; }; C1E961EB2B879E79001760E1 /* MockAutofillActionPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1E961E72B879E4D001760E1 /* MockAutofillActionPresenter.swift */; }; @@ -3113,8 +3094,6 @@ EE3424612BA0853900173B1B /* VPNUninstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE34245D2BA0853900173B1B /* VPNUninstaller.swift */; }; EE42CBCC2BC8004700AD411C /* PermissionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE42CBCB2BC8004700AD411C /* PermissionsTests.swift */; }; EE54F7B32BBFEA49006218DB /* BookmarksAndFavoritesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE54F7B22BBFEA48006218DB /* BookmarksAndFavoritesTests.swift */; }; - EE66418C2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE66418B2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift */; }; - EE66418D2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE66418B2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift */; }; EE66666F2B56EDE4001D898D /* VPNLocationsHostingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE66666E2B56EDE4001D898D /* VPNLocationsHostingViewController.swift */; }; EE6666702B56EDE4001D898D /* VPNLocationsHostingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE66666E2B56EDE4001D898D /* VPNLocationsHostingViewController.swift */; }; EE66F10C2C3431030071856E /* WebsiteAccount_isDuplicateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE66F10B2C3431030071856E /* WebsiteAccount_isDuplicateTests.swift */; }; @@ -3180,6 +3159,8 @@ F118EA862BEACC7000F77634 /* NonStandardPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F118EA842BEACC7000F77634 /* NonStandardPixel.swift */; }; F1476FC02C1359FB00EAE46A /* SubscriptionUIHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1476FBF2C1359FB00EAE46A /* SubscriptionUIHandler.swift */; }; F1476FC12C1359FB00EAE46A /* SubscriptionUIHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1476FBF2C1359FB00EAE46A /* SubscriptionUIHandler.swift */; }; + F14E5D562CFE1BE200B91BE6 /* BookmarksBarViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AC09C62C2D7DD6002D70E0 /* BookmarksBarViewControllerTests.swift */; }; + F14E5D572CFE1BE200B91BE6 /* BookmarksBarViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AC09C62C2D7DD6002D70E0 /* BookmarksBarViewControllerTests.swift */; }; F17114822C7C98FB009836C1 /* Logger+Favicons.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17114812C7C98FB009836C1 /* Logger+Favicons.swift */; }; F17114832C7C98FB009836C1 /* Logger+Favicons.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17114812C7C98FB009836C1 /* Logger+Favicons.swift */; }; F17114852C7C9D28009836C1 /* Logger+Fire.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17114842C7C9D28009836C1 /* Logger+Fire.swift */; }; @@ -3199,6 +3180,9 @@ 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 */; }; + F18E51022CF8C5650020D129 /* BrowserTabViewControllerOnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567A23E02C89B1EE0010F66C /* BrowserTabViewControllerOnboardingTests.swift */; }; + F18E51032CF8DAFB0020D129 /* WindowManagerStateRestorationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A5A2A725BAA35500AA7ADA /* WindowManagerStateRestorationTests.swift */; }; + F18E51042CF8DB2B0020D129 /* AppStateChangePublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A5A29F25B96E8300AA7ADA /* AppStateChangePublisherTests.swift */; }; 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 */; }; @@ -3211,12 +3195,15 @@ 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 */; }; + F1B09DA82D020F7A0045AD44 /* NetworkProtectionKeychainStore+TokenStoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B09DA72D020F7A0045AD44 /* NetworkProtectionKeychainStore+TokenStoring.swift */; }; + F1B09DA92D020F7A0045AD44 /* NetworkProtectionKeychainStore+TokenStoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B09DA72D020F7A0045AD44 /* NetworkProtectionKeychainStore+TokenStoring.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 */; }; + F1C074232D2D779D00999B02 /* Utilities in Frameworks */ = {isa = PBXBuildFile; productRef = F1C074222D2D779D00999B02 /* Utilities */; }; 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 */; }; @@ -3236,14 +3223,12 @@ F1D042902BFB9FA300A31506 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = F1D0428F2BFB9FA300A31506 /* Subscription */; }; F1D042942BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042932BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift */; }; F1D042952BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042932BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift */; }; - F1D042992BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042982BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift */; }; - F1D0429A2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042982BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift */; }; - F1D0429B2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042982BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift */; }; - F1D0429C2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042982BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift */; }; - F1D0429D2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042982BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift */; }; - F1D0429E2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042982BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift */; }; - F1D0429F2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042982BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift */; }; - F1D042A02BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042982BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift */; }; + F1D042992BFBABA100A31506 /* DefaultSubscriptionManager+StandardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042982BFBABA100A31506 /* DefaultSubscriptionManager+StandardConfiguration.swift */; }; + F1D0429A2BFBABA100A31506 /* DefaultSubscriptionManager+StandardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042982BFBABA100A31506 /* DefaultSubscriptionManager+StandardConfiguration.swift */; }; + F1D0429D2BFBABA100A31506 /* DefaultSubscriptionManager+StandardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042982BFBABA100A31506 /* DefaultSubscriptionManager+StandardConfiguration.swift */; }; + F1D0429E2BFBABA100A31506 /* DefaultSubscriptionManager+StandardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042982BFBABA100A31506 /* DefaultSubscriptionManager+StandardConfiguration.swift */; }; + F1D0429F2BFBABA100A31506 /* DefaultSubscriptionManager+StandardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042982BFBABA100A31506 /* DefaultSubscriptionManager+StandardConfiguration.swift */; }; + F1D042A02BFBABA100A31506 /* DefaultSubscriptionManager+StandardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042982BFBABA100A31506 /* DefaultSubscriptionManager+StandardConfiguration.swift */; }; F1D042A12BFBB4DD00A31506 /* DataBrokerProtectionSettings+Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042932BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift */; }; F1D042A22BFBB4DD00A31506 /* DataBrokerProtectionSettings+Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042932BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift */; }; F1D43AEE2B98D8DF00BAB743 /* MainMenuActions+VanillaBrowser.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D43AED2B98D8DF00BAB743 /* MainMenuActions+VanillaBrowser.swift */; }; @@ -3578,7 +3563,7 @@ 1EC711542D03421B0009EB5C /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; 1EC711572D035BC30009EB5C /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; 1ED910D42B63BFB300936947 /* IdentityTheftRestorationPagesUserScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityTheftRestorationPagesUserScript.swift; sourceTree = ""; }; - 1EEB2D792C986A40000D908B /* SubscriptionPagesUseSubscriptionFeatureTestsForStripe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionPagesUseSubscriptionFeatureTestsForStripe.swift; sourceTree = ""; }; + 1EEB2D792C986A40000D908B /* SubscriptionPagesUseSubscriptionFeatureForStripeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionPagesUseSubscriptionFeatureForStripeTests.swift; sourceTree = ""; }; 31031EB62CC94C6C00684340 /* AIChatRemoteSettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AIChatRemoteSettingsTests.swift; sourceTree = ""; }; 31031EB92CC9736500684340 /* AIChatOnboardingTabExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AIChatOnboardingTabExtensionTests.swift; sourceTree = ""; }; 310E79BE294A19A8007C49E8 /* FireproofingReferenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FireproofingReferenceTests.swift; sourceTree = ""; }; @@ -3641,7 +3626,6 @@ 31E163BC293A579E00963C10 /* PrivacyReferenceTestHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyReferenceTestHelper.swift; sourceTree = ""; }; 31E163BF293A581900963C10 /* privacy-reference-tests */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = "privacy-reference-tests"; path = "Submodules/privacy-reference-tests"; sourceTree = SOURCE_ROOT; }; 31E3FD702CE39C4600A392C8 /* AIChatDebugURLSettingsRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AIChatDebugURLSettingsRepresentable.swift; sourceTree = ""; }; - 31ECDA102BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerAuthenticationManagerBuilder.swift; sourceTree = ""; }; 31F25EFE2CC3CA00002F9084 /* AIChatMenuConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AIChatMenuConfigurationTests.swift; sourceTree = ""; }; 31F28C4C28C8EEC500119F70 /* YoutubePlayerUserScript.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = YoutubePlayerUserScript.swift; sourceTree = ""; }; 31F28C4E28C8EEC500119F70 /* YoutubeOverlayUserScript.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = YoutubeOverlayUserScript.swift; sourceTree = ""; }; @@ -4953,7 +4937,6 @@ C1C405862C7F80E50089DE8A /* PromotionView+FreemiumDBP.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PromotionView+FreemiumDBP.swift"; sourceTree = ""; }; C1CE84682C887CF60068913B /* FreemiumDBPScanResultPolling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FreemiumDBPScanResultPolling.swift; sourceTree = ""; }; C1CE846B2C88868D0068913B /* FreemiumDBPScanResultPollingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FreemiumDBPScanResultPollingTests.swift; sourceTree = ""; }; - C1D8BE442C1739E70057E426 /* DataBrokerProtectionMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionMocks.swift; sourceTree = ""; }; C1DAF3B42B9A44860059244F /* AutofillPopoverPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillPopoverPresenter.swift; sourceTree = ""; }; C1E961E72B879E4D001760E1 /* MockAutofillActionPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAutofillActionPresenter.swift; sourceTree = ""; }; C1E961EC2B879ED9001760E1 /* MockAutofillActionExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAutofillActionExecutor.swift; sourceTree = ""; }; @@ -4991,7 +4974,6 @@ EE34245D2BA0853900173B1B /* VPNUninstaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNUninstaller.swift; sourceTree = ""; }; EE42CBCB2BC8004700AD411C /* PermissionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PermissionsTests.swift; sourceTree = ""; }; EE54F7B22BBFEA48006218DB /* BookmarksAndFavoritesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarksAndFavoritesTests.swift; sourceTree = ""; }; - EE66418B2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift"; sourceTree = ""; }; EE66666E2B56EDE4001D898D /* VPNLocationsHostingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNLocationsHostingViewController.swift; sourceTree = ""; }; EE66F10B2C3431030071856E /* WebsiteAccount_isDuplicateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteAccount_isDuplicateTests.swift; sourceTree = ""; }; EE7F74902BB5D76600CD9456 /* BookmarksBarTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarksBarTests.swift; sourceTree = ""; }; @@ -5031,6 +5013,7 @@ 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 = ""; }; + F1B09DA72D020F7A0045AD44 /* NetworkProtectionKeychainStore+TokenStoring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NetworkProtectionKeychainStore+TokenStoring.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 = ""; }; @@ -5039,7 +5022,7 @@ F1C70D7B2BFF510000599292 /* SubscriptionEnvironment+Default.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SubscriptionEnvironment+Default.swift"; sourceTree = ""; }; F1CA67052C7DCA2300264E6A /* Logger+BitWarden.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+BitWarden.swift"; sourceTree = ""; }; F1D042932BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataBrokerProtectionSettings+Environment.swift"; sourceTree = ""; }; - F1D042982BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SubscriptionManager+StandardConfiguration.swift"; sourceTree = ""; }; + F1D042982BFBABA100A31506 /* DefaultSubscriptionManager+StandardConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DefaultSubscriptionManager+StandardConfiguration.swift"; sourceTree = ""; }; F1D43AED2B98D8DF00BAB743 /* MainMenuActions+VanillaBrowser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MainMenuActions+VanillaBrowser.swift"; sourceTree = ""; }; 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 = ""; }; @@ -5091,7 +5074,6 @@ 4BCBE45A2BA7E17800FC75A1 /* Subscription in Frameworks */, B6EC37FF29B8D915001ACE79 /* Configuration in Frameworks */, 567A23C52C7F75BB0010F66C /* SpecialErrorPages in Frameworks */, - 372217822B33380700B8E9C2 /* TestUtils in Frameworks */, 3706FCAA293F65D500E42796 /* UserScript in Frameworks */, 5641734D2CFE169400F4B716 /* PixelExperimentKit in Frameworks */, 85E2BBD02B8F534A00DBEC7A /* History in Frameworks */, @@ -5114,7 +5096,6 @@ files = ( 3706FE88293F661700E42796 /* OHHTTPStubs in Frameworks */, F116A7C72BD1925500F3FCF7 /* PixelKitTestingUtilities in Frameworks */, - 84BBC7FF2CFA0D2F00BAE57A /* TestUtils in Frameworks */, B65CD8CF2B316E0200A595BB /* SnapshotTesting in Frameworks */, 9DC5FACD2C6B8E620011F068 /* AppKitExtensions in Frameworks */, 374EFDF12D01C70A00B30939 /* Utilities in Frameworks */, @@ -5371,7 +5352,6 @@ 37BA812D29B3CD690053F1A3 /* SyncUI in Frameworks */, 9D9DE5732C63AA0700D20B15 /* AppKitExtensions in Frameworks */, F1DF95E72BD188B60045E591 /* LoginItems in Frameworks */, - 372217802B3337FE00B8E9C2 /* TestUtils in Frameworks */, CBECDB8A2CDBD616005B8B87 /* PageRefreshMonitor in Frameworks */, 7BA076BB2B65D61400D7FB72 /* NetworkProtectionProxy in Frameworks */, 4B4D60B12A0C83B900BCD287 /* NetworkProtectionUI in Frameworks */, @@ -5388,10 +5368,9 @@ 46066CBC2D1330A100AB683B /* Persistence in Frameworks */, B6DA44172616C13800DD1EC2 /* OHHTTPStubs in Frameworks */, F116A7C32BD1924B00F3FCF7 /* PixelKitTestingUtilities in Frameworks */, - 84BBC8012CFA0D3800BAE57A /* TestUtils in Frameworks */, + F1C074232D2D779D00999B02 /* Utilities in Frameworks */, B65CD8CB2B316DF100A595BB /* SnapshotTesting in Frameworks */, 9DC5FACB2C6B8E050011F068 /* AppKitExtensions in Frameworks */, - 374EFDEF2D01C70300B30939 /* Utilities in Frameworks */, F1DA51A92BF6114C00CF29FA /* SubscriptionTestingUtilities in Frameworks */, B6DA44192616C13800DD1EC2 /* OHHTTPStubsSwift in Frameworks */, ); @@ -5711,7 +5690,6 @@ isa = PBXGroup; children = ( C1D8BE432C1739BF0057E426 /* Tests */, - C1D8BE422C1739BA0057E426 /* Mocks */, ); path = DBP; sourceTree = ""; @@ -6310,7 +6288,6 @@ children = ( 4B43468E285ED6CB00177407 /* ViewModel */, 9F3344612BBFBDA40040CBEB /* BookmarksBarVisibilityManagerTests.swift */, - 56AC09C62C2D7DD6002D70E0 /* BookmarksBarViewControllerTests.swift */, ); path = BookmarksBar; sourceTree = ""; @@ -6318,6 +6295,7 @@ 4B43468E285ED6CB00177407 /* ViewModel */ = { isa = PBXGroup; children = ( + 56AC09C62C2D7DD6002D70E0 /* BookmarksBarViewControllerTests.swift */, 4B43468F285ED7A100177407 /* BookmarksBarViewModelTests.swift */, ); path = ViewModel; @@ -6465,8 +6443,8 @@ EEBCA0C12BD7CDDA004DF19C /* Pixels */, 4B41ED9F2B15437A001EEDF4 /* NetworkProtectionNotificationsPresenterFactory.swift */, EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */, + F1B09DA72D020F7A0045AD44 /* NetworkProtectionKeychainStore+TokenStoring.swift */, 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */, - EE66418B2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift */, ); path = NetworkExtensionTargets; sourceTree = ""; @@ -7134,7 +7112,6 @@ 56CE77602C7DFCF800AC1ED2 /* OnboardingSuggestedSearchesProviderTests.swift */, 567A23DA2C8894CD0010F66C /* ContextualDaxDialogsFactoryTests.swift */, 567A23DD2C89980A0010F66C /* OnboardingNavigationDelegateTests.swift */, - 567A23E02C89B1EE0010F66C /* BrowserTabViewControllerOnboardingTests.swift */, 5677A9392C983FF100DA7B0A /* ContextualOnboardingStateMachineTests.swift */, 56A214AE2CB583BF00E5BC0E /* TrackerMessageProviderTests.swift */, 9FBB0C082CBD39B70006B6A6 /* ViewHighlighterTests.swift */, @@ -7186,6 +7163,9 @@ 7B4CE8DB26F02108009134B1 /* UITests */ = { isa = PBXGroup; children = ( + B6A5A29F25B96E8300AA7ADA /* AppStateChangePublisherTests.swift */, + B6A5A2A725BAA35500AA7ADA /* WindowManagerStateRestorationTests.swift */, + 567A23E02C89B1EE0010F66C /* BrowserTabViewControllerOnboardingTests.swift */, BB731F302CDBA6320023D2E4 /* FireWindowTests.swift */, 376E708D2BD686260082B7EB /* UI Tests.xctestplan */, EEBCE6802BA444FA00B9DF00 /* Common */, @@ -7719,7 +7699,6 @@ 9D9AE9152AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift */, F17E7DDB2C7C7F8100907A84 /* Logger+DBPBackgroundAgent.swift */, 9D9AE9172AAA3B450026E7DC /* UserText.swift */, - 31ECDA102BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift */, 9D9AE9162AAA3B450026E7DC /* Assets.xcassets */, 9D9AE91A2AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgent.entitlements */, 9D9AE9192AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppStore.entitlements */, @@ -7764,7 +7743,7 @@ F1AFDBD12C231B7A00710F2C /* SubscriptionAppStoreRestorerTests.swift */, F1AFDBD32C231B9700710F2C /* SubscriptionErrorReporterTests.swift */, 1E2BEAE32C8B00B5002741A3 /* SubscriptionPagesUseSubscriptionFeatureTests.swift */, - 1EEB2D792C986A40000D908B /* SubscriptionPagesUseSubscriptionFeatureTestsForStripe.swift */, + 1EEB2D792C986A40000D908B /* SubscriptionPagesUseSubscriptionFeatureForStripeTests.swift */, ); path = Subscription; sourceTree = ""; @@ -9382,8 +9361,6 @@ B6A5A28C25B962CB00AA7ADA /* App */ = { isa = PBXGroup; children = ( - B6A5A2A725BAA35500AA7ADA /* WindowManagerStateRestorationTests.swift */, - B6A5A29F25B96E8300AA7ADA /* AppStateChangePublisherTests.swift */, 1D7693FE2BE3A1AA0016A22B /* DockCustomizerMock.swift */, 1DA860712BE3AE950027B813 /* DockPositionProviderTests.swift */, ); @@ -9754,14 +9731,6 @@ path = Promotion; sourceTree = ""; }; - C1D8BE422C1739BA0057E426 /* Mocks */ = { - isa = PBXGroup; - children = ( - C1D8BE442C1739E70057E426 /* DataBrokerProtectionMocks.swift */, - ); - path = Mocks; - sourceTree = ""; - }; C1D8BE432C1739BF0057E426 /* Tests */ = { isa = PBXGroup; children = ( @@ -9943,7 +9912,7 @@ F1D042932BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift */, F118EA7C2BEA2B8700F77634 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift */, F1DA51842BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift */, - F1D042982BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift */, + F1D042982BFBABA100A31506 /* DefaultSubscriptionManager+StandardConfiguration.swift */, F1DA51852BF607D200CF29FA /* SubscriptionRedirectManager.swift */, F1FDC9372BF51F41006B1435 /* VPNSettings+Environment.swift */, 1E25A4FD2CC937120080EFD4 /* SubscriptionCookieManageEventPixelMapping.swift */, @@ -10011,7 +9980,6 @@ 37DF000629F9C061002B7D3E /* SyncDataProviders */, 9DC70B192AA1FA5B005A844B /* LoginItems */, 37269EFC2B332FAC005E8E46 /* Common */, - 372217812B33380700B8E9C2 /* TestUtils */, 4BF97AD02B43C43F00EB4240 /* NetworkProtectionIPC */, 4BF97AD22B43C43F00EB4240 /* NetworkProtectionUI */, 4BF97AD42B43C43F00EB4240 /* NetworkProtection */, @@ -10070,7 +10038,6 @@ F116A7C62BD1925500F3FCF7 /* PixelKitTestingUtilities */, F1DA51A42BF6114200CF29FA /* SubscriptionTestingUtilities */, 9DC5FACC2C6B8E620011F068 /* AppKitExtensions */, - 84BBC7FE2CFA0D2F00BAE57A /* TestUtils */, 374EFDF02D01C70A00B30939 /* Utilities */, ); productName = DuckDuckGoTests; @@ -10502,7 +10469,6 @@ 7BA59C9A2AE18B49009A97B1 /* SystemExtensionManager */, 31A3A4E22B0C115F0021063C /* DataBrokerProtection */, 37269EFA2B332F9E005E8E46 /* Common */, - 3722177F2B3337FE00B8E9C2 /* TestUtils */, 7BA076BA2B65D61400D7FB72 /* NetworkProtectionProxy */, 85E2BBCD2B8F534000DBEC7A /* History */, 1EA7B8D22B7E078C000330A4 /* SubscriptionUI */, @@ -10557,9 +10523,8 @@ F116A7C22BD1924B00F3FCF7 /* PixelKitTestingUtilities */, F1DA51A82BF6114C00CF29FA /* SubscriptionTestingUtilities */, 9DC5FACA2C6B8E050011F068 /* AppKitExtensions */, - 84BBC8002CFA0D3800BAE57A /* TestUtils */, - 374EFDEE2D01C70300B30939 /* Utilities */, 46066CBB2D1330A100AB683B /* Persistence */, + F1C074222D2D779D00999B02 /* Utilities */, ); productName = DuckDuckGoTests; productReference = AA585D90248FD31400E9A3E2 /* Unit Tests.xctest */; @@ -11578,7 +11543,7 @@ 85774B042A71CDD000DE0561 /* BlockMenuItem.swift in Sources */, 3706FB19293F65D500E42796 /* FireViewController.swift in Sources */, B6E3E55C2BC0041A00A41922 /* DownloadListStoreMock.swift in Sources */, - F1D0429A2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */, + F1D0429A2BFBABA100A31506 /* DefaultSubscriptionManager+StandardConfiguration.swift in Sources */, 4B4D60D42A0C84F700BCD287 /* UserText+NetworkProtection.swift in Sources */, 3706FB1A293F65D500E42796 /* OutlineSeparatorViewCell.swift in Sources */, 3706FB1B293F65D500E42796 /* SafariDataImporter.swift in Sources */, @@ -12281,7 +12246,6 @@ B602E8172A1E2570006D261F /* URL+NetworkProtection.swift in Sources */, 3706FCA1293F65D500E42796 /* AdjacentItemEnumerator.swift in Sources */, 9F9C49FA2BC7BC970099738D /* BookmarkAllTabsDialogView.swift in Sources */, - 31ECDA122BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */, 3706FCA2293F65D500E42796 /* ChromiumKeychainPrompt.swift in Sources */, 3707C71E294B5D2900682A9F /* URLRequestExtension.swift in Sources */, 3706FCA3293F65D500E42796 /* WKProcessPool+GeolocationProvider.swift in Sources */, @@ -12345,7 +12309,7 @@ 31031EBB2CC9736500684340 /* AIChatOnboardingTabExtensionTests.swift in Sources */, 56534DEE29DF252C00121467 /* CapturingDefaultBrowserProvider.swift in Sources */, 561D29C32BDA745B007B91D0 /* MockSyncPausedStateManaging.swift in Sources */, - 1EEB2D7B2C986A40000D908B /* SubscriptionPagesUseSubscriptionFeatureTestsForStripe.swift in Sources */, + 1EEB2D7B2C986A40000D908B /* SubscriptionPagesUseSubscriptionFeatureForStripeTests.swift in Sources */, 3706FDF8293F661700E42796 /* FileStoreTests.swift in Sources */, 1D838A342C44F0320078373F /* ReleaseNotesParserTests.swift in Sources */, 5603D90729B7B746007F9F01 /* MockTabViewItemDelegate.swift in Sources */, @@ -12423,7 +12387,6 @@ 9F39106A2B68D87B00CB5112 /* ProgressExtensionTests.swift in Sources */, 9FAD623B2BCFDB32007F3A65 /* WebsiteInfoHelpers.swift in Sources */, 56A0542E2C201DAA007D8FAB /* MockContentBlocking.swift in Sources */, - 3706FE1F293F661700E42796 /* AppStateChangePublisherTests.swift in Sources */, 9FA5A0B12BC9039300153786 /* BookmarkFolderStoreMock.swift in Sources */, BBBEE1C02C4FF63600035ABA /* SortBookmarksViewModelTests.swift in Sources */, 9F0660792BECC81C00B8EEF1 /* PixelCapturedParameters.swift in Sources */, @@ -12455,8 +12418,8 @@ 3706FE29293F661700E42796 /* SuggestionContainerViewModelTests.swift in Sources */, 567A23CF2C80CF4B0010F66C /* ErrorPageTabExtensionTest.swift in Sources */, 37D046A22C7DA9A200AEAA50 /* UserBackgroundImagesManagerTests.swift in Sources */, + F14E5D572CFE1BE200B91BE6 /* BookmarksBarViewControllerTests.swift in Sources */, 37DB56F02C3B31CD0093D4DC /* MockRemoteMessagingAvailabilityProvider.swift in Sources */, - 567A23E22C89B1EE0010F66C /* BrowserTabViewControllerOnboardingTests.swift in Sources */, 1D8C2FEB2B70F5A7005E4BBD /* MockWebViewSnapshotRenderer.swift in Sources */, 56A0540E2C1C375E007D8FAB /* MockWindow.swift in Sources */, 3706FE2A293F661700E42796 /* SafariVersionReaderTests.swift in Sources */, @@ -12478,7 +12441,6 @@ B60C6F8229B1B4AD007BFAA8 /* TestRunHelper.swift in Sources */, 567DA94029E8045D008AC5EE /* MockEmailStorage.swift in Sources */, 9FBB0C0A2CBD3B800006B6A6 /* ViewHighlighterTests.swift in Sources */, - C1D8BE462C1739EC0057E426 /* DataBrokerProtectionMocks.swift in Sources */, 317295D32AF058D3002C3206 /* MockWaitlistTermsAndConditionsActionHandler.swift in Sources */, 9F0FFFB52BCCAE37007C87DD /* BookmarkAllTabsDialogCoordinatorViewModelTests.swift in Sources */, 3706FE34293F661700E42796 /* PermissionStoreTests.swift in Sources */, @@ -12536,7 +12498,6 @@ 3706FE4E293F661700E42796 /* BookmarkListTests.swift in Sources */, 3706FE4F293F661700E42796 /* BookmarksExporterTests.swift in Sources */, 566B196629CDB829007E38F4 /* CapturingOptionsButtonMenuDelegate.swift in Sources */, - 3706FE50293F661700E42796 /* WindowManagerStateRestorationTests.swift in Sources */, 3706FE51293F661700E42796 /* SafariBookmarksReaderTests.swift in Sources */, 3706FE52293F661700E42796 /* FileSystemDSLTests.swift in Sources */, 3706FE53293F661700E42796 /* CoreDataEncryptionTests.swift in Sources */, @@ -12551,7 +12512,6 @@ 3706FE58293F661700E42796 /* HistoryStoreTests.swift in Sources */, 3706FE59293F661700E42796 /* EncryptionKeyGeneratorTests.swift in Sources */, 3706FE5A293F661700E42796 /* GeolocationServiceMock.swift in Sources */, - 56AC09C82C2D7DD6002D70E0 /* BookmarksBarViewControllerTests.swift in Sources */, 3706FE5B293F661700E42796 /* FirefoxLoginReaderTests.swift in Sources */, 1D1C36E429FAE8DA001FA40C /* FaviconManagerTests.swift in Sources */, 31F25EFF2CC3CA02002F9084 /* AIChatMenuConfigurationTests.swift in Sources */, @@ -12750,9 +12710,9 @@ 4B25377A2A11C01700610219 /* UserText+NetworkProtectionExtensions.swift in Sources */, B65DA5F42A77D3FA00CBEE8D /* BundleExtension.swift in Sources */, 4B52354D2C854CB600AFAF64 /* DuckDuckGoUserAgent.swift in Sources */, - EE66418D2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift in Sources */, EEBCA0C72BD7CE2C004DF19C /* VPNFailureRecoveryPixel.swift in Sources */, F1DA51932BF6081D00CF29FA /* AttributionPixelHandler.swift in Sources */, + F1B09DA92D020F7A0045AD44 /* NetworkProtectionKeychainStore+TokenStoring.swift in Sources */, 7B2E52252A5FEC09000C6D39 /* NetworkProtectionAgentNotificationsPresenter.swift in Sources */, F1DA51892BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift in Sources */, B602E8232A1E260E006D261F /* Bundle+NetworkProtectionExtensions.swift in Sources */, @@ -12767,7 +12727,6 @@ F1C70D7F2BFF510000599292 /* SubscriptionEnvironment+Default.swift in Sources */, EEF12E6F2A2111880023E6BF /* MacPacketTunnelProvider.swift in Sources */, 4B2D06292A11C0C900DE1F49 /* Bundle+VPN.swift in Sources */, - F1D0429C2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */, F1DA51972BF6083A00CF29FA /* PrivacyProPixel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -12787,7 +12746,7 @@ 7B2DDCFA2A93B25F0039D884 /* KeychainType+ClientDefault.swift in Sources */, 02FDA6592C764B970024CD8B /* ConfigurationManager.swift in Sources */, 7BA7CC4C2AD11EC70042E5CE /* NetworkProtectionControllerErrorStore.swift in Sources */, - F1D0429D2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */, + F1D0429D2BFBABA100A31506 /* DefaultSubscriptionManager+StandardConfiguration.swift in Sources */, 7B8DB31A2B504D7500EC16DA /* VPNAppEventsHandler.swift in Sources */, 02FDA65F2C764E220024CD8B /* VPNPrivacyConfigurationManager.swift in Sources */, 7BA7CC532AD11FCE0042E5CE /* Bundle+VPN.swift in Sources */, @@ -12833,7 +12792,7 @@ 7B22D8702CCFD7B7006A76E1 /* TipKitController.swift in Sources */, F1DA51952BF6081E00CF29FA /* AttributionPixelHandler.swift in Sources */, 4BA7C4D92B3F61FB00AFE511 /* BundleExtension.swift in Sources */, - F1D0429E2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */, + F1D0429E2BFBABA100A31506 /* DefaultSubscriptionManager+StandardConfiguration.swift in Sources */, 02FDA65C2C764CB00024CD8B /* ConfigurationManager.swift in Sources */, 7BA7CC3F2AD11E3D0042E5CE /* AppLauncher+DefaultInitializer.swift in Sources */, 4B0EF7292B5780EB009D6481 /* VPNAppEventsHandler.swift in Sources */, @@ -12888,14 +12847,13 @@ buildActionMask = 2147483647; files = ( 4B41EDA02B15437A001EEDF4 /* NetworkProtectionNotificationsPresenterFactory.swift in Sources */, - EE66418C2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift in Sources */, 7B7DFB202B7E736B009EA1A3 /* MacPacketTunnelProvider.swift in Sources */, F1DA51962BF6083700CF29FA /* PrivacyProPixel.swift in Sources */, EEBCA0C62BD7CE2C004DF19C /* VPNFailureRecoveryPixel.swift in Sources */, 4B4D60A12A0B2D6100BCD287 /* NetworkProtectionOptionKeyExtension.swift in Sources */, - F1D0429B2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */, B602E8182A1E2570006D261F /* URL+NetworkProtection.swift in Sources */, B65DA5F52A77D3FA00CBEE8D /* BundleExtension.swift in Sources */, + F1B09DA82D020F7A0045AD44 /* NetworkProtectionKeychainStore+TokenStoring.swift in Sources */, 4B52354E2C854CB700AFAF64 /* DuckDuckGoUserAgent.swift in Sources */, 4B4D60892A0B2A1C00BCD287 /* NetworkProtectionUNNotificationsPresenter.swift in Sources */, F1C70D7E2BFF510000599292 /* SubscriptionEnvironment+Default.swift in Sources */, @@ -12927,6 +12885,7 @@ EE7F74912BB5D76600CD9456 /* BookmarksBarTests.swift in Sources */, EE02D41C2BB460A600DBE6B3 /* BrowsingHistoryTests.swift in Sources */, EE02D41A2BB4609900DBE6B3 /* UITests.swift in Sources */, + F18E51032CF8DAFB0020D129 /* WindowManagerStateRestorationTests.swift in Sources */, EE0429E02BA31D2F009EB20F /* FindInPageTests.swift in Sources */, BBCD467A2C8643EC004DB483 /* XCUIApplicationExtension.swift in Sources */, BBBB65402C77BB9400E69AC6 /* BookmarkSearchTests.swift in Sources */, @@ -12935,6 +12894,7 @@ EEC7BE2E2BC6C09500F86835 /* AddressBarKeyboardShortcutsTests.swift in Sources */, EE54F7B32BBFEA49006218DB /* BookmarksAndFavoritesTests.swift in Sources */, EE02D4222BB4611A00DBE6B3 /* TestsURLExtension.swift in Sources */, + F18E51042CF8DB2B0020D129 /* AppStateChangePublisherTests.swift in Sources */, BB731F312CDBA6360023D2E4 /* FireWindowTests.swift in Sources */, EE42CBCC2BC8004700AD411C /* PermissionsTests.swift in Sources */, 7B4CE8E726F02135009134B1 /* TabBarTests.swift in Sources */, @@ -12942,6 +12902,7 @@ EEBCE6832BA463DD00B9DF00 /* NSImageExtensions.swift in Sources */, EEBCE6822BA444FA00B9DF00 /* XCUIElementExtension.swift in Sources */, EED735362BB46B6000F173D6 /* AutocompleteTests.swift in Sources */, + F18E51022CF8C5650020D129 /* BrowserTabViewControllerOnboardingTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -12990,8 +12951,7 @@ 1EFA1A072C7C7F0E0099F508 /* PrivacyProPixel.swift in Sources */, 9D9AE91D2AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift in Sources */, 9D9AE9212AAA3B450026E7DC /* UserText.swift in Sources */, - 31ECDA132BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */, - F1D0429F2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */, + F1D0429F2BFBABA100A31506 /* DefaultSubscriptionManager+StandardConfiguration.swift in Sources */, 31ECDA0E2BED317300AE679F /* BundleExtension.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -13007,8 +12967,7 @@ 1EFA1A082C7C7F0F0099F508 /* PrivacyProPixel.swift in Sources */, 9D9AE91E2AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift in Sources */, 9D9AE9222AAA3B450026E7DC /* UserText.swift in Sources */, - 31ECDA142BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */, - F1D042A02BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */, + F1D042A02BFBABA100A31506 /* DefaultSubscriptionManager+StandardConfiguration.swift in Sources */, 31ECDA0F2BED317300AE679F /* BundleExtension.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -13026,7 +12985,7 @@ 4B9DB0412A983B24000927DB /* WaitlistDialogView.swift in Sources */, 37D2377A287EB8CA00BCE03B /* TabIndex.swift in Sources */, B60C6F8D29B200AB007BFAA8 /* SavePanelAccessoryView.swift in Sources */, - F1D042992BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */, + F1D042992BFBABA100A31506 /* DefaultSubscriptionManager+StandardConfiguration.swift in Sources */, 37534CA3281132CB002621E7 /* TabLazyLoaderDataSource.swift in Sources */, 84537A042C998C28008723BC /* FireWindowSession.swift in Sources */, 3158B15C2B0BF76D00AF130C /* DataBrokerProtectionAppEvents.swift in Sources */, @@ -13376,7 +13335,6 @@ B60293E62BA19ECD0033186B /* NetPPopoverManagerMock.swift in Sources */, B6B3E0E12657EA7A0040E0A2 /* NSScreenExtension.swift in Sources */, B65E6BA026D9F10600095F96 /* NSBezierPathExtension.swift in Sources */, - 31ECDA112BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */, 4B4D60E02A0C875F00BCD287 /* Bundle+VPN.swift in Sources */, AA6820E425502F19005ED0D5 /* WebsiteDataStore.swift in Sources */, 3199AF732C80734A003AEBDC /* DuckPlayerOnboardingModalManager.swift in Sources */, @@ -14068,7 +14026,6 @@ 028904202A7B25380028369C /* AppConfigurationURLProviderTests.swift in Sources */, B65349AA265CF45000DCC645 /* DispatchQueueExtensionsTests.swift in Sources */, 858A798A26A9B35E00A75A42 /* PasswordManagementItemModelTests.swift in Sources */, - 56AC09C72C2D7DD6002D70E0 /* BookmarksBarViewControllerTests.swift in Sources */, 1D3B1AB92934062B006F4388 /* PasswordManagerCoordinatingMock.swift in Sources */, 1D77921828FDC54C00BE0210 /* FaviconReferenceCacheTests.swift in Sources */, FD23FD2B28816606007F6985 /* AutoconsentMessageProtocolTests.swift in Sources */, @@ -14125,7 +14082,6 @@ B6106BB526A809E60013B453 /* GeolocationProviderTests.swift in Sources */, BDA764912BC4E57200D0400C /* MockVPNLocationFormatter.swift in Sources */, B6E6BA232BA2EDDE008AA7E1 /* FileReadResult.swift in Sources */, - B6A5A2A025B96E8300AA7ADA /* AppStateChangePublisherTests.swift in Sources */, B63ED0E326B3E7FA00A9DAD1 /* CLLocationManagerMock.swift in Sources */, 37CD54BB27F25A4000F1F7B9 /* DownloadsPreferencesTests.swift in Sources */, 4BE344EE2B2376DF003FC223 /* UnifiedFeedbackFormViewModelTests.swift in Sources */, @@ -14160,7 +14116,6 @@ 4B98D27A28D95F1A003C2B6F /* ChromiumFaviconsReaderTests.swift in Sources */, 9F0660732BECC71200B8EEF1 /* SubscriptionAttributionPixelHandlerTests.swift in Sources */, BDCB66D82C7CE1A600E8ABC9 /* VPNFeedbackFormViewModelTests.swift in Sources */, - 567A23E12C89B1EE0010F66C /* BrowserTabViewControllerOnboardingTests.swift in Sources */, 986189E62A7CFB3E001B4519 /* LocalBookmarkStoreSavingTests.swift in Sources */, AA652CD325DDA6E9009059CC /* LocalBookmarkManagerTests.swift in Sources */, CBDD5DE329A67F2700832877 /* MockConfigurationStore.swift in Sources */, @@ -14224,11 +14179,9 @@ BDA7648D2BC4E4EF00D0400C /* DefaultVPNLocationFormatterTests.swift in Sources */, 85F487B5276A8F2E003CE668 /* OnboardingTests.swift in Sources */, B626A7642992506A00053070 /* SerpHeadersNavigationResponderTests.swift in Sources */, - C1D8BE452C1739E70057E426 /* DataBrokerProtectionMocks.swift in Sources */, 5677A93D2C98414900DA7B0A /* ContextualOnboardingStateMachineTests.swift in Sources */, AA652CCE25DD9071009059CC /* BookmarkListTests.swift in Sources */, 859E7D6D274548F2009C2B69 /* BookmarksExporterTests.swift in Sources */, - B6A5A2A825BAA35500AA7ADA /* WindowManagerStateRestorationTests.swift in Sources */, B6AE39F129373AF200C37AA4 /* EmptyAttributionRulesProver.swift in Sources */, 4BB99D1126FE1A84001E4761 /* SafariBookmarksReaderTests.swift in Sources */, BBC063E82C5A9E4B007BDC18 /* BookmarkManagementDetailViewModelTests.swift in Sources */, @@ -14264,7 +14217,7 @@ 4BBF09232830812900EE1418 /* FileSystemDSL.swift in Sources */, 7B4C5CF52BE51D640007A164 /* VPNUninstallerTests.swift in Sources */, 4B9DB05C2A983B55000927DB /* WaitlistViewModelTests.swift in Sources */, - 1EEB2D7A2C986A40000D908B /* SubscriptionPagesUseSubscriptionFeatureTestsForStripe.swift in Sources */, + 1EEB2D7A2C986A40000D908B /* SubscriptionPagesUseSubscriptionFeatureForStripeTests.swift in Sources */, 1D1C36E329FAE8DA001FA40C /* FaviconManagerTests.swift in Sources */, 9F0FFFB42BCCAE37007C87DD /* BookmarkAllTabsDialogCoordinatorViewModelTests.swift in Sources */, 4B723E0526B0003E00E14D75 /* DataImportMocks.swift in Sources */, @@ -14275,6 +14228,7 @@ 37E260922C8A3EB4006EE07F /* MockHomePageSettingsModelNavigator.swift in Sources */, 4BF4EA5027C71F26004E57C4 /* PasswordManagementListSectionTests.swift in Sources */, AA7E9176286DB05D00AB6B62 /* RecentlyClosedCoordinatorMock.swift in Sources */, + F14E5D562CFE1BE200B91BE6 /* BookmarksBarViewControllerTests.swift in Sources */, 31E163BD293A579E00963C10 /* PrivacyReferenceTestHelper.swift in Sources */, B693955D26F19CD70015B914 /* DownloadListStoreTests.swift in Sources */, B610F2EB27AA8E4500FCEBE9 /* ContentBlockingUpdatingTests.swift in Sources */, @@ -15385,8 +15339,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { - kind = exactVersion; - version = 226.0.0; + branch = fcappelli/subscription_oauth_api_v2; + kind = branch; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { @@ -15657,16 +15611,6 @@ package = 371D00DF29D8509400EC8598 /* XCRemoteSwiftPackageReference "OpenSSL-XCFramework" */; productName = OpenSSL; }; - 3722177F2B3337FE00B8E9C2 /* TestUtils */ = { - isa = XCSwiftPackageProductDependency; - package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; - productName = TestUtils; - }; - 372217812B33380700B8E9C2 /* TestUtils */ = { - isa = XCSwiftPackageProductDependency; - package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; - productName = TestUtils; - }; 37269EFA2B332F9E005E8E46 /* Common */ = { isa = XCSwiftPackageProductDependency; package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; @@ -15714,10 +15658,6 @@ isa = XCSwiftPackageProductDependency; productName = Utilities; }; - 374EFDEE2D01C70300B30939 /* Utilities */ = { - isa = XCSwiftPackageProductDependency; - productName = Utilities; - }; 374EFDF02D01C70A00B30939 /* Utilities */ = { isa = XCSwiftPackageProductDependency; productName = Utilities; @@ -16041,16 +15981,6 @@ package = B6DA44152616C13800DD1EC2 /* XCRemoteSwiftPackageReference "OHHTTPStubs" */; productName = OHHTTPStubsSwift; }; - 84BBC7FE2CFA0D2F00BAE57A /* TestUtils */ = { - isa = XCSwiftPackageProductDependency; - package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; - productName = TestUtils; - }; - 84BBC8002CFA0D3800BAE57A /* TestUtils */ = { - isa = XCSwiftPackageProductDependency; - package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; - productName = TestUtils; - }; 85D44B852BA08D29001B4AB5 /* Suggestions */ = { isa = XCSwiftPackageProductDependency; package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; @@ -16433,6 +16363,10 @@ package = B6F997B92B8F352500476735 /* XCRemoteSwiftPackageReference "apple-toolbox" */; productName = SwiftLintTool; }; + F1C074222D2D779D00999B02 /* Utilities */ = { + isa = XCSwiftPackageProductDependency; + productName = Utilities; + }; F1D0428D2BFB9F9C00A31506 /* Subscription */ = { 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 244ddfe15b..a92789fe7e 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "d1275238d88f25cd31a29be2b4f20bbba5de9aa1", - "version" : "226.0.0" + "branch" : "fcappelli/subscription_oauth_api_v2", + "revision" : "0eebb62e9ecc995185ea1c79c31318d50d05ba40" } }, { @@ -72,6 +72,15 @@ "version" : "6.0.1" } }, + { + "identity" : "jwt-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/jwt-kit.git", + "state" : { + "revision" : "c2595b9ad7f512d7f334830b4df1fed6e917946a", + "version" : "4.13.4" + } + }, { "identity" : "lottie-spm", "kind" : "remoteSourceControl", @@ -135,6 +144,24 @@ "version" : "1.4.0" } }, + { + "identity" : "swift-asn1", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-asn1.git", + "state" : { + "revision" : "ae33e5941bb88d88538d0a6b19ca0b01e6c76dcf", + "version" : "1.3.1" + } + }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto.git", + "state" : { + "revision" : "ff0f781cf7c6a22d52957e50b104f5768b50c779", + "version" : "3.10.0" + } + }, { "identity" : "swift-snapshot-testing", "kind" : "remoteSourceControl", diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/Unit Tests.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/Unit Tests.xcscheme new file mode 100644 index 0000000000..609dd378bd --- /dev/null +++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/Unit Tests.xcscheme @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index 46428b6482..793ab21ae4 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -107,7 +107,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { let remoteMessagingClient: RemoteMessagingClient! let onboardingStateMachine: ContextualOnboardingStateMachine & ContextualOnboardingStateUpdater - public let subscriptionManager: SubscriptionManager + public let subscriptionManager: any SubscriptionManager public let subscriptionUIHandler: SubscriptionUIHandling private let subscriptionCookieManager: SubscriptionCookieManaging private var subscriptionCookieManagerFeatureFlagCancellable: AnyCancellable? @@ -133,9 +133,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate { // MARK: - DBP private lazy var dataBrokerProtectionSubscriptionEventHandler: DataBrokerProtectionSubscriptionEventHandler = { - let authManager = DataBrokerAuthenticationManagerBuilder.buildAuthenticationManager(subscriptionManager: subscriptionManager) + let authenticationManager = DataBrokerProtectionAuthenticationManager( + subscriptionManager: subscriptionManager) return DataBrokerProtectionSubscriptionEventHandler(featureDisabler: DataBrokerProtectionFeatureDisabler(), - authenticationManager: authManager, + authenticationManager: authenticationManager, pixelHandler: DataBrokerProtectionPixelsHandler()) }() @@ -152,7 +153,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { ) return VPNRedditSessionWorkaround( - accountManager: subscriptionManager.accountManager, + subscriptionManager: subscriptionManager, ipcClient: ipcClient, statusReporter: statusReporter ) @@ -292,8 +293,16 @@ final class AppDelegate: NSObject, NSApplicationDelegate { onboardingStateMachine = ContextualOnboardingStateMachine() - // Configure Subscription - subscriptionManager = DefaultSubscriptionManager(featureFlagger: featureFlagger) + // MARK: - Subscription configuration + let subscriptionAppGroup = Bundle.main.appGroup(bundle: .subs) + let subscriptionUserDefaults = UserDefaults(suiteName: subscriptionAppGroup)! + let subscriptionEnvironment = DefaultSubscriptionManager.getSavedOrDefaultEnvironment(userDefaults: subscriptionUserDefaults) + subscriptionManager = DefaultSubscriptionManager(keychainType: .dataProtection(.named(subscriptionAppGroup)), + environment: subscriptionEnvironment, + userDefaults: subscriptionUserDefaults, + canPerformAuthMigration: true, + canHandlePixels: true) + subscriptionUIHandler = SubscriptionUIHandler(windowControllersManagerProvider: { return WindowControllersManager.shared }) @@ -302,6 +311,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate { WKHTTPCookieStoreWrapper(store: WKWebsiteDataStore.default().httpCookieStore) }, eventMapping: SubscriptionCookieManageEventPixelMapping()) + // MARK: - + // Update VPN environment and match the Subscription environment vpnSettings.alignTo(subscriptionEnvironment: subscriptionManager.currentEnvironment) @@ -321,7 +332,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate { freemiumDBPFeature = DefaultFreemiumDBPFeature(privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager, experimentManager: experimentManager, subscriptionManager: subscriptionManager, - accountManager: subscriptionManager.accountManager, freemiumDBPUserStateManager: freemiumDBPUserStateManager) freemiumDBPPromotionViewCoordinator = FreemiumDBPPromotionViewCoordinator(freemiumDBPUserStateManager: freemiumDBPUserStateManager, freemiumDBPFeature: freemiumDBPFeature) @@ -406,8 +416,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate { startupSync() - subscriptionManager.loadInitialData() - let privacyConfigurationManager = ContentBlocking.shared.privacyConfigurationManager // Enable subscriptionCookieManager if feature flag is present @@ -481,8 +489,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { dataBrokerProtectionSubscriptionEventHandler.registerForSubscriptionAccountManagerEvents() let freemiumDBPUserStateManager = DefaultFreemiumDBPUserStateManager(userDefaults: .dbp) - let pirGatekeeper = DefaultDataBrokerProtectionFeatureGatekeeper(accountManager: - subscriptionManager.accountManager, + let pirGatekeeper = DefaultDataBrokerProtectionFeatureGatekeeper(subscriptionManager: subscriptionManager, freemiumDBPUserStateManager: freemiumDBPUserStateManager) DataBrokerProtectionAppEvents(featureGatekeeper: pirGatekeeper).applicationDidFinishLaunching() @@ -539,23 +546,18 @@ final class AppDelegate: NSObject, NSApplicationDelegate { NetworkProtectionAppEvents(featureGatekeeper: DefaultVPNFeatureGatekeeper(subscriptionManager: subscriptionManager)).applicationDidBecomeActive() let freemiumDBPUserStateManager = DefaultFreemiumDBPUserStateManager(userDefaults: .dbp) - let pirGatekeeper = DefaultDataBrokerProtectionFeatureGatekeeper(accountManager: - subscriptionManager.accountManager, + let pirGatekeeper = DefaultDataBrokerProtectionFeatureGatekeeper(subscriptionManager: subscriptionManager, freemiumDBPUserStateManager: freemiumDBPUserStateManager) DataBrokerProtectionAppEvents(featureGatekeeper: pirGatekeeper).applicationDidBecomeActive() - subscriptionManager.refreshCachedSubscriptionAndEntitlements { isSubscriptionActive in - if isSubscriptionActive { - PixelKit.fire(PrivacyProPixel.privacyProSubscriptionActive, frequency: .daily) - } + // Subscription initial tasks + Task { + await subscriptionManager.loadInitialData() } Task { @MainActor in await vpnRedditSessionWorkaround.installRedditSessionWorkaround() - } - - Task { @MainActor in await subscriptionCookieManager.refreshSubscriptionCookie() } } diff --git a/DuckDuckGo/Configuration/ConfigurationManager.swift b/DuckDuckGo/Configuration/ConfigurationManager.swift index 5fec8cd9b6..db1d2d94bf 100644 --- a/DuckDuckGo/Configuration/ConfigurationManager.swift +++ b/DuckDuckGo/Configuration/ConfigurationManager.swift @@ -148,6 +148,8 @@ final class ConfigurationManager: DefaultConfigurationManager { do { try await task.value didFetchAnyTrackerBlockingDependencies = true + } catch APIRequest.Error.invalidStatusCode(304) { + tryAgainSoon() } catch { Logger.config.error( "Failed to complete configuration update to \(configuration.rawValue, privacy: .public): \(error.localizedDescription, privacy: .public)" diff --git a/DuckDuckGo/DBP/DataBrokerPrerequisitesStatusVerifier.swift b/DuckDuckGo/DBP/DataBrokerPrerequisitesStatusVerifier.swift index f5f5c4d091..36c49c139e 100644 --- a/DuckDuckGo/DBP/DataBrokerPrerequisitesStatusVerifier.swift +++ b/DuckDuckGo/DBP/DataBrokerPrerequisitesStatusVerifier.swift @@ -20,6 +20,7 @@ import Foundation import Combine import DataBrokerProtection import LoginItems +import os.log enum DataBrokerPrerequisitesStatus { case invalidDirectory @@ -40,10 +41,13 @@ final class DefaultDataBrokerPrerequisitesStatusVerifier: DataBrokerPrerequisite func checkStatus() -> DataBrokerPrerequisitesStatus { if !statusChecker.doesHaveNecessaryPermissions() { + Logger.dataBrokerProtection.log("Invalid system permissions") return .invalidSystemPermission } else if !statusChecker.isInCorrectDirectory() { + Logger.dataBrokerProtection.log("Invalid directory") return .invalidDirectory } else { + Logger.dataBrokerProtection.log("Valid system permissions") return .valid } } diff --git a/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift b/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift index f0b173a42a..eeb397a7db 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift @@ -20,6 +20,7 @@ import Foundation import LoginItems import Common import DataBrokerProtection +import os.log struct DataBrokerProtectionAppEvents { @@ -52,6 +53,7 @@ struct DataBrokerProtectionAppEvents { // In this case, let's disable the agent and delete any left-over data because there's nothing for it to do if let profileQueriesCount = try? DataBrokerProtectionManager.shared.dataManager.profileQueriesCount(), profileQueriesCount > 0 { + Logger.dataBrokerProtection.log("Found \(profileQueriesCount) profile queries in DB. Restarting agent.") restartBackgroundAgent(loginItemsManager: loginItemsManager) // Wait to make sure the agent has had time to restart before attempting to call a method on it diff --git a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift index e81918a98c..a0327bfbda 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift @@ -249,7 +249,8 @@ final class DataBrokerProtectionDebugMenu: NSMenu { } @objc private func runCustomJSON() { - let authenticationManager = DataBrokerAuthenticationManagerBuilder.buildAuthenticationManager(subscriptionManager: Application.appDelegate.subscriptionManager) + let authenticationManager = DataBrokerProtectionAuthenticationManager( + subscriptionManager: Application.appDelegate.subscriptionManager) let viewController = DataBrokerRunCustomJSONViewController(authenticationManager: authenticationManager) let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 500, height: 400), styleMask: [.titled, .closable, .miniaturizable, .resizable], diff --git a/DuckDuckGo/DBP/DataBrokerProtectionFeatureGatekeeper.swift b/DuckDuckGo/DBP/DataBrokerProtectionFeatureGatekeeper.swift index e3c36af173..9c5c8d1a77 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionFeatureGatekeeper.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionFeatureGatekeeper.swift @@ -35,7 +35,7 @@ struct DefaultDataBrokerProtectionFeatureGatekeeper: DataBrokerProtectionFeature private let pixelHandler: EventMapping private let userDefaults: UserDefaults private let subscriptionAvailability: SubscriptionFeatureAvailability - private let accountManager: AccountManager + private let subscriptionManager: any SubscriptionManager private let freemiumDBPUserStateManager: FreemiumDBPUserStateManager init(privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager, @@ -43,14 +43,14 @@ struct DefaultDataBrokerProtectionFeatureGatekeeper: DataBrokerProtectionFeature pixelHandler: EventMapping = DataBrokerProtectionPixelsHandler(), userDefaults: UserDefaults = .standard, subscriptionAvailability: SubscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability(), - accountManager: AccountManager, + subscriptionManager: any SubscriptionManager, freemiumDBPUserStateManager: FreemiumDBPUserStateManager) { self.privacyConfigurationManager = privacyConfigurationManager self.featureDisabler = featureDisabler self.pixelHandler = pixelHandler self.userDefaults = userDefaults self.subscriptionAvailability = subscriptionAvailability - self.accountManager = accountManager + self.subscriptionManager = subscriptionManager self.freemiumDBPUserStateManager = freemiumDBPUserStateManager } @@ -80,28 +80,18 @@ struct DefaultDataBrokerProtectionFeatureGatekeeper: DataBrokerProtectionFeature /// Checks DBP prerequisites /// - /// Prerequisites are satisified if either: + /// Prerequisites are satisfied if either: /// 1. The user is an active freemium user (e.g has activated freemium and is not authenticated) /// 2. The user has a subscription with valid entitlements /// /// - Returns: Bool indicating prerequisites are satisfied func arePrerequisitesSatisfied() async -> Bool { - let isAuthenticated = accountManager.isUserAuthenticated + let isAuthenticated = subscriptionManager.isUserAuthenticated if !isAuthenticated && freemiumDBPUserStateManager.didActivate { return true } - let entitlements = await accountManager.hasEntitlement(forProductName: .dataBrokerProtection, - cachePolicy: .reloadIgnoringLocalCacheData) - var hasEntitlements: Bool - switch entitlements { - case .success(let value): - hasEntitlements = value - case .failure: - hasEntitlements = false - } - + let hasEntitlements = await subscriptionManager.isFeatureAvailableForUser(.dataBrokerProtection) firePrerequisitePixelsAndLogIfNecessary(hasEntitlements: hasEntitlements, isAuthenticatedResult: isAuthenticated) - return hasEntitlements && isAuthenticated } } @@ -114,11 +104,11 @@ private extension DefaultDataBrokerProtectionFeatureGatekeeper { func firePrerequisitePixelsAndLogIfNecessary(hasEntitlements: Bool, isAuthenticatedResult: Bool) { if !hasEntitlements { - Logger.dataBrokerProtection.error("DBP feature Gatekeeper: Entitlement check failed") + Logger.dataBrokerProtection.log("DBP feature Gatekeeper: No Entitlements available") } if !isAuthenticatedResult { - Logger.dataBrokerProtection.error("DBP feature Gatekeeper: Authentication check failed") + Logger.dataBrokerProtection.log("DBP feature Gatekeeper: Authentication check failed") } } } diff --git a/DuckDuckGo/DBP/DataBrokerProtectionManager.swift b/DuckDuckGo/DBP/DataBrokerProtectionManager.swift index 8bb55f7893..2eed43121a 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionManager.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionManager.swift @@ -33,10 +33,8 @@ public final class DataBrokerProtectionManager { private lazy var freemiumDBPFirstProfileSavedNotifier: FreemiumDBPFirstProfileSavedNotifier = { let freemiumDBPUserStateManager = DefaultFreemiumDBPUserStateManager(userDefaults: .dbp) - let accountManager = Application.appDelegate.subscriptionManager.accountManager - let freemiumDBPFirstProfileSavedNotifier = FreemiumDBPFirstProfileSavedNotifier(freemiumDBPUserStateManager: freemiumDBPUserStateManager, - accountManager: accountManager) - return freemiumDBPFirstProfileSavedNotifier + return FreemiumDBPFirstProfileSavedNotifier(freemiumDBPUserStateManager: freemiumDBPUserStateManager, + subscriptionManager: Application.appDelegate.subscriptionManager) }() lazy var dataManager: DataBrokerProtectionDataManager = { @@ -57,7 +55,8 @@ public final class DataBrokerProtectionManager { }() private init() { - self.authenticationManager = DataBrokerAuthenticationManagerBuilder.buildAuthenticationManager(subscriptionManager: Application.appDelegate.subscriptionManager) + self.authenticationManager = DataBrokerProtectionAuthenticationManager( + subscriptionManager: Application.appDelegate.subscriptionManager) } public func isUserAuthenticated() -> Bool { diff --git a/DuckDuckGo/DBP/DataBrokerProtectionSubscriptionEventHandler.swift b/DuckDuckGo/DBP/DataBrokerProtectionSubscriptionEventHandler.swift index a4ed56afc8..4c32d79bbc 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionSubscriptionEventHandler.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionSubscriptionEventHandler.swift @@ -22,6 +22,7 @@ import Subscription import DataBrokerProtection import PixelKit import Common +import Networking final class DataBrokerProtectionSubscriptionEventHandler { @@ -58,28 +59,13 @@ final class DataBrokerProtectionSubscriptionEventHandler { } private func entitlementsDidChange(_ notification: Notification) { - guard let entitlements = notification.userInfo?[UserDefaultsCacheKey.subscriptionEntitlements] as? [Entitlement] else { - - assertionFailure("Missing entitlements are truly unexpected") - return - } - - let hasEntitlements = entitlements.contains { entitlement in - entitlement.product == .dataBrokerProtection - } - - Task { - await entitlementsDidChange(hasEntitlements: hasEntitlements) - } - } - - @MainActor - private func entitlementsDidChange(hasEntitlements: Bool) async { - if hasEntitlements { - pixelHandler.fire(.entitlementCheckValid) - } else { - pixelHandler.fire(.entitlementCheckInvalid) - featureDisabler.disableAndDelete() + Task { @MainActor in + if await authenticationManager.hasValidEntitlement() { + pixelHandler.fire(.entitlementCheckValid) + } else { + pixelHandler.fire(.entitlementCheckInvalid) + featureDisabler.disableAndDelete() + } } } } diff --git a/DuckDuckGo/Freemium/DBP/Experiment/FreemiumDBPPixelExperimentManaging.swift b/DuckDuckGo/Freemium/DBP/Experiment/FreemiumDBPPixelExperimentManaging.swift index c14894d0fe..119e0631d1 100644 --- a/DuckDuckGo/Freemium/DBP/Experiment/FreemiumDBPPixelExperimentManaging.swift +++ b/DuckDuckGo/Freemium/DBP/Experiment/FreemiumDBPPixelExperimentManaging.swift @@ -108,10 +108,29 @@ private extension FreemiumDBPPixelExperimentManager { /// Determines if the user is eligible for the experiment based on subscription status and locale. var userIsEligible: Bool { - subscriptionManager.isPotentialPrivacyProSubscriber + isPotentialPrivacyProSubscriber && locale.isUSRegion } + /// Returns true if a user is a "potential" Privacy Pro subscriber. This means: + /// + /// 1. Is eligible to purchase + /// 2. Is not a current subscriber + var isPotentialPrivacyProSubscriber: Bool { + isPrivacyProPurchaseAvailable + && !subscriptionManager.isUserAuthenticated + } + + private var isPrivacyProPurchaseAvailable: Bool { + let platform = subscriptionManager.currentEnvironment.purchasePlatform + switch platform { + case .appStore: + return subscriptionManager.canPurchase + case .stripe: + return true + } + } + /// Checks if the user is not already enrolled in the experiment. var userIsNotEnrolled: Bool { userDefaults.enrollmentDate == nil diff --git a/DuckDuckGo/Freemium/DBP/FreemiumDBPFeature.swift b/DuckDuckGo/Freemium/DBP/FreemiumDBPFeature.swift index 60141b4b7b..39808dc700 100644 --- a/DuckDuckGo/Freemium/DBP/FreemiumDBPFeature.swift +++ b/DuckDuckGo/Freemium/DBP/FreemiumDBPFeature.swift @@ -56,7 +56,26 @@ final class DefaultFreemiumDBPFeature: FreemiumDBPFeature { var isAvailable: Bool { privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(DBPSubfeature.freemium) && experimentManager.isTreatment - && subscriptionManager.isPotentialPrivacyProSubscriber + && isPotentialPrivacyProSubscriber + } + + /// Returns true if a user is a "potential" Privacy Pro subscriber. This means: + /// + /// 1. Is eligible to purchase + /// 2. Is not a current subscriber + var isPotentialPrivacyProSubscriber: Bool { + isPrivacyProPurchaseAvailable + && !subscriptionManager.isUserAuthenticated + } + + private var isPrivacyProPurchaseAvailable: Bool { + let platform = subscriptionManager.currentEnvironment.purchasePlatform + switch platform { + case .appStore: + return subscriptionManager.canPurchase + case .stripe: + return true + } } /// A publisher that emits updates when the availability of the Freemium DBP feature changes. @@ -70,7 +89,6 @@ final class DefaultFreemiumDBPFeature: FreemiumDBPFeature { private let privacyConfigurationManager: PrivacyConfigurationManaging private let experimentManager: FreemiumDBPPixelExperimentManaging private let subscriptionManager: SubscriptionManager - private let accountManager: AccountManager private var freemiumDBPUserStateManager: FreemiumDBPUserStateManager private let notificationCenter: NotificationCenter private lazy var featureDisabler: DataBrokerProtectionFeatureDisabling = DataBrokerProtectionFeatureDisabler() @@ -92,7 +110,6 @@ final class DefaultFreemiumDBPFeature: FreemiumDBPFeature { init(privacyConfigurationManager: PrivacyConfigurationManaging, experimentManager: FreemiumDBPPixelExperimentManaging, subscriptionManager: SubscriptionManager, - accountManager: AccountManager, freemiumDBPUserStateManager: FreemiumDBPUserStateManager, notificationCenter: NotificationCenter = .default, featureDisabler: DataBrokerProtectionFeatureDisabling? = nil) { @@ -100,7 +117,6 @@ final class DefaultFreemiumDBPFeature: FreemiumDBPFeature { self.privacyConfigurationManager = privacyConfigurationManager self.experimentManager = experimentManager self.subscriptionManager = subscriptionManager - self.accountManager = accountManager self.freemiumDBPUserStateManager = freemiumDBPUserStateManager self.notificationCenter = notificationCenter @@ -158,7 +174,7 @@ private extension DefaultFreemiumDBPFeature { guard freemiumDBPUserStateManager.didActivate else { return false } return !privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(DBPSubfeature.freemium) - && subscriptionManager.isPotentialPrivacyProSubscriber + && isPotentialPrivacyProSubscriber } /// This method offboards a Freemium user if the feature flag was disabled @@ -174,25 +190,3 @@ private extension DefaultFreemiumDBPFeature { } } } - -extension SubscriptionManager { - - /// Returns true if a user is a "potential" Privacy Pro subscriber. This means: - /// - /// 1. Is eligible to purchase - /// 2. Is not a current subscriber - var isPotentialPrivacyProSubscriber: Bool { - isPrivacyProPurchaseAvailable - && !accountManager.isUserAuthenticated - } - - private var isPrivacyProPurchaseAvailable: Bool { - let platform = currentEnvironment.purchasePlatform - switch platform { - case .appStore: - return canPurchase - case .stripe: - return true - } - } -} diff --git a/DuckDuckGo/Freemium/DBP/FreemiumDBPFirstProfileSavedNotifier.swift b/DuckDuckGo/Freemium/DBP/FreemiumDBPFirstProfileSavedNotifier.swift index d7493816e0..f9e3341bc7 100644 --- a/DuckDuckGo/Freemium/DBP/FreemiumDBPFirstProfileSavedNotifier.swift +++ b/DuckDuckGo/Freemium/DBP/FreemiumDBPFirstProfileSavedNotifier.swift @@ -27,7 +27,7 @@ import OSLog final class FreemiumDBPFirstProfileSavedNotifier: DBPProfileSavedNotifier { private var freemiumDBPUserStateManager: FreemiumDBPUserStateManager - private var accountManager: AccountManager + private var subscriptionManager: SubscriptionManager private let notificationCenter: NotificationCenter /// Initializes the notifier with the necessary dependencies to check user state and post notifications. @@ -36,9 +36,11 @@ final class FreemiumDBPFirstProfileSavedNotifier: DBPProfileSavedNotifier { /// - freemiumDBPUserStateManager: Manages the user state related to Freemium DBP. /// - accountManager: Manages account-related information, such as whether the user is authenticated. /// - notificationCenter: The notification center for posting notifications. Defaults to the system's default notification center. - init(freemiumDBPUserStateManager: FreemiumDBPUserStateManager, accountManager: AccountManager, notificationCenter: NotificationCenter = .default) { + init(freemiumDBPUserStateManager: FreemiumDBPUserStateManager, + subscriptionManager: SubscriptionManager, + notificationCenter: NotificationCenter = .default) { self.freemiumDBPUserStateManager = freemiumDBPUserStateManager - self.accountManager = accountManager + self.subscriptionManager = subscriptionManager self.notificationCenter = notificationCenter } @@ -49,7 +51,7 @@ final class FreemiumDBPFirstProfileSavedNotifier: DBPProfileSavedNotifier { /// /// If all conditions are met, the method posts a `pirProfileSaved` notification via the `NotificationCenter` and records that the notification has been posted. func postProfileSavedNotificationIfPermitted() { - guard !accountManager.isUserAuthenticated + guard !subscriptionManager.isUserAuthenticated && freemiumDBPUserStateManager.didActivate && !freemiumDBPUserStateManager.didPostFirstProfileSavedNotification else { return } diff --git a/DuckDuckGo/InfoPlist.xcstrings b/DuckDuckGo/InfoPlist.xcstrings index 768c51c8bc..f7cdec22b9 100644 --- a/DuckDuckGo/InfoPlist.xcstrings +++ b/DuckDuckGo/InfoPlist.xcstrings @@ -7,7 +7,7 @@ "localizations" : { "de" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "DuckDuckGo" } }, @@ -19,43 +19,43 @@ }, "es" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "DuckDuckGo" } }, "fr" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "DuckDuckGo" } }, "it" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "DuckDuckGo" } }, "nl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "DuckDuckGo" } }, "pl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "DuckDuckGo" } }, "pt" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "DuckDuckGo" } }, "ru" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "DuckDuckGo" } } diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index fddda99763..1c9439a4e7 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -1233,46 +1233,46 @@ "comment" : "Suffix of DuckDuckGo Search open tab suggestion. Example: cats – DuckDuckGo Search", "extractionState" : "extracted_with_value", "localizations" : { - "en" : { + "de" : { "stringUnit" : { - "state" : "new", - "value" : "DuckDuckGo Search" + "state" : "translated", + "value" : "DuckDuckGo-Suche" } }, - "fr" : { + "en" : { "stringUnit" : { - "state" : "translated", - "value" : "Recherche DuckDuckGo" + "state" : "new", + "value" : "DuckDuckGo Search" } }, - "de" : { + "es" : { "stringUnit" : { "state" : "translated", - "value" : "DuckDuckGo-Suche" + "value" : "Búsqueda de DuckDuckGo" } }, - "pl" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Wyszukiwanie DuckDuckGo" + "value" : "Recherche DuckDuckGo" } }, - "es" : { + "it" : { "stringUnit" : { "state" : "translated", - "value" : "Búsqueda de DuckDuckGo" + "value" : "Ricerca DuckDuckGo" } }, - "ru" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Поиск DuckDuckGo" + "value" : "Zoeken met DuckDuckGo" } }, - "nl" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Zoeken met DuckDuckGo" + "value" : "Wyszukiwanie DuckDuckGo" } }, "pt" : { @@ -1281,10 +1281,10 @@ "value" : "Pesquisa DuckDuckGo" } }, - "it" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Ricerca DuckDuckGo" + "value" : "Поиск DuckDuckGo" } } } @@ -64019,46 +64019,46 @@ "comment" : "Suggestion to switch to an open tab button title", "extractionState" : "extracted_with_value", "localizations" : { - "en" : { + "de" : { "stringUnit" : { - "state" : "new", - "value" : "Switch to Tab" + "state" : "translated", + "value" : "Zu Tab wechseln" } }, - "fr" : { + "en" : { "stringUnit" : { - "state" : "translated", - "value" : "Passer à l'onglet" + "state" : "new", + "value" : "Switch to Tab" } }, - "de" : { + "es" : { "stringUnit" : { "state" : "translated", - "value" : "Zu Tab wechseln" + "value" : "Cambiar a Pestaña" } }, - "pl" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Przełącz na kartę" + "value" : "Passer à l'onglet" } }, - "es" : { + "it" : { "stringUnit" : { "state" : "translated", - "value" : "Cambiar a Pestaña" + "value" : "Passa alla scheda" } }, - "ru" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Перейти на вкладку" + "value" : "Overschakelen naar tabblad" } }, - "nl" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Overschakelen naar tabblad" + "value" : "Przełącz na kartę" } }, "pt" : { @@ -64067,10 +64067,10 @@ "value" : "Mudar para separador" } }, - "it" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Passa alla scheda" + "value" : "Перейти на вкладку" } } } @@ -72364,4 +72364,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/DuckDuckGo/NavigationBar/PinningManager.swift b/DuckDuckGo/NavigationBar/PinningManager.swift index 01858412b0..29f3c9bf98 100644 --- a/DuckDuckGo/NavigationBar/PinningManager.swift +++ b/DuckDuckGo/NavigationBar/PinningManager.swift @@ -18,6 +18,7 @@ import Foundation import NetworkProtection +import Subscription enum PinnableView: String { case autofill @@ -40,7 +41,7 @@ protocol PinningManager { final class LocalPinningManager: PinningManager { - static let shared = LocalPinningManager(networkProtectionFeatureActivation: NetworkProtectionKeychainTokenStore()) + static let shared = LocalPinningManager() static let pinnedViewChangedNotificationViewTypeKey = "pinning.pinnedViewChanged.viewType" @@ -50,12 +51,6 @@ final class LocalPinningManager: PinningManager { @UserDefaultsWrapper(key: .manuallyToggledPinnedViews, defaultValue: []) private var manuallyToggledPinnedViewsStrings: [String] - private let networkProtectionFeatureActivation: NetworkProtectionFeatureActivation - - init(networkProtectionFeatureActivation: NetworkProtectionFeatureActivation) { - self.networkProtectionFeatureActivation = networkProtectionFeatureActivation - } - func togglePinning(for view: PinnableView) { flagAsManuallyToggled(view) @@ -72,7 +67,6 @@ final class LocalPinningManager: PinningManager { /// Do not call this for user-initiated toggling. This is only meant to be used for scenarios in which certain conditions /// may require a view to be pinned. - /// func pin(_ view: PinnableView) { guard !isPinned(view) else { return diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index 308f7b95da..82d44ca413 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -59,7 +59,6 @@ final class MoreOptionsMenu: NSMenu, NSMenuDelegate { private let internalUserDecider: InternalUserDecider @MainActor private lazy var sharingMenu: NSMenu = SharingMenu(title: UserText.shareMenuItem) - private var accountManager: AccountManager { subscriptionManager.accountManager } private let subscriptionManager: SubscriptionManager private let freemiumDBPUserStateManager: FreemiumDBPUserStateManager private let freemiumDBPFeature: FreemiumDBPFeature @@ -145,7 +144,7 @@ final class MoreOptionsMenu: NSMenu, NSMenuDelegate { feedbackMenuItem.submenu = FeedbackSubMenu(targetting: self, tabCollectionViewModel: tabCollectionViewModel, subscriptionFeatureAvailability: subscriptionFeatureAvailability, - accountManager: accountManager) + subscriptionManager: subscriptionManager) addItem(feedbackMenuItem) #endif // FEEDBACK @@ -426,7 +425,7 @@ final class MoreOptionsMenu: NSMenu, NSMenuDelegate { let privacyProItem = NSMenuItem(title: UserText.subscriptionOptionsMenuItem).withImage(.subscriptionIcon) - if !accountManager.isUserAuthenticated { + if !subscriptionManager.isUserAuthenticated { privacyProItem.target = self privacyProItem.action = #selector(openSubscriptionPurchasePage(_:)) @@ -611,14 +610,14 @@ final class EmailOptionsButtonSubMenu: NSMenu { final class FeedbackSubMenu: NSMenu { private let subscriptionFeatureAvailability: SubscriptionFeatureAvailability - private let accountManager: AccountManager + private let subscriptionManager: any SubscriptionManager init(targetting target: AnyObject, tabCollectionViewModel: TabCollectionViewModel, subscriptionFeatureAvailability: SubscriptionFeatureAvailability, - accountManager: AccountManager) { + subscriptionManager: any SubscriptionManager) { self.subscriptionFeatureAvailability = subscriptionFeatureAvailability - self.accountManager = accountManager + self.subscriptionManager = subscriptionManager super.init(title: UserText.sendFeedback) updateMenuItems(with: tabCollectionViewModel, targetting: target) } @@ -642,7 +641,7 @@ final class FeedbackSubMenu: NSMenu { .withImage(.siteBreakage) addItem(reportBrokenSiteItem) - if subscriptionFeatureAvailability.usesUnifiedFeedbackForm, accountManager.isUserAuthenticated { + if subscriptionFeatureAvailability.usesUnifiedFeedbackForm, subscriptionManager.isUserAuthenticated { addItem(.separator()) let sendPProFeedbackItem = NSMenuItem(title: UserText.sendPProFeedback, @@ -906,7 +905,6 @@ final class SubscriptionSubMenu: NSMenu, NSMenuDelegate { self.subscriptionSettingsItem = makeSubscriptionSettingsItem(target: target) delegate = self - Task { await addMenuItems() } @@ -917,15 +915,15 @@ final class SubscriptionSubMenu: NSMenu, NSMenuDelegate { } private func addMenuItems() async { - let features = await subscriptionManager.currentSubscriptionFeatures() - - if features.contains(.networkProtection) { + let features = await subscriptionManager.currentSubscriptionFeatures(forceRefresh: false) + let entitlements = features.map { $0.entitlement } + if entitlements.contains(.networkProtection) { addItem(networkProtectionItem) } - if features.contains(.dataBrokerProtection) { + if entitlements.contains(.dataBrokerProtection) { addItem(dataBrokerProtectionItem) } - if features.contains(.identityTheftRestoration) || features.contains(.identityTheftRestorationGlobal) { + if entitlements.contains(.identityTheftRestoration) || entitlements.contains(.identityTheftRestorationGlobal) { addItem(identityTheftRestorationItem) } addItem(NSMenuItem.separator()) @@ -963,38 +961,27 @@ final class SubscriptionSubMenu: NSMenu, NSMenuDelegate { .targetting(target) } - private func refreshAvailabilityBasedOnEntitlements() { - guard subscriptionManager.accountManager.isUserAuthenticated else { return } - - @Sendable func hasEntitlement(for productName: Entitlement.ProductName) async -> Bool { - switch await self.subscriptionManager.accountManager.hasEntitlement(forProductName: productName) { - case let .success(result): - return result - case .failure: - return false - } - } - - Task.detached(priority: .background) { [weak self] in - guard let self else { return } - - let isNetworkProtectionItemEnabled = await hasEntitlement(for: .networkProtection) - let isDataBrokerProtectionItemEnabled = await hasEntitlement(for: .dataBrokerProtection) - - let hasIdentityTheftRestoration = await hasEntitlement(for: .identityTheftRestoration) - let hasIdentityTheftRestorationGlobal = await hasEntitlement(for: .identityTheftRestorationGlobal) - let isIdentityTheftRestorationItemEnabled = hasIdentityTheftRestoration || hasIdentityTheftRestorationGlobal - - Task { @MainActor in - self.networkProtectionItem.isEnabled = isNetworkProtectionItemEnabled - self.dataBrokerProtectionItem.isEnabled = isDataBrokerProtectionItemEnabled - self.identityTheftRestorationItem.isEnabled = isIdentityTheftRestorationItemEnabled - } + private func refreshAvailabilityBasedOnEntitlements() async { + guard subscriptionManager.isUserAuthenticated else { return } + let features = await subscriptionManager.currentSubscriptionFeatures(forceRefresh: false) + let vpnFeature = features.first { $0.entitlement == .networkProtection } + let dbpFeature = features.first { $0.entitlement == .dataBrokerProtection } + let itrFeature = features.first { $0.entitlement == .identityTheftRestoration } + let itrgFeature = features.first { $0.entitlement == .identityTheftRestorationGlobal } + + Task { @MainActor in + self.networkProtectionItem.isEnabled = vpnFeature?.availableForUser ?? false + self.dataBrokerProtectionItem.isEnabled = dbpFeature?.availableForUser ?? false + let hasIdentityTheftRestoration = itrFeature?.availableForUser ?? false + let hasIdentityTheftRestorationGlobal = itrgFeature?.availableForUser ?? false + self.identityTheftRestorationItem.isEnabled = hasIdentityTheftRestoration || hasIdentityTheftRestorationGlobal } } public func menuWillOpen(_ menu: NSMenu) { - refreshAvailabilityBasedOnEntitlements() + Task { + await refreshAvailabilityBasedOnEntitlements() + } } } diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index 11280b7ede..cad46c7aa6 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -121,10 +121,8 @@ final class NavigationBarViewController: NSViewController { static private let homeButtonLeftPosition = 0 private let networkProtectionButtonModel: NetworkProtectionNavBarButtonModel - private let networkProtectionFeatureActivation: NetworkProtectionFeatureActivation static func create(tabCollectionViewModel: TabCollectionViewModel, - networkProtectionFeatureActivation: NetworkProtectionFeatureActivation = NetworkProtectionKeychainTokenStore(), downloadListCoordinator: DownloadListCoordinator = .shared, dragDropManager: BookmarkDragDropManager = .shared, networkProtectionPopoverManager: NetPPopoverManager, @@ -133,17 +131,31 @@ final class NavigationBarViewController: NSViewController { aiChatMenuConfig: AIChatMenuVisibilityConfigurable, brokenSitePromptLimiter: BrokenSitePromptLimiter) -> NavigationBarViewController { NSStoryboard(name: "NavigationBar", bundle: nil).instantiateInitialController { coder in - self.init(coder: coder, tabCollectionViewModel: tabCollectionViewModel, networkProtectionFeatureActivation: networkProtectionFeatureActivation, downloadListCoordinator: downloadListCoordinator, dragDropManager: dragDropManager, networkProtectionPopoverManager: networkProtectionPopoverManager, networkProtectionStatusReporter: networkProtectionStatusReporter, autofillPopoverPresenter: autofillPopoverPresenter, aiChatMenuConfig: aiChatMenuConfig, brokenSitePromptLimiter: brokenSitePromptLimiter) + self.init(coder: coder, + tabCollectionViewModel: tabCollectionViewModel, + downloadListCoordinator: downloadListCoordinator, + dragDropManager: dragDropManager, + networkProtectionPopoverManager: networkProtectionPopoverManager, + networkProtectionStatusReporter: networkProtectionStatusReporter, + autofillPopoverPresenter: autofillPopoverPresenter, + aiChatMenuConfig: aiChatMenuConfig, + brokenSitePromptLimiter: brokenSitePromptLimiter) }! } - init?(coder: NSCoder, tabCollectionViewModel: TabCollectionViewModel, networkProtectionFeatureActivation: NetworkProtectionFeatureActivation, downloadListCoordinator: DownloadListCoordinator, dragDropManager: BookmarkDragDropManager, networkProtectionPopoverManager: NetPPopoverManager, networkProtectionStatusReporter: NetworkProtectionStatusReporter, autofillPopoverPresenter: AutofillPopoverPresenter, - aiChatMenuConfig: AIChatMenuVisibilityConfigurable, brokenSitePromptLimiter: BrokenSitePromptLimiter) { + init?(coder: NSCoder, + tabCollectionViewModel: TabCollectionViewModel, + downloadListCoordinator: DownloadListCoordinator, + dragDropManager: BookmarkDragDropManager, + networkProtectionPopoverManager: NetPPopoverManager, + networkProtectionStatusReporter: NetworkProtectionStatusReporter, + autofillPopoverPresenter: AutofillPopoverPresenter, + aiChatMenuConfig: AIChatMenuVisibilityConfigurable, + brokenSitePromptLimiter: BrokenSitePromptLimiter) { self.popovers = NavigationBarPopovers(networkProtectionPopoverManager: networkProtectionPopoverManager, autofillPopoverPresenter: autofillPopoverPresenter, isBurner: tabCollectionViewModel.isBurner) self.tabCollectionViewModel = tabCollectionViewModel self.networkProtectionButtonModel = NetworkProtectionNavBarButtonModel(popoverManager: networkProtectionPopoverManager, statusReporter: networkProtectionStatusReporter) - self.networkProtectionFeatureActivation = networkProtectionFeatureActivation self.downloadListCoordinator = downloadListCoordinator self.dragDropManager = dragDropManager self.aiChatMenuConfig = aiChatMenuConfig @@ -329,10 +341,9 @@ final class NavigationBarViewController: NSViewController { } private func toggleNetworkProtectionPopover() { - guard NetworkProtectionKeychainTokenStore().isFeatureActivated else { + guard subscriptionManager.isUserAuthenticated else { return } - popovers.toggleNetworkProtectionPopover(from: networkProtectionButton, withDelegate: networkProtectionButtonModel) } diff --git a/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/Bundle+VPN.swift b/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/Bundle+VPN.swift index 169f0ceb50..40cb64c560 100644 --- a/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/Bundle+VPN.swift +++ b/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/Bundle+VPN.swift @@ -18,6 +18,7 @@ import Foundation import NetworkProtection +import Common extension Bundle { diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/KeychainType+ClientDefault.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/KeychainType+ClientDefault.swift index 61f848178a..35d0fcfe1e 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/KeychainType+ClientDefault.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/KeychainType+ClientDefault.swift @@ -18,6 +18,7 @@ import Foundation import NetworkProtection +import Common /// Implements convenience default for the client apps making use of this. /// diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift index b39e026096..f8f1502fc1 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift @@ -29,28 +29,13 @@ extension NetworkProtectionDeviceManager { static func create() -> NetworkProtectionDeviceManager { let settings = Application.appDelegate.vpnSettings let keyStore = NetworkProtectionKeychainKeyStore() - let tokenStore = NetworkProtectionKeychainTokenStore() return NetworkProtectionDeviceManager(environment: settings.selectedEnvironment, - tokenStore: tokenStore, + tokenProvider: Application.appDelegate.subscriptionManager, keyStore: keyStore, errorEvents: .networkProtectionAppDebugEvents) } } -extension NetworkProtectionKeychainTokenStore { - convenience init() { - self.init(useAccessTokenProvider: true) - } - - convenience init(useAccessTokenProvider: Bool) { - let accessTokenProvider: () -> String? = { Application.appDelegate.subscriptionManager.accountManager.accessToken } - self.init(keychainType: .default, - errorEvents: .networkProtectionAppDebugEvents, - useAccessTokenProvider: useAccessTokenProvider, - accessTokenProvider: accessTokenProvider) - } -} - extension NetworkProtectionKeychainKeyStore { convenience init() { self.init(keychainType: .default, @@ -63,7 +48,7 @@ extension NetworkProtectionLocationListCompositeRepository { let settings = Application.appDelegate.vpnSettings self.init( environment: settings.selectedEnvironment, - tokenStore: NetworkProtectionKeychainTokenStore(), + tokenProvider: Application.appDelegate.subscriptionManager, errorEvents: .networkProtectionAppDebugEvents ) } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift index 9b32eb21ff..6400c131ee 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift @@ -264,7 +264,7 @@ final class NetworkProtectionDebugMenu: NSMenu { /// @objc func logFeedbackMetadataToConsole(_ sender: Any?) { Task { @MainActor in - let collector = DefaultVPNMetadataCollector(accountManager: Application.appDelegate.subscriptionManager.accountManager) + let collector = DefaultVPNMetadataCollector(subscriptionManager: Application.appDelegate.subscriptionManager) let metadata = await collector.collectMetadata() print(metadata.toPrettyPrintedJSON()!) diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift index 9c5606b3df..e2068e5617 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift @@ -68,7 +68,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr // MARK: - Subscriptions - private let accessTokenStorage: SubscriptionTokenKeychainStorage + private let subscriptionManager: any SubscriptionManager // MARK: - Debug Options Support @@ -158,7 +158,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr settings: VPNSettings, defaults: UserDefaults, notificationCenter: NotificationCenter = .default, - accessTokenStorage: SubscriptionTokenKeychainStorage) { + subscriptionManager: any SubscriptionManager) { self.featureFlagger = featureFlagger self.networkExtensionBundleID = networkExtensionBundleID @@ -166,7 +166,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr self.notificationCenter = notificationCenter self.settings = settings self.defaults = defaults - self.accessTokenStorage = accessTokenStorage + self.subscriptionManager = subscriptionManager subscribeToSettingsChanges() subscribeToStatusChanges() @@ -532,6 +532,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr /// Starts the VPN connection /// func start() async { + Logger.networkProtection.log("Start VPN") VPNOperationErrorRecorder().beginRecordingControllerStart() PixelKit.fire(NetworkProtectionPixelEvent.networkProtectionControllerStartAttempt, frequency: .legacyDailyAndCount) @@ -572,7 +573,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr // It's important to note that we've seen instances where the above call to start() // doesn't throw any errors, yet the tunnel fails to start. In any case this pixel - // should be interpreted as "the controller successfully requrested the tunnel to be + // should be interpreted as "the controller successfully requested the tunnel to be // started". Meaning there's no error caught in this start attempt. There are pixels // in the packet tunnel provider side that can be used to debug additional logic. // @@ -580,6 +581,8 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr frequency: .legacyDailyAndCount) } } catch { + Logger.networkProtection.error("Starting tunnel error: \(error, privacy: .public)") + VPNOperationErrorRecorder().recordControllerStartFailure(error) knownFailureStore.lastKnownFailure = KnownFailure(error) @@ -606,12 +609,11 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr private func start(_ tunnelManager: NETunnelProviderManager) async throws { var options = [String: NSObject]() - options[NetworkProtectionOptionKey.activationAttemptId] = UUID().uuidString as NSString - guard let authToken = try fetchAuthToken() else { - throw StartError.noAuthToken - } - options[NetworkProtectionOptionKey.authToken] = authToken + + let tokenContainer = try await fetchTokenContainerAndRefresh() + options[NetworkProtectionOptionKey.tokenContainer] = tokenContainer.data + options[NetworkProtectionOptionKey.selectedEnvironment] = settings.selectedEnvironment.rawValue as NSString options[NetworkProtectionOptionKey.selectedServer] = settings.selectedServer.stringValue as? NSString @@ -647,8 +649,10 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr } do { + Logger.networkProtection.log("Starting NetworkProtectionTunnelController, options: \(options, privacy: .public)") try tunnelManager.connection.startVPNTunnel(options: options) } catch { + Logger.networkProtection.fault("Failed to start VPN tunnel: \(error, privacy: .public)") throw StartError.startTunnelFailure(error) } @@ -665,6 +669,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr /// @MainActor func stop() async { + Logger.networkProtection.log("Stop VPN") await stop(disableOnDemand: true) } @@ -792,17 +797,18 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr } } - private func fetchAuthToken() throws -> NSString? { - if let accessToken = try? accessTokenStorage.getAccessToken() { - Logger.networkProtection.log("🟢 TunnelController found token") - return Self.adaptAccessTokenForVPN(accessToken) as NSString? - } else { - Logger.networkProtection.error("TunnelController found no token") - return nil - } - } + private func fetchTokenContainerAndRefresh() async throws -> TokenContainer { + do { + let tokenContainer = try await subscriptionManager.getTokenContainer(policy: .localValid) + Logger.networkProtection.log("🟢 TunnelController found token container") + + // refresh token in order to brach it from the one sent to VPN + try await subscriptionManager.getTokenContainer(policy: .localForceRefresh) - private static func adaptAccessTokenForVPN(_ token: String) -> String { - "ddg:\(token)" + return tokenContainer + } catch { + Logger.networkProtection.fault("🔴 TunnelController found no token container") + throw StartError.noAuthToken + } } } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNRedditSessionWorkaround.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNRedditSessionWorkaround.swift index 5c376db928..a4c557695a 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNRedditSessionWorkaround.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNRedditSessionWorkaround.swift @@ -26,14 +26,14 @@ import os.log final class VPNRedditSessionWorkaround { - private let accountManager: AccountManager + private let subscriptionManager: any SubscriptionManager private let ipcClient: VPNControllerXPCClient private let statusReporter: NetworkProtectionStatusReporter - init(accountManager: AccountManager, + init(subscriptionManager: any SubscriptionManager, ipcClient: VPNControllerXPCClient = .shared, statusReporter: NetworkProtectionStatusReporter) { - self.accountManager = accountManager + self.subscriptionManager = subscriptionManager self.ipcClient = ipcClient self.statusReporter = statusReporter self.statusReporter.forceRefresh() @@ -53,7 +53,7 @@ final class VPNRedditSessionWorkaround { @MainActor func installRedditSessionWorkaround(to cookieStore: WKHTTPCookieStore) async { - guard accountManager.isUserAuthenticated, + guard subscriptionManager.isUserAuthenticated, statusReporter.statusObserver.recentValue.isConnected, let redditSessionCookie = HTTPCookie.emptyRedditSession else { return diff --git a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift index b7f2ee4afd..7aa4dc281b 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift @@ -100,7 +100,7 @@ extension NetworkProtectionIPCTunnelController: TunnelController { } do { - guard try await featureGatekeeper.canStartVPN() else { + guard await featureGatekeeper.canStartVPN() else { throw RequestError.notAuthorizedToEnableLoginItem } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift index 127197a709..4eb32b98d8 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift @@ -22,24 +22,22 @@ import Foundation import Subscription import NetworkProtection import NetworkProtectionUI +import Networking final class NetworkProtectionSubscriptionEventHandler { private let subscriptionManager: SubscriptionManager private let tunnelController: TunnelController - private let networkProtectionTokenStorage: NetworkProtectionTokenStore private let vpnUninstaller: VPNUninstalling private let userDefaults: UserDefaults private var cancellables = Set() init(subscriptionManager: SubscriptionManager, tunnelController: TunnelController, - networkProtectionTokenStorage: NetworkProtectionTokenStore = NetworkProtectionKeychainTokenStore(), vpnUninstaller: VPNUninstalling, userDefaults: UserDefaults = .netP) { self.subscriptionManager = subscriptionManager self.tunnelController = tunnelController - self.networkProtectionTokenStorage = networkProtectionTokenStorage self.vpnUninstaller = vpnUninstaller self.userDefaults = userDefaults @@ -48,33 +46,17 @@ final class NetworkProtectionSubscriptionEventHandler { private func subscribeToEntitlementChanges() { Task { - switch await subscriptionManager.accountManager.hasEntitlement(forProductName: .networkProtection) { - case .success(let hasEntitlements): - Task { - await handleEntitlementsChange(hasEntitlements: hasEntitlements) - } - case .failure: - break - } - NotificationCenter.default .publisher(for: .entitlementsDidChange) .receive(on: DispatchQueue.main) .sink { [weak self] notification in - guard let self else { - return - } - - guard let entitlements = notification.userInfo?[UserDefaultsCacheKey.subscriptionEntitlements] as? [Entitlement] else { - + guard let self else { return } + guard let entitlements = notification.userInfo?[UserDefaultsCacheKey.subscriptionEntitlements] as? [SubscriptionEntitlement] else { assertionFailure("Missing entitlements are truly unexpected") return } - let hasEntitlements = entitlements.contains { entitlement in - entitlement.product == .networkProtection - } - + let hasEntitlements = entitlements.contains(.networkProtection) Task { await self.handleEntitlementsChange(hasEntitlements: hasEntitlements) } @@ -98,10 +80,6 @@ final class NetworkProtectionSubscriptionEventHandler { } @objc private func handleAccountDidSignIn() { - guard subscriptionManager.accountManager.accessToken != nil else { - assertionFailure("[NetP Subscription] AccountManager signed in but token could not be retrieved") - return - } userDefaults.networkProtectionEntitlementsExpired = false } diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift index 59f9b32b91..e8a3d5a501 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift @@ -29,18 +29,6 @@ import WireGuard final class MacPacketTunnelProvider: PacketTunnelProvider { - static var isAppex: Bool { -#if NETP_SYSTEM_EXTENSION - false -#else - true -#endif - } - - static var subscriptionsAppGroup: String? { - isAppex ? Bundle.main.appGroup(bundle: .subs) : nil - } - // MARK: - Additional Status Info /// Holds the date when the status was last changed so we can send it out as additional information @@ -393,17 +381,16 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { } } - static var tokenServiceName: String { #if NETP_SYSTEM_EXTENSION - "\(Bundle.main.bundleIdentifier!).authToken" + static let tokenServiceName = "\(Bundle.main.bundleIdentifier!).authToken" #else - NetworkProtectionKeychainTokenStore.Defaults.tokenStoreService + static let tokenServiceName = "com.duckduckgo.networkprotection.authToken" #endif - } // MARK: - Initialization @MainActor @objc public init() { + Logger.networkProtection.log("[+] MacPacketTunnelProvider") #if NETP_SYSTEM_EXTENSION let defaults = UserDefaults.standard #else @@ -414,20 +401,12 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { NetworkProtectionLastVersionRunStore(userDefaults: defaults).lastExtensionVersionRun = AppVersion.shared.versionAndBuildNumber let settings = VPNSettings(defaults: defaults) - // MARK: - Configure Subscription - let subscriptionUserDefaults = UserDefaults(suiteName: MacPacketTunnelProvider.subscriptionsAppGroup)! let notificationCenter: NetworkProtectionNotificationCenter = DistributedNotificationCenter.default() let controllerErrorStore = NetworkProtectionTunnelErrorStore(notificationCenter: notificationCenter) let debugEvents = Self.networkProtectionDebugEvents(controllerErrorStore: controllerErrorStore) - let tokenStore = NetworkProtectionKeychainTokenStore(keychainType: Bundle.keychainType, - serviceName: Self.tokenServiceName, - errorEvents: debugEvents, - useAccessTokenProvider: false, - accessTokenProvider: { nil } - ) - let entitlementsCache = UserDefaultsCache<[Entitlement]>(userDefaults: subscriptionUserDefaults, - key: UserDefaultsCacheKey.subscriptionEntitlements, - settings: UserDefaultsCacheSettings(defaultExpirationInterval: .minutes(20))) + + // MARK: - Configure Subscription + // Align Subscription environment to the VPN environment var subscriptionEnvironment = SubscriptionEnvironment.default switch settings.selectedEnvironment { @@ -437,15 +416,71 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { subscriptionEnvironment.serviceEnvironment = .staging } - let subscriptionEndpointService = DefaultSubscriptionEndpointService(currentServiceEnvironment: subscriptionEnvironment.serviceEnvironment) - let authEndpointService = DefaultAuthEndpointService(currentServiceEnvironment: subscriptionEnvironment.serviceEnvironment) - let accountManager = DefaultAccountManager(accessTokenStorage: tokenStore, - entitlementsCache: entitlementsCache, - subscriptionEndpointService: subscriptionEndpointService, - authEndpointService: authEndpointService) + // The SysExt doesn't care about the purchase platform because the only operations executed here are about the Auth token. No purchase or + // platforms-related operations are performed. + subscriptionEnvironment.purchasePlatform = .stripe + + Logger.networkProtection.debug("Subscription ServiceEnvironment: \(subscriptionEnvironment.serviceEnvironment.rawValue, privacy: .public)") + + let configuration = URLSessionConfiguration.default + configuration.httpCookieStorage = nil + configuration.requestCachePolicy = .reloadIgnoringLocalCacheData + let urlSession = URLSession(configuration: configuration, delegate: SessionDelegate(), delegateQueue: nil) + let apiService = DefaultAPIService(urlSession: urlSession) + let authService = DefaultOAuthService(baseURL: subscriptionEnvironment.authEnvironment.url, apiService: apiService) + let tokenStorage = NetworkProtectionKeychainStore(label: "DuckDuckGo Network Protection Auth Token", + serviceName: Self.tokenServiceName, + keychainType: Bundle.keychainType) + let legacyTokenStore = NetworkProtectionKeychainTokenStore(keychainType: Bundle.keychainType, + serviceName: Self.tokenServiceName, + errorEvents: debugEvents, + useAccessTokenProvider: false, + accessTokenProvider: { nil }) + let authClient = DefaultOAuthClient(tokensStorage: tokenStorage, + legacyTokenStorage: legacyTokenStore, + authService: authService) + apiService.authorizationRefresherCallback = { _ in + guard let tokenContainer = tokenStorage.tokenContainer else { + throw OAuthClientError.internalError("Missing refresh token") + } + if tokenContainer.decodedAccessToken.isExpired() { + Logger.networkProtection.debug("Refreshing tokens") + let tokens = try await authClient.getTokens(policy: .localForceRefresh) + return VPNAuthTokenBuilder.getVPNAuthToken(from: tokens.accessToken) + } else { + Logger.networkProtection.error("Trying to refresh valid token, using the old one") + return VPNAuthTokenBuilder.getVPNAuthToken(from: tokenContainer.accessToken) + } + } + + let subscriptionEndpointService = DefaultSubscriptionEndpointService(apiService: apiService, + baseURL: subscriptionEnvironment.serviceEnvironment.url) + let pixelHandler: SubscriptionManager.PixelHandler = { type in + // The SysExt handles only dead token pixels + switch type { + case .deadToken: + PixelKit.fire(PrivacyProPixel.privacyProDeadTokenDetected) + case .subscriptionIsActive: // handled by the main app only + break + case .v1MigrationFailed: + PixelKit.fire(PrivacyProPixel.authV1MigrationFailed) + case .v1MigrationSuccessful: + PixelKit.fire(PrivacyProPixel.authV1MigrationSucceeded) + } + } + + let subscriptionManager = DefaultSubscriptionManager(oAuthClient: authClient, + subscriptionEndpointService: subscriptionEndpointService, + subscriptionEnvironment: subscriptionEnvironment, + pixelHandler: pixelHandler) + + // MARK: - - let entitlementsCheck = { - await accountManager.hasEntitlement(forProductName: .networkProtection, cachePolicy: .reloadIgnoringLocalCacheData) + let entitlementsCheck: (() async -> Result) = { + Logger.networkProtection.log("Subscription Entitlements check...") + let isNetworkProtectionEnabled = await subscriptionManager.isFeatureAvailableForUser(.networkProtection) + Logger.networkProtection.log("Network protection is \( isNetworkProtectionEnabled ? "🟢 Enabled" : "⚫️ Disabled", privacy: .public)") + return .success(isNetworkProtectionEnabled) } let tunnelHealthStore = NetworkProtectionTunnelHealthStore(notificationCenter: notificationCenter) @@ -457,7 +492,7 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { snoozeTimingStore: NetworkProtectionSnoozeTimingStore(userDefaults: .netP), wireGuardInterface: DefaultWireGuardInterface(), keychainType: Bundle.keychainType, - tokenStore: tokenStore, + subscriptionManager: subscriptionManager, debugEvents: debugEvents, providerEvents: Self.packetTunnelProviderEvents, settings: settings, @@ -465,7 +500,6 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { entitlementCheck: entitlementsCheck) setupPixels() - accountManager.delegate = self observeServerChanges() observeStatusUpdateRequests() } @@ -551,8 +585,9 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { // MARK: - NEPacketTunnelProvider - public override func load(options: StartupOptions) throws { - try super.load(options: options) + public override func load(options: StartupOptions) async throws { + Logger.networkProtection.log("Loading startup options...") + try await super.load(options: options) #if NETP_SYSTEM_EXTENSION loadExcludeLocalNetworks(from: options) @@ -670,11 +705,3 @@ final class DefaultWireGuardInterface: WireGuardInterface { wgSetLogger(context, logFunction) } } - -extension MacPacketTunnelProvider: AccountManagerKeychainAccessDelegate { - - public func accountManagerKeychainAccessFailed(accessType: AccountKeychainAccessType, error: AccountKeychainAccessError) { - PixelKit.fire(PrivacyProErrorPixel.privacyProKeychainAccessError(accessType: accessType, accessError: error), - frequency: .legacyDailyAndCount) - } -} diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/NetworkProtectionKeychainStore+TokenStoring.swift similarity index 51% rename from DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift rename to DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/NetworkProtectionKeychainStore+TokenStoring.swift index 4716231a6f..e6c331f9fb 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/NetworkProtectionKeychainStore+TokenStoring.swift @@ -1,5 +1,5 @@ // -// NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift +// NetworkProtectionKeychainStore+TokenStoring.swift // // Copyright © 2024 DuckDuckGo. All rights reserved. // @@ -19,25 +19,24 @@ import Foundation import Subscription import NetworkProtection -import Common -import os.log +import Networking -extension NetworkProtectionKeychainTokenStore: SubscriptionTokenStoring { +extension NetworkProtectionKeychainStore: @retroactive TokenStoring { + static var name = "com.duckduckgo.networkprotection.tokenContainer" - public func store(accessToken: String) throws { - try store(accessToken) - } - - public func getAccessToken() throws -> String? { - guard var token = try fetchToken() else { return nil } - if token.hasPrefix("ddg:") { - token = token.replacingOccurrences(of: "ddg:", with: "") + public var tokenContainer: Networking.TokenContainer? { + get { + if let data = try? readData(named: Self.name) as? NSData { + return try? TokenContainer(with: data) + } + return nil + } + set(newValue) { + if newValue == nil { + try? deleteAll() + } else if let data = newValue?.data as? Data { + try? writeData(data, named: Self.name) + } } - Logger.networkProtection.log("🟢 Wrapper successfully fetched token \(token)") - return token - } - - public func removeAccessToken() throws { - try deleteToken() } } diff --git a/DuckDuckGo/Preferences/Model/PreferencesSection.swift b/DuckDuckGo/Preferences/Model/PreferencesSection.swift index 8af316402f..f696f5f3a4 100644 --- a/DuckDuckGo/Preferences/Model/PreferencesSection.swift +++ b/DuckDuckGo/Preferences/Model/PreferencesSection.swift @@ -69,7 +69,7 @@ struct PreferencesSection: Hashable, Identifiable { let platform = subscriptionManager.currentEnvironment.purchasePlatform var shouldHidePrivacyProDueToNoProducts = platform == .appStore && subscriptionManager.canPurchase == false - if subscriptionManager.accountManager.isUserAuthenticated { + if subscriptionManager.isUserAuthenticated { shouldHidePrivacyProDueToNoProducts = false } diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index bce9f56129..9a108a47b8 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -185,14 +185,11 @@ enum Preferences { }, restorePurchases: { if #available(macOS 12.0, *) { Task { - let appStoreRestoreFlow = DefaultAppStoreRestoreFlow(accountManager: subscriptionManager.accountManager, - storePurchaseManager: subscriptionManager.storePurchaseManager(), - subscriptionEndpointService: subscriptionManager.subscriptionEndpointService, - authEndpointService: subscriptionManager.authEndpointService) - let subscriptionAppStoreRestorer = DefaultSubscriptionAppStoreRestorer( - subscriptionManager: subscriptionManager, - appStoreRestoreFlow: appStoreRestoreFlow, - uiHandler: subscriptionUIHandler) + let appStoreRestoreFlow = DefaultAppStoreRestoreFlow(subscriptionManager: subscriptionManager, + storePurchaseManager: subscriptionManager.storePurchaseManager()) + let subscriptionAppStoreRestorer = DefaultSubscriptionAppStoreRestorer(subscriptionManager: subscriptionManager, + appStoreRestoreFlow: appStoreRestoreFlow, + uiHandler: subscriptionUIHandler) await subscriptionAppStoreRestorer.restoreAppStoreSubscription() } } diff --git a/DuckDuckGo/RemoteMessaging/RemoteMessagingConfigMatcherProvider.swift b/DuckDuckGo/RemoteMessaging/RemoteMessagingConfigMatcherProvider.swift index 3d0e49a0c5..5821278c61 100644 --- a/DuckDuckGo/RemoteMessaging/RemoteMessagingConfigMatcherProvider.swift +++ b/DuckDuckGo/RemoteMessaging/RemoteMessagingConfigMatcherProvider.swift @@ -70,7 +70,7 @@ final class RemoteMessagingConfigMatcherProvider: RemoteMessagingConfigMatcherPr let subscriptionManager = await Application.appDelegate.subscriptionManager - let isPrivacyProSubscriber = subscriptionManager.accountManager.isUserAuthenticated + let isPrivacyProSubscriber = subscriptionManager.isUserAuthenticated let isPrivacyProEligibleUser = subscriptionManager.canPurchase let activationDateStore = DefaultWaitlistActivationDateStore(source: .netP) @@ -84,10 +84,8 @@ final class RemoteMessagingConfigMatcherProvider: RemoteMessagingConfigMatcherPr var privacyProPurchasePlatform: String? let surveyActionMapper: RemoteMessagingSurveyActionMapping - if let accessToken = subscriptionManager.accountManager.accessToken { - let subscriptionResult = await subscriptionManager.subscriptionEndpointService.getSubscription(accessToken: accessToken) - - if case let .success(subscription) = subscriptionResult { + if isPrivacyProSubscriber { + if let subscription = try? await subscriptionManager.getSubscription(cachePolicy: .returnCacheDataElseLoad) { privacyProDaysSinceSubscribed = Calendar.current.numberOfDaysBetween(subscription.startedAt, and: Date()) ?? -1 privacyProDaysUntilExpiry = Calendar.current.numberOfDaysBetween(Date(), and: subscription.expiresOrRenewsAt) ?? -1 privacyProPurchasePlatform = subscription.platform.rawValue @@ -138,7 +136,7 @@ final class RemoteMessagingConfigMatcherProvider: RemoteMessagingConfigMatcherPr let deprecatedRemoteMessageStorage = DefaultSurveyRemoteMessagingStorage.surveys() let freemiumDBPUserStateManager = DefaultFreemiumDBPUserStateManager(userDefaults: .dbp) - let isCurrentFreemiumDBPUser = !subscriptionManager.accountManager.isUserAuthenticated && freemiumDBPUserStateManager.didActivate + let isCurrentFreemiumDBPUser = !isPrivacyProSubscriber && freemiumDBPUserStateManager.didActivate return RemoteMessagingConfigMatcher( appAttributeMatcher: AppAttributeMatcher(statisticsStore: statisticsStore, diff --git a/DuckDuckGo/Statistics/PrivacyProPixel.swift b/DuckDuckGo/Statistics/PrivacyProPixel.swift index c4a88a6b52..345a9f68d4 100644 --- a/DuckDuckGo/Statistics/PrivacyProPixel.swift +++ b/DuckDuckGo/Statistics/PrivacyProPixel.swift @@ -68,6 +68,11 @@ enum PrivacyProPixel: PixelKitEventV2 { case privacyProOfferYearlyPriceClick case privacyProAddEmailSuccess case privacyProWelcomeFAQClick + // Auth v2 + case privacyProDeadTokenDetected + case authV1MigrationFailed + case authV1MigrationSucceeded + case setSubscriptionInvalidSubscriptionValues var name: String { switch self { @@ -109,6 +114,11 @@ enum PrivacyProPixel: PixelKitEventV2 { case .privacyProOfferYearlyPriceClick: return "m_mac_\(appDistribution)_privacy-pro_offer_yearly-price_click" case .privacyProAddEmailSuccess: return "m_mac_\(appDistribution)_privacy-pro_app_add-email_success_u" case .privacyProWelcomeFAQClick: return "m_mac_\(appDistribution)_privacy-pro_welcome_faq_click_u" + // Auth v2 + case .privacyProDeadTokenDetected: return "m_privacy-pro_dead_token_detected" + case .authV1MigrationFailed: return "m_privacy-pro_v1migration_failed" + case .authV1MigrationSucceeded: return "m_privacy-pro_v1migration_succeeded" + case .setSubscriptionInvalidSubscriptionValues: return "m_privacy-pro_invalid_subscriptionvalues" } } diff --git a/DuckDuckGo/Subscription/DefaultSubscriptionManager+StandardConfiguration.swift b/DuckDuckGo/Subscription/DefaultSubscriptionManager+StandardConfiguration.swift new file mode 100644 index 0000000000..14b5e01e72 --- /dev/null +++ b/DuckDuckGo/Subscription/DefaultSubscriptionManager+StandardConfiguration.swift @@ -0,0 +1,122 @@ +// +// DefaultSubscriptionManager+StandardConfiguration.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 Subscription +import Common +import PixelKit +import Networking +import os.log +import BrowserServicesKit +import FeatureFlags +import NetworkProtection + +extension DefaultSubscriptionManager { + // Init the SubscriptionManager using the standard dependencies and configuration, to be used only in the dependencies tree root + public convenience init(keychainType: KeychainType, + environment: SubscriptionEnvironment, + featureFlagger: FeatureFlagger? = nil, + userDefaults: UserDefaults, + canPerformAuthMigration: Bool, + canHandlePixels: Bool) { + + let configuration = URLSessionConfiguration.default + configuration.httpCookieStorage = nil + configuration.requestCachePolicy = .reloadIgnoringLocalCacheData + let urlSession = URLSession(configuration: configuration, + delegate: SessionDelegate(), + delegateQueue: nil) + let apiService = DefaultAPIService(urlSession: urlSession) + let authService = DefaultOAuthService(baseURL: environment.authEnvironment.url, apiService: apiService) + let tokenStorage = SubscriptionTokenKeychainStorageV2(keychainType: keychainType) { keychainType, error in + PixelKit.fire(PrivacyProErrorPixel.privacyProKeychainAccessError(accessType: keychainType, accessError: error), + frequency: .legacyDailyAndCount) + } + let legacyAccountStorage = canPerformAuthMigration == true ? SubscriptionTokenKeychainStorage(keychainType: keychainType) : nil + let authClient = DefaultOAuthClient(tokensStorage: tokenStorage, + legacyTokenStorage: legacyAccountStorage, + authService: authService) + apiService.authorizationRefresherCallback = { _ in + guard let tokenContainer = tokenStorage.tokenContainer else { + throw OAuthClientError.internalError("Missing refresh token") + } + + if tokenContainer.decodedAccessToken.isExpired() { + Logger.OAuth.debug("Refreshing tokens") + let tokens = try await authClient.getTokens(policy: .localForceRefresh) + return tokens.accessToken + } else { + Logger.general.debug("Trying to refresh valid token, using the old one") + return tokenContainer.accessToken + } + } + + let subscriptionEndpointService = DefaultSubscriptionEndpointService(apiService: apiService, + baseURL: environment.serviceEnvironment.url) + let subscriptionFeatureFlagger: FeatureFlaggerMapping = FeatureFlaggerMapping { feature in + guard let featureFlagger else { + // With no featureFlagger provided there is no gating of features + return feature.defaultState + } + + switch feature { + case .usePrivacyProUSARegionOverride: + return (featureFlagger.internalUserDecider.isInternalUser && + environment.serviceEnvironment == .staging && + userDefaults.storefrontRegionOverride == .usa) + case .usePrivacyProROWRegionOverride: + return (featureFlagger.internalUserDecider.isInternalUser && + environment.serviceEnvironment == .staging && + userDefaults.storefrontRegionOverride == .restOfWorld) + } + } + + // Pixel handler configuration + let pixelHandler: SubscriptionManager.PixelHandler + if canHandlePixels { + pixelHandler = { type in + switch type { + case .deadToken: + PixelKit.fire(PrivacyProPixel.privacyProDeadTokenDetected) + case .subscriptionIsActive: + PixelKit.fire(PrivacyProPixel.privacyProSubscriptionActive, frequency: .daily) + case .v1MigrationFailed: + PixelKit.fire(PrivacyProPixel.authV1MigrationFailed) + case .v1MigrationSuccessful: + PixelKit.fire(PrivacyProPixel.authV1MigrationSucceeded) + } + } + } else { + pixelHandler = { _ in } + } + + if #available(macOS 12.0, *) { + self.init(storePurchaseManager: DefaultStorePurchaseManager(subscriptionFeatureMappingCache: subscriptionEndpointService, + subscriptionFeatureFlagger: subscriptionFeatureFlagger), + oAuthClient: authClient, + subscriptionEndpointService: subscriptionEndpointService, + subscriptionEnvironment: environment, + pixelHandler: pixelHandler) + } else { + self.init(oAuthClient: authClient, + subscriptionEndpointService: subscriptionEndpointService, + subscriptionEnvironment: environment, + pixelHandler: pixelHandler) + } + } +} diff --git a/DuckDuckGo/Subscription/VPNSettings+Environment.swift b/DuckDuckGo/Subscription/VPNSettings+Environment.swift index cffa7251a6..4e046637ed 100644 --- a/DuckDuckGo/Subscription/VPNSettings+Environment.swift +++ b/DuckDuckGo/Subscription/VPNSettings+Environment.swift @@ -26,10 +26,8 @@ public extension VPNSettings { func alignTo(subscriptionEnvironment: SubscriptionEnvironment) { switch subscriptionEnvironment.serviceEnvironment { case .production: - // Do nothing for a production subscription, as it can be used for both VPN environments. - break + self.selectedEnvironment = .production case .staging: - // If using a staging subscription, force the staging VPN environment as it is not compatible with anything else. self.selectedEnvironment = .staging } } diff --git a/DuckDuckGo/Tab/UserScripts/IdentityTheftRestorationPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/IdentityTheftRestorationPagesUserScript.swift index 967b41c8ee..cb9e5a6e72 100644 --- a/DuckDuckGo/Tab/UserScripts/IdentityTheftRestorationPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/IdentityTheftRestorationPagesUserScript.swift @@ -23,6 +23,7 @@ import Foundation import WebKit import Subscription import UserScript +import os.log /// /// The user script that will be the broker for all subscription features @@ -72,7 +73,7 @@ extension IdentityTheftRestorationPagesUserScript: WKScriptMessageHandler { final class IdentityTheftRestorationPagesFeature: Subfeature { weak var broker: UserScriptMessageBroker? private let subscriptionFeatureAvailability: SubscriptionFeatureAvailability - + private let subscriptionManager: any SubscriptionManager var featureName = "useIdentityTheftRestoration" var messageOriginPolicy: MessageOriginPolicy = .only(rules: [ @@ -80,8 +81,10 @@ final class IdentityTheftRestorationPagesFeature: Subfeature { .exact(hostname: "abrown.duckduckgo.com") ]) - init(subscriptionFeatureAvailability: SubscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability()) { + init(subscriptionFeatureAvailability: SubscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability(), + subscriptionManager: any SubscriptionManager) { self.subscriptionFeatureAvailability = subscriptionFeatureAvailability + self.subscriptionManager = subscriptionManager } func with(broker: UserScriptMessageBroker) { @@ -99,9 +102,11 @@ final class IdentityTheftRestorationPagesFeature: Subfeature { } func getAccessToken(params: Any, original: WKScriptMessage) async throws -> Encodable? { - if let accessToken = await Application.appDelegate.subscriptionManager.accountManager.accessToken { + do { + let accessToken = try await subscriptionManager.getTokenContainer(policy: .localValid).accessToken return ["token": accessToken] - } else { + } catch { + Logger.subscription.debug("No access token available: \(error)") return [String: String]() } } diff --git a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionErrorReporter.swift b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionErrorReporter.swift index ae370e03f4..4279dbadb3 100644 --- a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionErrorReporter.swift +++ b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionErrorReporter.swift @@ -21,7 +21,7 @@ import Common import PixelKit import os.log -enum SubscriptionError: Error { +enum SubscriptionError: LocalizedError { case purchaseFailed, missingEntitlements, failedToGetSubscriptionOptions, @@ -36,6 +36,39 @@ enum SubscriptionError: Error { accountCreationFailed, activeSubscriptionAlreadyPresent, generalError + + var localizedDescription: String { + switch self { + case .purchaseFailed: + return "Purchase process failed. Please try again." + case .missingEntitlements: + return "Required entitlements are missing." + case .failedToGetSubscriptionOptions: + return "Unable to retrieve subscription options." + case .failedToSetSubscription: + return "Failed to set the subscription." + case .failedToRestoreFromEmail: + return "Email restore process failed." + case .failedToRestoreFromEmailSubscriptionInactive: + return "Cannot restore; email subscription is inactive." + case .failedToRestorePastPurchase: + return "Failed to restore your past purchase." + case .subscriptionNotFound: + return "No subscription could be found." + case .subscriptionExpired: + return "Your subscription has expired." + case .hasActiveSubscription: + return "You already have an active subscription." + case .cancelledByUser: + return "Action was cancelled by the user." + case .accountCreationFailed: + return "Account creation failed. Please try again." + case .activeSubscriptionAlreadyPresent: + return "There is already an active subscription present." + case .generalError: + return "A general error has occurred." + } + } } protocol SubscriptionErrorReporter { diff --git a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUseSubscriptionFeature.swift b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUseSubscriptionFeature.swift index 8ade538671..a5de5c915f 100644 --- a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUseSubscriptionFeature.swift +++ b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUseSubscriptionFeature.swift @@ -26,6 +26,7 @@ import PixelKit import os.log import Freemium import DataBrokerProtection +import Networking /// Use Subscription sub-feature final class SubscriptionPagesUseSubscriptionFeature: Subfeature { @@ -36,16 +37,12 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { .exact(hostname: "abrown.duckduckgo.com") ]) let subscriptionManager: SubscriptionManager - var accountManager: AccountManager { subscriptionManager.accountManager } var subscriptionPlatform: SubscriptionEnvironment.PurchasePlatform { subscriptionManager.currentEnvironment.purchasePlatform } - - let stripePurchaseFlow: StripePurchaseFlow + let stripePurchaseFlow: any StripePurchaseFlow let subscriptionErrorReporter = DefaultSubscriptionErrorReporter() let subscriptionSuccessPixelHandler: SubscriptionAttributionPixelHandler let uiHandler: SubscriptionUIHandling - let subscriptionFeatureAvailability: SubscriptionFeatureAvailability - private var freemiumDBPUserStateManager: FreemiumDBPUserStateManager private let freemiumDBPPixelExperimentManager: FreemiumDBPPixelExperimentManaging private let notificationCenter: NotificationCenter @@ -96,6 +93,8 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { } func handler(forMethodNamed methodName: String) -> Subfeature.Handler? { + Logger.subscription.debug("WebView handler: \(methodName)") + switch methodName { case Handlers.getSubscription: return getSubscription case Handlers.setSubscription: return setSubscription @@ -130,39 +129,55 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { } func getSubscription(params: Any, original: WKScriptMessage) async throws -> Encodable? { - let authToken = accountManager.authToken ?? "" - return Subscription(token: authToken) + guard subscriptionManager.isUserAuthenticated else { return Subscription(token: "") } + + do { + let accessToken = try await subscriptionManager.getTokenContainer(policy: .localValid).accessToken + return Subscription(token: accessToken) + } catch { + Logger.subscription.debug("No subscription available: \(error)") + return Subscription(token: "") + } } func setSubscription(params: Any, original: WKScriptMessage) async throws -> Encodable? { + // Note: This is called by the web FE when a subscription is retrieved, `params` contains an auth token V1 that will need to be exchanged for a V2. This is a temporary workaround until the FE fully supports v2 auth. PixelKit.fire(PrivacyProPixel.privacyProRestorePurchaseEmailSuccess, frequency: .legacyDailyAndCount) - guard let subscriptionValues: SubscriptionValues = DecodableHelper.decode(from: params) else { + guard let subscriptionValues: SubscriptionValues = CodableHelper.decode(from: params) else { + Logger.subscription.fault("SubscriptionPagesUserScript: expected JSON representation of SubscriptionValues") + PixelKit.fire(PrivacyProPixel.setSubscriptionInvalidSubscriptionValues) 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) + // Clear subscription Cache + subscriptionManager.clearSubscriptionCache() + + guard !subscriptionValues.token.isEmpty else { + Logger.subscription.fault("Empty token provided, Failed to exchange v1 token for v2") + PixelKit.fire(PrivacyProPixel.setSubscriptionInvalidSubscriptionValues) + return nil } + do { + _ = try await subscriptionManager.exchange(tokenV1: subscriptionValues.token) + Logger.subscription.log("v1 token exchanged for v2") + // forcing subscription refresh + try await subscriptionManager.getSubscription(cachePolicy: .reloadIgnoringLocalCacheData) + Logger.subscription.log("Subscription retrieved") + } catch { + Logger.subscription.error("Failed to exchange v1 token for v2 \(error, privacy: .public)") + } 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) - } - + _ = try? await subscriptionManager.getTokenContainer(policy: .localForceRefresh) DispatchQueue.main.async { [weak self] in self?.notificationCenter.post(name: .subscriptionPageCloseAndOpenPreferences, object: self) } - return nil } @@ -202,41 +217,38 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { if subscriptionManager.currentEnvironment.purchasePlatform == .appStore { if #available(macOS 12.0, *) { - guard let subscriptionSelection: SubscriptionSelection = DecodableHelper.decode(from: params) else { + guard let subscriptionSelection: SubscriptionSelection = CodableHelper.decode(from: params) else { assertionFailure("SubscriptionPagesUserScript: expected JSON representation of SubscriptionSelection") subscriptionErrorReporter.report(subscriptionActivationError: .generalError) await uiHandler.dismissProgressViewController() return nil } - Logger.subscription.info("[Purchase] Starting purchase for: \(subscriptionSelection.id, privacy: .public)") + Logger.subscription.log("[Purchase] Starting purchase for: \(subscriptionSelection.id, privacy: .public)") await uiHandler.presentProgressViewController(withTitle: UserText.purchasingSubscriptionTitle) // Check for active subscriptions if await subscriptionManager.storePurchaseManager().hasActiveSubscription() { + // Sandbox note: Looks like our BE is not receiving updates when a subscription transitions from grace period to expired, so during testing we can end up with a subscription in grace period and we will not be able to purchase a new one, only restore it because Transaction.currentEntitlements will not return the subscription to restore. PixelKit.fire(PrivacyProPixel.privacyProRestoreAfterPurchaseAttempt) - Logger.subscription.info("[Purchase] Found active subscription during purchase") + Logger.subscription.log("[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, + let appStoreRestoreFlow = DefaultAppStoreRestoreFlow(subscriptionManager: subscriptionManager, + storePurchaseManager: subscriptionManager.storePurchaseManager()) + let appStorePurchaseFlow = DefaultAppStorePurchaseFlow(subscriptionManager: subscriptionManager, storePurchaseManager: subscriptionManager.storePurchaseManager(), - accountManager: subscriptionManager.accountManager, - appStoreRestoreFlow: appStoreRestoreFlow, - authEndpointService: subscriptionManager.authEndpointService) + appStoreRestoreFlow: appStoreRestoreFlow) - Logger.subscription.info("[Purchase] Purchasing") - switch await appStorePurchaseFlow.purchaseSubscription(with: subscriptionSelection.id, emailAccessToken: emailAccessToken) { + Logger.subscription.log("[Purchase] Purchasing") + let purchaseResult = await appStorePurchaseFlow.purchaseSubscription(with: subscriptionSelection.id) + switch purchaseResult { case .success(let transactionJWS): purchaseTransactionJWS = transactionJWS case .failure(let error): @@ -270,11 +282,10 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { await uiHandler.updateProgressViewController(title: UserText.completingPurchaseTitle) - Logger.subscription.info("[Purchase] Completing purchase") - let completePurchaseResult = await appStorePurchaseFlow.completeSubscriptionPurchase(with: purchaseTransactionJWS) + let completePurchaseResult = await appStorePurchaseFlow.completeSubscriptionPurchase(with: purchaseTransactionJWS, additionalParams: nil) switch completePurchaseResult { case .success(let purchaseUpdate): - Logger.subscription.info("[Purchase] Purchase complete") + Logger.subscription.log("[Purchase] Purchase completed") PixelKit.fire(PrivacyProPixel.privacyProPurchaseSuccess, frequency: .legacyDailyAndCount) sendFreemiumSubscriptionPixelIfFreemiumActivated() saveSubscriptionUpgradeTimestampIfFreemiumActivated() @@ -344,10 +355,10 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { func featureSelected(params: Any, original: WKScriptMessage) async throws -> Encodable? { struct FeatureSelection: Codable { - let productFeature: Entitlement.ProductName + let productFeature: SubscriptionEntitlement } - guard let featureSelection: FeatureSelection = DecodableHelper.decode(from: params) else { + guard let featureSelection: FeatureSelection = CodableHelper.decode(from: params) else { assertionFailure("SubscriptionPagesUserScript: expected JSON representation of FeatureSelection") return nil } @@ -412,9 +423,11 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { } func getAccessToken(params: Any, original: WKScriptMessage) async throws -> Encodable? { - if let accessToken = accountManager.accessToken { + do { + let accessToken = try await subscriptionManager.getTokenContainer(policy: .localValid).accessToken return ["token": accessToken] - } else { + } catch { + Logger.subscription.debug("No access token available: \(error)") return [String: String]() } } @@ -427,7 +440,10 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { @MainActor func pushPurchaseUpdate(originalMessage: WKScriptMessage, purchaseUpdate: PurchaseUpdate) { - pushAction(method: .onPurchaseUpdate, webView: originalMessage.webView!, params: purchaseUpdate) + guard let webView = originalMessage.webView else { + return + } + pushAction(method: .onPurchaseUpdate, webView: webView, params: purchaseUpdate) } func pushAction(method: SubscribeActionName, webView: WKWebView, params: Encodable) { @@ -463,14 +479,14 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { 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 appStoreRestoreFlow = DefaultAppStoreRestoreFlow(subscriptionManager: subscriptionManager, + storePurchaseManager: subscriptionManager.storePurchaseManager()) let result = await appStoreRestoreFlow.restoreAccountFromPastPurchase() switch result { - case .success: PixelKit.fire(PrivacyProPixel.privacyProRestorePurchaseStoreSuccess, frequency: .legacyDailyAndCount) - case .failure: break + case .success: + PixelKit.fire(PrivacyProPixel.privacyProRestorePurchaseStoreSuccess, frequency: .legacyDailyAndCount) + case .failure(let error): + Logger.subscription.error("Failed to restore account from past purchase: \(error, privacy: .public)") } Task { @MainActor in originalMessage.webView?.reload() @@ -498,10 +514,8 @@ extension SubscriptionPagesUseSubscriptionFeature: SubscriptionAccessActionHandl 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 appStoreRestoreFlow = DefaultAppStoreRestoreFlow(subscriptionManager: subscriptionManager, + storePurchaseManager: subscriptionManager.storePurchaseManager()) let subscriptionAppStoreRestorer = DefaultSubscriptionAppStoreRestorer(subscriptionManager: self.subscriptionManager, appStoreRestoreFlow: appStoreRestoreFlow, uiHandler: self.uiHandler) diff --git a/DuckDuckGo/Tab/UserScripts/UserScripts.swift b/DuckDuckGo/Tab/UserScripts/UserScripts.swift index 9febd742e8..5dc1e376f7 100644 --- a/DuckDuckGo/Tab/UserScripts/UserScripts.swift +++ b/DuckDuckGo/Tab/UserScripts/UserScripts.swift @@ -126,9 +126,7 @@ final class UserScripts: UserScriptsProvider { } let subscriptionManager = Application.appDelegate.subscriptionManager - let stripePurchaseFlow = DefaultStripePurchaseFlow(subscriptionEndpointService: subscriptionManager.subscriptionEndpointService, - authEndpointService: subscriptionManager.authEndpointService, - accountManager: subscriptionManager.accountManager) + let stripePurchaseFlow = DefaultStripePurchaseFlow(subscriptionManager: subscriptionManager) let freemiumDBPPixelExperimentManager = FreemiumDBPPixelExperimentManager(subscriptionManager: subscriptionManager) let delegate = SubscriptionPagesUseSubscriptionFeature(subscriptionManager: subscriptionManager, stripePurchaseFlow: stripePurchaseFlow, @@ -137,7 +135,8 @@ final class UserScripts: UserScriptsProvider { subscriptionPagesUserScript.registerSubfeature(delegate: delegate) userScripts.append(subscriptionPagesUserScript) - identityTheftRestorationPagesUserScript.registerSubfeature(delegate: IdentityTheftRestorationPagesFeature()) + let identityTheftRestorationPagesFeature = IdentityTheftRestorationPagesFeature(subscriptionManager: subscriptionManager) + identityTheftRestorationPagesUserScript.registerSubfeature(delegate: identityTheftRestorationPagesFeature) userScripts.append(identityTheftRestorationPagesUserScript) } diff --git a/DuckDuckGo/UnifiedFeedbackForm/UnifiedFeedbackFormViewController.swift b/DuckDuckGo/UnifiedFeedbackForm/UnifiedFeedbackFormViewController.swift index f502037fc4..8092a7fa20 100644 --- a/DuckDuckGo/UnifiedFeedbackForm/UnifiedFeedbackFormViewController.swift +++ b/DuckDuckGo/UnifiedFeedbackForm/UnifiedFeedbackFormViewController.swift @@ -49,10 +49,9 @@ final class UnifiedFeedbackFormViewController: NSViewController { self.feedbackSender = feedbackSender self.viewModel = UnifiedFeedbackFormViewModel(subscriptionManager: Application.appDelegate.subscriptionManager, apiService: DefaultAPIService(), - vpnMetadataCollector: DefaultVPNMetadataCollector(accountManager: Application.appDelegate.subscriptionManager.accountManager), + vpnMetadataCollector: DefaultVPNMetadataCollector(subscriptionManager: Application.appDelegate.subscriptionManager), feedbackSender: feedbackSender, source: source) - super.init(nibName: nil, bundle: nil) self.viewModel.delegate = self } diff --git a/DuckDuckGo/UnifiedFeedbackForm/UnifiedFeedbackFormViewModel.swift b/DuckDuckGo/UnifiedFeedbackForm/UnifiedFeedbackFormViewModel.swift index 01c95d4a07..8f24856381 100644 --- a/DuckDuckGo/UnifiedFeedbackForm/UnifiedFeedbackFormViewModel.swift +++ b/DuckDuckGo/UnifiedFeedbackForm/UnifiedFeedbackFormViewModel.swift @@ -28,7 +28,7 @@ protocol UnifiedFeedbackFormViewModelDelegate: AnyObject { } final class UnifiedFeedbackFormViewModel: ObservableObject { - private static let feedbackEndpoint = URL(string: "https://subscriptions.duckduckgo.com/api/feedback")! + static let feedbackEndpoint = URL(string: "https://subscriptions.duckduckgo.com/api/feedback")! private static let platform = "macos" enum ViewState { @@ -50,6 +50,7 @@ final class UnifiedFeedbackFormViewModel: ObservableObject { enum Error: String, Swift.Error { case missingAccessToken case invalidResponse + case invalidRequest } enum ViewAction { @@ -149,7 +150,7 @@ final class UnifiedFeedbackFormViewModel: ObservableObject { weak var delegate: UnifiedFeedbackFormViewModelDelegate? - private let accountManager: any AccountManager + private let subscriptionManager: any SubscriptionManager private let apiService: any Networking.APIService private let vpnMetadataCollector: any UnifiedMetadataCollector private let defaultMetadataCollector: any UnifiedMetadataCollector @@ -165,24 +166,29 @@ final class UnifiedFeedbackFormViewModel: ObservableObject { feedbackSender: any UnifiedFeedbackSender = DefaultFeedbackSender(), source: UnifiedFeedbackSource = .default) { self.viewState = .feedbackPending - - self.accountManager = subscriptionManager.accountManager self.apiService = apiService self.vpnMetadataCollector = vpnMetadataCollector self.defaultMetadataCollector = defaultMetadataCollector self.feedbackSender = feedbackSender self.source = source + self.subscriptionManager = subscriptionManager Task { - let features = await subscriptionManager.currentSubscriptionFeatures() + let features = await subscriptionManager.currentSubscriptionFeatures(forceRefresh: false) + let vpnFeature = features.first { $0.entitlement == .networkProtection } + let dbpFeature = features.first { $0.entitlement == .dataBrokerProtection } + let itrFeature = features.first { $0.entitlement == .identityTheftRestoration } + let itrgFeature = features.first { $0.entitlement == .identityTheftRestorationGlobal } - if features.contains(.networkProtection) { + if vpnFeature?.availableForUser ?? false { availableCategories.append(.vpn) } - if features.contains(.dataBrokerProtection) { + if dbpFeature?.availableForUser ?? false { availableCategories.append(.pir) } - if features.contains(.identityTheftRestoration) || features.contains(.identityTheftRestorationGlobal) { + let idpEnabled = itrFeature?.availableForUser ?? false + let idpgEnabled = itrgFeature?.availableForUser ?? false + if idpEnabled || idpgEnabled { availableCategories.append(.itr) } } @@ -281,7 +287,7 @@ final class UnifiedFeedbackFormViewModel: ObservableObject { private func submitIssue(metadata: UnifiedFeedbackMetadata?) async throws { guard !userEmail.isEmpty else { return } - guard let accessToken = accountManager.accessToken else { + guard let tokenContainer: TokenContainer = try? await subscriptionManager.getTokenContainer(policy: .localValid) else { throw Error.missingAccessToken } @@ -292,8 +298,10 @@ final class UnifiedFeedbackFormViewModel: ObservableObject { feedbackText: feedbackFormText, problemSubCategory: selectedSubcategory, customMetadata: metadata?.toString() ?? "") - let headers = APIRequestV2.HeadersV2(additionalHeaders: [HTTPHeaderKey.authorization: "Bearer \(accessToken)"]) - let request = APIRequestV2(url: Self.feedbackEndpoint, method: .post, headers: headers, body: payload.toData()) + let headers = APIRequestV2.HeadersV2(additionalHeaders: [HTTPHeaderKey.authorization: "Bearer \(tokenContainer.accessToken)"]) + guard let request = APIRequestV2(url: Self.feedbackEndpoint, method: .post, headers: headers, body: payload.toData()) else { + throw Error.invalidRequest + } let response: Response = try await apiService.fetch(request: request).decodeBody() if let error = response.error, !error.isEmpty { diff --git a/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormViewController.swift b/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormViewController.swift index 22e38142a2..9d374b3a33 100644 --- a/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormViewController.swift +++ b/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormViewController.swift @@ -40,7 +40,7 @@ final class VPNFeedbackFormViewController: NSViewController { private var cancellables = Set() init() { - self.viewModel = VPNFeedbackFormViewModel(metadataCollector: DefaultVPNMetadataCollector(accountManager: Application.appDelegate.subscriptionManager.accountManager)) + self.viewModel = VPNFeedbackFormViewModel(metadataCollector: DefaultVPNMetadataCollector(subscriptionManager: Application.appDelegate.subscriptionManager)) super.init(nibName: nil, bundle: nil) self.viewModel.delegate = self } diff --git a/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift b/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift index 778b039358..0304f2421f 100644 --- a/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift +++ b/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift @@ -123,16 +123,16 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { private let statusReporter: NetworkProtectionStatusReporter private let ipcClient: VPNControllerXPCClient private let defaults: UserDefaults - private let accountManager: AccountManager + private let subscriptionManager: any SubscriptionManager private let settings: VPNSettings init(defaults: UserDefaults = .netP, - accountManager: AccountManager) { + subscriptionManager: any SubscriptionManager) { let ipcClient = VPNControllerXPCClient.shared ipcClient.register { _ in } - self.accountManager = accountManager + self.subscriptionManager = subscriptionManager self.ipcClient = ipcClient self.defaults = defaults @@ -321,10 +321,9 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { } func collectPrivacyProInfo() async -> VPNMetadata.PrivacyProInfo { - let hasVPNEntitlement = (try? await accountManager.hasEntitlement(forProductName: .networkProtection).get()) ?? false return .init( - hasPrivacyProAccount: accountManager.isUserAuthenticated, - hasVPNEntitlement: hasVPNEntitlement + hasPrivacyProAccount: subscriptionManager.isUserAuthenticated, + hasVPNEntitlement: await subscriptionManager.isFeatureAvailableForUser(.networkProtection) ) } diff --git a/DuckDuckGo/Waitlist/VPNFeatureGatekeeper.swift b/DuckDuckGo/Waitlist/VPNFeatureGatekeeper.swift index 413d1bbc41..2b2c253a2c 100644 --- a/DuckDuckGo/Waitlist/VPNFeatureGatekeeper.swift +++ b/DuckDuckGo/Waitlist/VPNFeatureGatekeeper.swift @@ -29,7 +29,7 @@ import Subscription protocol VPNFeatureGatekeeper { var isInstalled: Bool { get } - func canStartVPN() async throws -> Bool + func canStartVPN() async -> Bool func isVPNVisible() -> Bool func shouldUninstallAutomatically() -> Bool func disableIfUserHasNoAccess() async @@ -40,16 +40,13 @@ protocol VPNFeatureGatekeeper { struct DefaultVPNFeatureGatekeeper: VPNFeatureGatekeeper { private static var subscriptionAuthTokenPrefix: String { "ddg:" } private let vpnUninstaller: VPNUninstalling - private let networkProtectionFeatureActivation: NetworkProtectionFeatureActivation private let defaults: UserDefaults private let subscriptionManager: SubscriptionManager - init(networkProtectionFeatureActivation: NetworkProtectionFeatureActivation = NetworkProtectionKeychainTokenStore(), - vpnUninstaller: VPNUninstalling = VPNUninstaller(), + init(vpnUninstaller: VPNUninstalling = VPNUninstaller(), defaults: UserDefaults = .netP, subscriptionManager: SubscriptionManager) { - self.networkProtectionFeatureActivation = networkProtectionFeatureActivation self.vpnUninstaller = vpnUninstaller self.defaults = defaults self.subscriptionManager = subscriptionManager @@ -64,13 +61,8 @@ struct DefaultVPNFeatureGatekeeper: VPNFeatureGatekeeper { /// For beta users this means they have an auth token. /// For subscription users this means they have entitlements. /// - func canStartVPN() async throws -> Bool { - switch await subscriptionManager.accountManager.hasEntitlement(forProductName: .networkProtection) { - case .success(let hasEntitlement): - return hasEntitlement - case .failure(let error): - throw error - } + func canStartVPN() async -> Bool { + return await subscriptionManager.isFeatureAvailableForUser(.networkProtection) } /// Whether the user can see the VPN entry points in the UI. @@ -79,13 +71,14 @@ struct DefaultVPNFeatureGatekeeper: VPNFeatureGatekeeper { /// For subscription users this means they are authenticated. /// func isVPNVisible() -> Bool { - subscriptionManager.accountManager.isUserAuthenticated + return subscriptionManager.isUserAuthenticated + // Validate approach if we should use: return await subscriptionManager.currentSubscriptionFeatures(forceRefresh: false).map { $0.entitlement}.contains(.networkProtection) } /// Returns whether the VPN should be uninstalled automatically. /// This is only true when the user is not an Easter Egg user, the waitlist test has ended, and the user is onboarded. func shouldUninstallAutomatically() -> Bool { - !subscriptionManager.accountManager.isUserAuthenticated && LoginItem.vpnMenu.status.isInstalled + !subscriptionManager.isUserAuthenticated && LoginItem.vpnMenu.status.isInstalled } /// Whether the user is fully onboarded diff --git a/DuckDuckGo/YoutubePlayer/DuckPlayer.swift b/DuckDuckGo/YoutubePlayer/DuckPlayer.swift index e739522169..b42abe6fd4 100644 --- a/DuckDuckGo/YoutubePlayer/DuckPlayer.swift +++ b/DuckDuckGo/YoutubePlayer/DuckPlayer.swift @@ -200,7 +200,7 @@ final class DuckPlayer { guard let self else { return nil } - guard let userValues: UserValues = DecodableHelper.decode(from: params) else { + guard let userValues: UserValues = CodableHelper.decode(from: params) else { assertionFailure("YoutubeOverlayUserScript: expected JSON representation of UserValues") return nil } diff --git a/DuckDuckGoDBPBackgroundAgent/DataBrokerAuthenticationManagerBuilder.swift b/DuckDuckGoDBPBackgroundAgent/DataBrokerAuthenticationManagerBuilder.swift deleted file mode 100644 index 0fb44dfb1c..0000000000 --- a/DuckDuckGoDBPBackgroundAgent/DataBrokerAuthenticationManagerBuilder.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// DataBrokerAuthenticationManagerBuilder.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 DataBrokerProtection -import Subscription - -final public class DataBrokerAuthenticationManagerBuilder { - - static func buildAuthenticationManager(redeemUseCase: RedeemUseCase = RedeemUseCase(), - subscriptionManager: SubscriptionManager) -> DataBrokerProtectionAuthenticationManager { - let subscriptionManager = DataBrokerProtectionSubscriptionManager(subscriptionManager: subscriptionManager) - return DataBrokerProtectionAuthenticationManager(redeemUseCase: redeemUseCase, - subscriptionManager: subscriptionManager) - - } -} - -extension DefaultAccountManager: DataBrokerProtectionAccountManaging { - public func hasEntitlement(for cachePolicy: APICachePolicy) async -> Result { - await hasEntitlement(forProductName: .dataBrokerProtection, cachePolicy: .reloadIgnoringLocalCacheData) - } -} diff --git a/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift b/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift index 130516c81f..f0f962b6aa 100644 --- a/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift +++ b/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift @@ -34,7 +34,6 @@ final class DuckDuckGoDBPBackgroundAgentApplication: NSApplication { override init() { Logger.dbpBackgroundAgent.log("🟢 Starting: \(NSRunningApplication.current.processIdentifier, privacy: .public)") - let dryRun: Bool #if DEBUG dryRun = true @@ -69,18 +68,25 @@ final class DuckDuckGoDBPBackgroundAgentApplication: NSApplication { } // Configure Subscription - subscriptionManager = DefaultSubscriptionManager() - + let subscriptionAppGroup = Bundle.main.appGroup(bundle: .subs) + let subscriptionUserDefaults = UserDefaults(suiteName: subscriptionAppGroup)! + let subscriptionEnvironment = DefaultSubscriptionManager.getSavedOrDefaultEnvironment(userDefaults: subscriptionUserDefaults) + subscriptionManager = DefaultSubscriptionManager(keychainType: .dataProtection(.named(subscriptionAppGroup)), + environment: subscriptionEnvironment, + userDefaults: subscriptionUserDefaults, + canPerformAuthMigration: false, + canHandlePixels: false) _delegate = DuckDuckGoDBPBackgroundAgentAppDelegate(subscriptionManager: subscriptionManager) super.init() self.delegate = _delegate + + Logger.dbpBackgroundAgent.debug("🟢 Started") } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - } @main @@ -88,29 +94,33 @@ final class DuckDuckGoDBPBackgroundAgentAppDelegate: NSObject, NSApplicationDele private let settings = DataBrokerProtectionSettings() private var cancellables = Set() private var statusBarMenu: StatusBarMenu? - private let subscriptionManager: SubscriptionManager + private let subscriptionManager: any SubscriptionManager private var manager: DataBrokerProtectionAgentManager? init(subscriptionManager: SubscriptionManager) { self.subscriptionManager = subscriptionManager + + // Aligning the environment with the Subscription one + settings.alignTo(subscriptionEnvironment: subscriptionManager.currentEnvironment) + let redeemUseCase = RedeemUseCase(authenticationService: AuthenticationService(), + authenticationRepository: KeychainAuthenticationData()) + let authenticationManager = DataBrokerProtectionAuthenticationManager( + redeemUseCase: redeemUseCase, + subscriptionManager: subscriptionManager) + self.manager = DataBrokerProtectionAgentManagerProvider.agentManager(authenticationManager: authenticationManager) } @MainActor func applicationDidFinishLaunching(_ aNotification: Notification) { - Logger.dbpBackgroundAgent.log("DuckDuckGoAgent started") + Logger.dbpBackgroundAgent.log("DuckDuckGo DBP Agent launched") - let redeemUseCase = RedeemUseCase(authenticationService: AuthenticationService(), - authenticationRepository: KeychainAuthenticationData()) - let authenticationManager = DataBrokerAuthenticationManagerBuilder.buildAuthenticationManager(redeemUseCase: redeemUseCase, - subscriptionManager: subscriptionManager) - manager = DataBrokerProtectionAgentManagerProvider.agentManager(authenticationManager: authenticationManager, - accountManager: subscriptionManager.accountManager) - manager?.agentFinishedLaunching() + // Subscription initial tasks + Task { + await subscriptionManager.loadInitialData() + } + manager?.agentFinishedLaunching() setupStatusBarMenu() - - // Aligning the environment with the Subscription one - settings.alignTo(subscriptionEnvironment: subscriptionManager.currentEnvironment) } @MainActor diff --git a/DuckDuckGoNotifications/DuckDuckGoNotificationsAppDelegate.swift b/DuckDuckGoNotifications/DuckDuckGoNotificationsAppDelegate.swift index cb2d280887..5f4ee6c632 100644 --- a/DuckDuckGoNotifications/DuckDuckGoNotificationsAppDelegate.swift +++ b/DuckDuckGoNotifications/DuckDuckGoNotificationsAppDelegate.swift @@ -29,7 +29,7 @@ final class DuckDuckGoNotificationsApplication: NSApplication { private let _delegate = DuckDuckGoNotificationsAppDelegate() override init() { - Logger.networkProtection.error("🟢 Notifications Agent starting: \(ProcessInfo.processInfo.processIdentifier, privacy: .public)") + Logger.networkProtection.log("🟢 Notifications Agent init: \(ProcessInfo.processInfo.processIdentifier, privacy: .public)") // prevent agent from running twice if let anotherInstance = NSRunningApplication.runningApplications(withBundleIdentifier: Bundle.main.bundleIdentifier!).first(where: { $0 != .current }) { diff --git a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift index 46028204d7..43a1e83f20 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift +++ b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift @@ -37,7 +37,7 @@ import os.log @objc(Application) final class DuckDuckGoVPNApplication: NSApplication { - public var accountManager: AccountManager + public var subscriptionManager: any SubscriptionManager private let _delegate: DuckDuckGoVPNAppDelegate override init() { @@ -53,35 +53,34 @@ final class DuckDuckGoVPNApplication: NSApplication { let subscriptionAppGroup = Bundle.main.appGroup(bundle: .subs) let subscriptionUserDefaults = UserDefaults(suiteName: subscriptionAppGroup)! let subscriptionEnvironment = DefaultSubscriptionManager.getSavedOrDefaultEnvironment(userDefaults: subscriptionUserDefaults) - let subscriptionEndpointService = DefaultSubscriptionEndpointService(currentServiceEnvironment: subscriptionEnvironment.serviceEnvironment) - let authEndpointService = DefaultAuthEndpointService(currentServiceEnvironment: subscriptionEnvironment.serviceEnvironment) - let entitlementsCache = UserDefaultsCache<[Entitlement]>(userDefaults: subscriptionUserDefaults, - key: UserDefaultsCacheKey.subscriptionEntitlements, - settings: UserDefaultsCacheSettings(defaultExpirationInterval: .minutes(20))) - let accessTokenStorage = SubscriptionTokenKeychainStorage(keychainType: .dataProtection(.named(subscriptionAppGroup))) - accountManager = DefaultAccountManager(accessTokenStorage: accessTokenStorage, - entitlementsCache: entitlementsCache, - subscriptionEndpointService: subscriptionEndpointService, - authEndpointService: authEndpointService) - - _delegate = DuckDuckGoVPNAppDelegate(accountManager: accountManager, - accessTokenStorage: accessTokenStorage, - subscriptionEnvironment: subscriptionEnvironment) + + subscriptionManager = DefaultSubscriptionManager(keychainType: .dataProtection(.named(subscriptionAppGroup)), + environment: subscriptionEnvironment, + userDefaults: subscriptionUserDefaults, + canPerformAuthMigration: false, + canHandlePixels: false) + _delegate = DuckDuckGoVPNAppDelegate(subscriptionManager: subscriptionManager) super.init() setupPixelKit() self.delegate = _delegate - accountManager.delegate = _delegate #if DEBUG - if accountManager.accessToken != nil { - Logger.networkProtection.error("🟢 VPN Agent found token") - } else { - Logger.networkProtection.error("VPN Agent found no token") - } + checkTokenPresence() #endif } + func checkTokenPresence() { + Task { + do { + _ = try await subscriptionManager.getTokenContainer(policy: .local) + Logger.networkProtection.log("🟢 VPN Agent found token") + } catch { + Logger.networkProtection.error("VPN Agent found no token \(error.localizedDescription)") + } + } + } + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -128,8 +127,7 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { private static let recentThreshold: TimeInterval = 5.0 private let appLauncher = AppLauncher() - private let accountManager: AccountManager - private let accessTokenStorage: SubscriptionTokenKeychainStorage + private let subscriptionManager: any SubscriptionManager private let configurationStore = ConfigurationStore() private let configurationManager: ConfigurationManager @@ -140,14 +138,10 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { privacyConfigManager: privacyConfigurationManager, experimentManager: nil) - public init(accountManager: AccountManager, - accessTokenStorage: SubscriptionTokenKeychainStorage, - subscriptionEnvironment: SubscriptionEnvironment) { - - self.accountManager = accountManager - self.accessTokenStorage = accessTokenStorage + public init(subscriptionManager: any SubscriptionManager) { + self.subscriptionManager = subscriptionManager self.tunnelSettings = VPNSettings(defaults: .netP) - self.tunnelSettings.alignTo(subscriptionEnvironment: subscriptionEnvironment) + self.tunnelSettings.alignTo(subscriptionEnvironment: subscriptionManager.currentEnvironment) self.configurationManager = ConfigurationManager(privacyConfigManager: privacyConfigurationManager, store: configurationStore) } @@ -241,7 +235,7 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { featureFlagger: featureFlagger, settings: tunnelSettings, defaults: userDefaults, - accessTokenStorage: accessTokenStorage) + subscriptionManager: subscriptionManager) /// An IPC server that provides access to the tunnel controller. /// @@ -393,6 +387,9 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { setupMenuVisibility() Task { @MainActor in + // Subscription initial tasks + await subscriptionManager.loadInitialData() + // Initialize lazy properties _ = tunnelControllerIPCService _ = vpnProxyLauncher @@ -419,6 +416,7 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { @MainActor private func setupMenuVisibility() { + if tunnelSettings.showInMenuBar { networkProtectionMenu.show() } else { @@ -439,10 +437,13 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { private lazy var entitlementMonitor = NetworkProtectionEntitlementMonitor() private func setUpSubscriptionMonitoring() { - guard accountManager.isUserAuthenticated else { return } + guard subscriptionManager.isUserAuthenticated else { return } - let entitlementsCheck = { - await self.accountManager.hasEntitlement(forProductName: .networkProtection, cachePolicy: .reloadIgnoringLocalCacheData) + let entitlementsCheck: (() async -> Result) = { + Logger.networkProtection.log("Subscription Entitlements check...") + let isNetworkProtectionEnabled = await self.subscriptionManager.isFeatureAvailableForUser(.networkProtection) + Logger.networkProtection.log("Network protection is \( isNetworkProtectionEnabled ? "🟢 Enabled" : "⚫️ Disabled", privacy: .public)") + return .success(isNetworkProtectionEnabled) } Task { @@ -469,11 +470,3 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { } } - -extension DuckDuckGoVPNAppDelegate: AccountManagerKeychainAccessDelegate { - - public func accountManagerKeychainAccessFailed(accessType: AccountKeychainAccessType, error: AccountKeychainAccessError) { - PixelKit.fire(PrivacyProErrorPixel.privacyProKeychainAccessError(accessType: accessType, accessError: error), - frequency: .legacyDailyAndCount) - } -} diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 22d1e610bd..945f4e3ffa 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -59,6 +59,8 @@ let package = Package( "DataBrokerProtection", "BrowserServicesKit", "Freemium", + .product(name: "PersistenceTestingUtils", package: "BrowserServicesKit"), + .product(name: "SubscriptionTestingUtilities", package: "BrowserServicesKit"), ], resources: [ .copy("Resources") diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionAuthenticationManaging.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionAuthenticationManaging.swift index 0aea2ac3ef..3f9ddc1a4d 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionAuthenticationManaging.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionAuthenticationManaging.swift @@ -17,40 +17,43 @@ // import Foundation +import Subscription public protocol DataBrokerProtectionAuthenticationManaging { var isUserAuthenticated: Bool { get } - var accessToken: String? { get } - func hasValidEntitlement() async throws -> Bool + func accessToken() async -> String? + func hasValidEntitlement() async -> Bool func shouldAskForInviteCode() -> Bool func redeem(inviteCode: String) async throws - func getAuthHeader() -> String? + func getAuthHeader() async -> String? } public final class DataBrokerProtectionAuthenticationManager: DataBrokerProtectionAuthenticationManaging { private let redeemUseCase: DataBrokerProtectionRedeemUseCase - private let subscriptionManager: DataBrokerProtectionSubscriptionManaging + private let subscriptionManager: any SubscriptionManager public var isUserAuthenticated: Bool { subscriptionManager.isUserAuthenticated } - public var accessToken: String? { - subscriptionManager.accessToken + public func accessToken() async -> String? { + try? await subscriptionManager.getTokenContainer(policy: .localValid).accessToken } - public init(redeemUseCase: any DataBrokerProtectionRedeemUseCase, - subscriptionManager: any DataBrokerProtectionSubscriptionManaging) { + public init(redeemUseCase: any DataBrokerProtectionRedeemUseCase = RedeemUseCase(), + subscriptionManager: any SubscriptionManager) { self.redeemUseCase = redeemUseCase self.subscriptionManager = subscriptionManager } - public func hasValidEntitlement() async throws -> Bool { - try await subscriptionManager.hasValidEntitlement() + public func hasValidEntitlement() async -> Bool { + let tokenContainer = try? await subscriptionManager.getTokenContainer(policy: .localValid) + return tokenContainer?.decodedAccessToken.subscriptionEntitlements.contains(.dataBrokerProtection) ?? false } - public func getAuthHeader() -> String? { - ServicesAuthHeaderBuilder().getAuthHeader(accessToken) + public func getAuthHeader() async -> String? { + guard let token = await accessToken() else { return nil } + return ServicesAuthHeaderBuilder().getAuthHeader(token) } // MARK: - Redeem code flow diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionSubscriptionManaging.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionSubscriptionManaging.swift deleted file mode 100644 index 793baac5bf..0000000000 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionSubscriptionManaging.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// DataBrokerProtectionSubscriptionManaging.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 Subscription -import Common -import AppKitExtensions - -public protocol DataBrokerProtectionSubscriptionManaging { - var isUserAuthenticated: Bool { get } - var accessToken: String? { get } - func hasValidEntitlement() async throws -> Bool -} - -public final class DataBrokerProtectionSubscriptionManager: DataBrokerProtectionSubscriptionManaging { - - let subscriptionManager: SubscriptionManager - - public var isUserAuthenticated: Bool { - accessToken != nil - } - - public var accessToken: String? { - // We use a staging token for privacy pro supplied through a github secret/action - // for PIR end to end tests. This is also stored in bitwarden if you want to run - // the tests locally - let dbpSettings = DataBrokerProtectionSettings() - if dbpSettings.storedRunType == .integrationTests, - let token = ProcessInfo.processInfo.environment["PRIVACYPRO_STAGING_TOKEN"] { - return token - } - return subscriptionManager.accountManager.accessToken - } - - public init(subscriptionManager: SubscriptionManager) { - self.subscriptionManager = subscriptionManager - } - - public func hasValidEntitlement() async throws -> Bool { - switch await subscriptionManager.accountManager.hasEntitlement(forProductName: .dataBrokerProtection, - cachePolicy: .reloadIgnoringLocalCacheData) { - case let .success(result): - return result - case .failure(let error): - throw error - } - } -} - -// MARK: - Wrapper Protocols - -/// This protocol exists only as a wrapper on top of the AccountManager since it is a concrete type on BSK -public protocol DataBrokerProtectionAccountManaging { - var accessToken: String? { get } - func hasEntitlement(for cachePolicy: APICachePolicy) async -> Result -} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift index d7bbc909f1..4097224d5c 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift @@ -31,15 +31,16 @@ import UserNotifications // This is to avoid exposing all the dependancies outside of the DBP package public class DataBrokerProtectionAgentManagerProvider { - public static func agentManager(authenticationManager: DataBrokerProtectionAuthenticationManaging, - accountManager: AccountManager) -> DataBrokerProtectionAgentManager { + public static func agentManager(authenticationManager: DataBrokerProtectionAuthenticationManaging) -> DataBrokerProtectionAgentManager { let pixelHandler = DataBrokerProtectionPixelsHandler() let dbpSettings = DataBrokerProtectionSettings() let executionConfig = DataBrokerExecutionConfig(mode: dbpSettings.storedRunType == .integrationTests ? .fastForIntegrationTests : .normal) let activityScheduler = DefaultDataBrokerProtectionBackgroundActivityScheduler(config: executionConfig) - let notificationService = DefaultDataBrokerProtectionUserNotificationService(pixelHandler: pixelHandler, userNotificationCenter: UNUserNotificationCenter.current(), authenticationManager: authenticationManager) + let notificationService = DefaultDataBrokerProtectionUserNotificationService( + pixelHandler: pixelHandler, userNotificationCenter: UNUserNotificationCenter.current(), + authenticationManager: authenticationManager) Configuration.setURLProvider(DBPAgentConfigurationURLProvider()) let configStore = ConfigurationStore() let privacyConfigurationManager = DBPPrivacyConfigurationManager() diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/CaptchaService.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/CaptchaService.swift index 762afd13b7..f88358ed35 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/CaptchaService.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/CaptchaService.swift @@ -187,7 +187,7 @@ struct CaptchaService: CaptchaServiceProtocol { Logger.service.debug("Submitting captcha request ...") var request = URLRequest(url: url) - guard let authHeader = authenticationManager.getAuthHeader() else { + guard let authHeader = await authenticationManager.getAuthHeader() else { servicePixel.fireEmptyAccessToken(callSite: .submitCaptchaInformationRequest) throw AuthenticationError.noAuthToken } @@ -273,7 +273,7 @@ struct CaptchaService: CaptchaServiceProtocol { } var request = URLRequest(url: url) - guard let authHeader = authenticationManager.getAuthHeader() else { + guard let authHeader = await authenticationManager.getAuthHeader() else { servicePixel.fireEmptyAccessToken(callSite: .submitCaptchaToBeResolvedRequest) throw AuthenticationError.noAuthToken } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/EmailService.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/EmailService.swift index 22486fea1d..a675ff431e 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/EmailService.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/EmailService.swift @@ -80,7 +80,7 @@ struct EmailService: EmailServiceProtocol { } var request = URLRequest(url: url) - guard let authHeader = authenticationManager.getAuthHeader() else { + guard let authHeader = await authenticationManager.getAuthHeader() else { servicePixel.fireEmptyAccessToken(callSite: .getEmail) throw AuthenticationError.noAuthToken } @@ -163,7 +163,7 @@ struct EmailService: EmailServiceProtocol { var request = URLRequest(url: url) - guard let authHeader = authenticationManager.getAuthHeader() else { + guard let authHeader = await authenticationManager.getAuthHeader() else { servicePixel.fireEmptyAccessToken(callSite: .extractEmailLink) throw AuthenticationError.noAuthToken } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionAgentStopper.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionAgentStopper.swift index afbd257719..bd61fd0592 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionAgentStopper.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionAgentStopper.swift @@ -75,7 +75,8 @@ struct DefaultDataBrokerProtectionAgentStopper: DataBrokerProtectionAgentStopper return } - let hasValidEntitlement = try await authenticationManager.hasValidEntitlement() + let hasValidEntitlement = await authenticationManager.hasValidEntitlement() + Logger.dataBrokerProtection.debug("Entitlements are \(hasValidEntitlement ? "valid" : "invalid")") stopAgentBasedOnEntitlementCheckResult(hasValidEntitlement ? .enabled : .disabled) } catch { diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentStopperTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentStopperTests.swift index d18285fc69..26d006cf8e 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentStopperTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentStopperTests.swift @@ -229,22 +229,6 @@ final class DataBrokerProtectionAgentStopperTests: XCTestCase { XCTAssertFalse(mockStopAction.wasStopCalled) } - func testErrorEntitlement_thenStopAgentIsNotCalled() async { - mockAuthenticationManager.isUserAuthenticatedValue = true - mockAuthenticationManager.shouldThrowEntitlementError = true - mockDataManager.profileToReturn = fakeProfile - - let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, - entitlementMonitor: mockEntitlementMonitor, - authenticationManager: mockAuthenticationManager, - pixelHandler: mockPixelHandler, - stopAction: mockStopAction, - freemiumDBPUserStateManager: mockFreemiumDBPUserStateManager) - await stopper.validateRunPrerequisitesAndStopAgentIfNecessary() - - XCTAssertFalse(mockStopAction.wasStopCalled) - } - func testValidEntitlement_andUserIsNotFreemium_thenStopAgentIsNotCalled() async { mockAuthenticationManager.isUserAuthenticatedValue = true mockAuthenticationManager.hasValidEntitlementValue = true @@ -374,27 +358,4 @@ final class DataBrokerProtectionAgentStopperTests: XCTestCase { wait(for: [expectation], timeout: 3) } - - func testEntitlementMonitorWithErrorResult_thenStopAgentIsNotCalled() { - mockAuthenticationManager.isUserAuthenticatedValue = true - mockAuthenticationManager.shouldThrowEntitlementError = true - mockDataManager.profileToReturn = fakeProfile - - let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, - entitlementMonitor: mockEntitlementMonitor, - authenticationManager: mockAuthenticationManager, - pixelHandler: mockPixelHandler, - stopAction: mockStopAction, - freemiumDBPUserStateManager: mockFreemiumDBPUserStateManager) - - let expectation = XCTestExpectation(description: "Wait for monitor") - stopper.monitorEntitlementAndStopAgentIfEntitlementIsInvalidAndUserIsNotFreemium(interval: 0.1) - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [self] in - XCTAssertFalse(mockStopAction.wasStopCalled) - expectation.fulfill() - } - - wait(for: [expectation], timeout: 3) - } } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAuthenticationManagerTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAuthenticationManagerTests.swift index b0603c8399..9c99c1cd34 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAuthenticationManagerTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAuthenticationManagerTests.swift @@ -18,15 +18,19 @@ import XCTest @testable import DataBrokerProtection +import Subscription +import SubscriptionTestingUtilities +import Networking +import NetworkingTestingUtils class DataBrokerProtectionAuthenticationManagerTests: XCTestCase { var authenticationManager: DataBrokerProtectionAuthenticationManager! var redeemUseCase: DataBrokerProtectionRedeemUseCase! - var subscriptionManager: MockDataBrokerProtectionSubscriptionManaging! + var subscriptionManager: SubscriptionManagerMock! override func setUp() async throws { redeemUseCase = MockRedeemUseCase() - subscriptionManager = MockDataBrokerProtectionSubscriptionManaging() + subscriptionManager = SubscriptionManagerMock() } override func tearDown() async throws { @@ -36,106 +40,49 @@ class DataBrokerProtectionAuthenticationManagerTests: XCTestCase { } func testUserNotAuthenticatedWhenSubscriptionManagerReturnsFalse() { - subscriptionManager.userAuthenticatedValue = false - authenticationManager = DataBrokerProtectionAuthenticationManager(redeemUseCase: redeemUseCase, subscriptionManager: subscriptionManager) - XCTAssertEqual(authenticationManager.isUserAuthenticated, false) } - func testEmptyAccessTokenResultsInNilAuthHeader() { - subscriptionManager.accessTokenValue = nil - + func testEmptyAccessTokenResultsInNilAuthHeader() async { authenticationManager = DataBrokerProtectionAuthenticationManager(redeemUseCase: redeemUseCase, subscriptionManager: subscriptionManager) - - XCTAssertNil(authenticationManager.getAuthHeader()) + let authHeader = await authenticationManager.getAuthHeader() + XCTAssertNil(authHeader) } func testUserAuthenticatedWhenSubscriptionManagerReturnsTrue() { - subscriptionManager.userAuthenticatedValue = true - + subscriptionManager.resultTokenContainer = OAuthTokensFactory.makeValidTokenContainer() authenticationManager = DataBrokerProtectionAuthenticationManager(redeemUseCase: redeemUseCase, subscriptionManager: subscriptionManager) XCTAssertEqual(authenticationManager.isUserAuthenticated, true) } - func testNonEmptyAccessTokenResultsInValidAuthHeader() { - let accessToken = "validAccessToken" - subscriptionManager.accessTokenValue = accessToken + func testNonEmptyAccessTokenResultsInValidAuthHeader() async { + subscriptionManager.resultTokenContainer = OAuthTokensFactory.makeValidTokenContainer() authenticationManager = DataBrokerProtectionAuthenticationManager(redeemUseCase: redeemUseCase, subscriptionManager: subscriptionManager) - - XCTAssertNotNil(authenticationManager.getAuthHeader()) + let authHeader = await authenticationManager.getAuthHeader() + XCTAssertNotNil(authHeader) } func testValidEntitlementCheckWithSuccess() async { - subscriptionManager.entitlementResultValue = true - + subscriptionManager.resultTokenContainer = OAuthTokensFactory.makeValidTokenContainerWithEntitlements() authenticationManager = DataBrokerProtectionAuthenticationManager(redeemUseCase: redeemUseCase, subscriptionManager: subscriptionManager) - do { - let result = try await authenticationManager.hasValidEntitlement() - XCTAssertTrue(result, "Entitlement check should return true for valid entitlement") - } catch { - XCTFail("Entitlement check should not fail: \(error)") - } + let result = await authenticationManager.hasValidEntitlement() + XCTAssertTrue(result, "Entitlement check should return true for valid entitlement") } func testValidEntitlementCheckWithSuccessFalse() async { - subscriptionManager.entitlementResultValue = false - + subscriptionManager.resultTokenContainer = OAuthTokensFactory.makeValidTokenContainer() authenticationManager = DataBrokerProtectionAuthenticationManager(redeemUseCase: redeemUseCase, subscriptionManager: subscriptionManager) - do { - let result = try await authenticationManager.hasValidEntitlement() - XCTAssertFalse(result, "Entitlement check should return false for valid entitlement") - } catch { - XCTFail("Entitlement check should not fail: \(error)") - } - } - - func testValidEntitlementCheckWithFailure() async { - let mockError = NSError(domain: "TestErrorDomain", code: 123, userInfo: nil) - subscriptionManager.entitlementError = mockError - - authenticationManager = DataBrokerProtectionAuthenticationManager(redeemUseCase: redeemUseCase, - subscriptionManager: subscriptionManager) - - do { - _ = try await authenticationManager.hasValidEntitlement() - XCTFail("Entitlement check should fail") - } catch let error as NSError { - XCTAssertEqual(mockError.domain, error.domain) - XCTAssertEqual(mockError.code, error.code) - } - } -} - -final class MockDataBrokerProtectionSubscriptionManaging: DataBrokerProtectionSubscriptionManaging { - typealias EntitlementResult = Result - - var userAuthenticatedValue = false - var accessTokenValue: String? - var entitlementResultValue = false - var entitlementError: Error? - - var isUserAuthenticated: Bool { - userAuthenticatedValue - } - - var accessToken: String? { - accessTokenValue - } - - func hasValidEntitlement() async throws -> Bool { - if let error = entitlementError { - throw error - } - return entitlementResultValue + let result = await authenticationManager.hasValidEntitlement() + XCTAssertFalse(result, "Entitlement check should return false for valid entitlement") } } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift index 76747680a5..319485d16a 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift @@ -1663,16 +1663,14 @@ final class MockAuthenticationManager: DataBrokerProtectionAuthenticationManagin var redeemCodeCalled = false var authHeaderValue: String? = "fake auth header" var hasValidEntitlementValue = false - var shouldThrowEntitlementError = false var isUserAuthenticated: Bool { isUserAuthenticatedValue } - var accessToken: String? { accessTokenValue } + func accessToken() async -> String? { + accessTokenValue + } - func hasValidEntitlement() async throws -> Bool { - if shouldThrowEntitlementError { - throw NSError(domain: "duck.com", code: 0, userInfo: [NSLocalizedDescriptionKey: "Error"]) - } + func hasValidEntitlement() async -> Bool { return hasValidEntitlementValue } @@ -1691,7 +1689,6 @@ final class MockAuthenticationManager: DataBrokerProtectionAuthenticationManagin redeemCodeCalled = false authHeaderValue = "fake auth header" hasValidEntitlementValue = false - shouldThrowEntitlementError = false } } diff --git a/LocalPackages/NewTabPage/Package.swift b/LocalPackages/NewTabPage/Package.swift index ce6bf40133..59186c440a 100644 --- a/LocalPackages/NewTabPage/Package.swift +++ b/LocalPackages/NewTabPage/Package.swift @@ -45,7 +45,7 @@ let package = Package( .product(name: "BrowserServicesKit", package: "BrowserServicesKit"), .product(name: "PrivacyStats", package: "BrowserServicesKit"), .product(name: "RemoteMessaging", package: "BrowserServicesKit"), - .product(name: "TestUtils", package: "BrowserServicesKit"), + .product(name: "PersistenceTestingUtils", package: "BrowserServicesKit"), .product(name: "WebKitExtensions", package: "WebKitExtensions"), ], swiftSettings: [ diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/PrivacyStats/NewTabPagePrivacyStatsClient.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/PrivacyStats/NewTabPagePrivacyStatsClient.swift index 62fc3804b3..d8eadacab9 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/PrivacyStats/NewTabPagePrivacyStatsClient.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/PrivacyStats/NewTabPagePrivacyStatsClient.swift @@ -82,7 +82,7 @@ public final class NewTabPagePrivacyStatsClient: NewTabPageScriptClient { @MainActor private func setConfig(params: Any, original: WKScriptMessage) async throws -> Encodable? { - guard let config: NewTabPageUserScript.WidgetConfig = DecodableHelper.decode(from: params) else { + guard let config: NewTabPageUserScript.WidgetConfig = CodableHelper.decode(from: params) else { return nil } model.isViewExpanded = config.expansion == .expanded diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesClientTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesClientTests.swift index 7714f8064d..ef296b384e 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesClientTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesClientTests.swift @@ -18,9 +18,9 @@ import Combine import RemoteMessaging -import TestUtils import XCTest @testable import NewTabPage +import PersistenceTestingUtils final class NewTabPageFavoritesClientTests: XCTestCase { typealias NewTabPageFavoritesClientUnderTest = NewTabPageFavoritesClient diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesModelTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesModelTests.swift index 7b30d97f9d..27e585088d 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesModelTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesModelTests.swift @@ -17,7 +17,7 @@ // import Combine -import TestUtils +import PersistenceTestingUtils import XCTest @testable import NewTabPage diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageNextStepsCardsClientTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageNextStepsCardsClientTests.swift index d863006a09..f77cc47ff0 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageNextStepsCardsClientTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageNextStepsCardsClientTests.swift @@ -17,7 +17,7 @@ // import Combine -import TestUtils +import PersistenceTestingUtils import XCTest @testable import NewTabPage diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPagePrivacyStatsClientTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPagePrivacyStatsClientTests.swift index 767fc19c4e..e027e62b34 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPagePrivacyStatsClientTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPagePrivacyStatsClientTests.swift @@ -18,7 +18,7 @@ import Combine import PrivacyStats -import TestUtils +import PersistenceTestingUtils import TrackerRadarKit import XCTest @testable import NewTabPage diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPagePrivacyStatsModelTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPagePrivacyStatsModelTests.swift index b7173cddce..d488e9368d 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPagePrivacyStatsModelTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPagePrivacyStatsModelTests.swift @@ -18,7 +18,7 @@ import Combine import PrivacyStats -import TestUtils +import PersistenceTestingUtils import TrackerRadarKit import XCTest @testable import NewTabPage diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseModel.swift index 75756d515c..4ff76c04d0 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseModel.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseModel.swift @@ -23,14 +23,14 @@ import Subscription @available(macOS 12.0, *) public final class DebugPurchaseModel: ObservableObject { - var purchaseManager: DefaultStorePurchaseManager - let appStorePurchaseFlow: DefaultAppStorePurchaseFlow + var purchaseManager: any StorePurchaseManager + let appStorePurchaseFlow: any AppStorePurchaseFlow @Published var subscriptions: [SubscriptionRowModel] - init(manager: DefaultStorePurchaseManager, + init(manager: any StorePurchaseManager, subscriptions: [SubscriptionRowModel] = [], - appStorePurchaseFlow: DefaultAppStorePurchaseFlow) { + appStorePurchaseFlow: any AppStorePurchaseFlow) { self.purchaseManager = manager self.subscriptions = subscriptions self.appStorePurchaseFlow = appStorePurchaseFlow @@ -41,7 +41,7 @@ public final class DebugPurchaseModel: ObservableObject { print("Attempting purchase: \(product.displayName)") Task { - await appStorePurchaseFlow.purchaseSubscription(with: product.id, emailAccessToken: nil) + await appStorePurchaseFlow.purchaseSubscription(with: product.id) } } } diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseView.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseView.swift index 01eb9a0fba..1db61bd518 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseView.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseView.swift @@ -164,7 +164,7 @@ extension Product { } } -extension String: Identifiable { +extension String: @retroactive Identifiable { public typealias ID = Int public var id: Int { return hash diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseViewController.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseViewController.swift index a539dc8963..ca593e4ce1 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseViewController.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseViewController.swift @@ -25,7 +25,7 @@ import Subscription @available(macOS 12.0, *) public final class DebugPurchaseViewController: NSViewController { - private let manager: DefaultStorePurchaseManager + private let manager: any StorePurchaseManager private let model: DebugPurchaseModel private var cancellables = Set() @@ -34,7 +34,7 @@ public final class DebugPurchaseViewController: NSViewController { fatalError("init(coder:) has not been implemented") } - public init(storePurchaseManager: DefaultStorePurchaseManager, appStorePurchaseFlow: DefaultAppStorePurchaseFlow) { + public init(storePurchaseManager: any StorePurchaseManager, appStorePurchaseFlow: any AppStorePurchaseFlow) { manager = storePurchaseManager model = DebugPurchaseModel(manager: manager, appStorePurchaseFlow: appStorePurchaseFlow) @@ -59,11 +59,11 @@ public final class DebugPurchaseViewController: NSViewController { } public override func viewDidLoad() { + guard let manager = manager as? DefaultStorePurchaseManager else { return } Task { await manager.updatePurchasedProducts() await manager.updateAvailableProducts() } - manager.$availableProducts.combineLatest(manager.$purchasedProductIDs, manager.$purchaseQueue).receive(on: RunLoop.main).sink { [weak self] availableProducts, purchasedProductIDs, purchaseQueue in // swiftlint:disable:next force_cast diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift index 16af2ad1dd..f8e7b70130 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift @@ -33,9 +33,6 @@ public final class SubscriptionDebugMenu: NSMenuItem { var currentViewController: () -> NSViewController? let subscriptionManager: SubscriptionManager let subscriptionUserDefaults: UserDefaults - var accountManager: AccountManager { - subscriptionManager.accountManager - } required init(coder: NSCoder) { fatalError("init(coder:) has not been implemented") @@ -202,26 +199,30 @@ public final class SubscriptionDebugMenu: NSMenuItem { @objc func signOut() { - accountManager.signOut() + Task { + await subscriptionManager.signOut(notifyUI: true) + } } @objc func showAccountDetails() { - let title = accountManager.isUserAuthenticated ? "Authenticated" : "Not Authenticated" - let message = accountManager.isUserAuthenticated ? ["AuthToken: \(accountManager.authToken ?? "")", - "AccessToken: \(accountManager.accessToken ?? "")", - "Email: \(accountManager.email ?? "")"].joined(separator: "\n") : nil - showAlert(title: title, message: message) + Task { + let title = subscriptionManager.isUserAuthenticated ? "Authenticated" : "Not Authenticated" + let tokenContainer = try? await subscriptionManager.getTokenContainer(policy: .local) + let message = subscriptionManager.isUserAuthenticated ? ["External ID: \(tokenContainer?.decodedAccessToken.externalID ?? "")", + "\(tokenContainer!.debugDescription)", + "Email: \(subscriptionManager.userEmail ?? "")"].joined(separator: "\n") : nil + showAlert(title: title, message: message) + } } @objc func validateToken() { Task { - guard let token = accountManager.accessToken else { return } - switch await subscriptionManager.authEndpointService.validateToken(accessToken: token) { - case .success(let response): - showAlert(title: "Validate token", message: "\(response)") - case .failure(let error): + do { + let tokenContainer = try await subscriptionManager.getTokenContainer(policy: .local) + showAlert(title: "Valid token", message: tokenContainer.debugDescription) + } catch { showAlert(title: "Validate token", message: "\(error)") } } @@ -230,29 +231,21 @@ public final class SubscriptionDebugMenu: NSMenuItem { @objc func checkEntitlements() { Task { - var results: [String] = [] - - let entitlements: [Entitlement.ProductName] = [.networkProtection, .dataBrokerProtection, .identityTheftRestoration] - for entitlement in entitlements { - if case let .success(result) = await accountManager.hasEntitlement(forProductName: entitlement, cachePolicy: .reloadIgnoringLocalCacheData) { - let resultSummary = "Entitlement check for \(entitlement.rawValue): \(result)" - results.append(resultSummary) - print(resultSummary) - } - } - - showAlert(title: "Check Entitlements", message: results.joined(separator: "\n")) + let features = await subscriptionManager.currentSubscriptionFeatures(forceRefresh: true) + let descriptions = features.map({ feature in + "\(feature.entitlement.rawValue): Available: \(feature.availableForUser)" + }) + showAlert(title: "Check Entitlements", message: descriptions.joined(separator: "\n")) } } @objc func getSubscriptionDetails() { Task { - guard let token = accountManager.accessToken else { return } - switch await subscriptionManager.subscriptionEndpointService.getSubscription(accessToken: token, cachePolicy: .reloadIgnoringLocalCacheData) { - case .success(let response): - showAlert(title: "Subscription info", message: "\(response)") - case .failure(let error): + do { + let subscription = try await subscriptionManager.getSubscription(cachePolicy: .reloadIgnoringLocalCacheData) + showAlert(title: "Subscription info", message: subscription.debugDescription) + } catch { showAlert(title: "Subscription info", message: "\(error)") } } @@ -268,17 +261,14 @@ public final class SubscriptionDebugMenu: NSMenuItem { @IBAction func showPurchaseView(_ sender: Any?) { if #available(macOS 12.0, *) { - let appStoreRestoreFlow = DefaultAppStoreRestoreFlow(accountManager: subscriptionManager.accountManager, - storePurchaseManager: subscriptionManager.storePurchaseManager(), - subscriptionEndpointService: subscriptionManager.subscriptionEndpointService, - authEndpointService: subscriptionManager.authEndpointService) - let appStorePurchaseFlow = DefaultAppStorePurchaseFlow(subscriptionEndpointService: subscriptionManager.subscriptionEndpointService, + let appStoreRestoreFlow = DefaultAppStoreRestoreFlow(subscriptionManager: subscriptionManager, + storePurchaseManager: subscriptionManager.storePurchaseManager()) + let appStorePurchaseFlow = DefaultAppStorePurchaseFlow(subscriptionManager: subscriptionManager, storePurchaseManager: subscriptionManager.storePurchaseManager(), - accountManager: subscriptionManager.accountManager, - appStoreRestoreFlow: appStoreRestoreFlow, - authEndpointService: subscriptionManager.authEndpointService) - // swiftlint:disable:next force_cast - let vc = DebugPurchaseViewController(storePurchaseManager: subscriptionManager.storePurchaseManager() as! DefaultStorePurchaseManager, appStorePurchaseFlow: appStorePurchaseFlow) + appStoreRestoreFlow: appStoreRestoreFlow) + + let vc = DebugPurchaseViewController(storePurchaseManager: subscriptionManager.storePurchaseManager(), + appStorePurchaseFlow: appStorePurchaseFlow) currentViewController()?.presentAsSheet(vc) } } @@ -314,7 +304,7 @@ public final class SubscriptionDebugMenu: NSMenuItem { } private func askAndUpdateServiceEnvironment(to newServiceEnvironment: SubscriptionEnvironment.ServiceEnvironment) { - let alert = makeAlert(title: "Are you sure you want to change the environment to \(newServiceEnvironment.description.capitalized)", + let alert = makeAlert(title: "Are you sure you want to change the environment to \(newServiceEnvironment.rawValue.capitalized)", message: """ Please make sure you have manually removed your current active Subscription and reset all related features. You may also need to change environment of related features. @@ -361,10 +351,8 @@ public final class SubscriptionDebugMenu: NSMenuItem { func restorePurchases(_ sender: Any?) { if #available(macOS 12.0, *) { Task { - let appStoreRestoreFlow = DefaultAppStoreRestoreFlow(accountManager: subscriptionManager.accountManager, - storePurchaseManager: subscriptionManager.storePurchaseManager(), - subscriptionEndpointService: subscriptionManager.subscriptionEndpointService, - authEndpointService: subscriptionManager.authEndpointService) + let appStoreRestoreFlow = DefaultAppStoreRestoreFlow(subscriptionManager: subscriptionManager, + storePurchaseManager: subscriptionManager.storePurchaseManager()) await appStoreRestoreFlow.restoreAccountFromPastPurchase() } } diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift index aede601bb7..fd9c2497ee 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift @@ -20,14 +20,16 @@ import AppKit import Subscription import struct Combine.AnyPublisher import enum Combine.Publishers +import Networking import FeatureFlags import BrowserServicesKit +import os.log public final class PreferencesSubscriptionModel: ObservableObject { @Published var isUserAuthenticated: Bool = false @Published var subscriptionDetails: String? - @Published var subscriptionStatus: Subscription.Status? + @Published var subscriptionStatus: PrivacyProSubscription.Status = .unknown @Published var subscriptionStorefrontRegion: SubscriptionRegion = .usa @@ -44,15 +46,13 @@ public final class PreferencesSubscriptionModel: ObservableObject { let featureFlagger: FeatureFlagger - private var subscriptionPlatform: Subscription.Platform? + private var subscriptionPlatform: PrivacyProSubscription.Platform? - lazy var sheetModel = SubscriptionAccessViewModel(actionHandlers: sheetActionHandler, + lazy var sheetModel = SubscriptionAccessViewModel( + actionHandlers: sheetActionHandler, purchasePlatform: subscriptionManager.currentEnvironment.purchasePlatform) private let subscriptionManager: SubscriptionManager - private var accountManager: AccountManager { - subscriptionManager.accountManager - } private let openURLHandler: (URL) -> Void public let userEventHandler: (UserEvent) -> Void private let sheetActionHandler: SubscriptionAccessActionHandlers @@ -61,6 +61,7 @@ public final class PreferencesSubscriptionModel: ObservableObject { private var signInObserver: Any? private var signOutObserver: Any? + private var entitlementsObserver: Any? private var subscriptionChangeObserver: Any? public enum UserEvent { @@ -79,9 +80,9 @@ public final class PreferencesSubscriptionModel: ObservableObject { } lazy var statePublisher: AnyPublisher = { - let isSubscriptionActivePublisher: AnyPublisher = $subscriptionStatus.map { - guard let status = $0 else { return nil} - return status != .expired && status != .inactive + let isSubscriptionActivePublisher: AnyPublisher = $subscriptionStatus.map { + let status = $0 + return status != .expired && status != .inactive && status != .unknown }.eraseToAnyPublisher() let hasAnyEntitlementPublisher = Publishers.CombineLatest3($hasAccessToVPN, $hasAccessToDBP, $hasAccessToITR).map { @@ -92,10 +93,15 @@ public final class PreferencesSubscriptionModel: ObservableObject { .map { isUserAuthenticated, isSubscriptionActive, hasAnyEntitlement in switch (isUserAuthenticated, isSubscriptionActive, hasAnyEntitlement) { case (false, _, _): return PreferencesSubscriptionState.noSubscription - case (true, .some(false), _): return PreferencesSubscriptionState.subscriptionExpired - case (true, nil, _): return PreferencesSubscriptionState.subscriptionPendingActivation - case (true, .some(true), false): return PreferencesSubscriptionState.subscriptionPendingActivation - case (true, .some(true), true): return PreferencesSubscriptionState.subscriptionActive + case (true, false, _): + switch self.subscriptionStatus { + case .expired, .inactive: + return PreferencesSubscriptionState.subscriptionExpired + default: + return PreferencesSubscriptionState.subscriptionPendingActivation + } + case (true, true, false): return PreferencesSubscriptionState.subscriptionPendingActivation + case (true, true, true): return PreferencesSubscriptionState.subscriptionActive } } .removeDuplicates() @@ -114,29 +120,33 @@ public final class PreferencesSubscriptionModel: ObservableObject { self.featureFlagger = featureFlagger self.subscriptionStorefrontRegion = currentStorefrontRegion() - self.isUserAuthenticated = accountManager.isUserAuthenticated + self.isUserAuthenticated = subscriptionManager.isUserAuthenticated - if accountManager.isUserAuthenticated { - Task { - await self.updateSubscription(cachePolicy: .returnCacheDataElseLoad) - await self.updateAvailableSubscriptionFeatures() - await self.loadCachedEntitlements() + if self.isUserAuthenticated { + Task { [weak self] in + await self?.updateSubscription(cachePolicy: .returnCacheDataElseLoad) } - self.email = accountManager.email + self.email = subscriptionManager.userEmail } signInObserver = NotificationCenter.default.addObserver(forName: .accountDidSignIn, object: nil, queue: .main) { [weak self] _ in - self?.updateUserAuthenticatedState(true) + self?.updateUserAuthenticatedState() } signOutObserver = NotificationCenter.default.addObserver(forName: .accountDidSignOut, object: nil, queue: .main) { [weak self] _ in - self?.updateUserAuthenticatedState(false) + self?.updateUserAuthenticatedState() } subscriptionChangeObserver = NotificationCenter.default.addObserver(forName: .subscriptionDidChange, object: nil, queue: .main) { _ in Task { [weak self] in + Logger.general.debug("SubscriptionDidChange notification received") await self?.updateSubscription(cachePolicy: .returnCacheDataDontLoad) + } + } + + entitlementsObserver = NotificationCenter.default.addObserver(forName: .entitlementsDidChange, object: nil, queue: .main) { [weak self] _ in + Task { [weak self] in await self?.updateAvailableSubscriptionFeatures() } } @@ -154,6 +164,10 @@ public final class PreferencesSubscriptionModel: ObservableObject { if let subscriptionChangeObserver { NotificationCenter.default.removeObserver(subscriptionChangeObserver) } + + if let entitlementsObserver { + NotificationCenter.default.removeObserver(entitlementsObserver) + } } @MainActor @@ -166,9 +180,11 @@ public final class PreferencesSubscriptionModel: ObservableObject { } } - private func updateUserAuthenticatedState(_ isUserAuthenticated: Bool) { - self.isUserAuthenticated = isUserAuthenticated - self.email = accountManager.email + private func updateUserAuthenticatedState() { + Task { @MainActor in + isUserAuthenticated = subscriptionManager.isUserAuthenticated + email = subscriptionManager.userEmail + } } @MainActor @@ -186,12 +202,8 @@ public final class PreferencesSubscriptionModel: ObservableObject { switch subscriptionPlatform { case .apple: - if await confirmIfSignedInToSameAccount() { - return .navigateToManageSubscription { [weak self] in - self?.changePlanOrBilling(for: .appStore) - } - } else { - return .presentSheet(.apple) + return .navigateToManageSubscription { [weak self] in + self?.changePlanOrBilling(for: .appStore) } case .google: return .presentSheet(.google) @@ -211,28 +223,29 @@ public final class PreferencesSubscriptionModel: ObservableObject { NSWorkspace.shared.open(subscriptionManager.url(for: .manageSubscriptionsInAppStore)) case .stripe: Task { - guard let accessToken = accountManager.accessToken, let externalID = accountManager.externalID, - case let .success(response) = await subscriptionManager.subscriptionEndpointService.getCustomerPortalURL(accessToken: accessToken, externalID: externalID) else { return } - guard let customerPortalURL = URL(string: response.customerPortalUrl) else { return } - - openURLHandler(customerPortalURL) + do { + let customerPortalURL = try await subscriptionManager.getCustomerPortalURL() + openURLHandler(customerPortalURL) + } catch { + Logger.general.log("Error getting customer portal URL: \(error, privacy: .public)") + } } } } - private func confirmIfSignedInToSameAccount() async -> Bool { - if #available(macOS 12.0, *) { - guard let lastTransactionJWSRepresentation = await subscriptionManager.storePurchaseManager().mostRecentTransaction() else { return false } - switch await subscriptionManager.authEndpointService.storeLogin(signature: lastTransactionJWSRepresentation) { - case .success(let response): - return response.externalID == accountManager.externalID - case .failure: - return false - } - } - - return false - } +// private func confirmIfSignedInToSameAccount() async -> Bool { +// if #available(macOS 12.0, *) { +// guard let lastTransactionJWSRepresentation = await subscriptionManager.storePurchaseManager().mostRecentTransaction() else { return false } +// switch await subscriptionManager.authEndpointService.storeLogin(signature: lastTransactionJWSRepresentation) { +// case .success(let response): +// return response.externalID == accountManager.externalID +// case .failure: +// return false +// } +// } +// +// return false +// } @MainActor func openVPN() { @@ -283,10 +296,7 @@ public final class PreferencesSubscriptionModel: ObservableObject { Task { if subscriptionManager.currentEnvironment.purchasePlatform == .appStore { if #available(macOS 12.0, iOS 15.0, *) { - let appStoreAccountManagementFlow = DefaultAppStoreAccountManagementFlow(authEndpointService: subscriptionManager.authEndpointService, - storePurchaseManager: subscriptionManager.storePurchaseManager(), - accountManager: subscriptionManager.accountManager) - await appStoreAccountManagementFlow.refreshAuthTokenIfNeeded() + try await subscriptionManager.getTokenContainer(policy: .localValid) } } @@ -300,7 +310,9 @@ public final class PreferencesSubscriptionModel: ObservableObject { @MainActor func removeFromThisDeviceAction() { userEventHandler(.removeSubscriptionClick) - accountManager.signOut() + Task { + await subscriptionManager.signOut(notifyUI: true) + } } @MainActor @@ -323,10 +335,8 @@ public final class PreferencesSubscriptionModel: ObservableObject { if subscriptionManager.currentEnvironment.purchasePlatform == .appStore { if #available(macOS 12.0, *) { Task { - let appStoreRestoreFlow = DefaultAppStoreRestoreFlow(accountManager: subscriptionManager.accountManager, - storePurchaseManager: subscriptionManager.storePurchaseManager(), - subscriptionEndpointService: subscriptionManager.subscriptionEndpointService, - authEndpointService: subscriptionManager.authEndpointService) + let appStoreRestoreFlow = DefaultAppStoreRestoreFlow(subscriptionManager: subscriptionManager, + storePurchaseManager: subscriptionManager.storePurchaseManager()) await appStoreRestoreFlow.restoreAccountFromPastPurchase() fetchAndUpdateSubscriptionDetails() } @@ -338,7 +348,7 @@ public final class PreferencesSubscriptionModel: ObservableObject { @MainActor private func fetchAndUpdateSubscriptionDetails() { - self.isUserAuthenticated = accountManager.isUserAuthenticated + updateUserAuthenticatedState() guard fetchSubscriptionDetailsTask == nil else { return } @@ -346,8 +356,7 @@ public final class PreferencesSubscriptionModel: ObservableObject { defer { self?.fetchSubscriptionDetailsTask = nil } - - await self?.fetchEmailAndRemoteEntitlements() + await self?.fetchEmail() await self?.updateSubscription(cachePolicy: .reloadIgnoringLocalCacheData) } } @@ -367,94 +376,54 @@ public final class PreferencesSubscriptionModel: ObservableObject { return region ?? .usa } - @MainActor private func updateAvailableSubscriptionFeatures() async { - let features = await currentSubscriptionFeatures() - - shouldShowVPN = features.contains(.networkProtection) - shouldShowDBP = features.contains(.dataBrokerProtection) - shouldShowITR = features.contains(.identityTheftRestoration) || features.contains(.identityTheftRestorationGlobal) - } - - private func currentSubscriptionFeatures() async -> [Entitlement.ProductName] { - if subscriptionManager.currentEnvironment.purchasePlatform == .appStore { - return await subscriptionManager.currentSubscriptionFeatures() - } else { - return [.networkProtection, .dataBrokerProtection, .identityTheftRestoration] + let features = await subscriptionManager.currentSubscriptionFeatures(forceRefresh: false) + let vpnFeature = features.first { $0.entitlement == .networkProtection } + let dbpFeature = features.first { $0.entitlement == .dataBrokerProtection } + let itrFeature = features.first { $0.entitlement == .identityTheftRestoration } + let itrgFeature = features.first { $0.entitlement == .identityTheftRestorationGlobal } + + Task { @MainActor in + // Should show + shouldShowVPN = vpnFeature != nil + shouldShowDBP = dbpFeature != nil + shouldShowITR = itrFeature != nil || itrgFeature != nil + + // is active/enabled + hasAccessToVPN = vpnFeature?.availableForUser ?? false + hasAccessToDBP = dbpFeature?.availableForUser ?? false + hasAccessToITR = itrFeature?.availableForUser ?? false || itrgFeature?.availableForUser ?? false } } - @MainActor - private func loadCachedEntitlements() async { - switch await self.accountManager.hasEntitlement(forProductName: .networkProtection, cachePolicy: .returnCacheDataDontLoad) { - case let .success(result): - hasAccessToVPN = result - case .failure: - hasAccessToVPN = false - } - - switch await self.accountManager.hasEntitlement(forProductName: .dataBrokerProtection, cachePolicy: .returnCacheDataDontLoad) { - case let .success(result): - hasAccessToDBP = result - case .failure: - hasAccessToDBP = false - } - - var hasITR = false - switch await self.accountManager.hasEntitlement(forProductName: .identityTheftRestoration, cachePolicy: .returnCacheDataDontLoad) { - case let .success(result): - hasITR = result - case .failure: - hasITR = false - } - - var hasITRGlobal = false - switch await self.accountManager.hasEntitlement(forProductName: .identityTheftRestorationGlobal, cachePolicy: .returnCacheDataDontLoad) { - case let .success(result): - hasITRGlobal = result - case .failure: - hasITRGlobal = false - } - - hasAccessToITR = hasITR || hasITRGlobal + @MainActor func fetchEmail() async { + let tokenContainer = try? await subscriptionManager.getTokenContainer(policy: .local) + email = tokenContainer?.decodedAccessToken.email } - @MainActor func fetchEmailAndRemoteEntitlements() async { - guard let accessToken = accountManager.accessToken else { return } + private func updateSubscription(cachePolicy: SubscriptionCachePolicy) async { + updateUserAuthenticatedState() - if case let .success(response) = await subscriptionManager.authEndpointService.validateToken(accessToken: accessToken) { - if accountManager.email != response.account.email { - email = response.account.email - accountManager.storeAccount(token: accessToken, email: response.account.email, externalID: response.account.externalID) + if isUserAuthenticated { + do { + let subscription = try await subscriptionManager.getSubscription(cachePolicy: cachePolicy) + Task { @MainActor in + updateDescription(for: subscription.expiresOrRenewsAt, status: subscription.status, period: subscription.billingPeriod) + subscriptionPlatform = subscription.platform + subscriptionStatus = subscription.status + } + } catch { + Task { @MainActor in + subscriptionPlatform = .unknown + subscriptionStatus = .unknown + } } - - let entitlements = response.account.entitlements.compactMap { $0.product } - hasAccessToVPN = entitlements.contains(.networkProtection) - hasAccessToDBP = entitlements.contains(.dataBrokerProtection) - hasAccessToITR = entitlements.contains(.identityTheftRestoration) || entitlements.contains(.identityTheftRestorationGlobal) - accountManager.updateCache(with: response.account.entitlements) - } - } - - @MainActor - private func updateSubscription(cachePolicy: APICachePolicy) async { - guard let token = accountManager.accessToken else { - subscriptionManager.subscriptionEndpointService.signOut() - return - } - - switch await subscriptionManager.subscriptionEndpointService.getSubscription(accessToken: token, cachePolicy: cachePolicy) { - case .success(let subscription): - updateDescription(for: subscription.expiresOrRenewsAt, status: subscription.status, period: subscription.billingPeriod) - subscriptionPlatform = subscription.platform - subscriptionStatus = subscription.status - case .failure: - break + await self.updateAvailableSubscriptionFeatures() } } @MainActor - func updateDescription(for date: Date, status: Subscription.Status, period: Subscription.BillingPeriod) { + func updateDescription(for date: Date, status: PrivacyProSubscription.Status, period: PrivacyProSubscription.BillingPeriod) { let formattedDate = dateFormatter.string(from: date) switch status { @@ -470,8 +439,11 @@ public final class PreferencesSubscriptionModel: ObservableObject { private var dateFormatter = { let dateFormatter = DateFormatter() dateFormatter.dateStyle = .long +#if DEBUG + dateFormatter.timeStyle = .medium +#else dateFormatter.timeStyle = .none - +#endif return dateFormatter }() } diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift index f28b2dc163..4c3f1c4210 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift @@ -55,7 +55,7 @@ enum UserText { static let preferencesSubscriptionFeedbackButton = NSLocalizedString("subscription.preferences.feedback.button", bundle: Bundle.module, value: "Send Feedback", comment: "Title for the subscription feedback button") static let preferencesPrivacyPolicyButton = NSLocalizedString("subscription.preferences.privacypolicy.button", bundle: Bundle.module, value: "Privacy Policy and Terms of Service", comment: "Title for the privacy policy button") - static func preferencesSubscriptionRenewingCaption(billingPeriod: Subscription.BillingPeriod, formattedDate: String) -> String { + static func preferencesSubscriptionRenewingCaption(billingPeriod: PrivacyProSubscription.BillingPeriod, formattedDate: String) -> String { let localized: String switch billingPeriod { @@ -79,7 +79,7 @@ enum UserText { return String(format: localized, formattedDate) } - static func preferencesSubscriptionExpiringCaption(billingPeriod: Subscription.BillingPeriod, formattedDate: String) -> String { + static func preferencesSubscriptionExpiringCaption(billingPeriod: PrivacyProSubscription.BillingPeriod, formattedDate: String) -> String { let localized: String switch billingPeriod { diff --git a/UnitTests/App/AppStateChangePublisherTests.swift b/UITests/AppStateChangePublisherTests.swift similarity index 100% rename from UnitTests/App/AppStateChangePublisherTests.swift rename to UITests/AppStateChangePublisherTests.swift diff --git a/UnitTests/Onboarding/ContextualOnboarding/BrowserTabViewControllerOnboardingTests.swift b/UITests/BrowserTabViewControllerOnboardingTests.swift similarity index 100% rename from UnitTests/Onboarding/ContextualOnboarding/BrowserTabViewControllerOnboardingTests.swift rename to UITests/BrowserTabViewControllerOnboardingTests.swift diff --git a/UnitTests/App/WindowManagerStateRestorationTests.swift b/UITests/WindowManagerStateRestorationTests.swift similarity index 100% rename from UnitTests/App/WindowManagerStateRestorationTests.swift rename to UITests/WindowManagerStateRestorationTests.swift diff --git a/UnitTests/BookmarksBar/BookmarksBarViewControllerTests.swift b/UnitTests/BookmarksBar/ViewModel/BookmarksBarViewControllerTests.swift similarity index 100% rename from UnitTests/BookmarksBar/BookmarksBarViewControllerTests.swift rename to UnitTests/BookmarksBar/ViewModel/BookmarksBarViewControllerTests.swift diff --git a/UnitTests/DBP/Mocks/DataBrokerProtectionMocks.swift b/UnitTests/DBP/Mocks/DataBrokerProtectionMocks.swift deleted file mode 100644 index bbcd473a1f..0000000000 --- a/UnitTests/DBP/Mocks/DataBrokerProtectionMocks.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// DataBrokerProtectionMocks.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 Subscription -@testable import DuckDuckGo_Privacy_Browser - -final class MockAccountManager: AccountManager { - var hasEntitlementResult: Result = .success(true) - - var delegate: AccountManagerKeychainAccessDelegate? - - var isUserAuthenticated = false - - var accessToken: String? = "" - - var authToken: String? - - var email: String? - - var externalID: String? - - func storeAuthToken(token: String) { - } - - func storeAccount(token: String, email: String?, externalID: String?) { - } - - func signOut(skipNotification: Bool) { - } - - func signOut() { - } - - func migrateAccessTokenToNewStore() throws { - } - - func hasEntitlement(forProductName productName: Entitlement.ProductName, cachePolicy: APICachePolicy) async -> Result { - hasEntitlementResult - } - - func hasEntitlement(forProductName productName: Entitlement.ProductName) async -> Result { - hasEntitlementResult - } - - func updateCache(with entitlements: [Entitlement]) { - } - - func fetchEntitlements(cachePolicy: APICachePolicy) async -> Result<[Entitlement], any Error> { - .success([]) - } - - func exchangeAuthTokenToAccessToken(_ authToken: String) async -> Result { - .success("") - } - - func fetchAccountDetails(with accessToken: String) async -> Result { - .success(AccountDetails(email: "", externalID: "")) - } - - func checkForEntitlements(wait waitTime: Double, retry retryCount: Int) async -> Bool { - true - } -} diff --git a/UnitTests/DBP/Tests/DataBrokerProtectionFeatureGatekeeperTests.swift b/UnitTests/DBP/Tests/DataBrokerProtectionFeatureGatekeeperTests.swift index 8e5d2ac1b3..9704df0f1a 100644 --- a/UnitTests/DBP/Tests/DataBrokerProtectionFeatureGatekeeperTests.swift +++ b/UnitTests/DBP/Tests/DataBrokerProtectionFeatureGatekeeperTests.swift @@ -17,8 +17,11 @@ // import XCTest -import BrowserServicesKit -import Subscription +@testable import BrowserServicesKit +@testable import Subscription +import Networking +import NetworkingTestingUtils +import SubscriptionTestingUtilities @testable import DuckDuckGo_Privacy_Browser @@ -27,7 +30,7 @@ final class DataBrokerProtectionFeatureGatekeeperTests: XCTestCase { private var sut: DefaultDataBrokerProtectionFeatureGatekeeper! private var mockFeatureDisabler: MockFeatureDisabler! private var mockFeatureAvailability: MockFeatureAvailability! - private var mockAccountManager: MockAccountManager! + private var mockSubscriptionManager: SubscriptionManagerMock! private var mockFreemiumDBPUserStateManager: MockFreemiumDBPUserStateManager! private func userDefaults() -> UserDefaults { @@ -37,19 +40,17 @@ final class DataBrokerProtectionFeatureGatekeeperTests: XCTestCase { override func setUpWithError() throws { mockFeatureDisabler = MockFeatureDisabler() mockFeatureAvailability = MockFeatureAvailability() - mockAccountManager = MockAccountManager() + mockSubscriptionManager = SubscriptionManagerMock() mockFreemiumDBPUserStateManager = MockFreemiumDBPUserStateManager() mockFreemiumDBPUserStateManager.didActivate = false } func testWhenNoAccessTokenIsFound_butEntitlementIs_andIsNotActiveFreemiumUser_thenFeatureIsDisabled() async { // Given - mockAccountManager.accessToken = nil - mockAccountManager.hasEntitlementResult = .success(true) sut = DefaultDataBrokerProtectionFeatureGatekeeper(featureDisabler: mockFeatureDisabler, userDefaults: userDefaults(), subscriptionAvailability: mockFeatureAvailability, - accountManager: mockAccountManager, + subscriptionManager: mockSubscriptionManager, freemiumDBPUserStateManager: mockFreemiumDBPUserStateManager) // When @@ -61,13 +62,12 @@ final class DataBrokerProtectionFeatureGatekeeperTests: XCTestCase { func testWhenAccessTokenIsFound_butNoEntitlementIs_andIsNotActiveFreemiumUser_thenFeatureIsDisabled() async { // Given - mockAccountManager.accessToken = "token" - mockAccountManager.hasEntitlementResult = .failure(MockError.someError) + mockSubscriptionManager.resultTokenContainer = OAuthTokensFactory.makeValidTokenContainer() mockFreemiumDBPUserStateManager.didActivate = false sut = DefaultDataBrokerProtectionFeatureGatekeeper(featureDisabler: mockFeatureDisabler, userDefaults: userDefaults(), subscriptionAvailability: mockFeatureAvailability, - accountManager: mockAccountManager, + subscriptionManager: mockSubscriptionManager, freemiumDBPUserStateManager: mockFreemiumDBPUserStateManager) // When @@ -79,13 +79,12 @@ final class DataBrokerProtectionFeatureGatekeeperTests: XCTestCase { func testWhenAccessTokenIsFound_butNoEntitlementIs_andIsActiveFreemiumUser_thenFeatureIsDisabled() async { // Given - mockAccountManager.accessToken = "token" - mockAccountManager.hasEntitlementResult = .failure(MockError.someError) + mockSubscriptionManager.resultTokenContainer = OAuthTokensFactory.makeValidTokenContainer() mockFreemiumDBPUserStateManager.didActivate = true sut = DefaultDataBrokerProtectionFeatureGatekeeper(featureDisabler: mockFeatureDisabler, userDefaults: userDefaults(), subscriptionAvailability: mockFeatureAvailability, - accountManager: mockAccountManager, + subscriptionManager: mockSubscriptionManager, freemiumDBPUserStateManager: mockFreemiumDBPUserStateManager) // When @@ -97,13 +96,11 @@ final class DataBrokerProtectionFeatureGatekeeperTests: XCTestCase { func testWhenAccessTokenAndEntitlementAreNotFound_andIsNotActiveFreemiumUser_thenFeatureIsDisabled() async { // Given - mockAccountManager.accessToken = nil - mockAccountManager.hasEntitlementResult = .failure(MockError.someError) mockFreemiumDBPUserStateManager.didActivate = false sut = DefaultDataBrokerProtectionFeatureGatekeeper(featureDisabler: mockFeatureDisabler, userDefaults: userDefaults(), subscriptionAvailability: mockFeatureAvailability, - accountManager: mockAccountManager, + subscriptionManager: mockSubscriptionManager, freemiumDBPUserStateManager: mockFreemiumDBPUserStateManager) // When @@ -115,13 +112,13 @@ final class DataBrokerProtectionFeatureGatekeeperTests: XCTestCase { func testWhenAccessTokenAndEntitlementAreFound_andIsNotActiveFreemiumUser_thenFeatureIsEnabled() async { // Given - mockAccountManager.accessToken = "token" - mockAccountManager.hasEntitlementResult = .success(true) + mockSubscriptionManager.resultTokenContainer = OAuthTokensFactory.makeValidTokenContainerWithEntitlements() + mockSubscriptionManager.resultFeatures = [ SubscriptionFeature(entitlement: .dataBrokerProtection, availableForUser: true) ] mockFreemiumDBPUserStateManager.didActivate = false sut = DefaultDataBrokerProtectionFeatureGatekeeper(featureDisabler: mockFeatureDisabler, userDefaults: userDefaults(), subscriptionAvailability: mockFeatureAvailability, - accountManager: mockAccountManager, + subscriptionManager: mockSubscriptionManager, freemiumDBPUserStateManager: mockFreemiumDBPUserStateManager) // When @@ -133,13 +130,11 @@ final class DataBrokerProtectionFeatureGatekeeperTests: XCTestCase { func testWhenAccessTokenAndEntitlementAreNotFound_andIsActiveFreemiumUser_thenFeatureIsEnabled() async { // Given - mockAccountManager.accessToken = nil - mockAccountManager.hasEntitlementResult = .failure(MockError.someError) mockFreemiumDBPUserStateManager.didActivate = true sut = DefaultDataBrokerProtectionFeatureGatekeeper(featureDisabler: mockFeatureDisabler, userDefaults: userDefaults(), subscriptionAvailability: mockFeatureAvailability, - accountManager: mockAccountManager, + subscriptionManager: mockSubscriptionManager, freemiumDBPUserStateManager: mockFreemiumDBPUserStateManager) // When diff --git a/UnitTests/Freemium/DBP/Experiment/FreemiumDBPPixelExperimentManagingTests.swift b/UnitTests/Freemium/DBP/Experiment/FreemiumDBPPixelExperimentManagingTests.swift index 00d1583b3e..dc2cd81837 100644 --- a/UnitTests/Freemium/DBP/Experiment/FreemiumDBPPixelExperimentManagingTests.swift +++ b/UnitTests/Freemium/DBP/Experiment/FreemiumDBPPixelExperimentManagingTests.swift @@ -20,32 +20,22 @@ import XCTest import SubscriptionTestingUtilities import Subscription @testable import DuckDuckGo_Privacy_Browser +import Networking +import NetworkingTestingUtils final class FreemiumDBPPixelExperimentManagingTests: XCTestCase { private var sut: FreemiumDBPPixelExperimentManaging! - private var mockAccountManager: MockAccountManager! private var mockSubscriptionManager: SubscriptionManagerMock! private var mockUserDefaults: MockUserDefaults! override func setUp() { super.setUp() - mockAccountManager = MockAccountManager() - let mockSubscriptionService = SubscriptionEndpointServiceMock() - let mockAuthService = AuthEndpointServiceMock() - let mockStorePurchaseManager = StorePurchaseManagerMock() - let mockSubscriptionFeatureMappingCache = SubscriptionFeatureMappingCacheMock() - + mockSubscriptionManager = SubscriptionManagerMock() let currentEnvironment = SubscriptionEnvironment(serviceEnvironment: .production, purchasePlatform: .appStore) - - mockSubscriptionManager = SubscriptionManagerMock(accountManager: mockAccountManager, - subscriptionEndpointService: mockSubscriptionService, - authEndpointService: mockAuthService, - storePurchaseManager: mockStorePurchaseManager, - currentEnvironment: currentEnvironment, - canPurchase: false, - subscriptionFeatureMappingCache: mockSubscriptionFeatureMappingCache) + mockSubscriptionManager.currentEnvironment = currentEnvironment + mockSubscriptionManager = SubscriptionManagerMock() mockUserDefaults = MockUserDefaults() let testLocale = Locale(identifier: "en_US") sut = FreemiumDBPPixelExperimentManager(subscriptionManager: mockSubscriptionManager, userDefaults: mockUserDefaults, locale: testLocale) @@ -63,7 +53,6 @@ final class FreemiumDBPPixelExperimentManagingTests: XCTestCase { func testAssignUserToCohort_whenUserEligibleAndNotEnrolled_assignsToCohort() { // Given mockSubscriptionManager.canPurchase = true - mockAccountManager.accessToken = nil mockUserDefaults.removeObject(forKey: MockUserDefaults.Keys.experimentCohort) mockUserDefaults.removeObject(forKey: MockUserDefaults.Keys.enrollmentDate) @@ -87,7 +76,6 @@ final class FreemiumDBPPixelExperimentManagingTests: XCTestCase { mockUserDefaults.set(existingCohort.rawValue, forKey: MockUserDefaults.Keys.experimentCohort) mockUserDefaults.set(existingDate, forKey: MockUserDefaults.Keys.enrollmentDate) mockSubscriptionManager.canPurchase = true - mockAccountManager.accessToken = nil // When sut.assignUserToCohort() @@ -104,7 +92,7 @@ final class FreemiumDBPPixelExperimentManagingTests: XCTestCase { func testAssignUserToCohort_whenUserNotEligible_dueToSubscription_doesNotAssign() { // Given mockSubscriptionManager.canPurchase = false - mockAccountManager.accessToken = "some_token" + mockSubscriptionManager.resultTokenContainer = OAuthTokensFactory.makeValidTokenContainerWithEntitlements() mockUserDefaults.removeObject(forKey: MockUserDefaults.Keys.experimentCohort) mockUserDefaults.removeObject(forKey: MockUserDefaults.Keys.enrollmentDate) @@ -124,7 +112,6 @@ final class FreemiumDBPPixelExperimentManagingTests: XCTestCase { let nonUSLocale = Locale(identifier: "en_GB") sut = FreemiumDBPPixelExperimentManager(subscriptionManager: mockSubscriptionManager, userDefaults: mockUserDefaults, locale: nonUSLocale) mockSubscriptionManager.canPurchase = true - mockAccountManager.accessToken = nil mockUserDefaults.removeObject(forKey: MockUserDefaults.Keys.experimentCohort) mockUserDefaults.removeObject(forKey: MockUserDefaults.Keys.enrollmentDate) diff --git a/UnitTests/Freemium/DBP/FreemiumDBPFeatureTests.swift b/UnitTests/Freemium/DBP/FreemiumDBPFeatureTests.swift index e8b84142ba..03a27896e5 100644 --- a/UnitTests/Freemium/DBP/FreemiumDBPFeatureTests.swift +++ b/UnitTests/Freemium/DBP/FreemiumDBPFeatureTests.swift @@ -23,13 +23,14 @@ import BrowserServicesKit import SubscriptionTestingUtilities import Freemium import Combine +import Networking +import NetworkingTestingUtils final class FreemiumDBPFeatureTests: XCTestCase { private var sut: FreemiumDBPFeature! private var mockPrivacyConfigurationManager: MockPrivacyConfigurationManaging! private var mockFreemiumDBPExperimentManager: MockFreemiumDBPExperimentManager! - private var mockAccountManager: MockAccountManager! private var mockSubscriptionManager: SubscriptionManagerMock! private var mockFreemiumDBPUserStateManagerManager: MockFreemiumDBPUserStateManager! private var mockFeatureDisabler: MockFeatureDisabler! @@ -40,37 +41,26 @@ final class FreemiumDBPFeatureTests: XCTestCase { mockPrivacyConfigurationManager = MockPrivacyConfigurationManaging() mockFreemiumDBPExperimentManager = MockFreemiumDBPExperimentManager() - mockAccountManager = MockAccountManager() let mockSubscriptionService = SubscriptionEndpointServiceMock() - let mockAuthService = AuthEndpointServiceMock() let mockStorePurchaseManager = StorePurchaseManagerMock() let mockSubscriptionFeatureMappingCache = SubscriptionFeatureMappingCacheMock() let currentEnvironment = SubscriptionEnvironment(serviceEnvironment: .production, purchasePlatform: .appStore) - mockSubscriptionManager = SubscriptionManagerMock(accountManager: mockAccountManager, - subscriptionEndpointService: mockSubscriptionService, - authEndpointService: mockAuthService, - storePurchaseManager: mockStorePurchaseManager, - currentEnvironment: currentEnvironment, - canPurchase: false, - subscriptionFeatureMappingCache: mockSubscriptionFeatureMappingCache) - + mockSubscriptionManager = SubscriptionManagerMock() + mockSubscriptionManager.currentEnvironment = currentEnvironment mockFreemiumDBPUserStateManagerManager = MockFreemiumDBPUserStateManager() mockFeatureDisabler = MockFeatureDisabler() - } func testWhenFeatureFlagDisabled_thenFreemiumDBPIsNotAvailable() throws { // Given mockPrivacyConfigurationManager.mockConfig.isSubfeatureKeyEnabled = { _, _ in false } mockSubscriptionManager.canPurchase = true - mockAccountManager.accessToken = nil sut = DefaultFreemiumDBPFeature(privacyConfigurationManager: mockPrivacyConfigurationManager, experimentManager: mockFreemiumDBPExperimentManager, subscriptionManager: mockSubscriptionManager, - accountManager: mockAccountManager, freemiumDBPUserStateManager: mockFreemiumDBPUserStateManagerManager, featureDisabler: mockFeatureDisabler) // When @@ -84,12 +74,10 @@ final class FreemiumDBPFeatureTests: XCTestCase { // Given mockPrivacyConfigurationManager.mockConfig.isSubfeatureKeyEnabled = { _, _ in true } mockSubscriptionManager.canPurchase = true - mockAccountManager.accessToken = nil mockFreemiumDBPExperimentManager.isTreatment = true sut = DefaultFreemiumDBPFeature(privacyConfigurationManager: mockPrivacyConfigurationManager, experimentManager: mockFreemiumDBPExperimentManager, subscriptionManager: mockSubscriptionManager, - accountManager: mockAccountManager, freemiumDBPUserStateManager: mockFreemiumDBPUserStateManagerManager, featureDisabler: mockFeatureDisabler) // When @@ -103,11 +91,9 @@ final class FreemiumDBPFeatureTests: XCTestCase { // Given mockPrivacyConfigurationManager.mockConfig.isSubfeatureKeyEnabled = { _, _ in true } mockSubscriptionManager.canPurchase = false - mockAccountManager.accessToken = nil sut = DefaultFreemiumDBPFeature(privacyConfigurationManager: mockPrivacyConfigurationManager, experimentManager: mockFreemiumDBPExperimentManager, subscriptionManager: mockSubscriptionManager, - accountManager: mockAccountManager, freemiumDBPUserStateManager: mockFreemiumDBPUserStateManagerManager, featureDisabler: mockFeatureDisabler) // When @@ -121,11 +107,10 @@ final class FreemiumDBPFeatureTests: XCTestCase { // Given mockPrivacyConfigurationManager.mockConfig.isSubfeatureKeyEnabled = { _, _ in false } mockSubscriptionManager.canPurchase = false - mockAccountManager.accessToken = "some_token" + mockSubscriptionManager.resultTokenContainer = OAuthTokensFactory.makeValidTokenContainerWithEntitlements() sut = DefaultFreemiumDBPFeature(privacyConfigurationManager: mockPrivacyConfigurationManager, experimentManager: mockFreemiumDBPExperimentManager, subscriptionManager: mockSubscriptionManager, - accountManager: mockAccountManager, freemiumDBPUserStateManager: mockFreemiumDBPUserStateManagerManager, featureDisabler: mockFeatureDisabler) // When @@ -139,11 +124,10 @@ final class FreemiumDBPFeatureTests: XCTestCase { // Given mockPrivacyConfigurationManager.mockConfig.isSubfeatureKeyEnabled = { _, _ in true } mockSubscriptionManager.canPurchase = true - mockAccountManager.accessToken = "some_token" + mockSubscriptionManager.resultTokenContainer = OAuthTokensFactory.makeValidTokenContainerWithEntitlements() sut = DefaultFreemiumDBPFeature(privacyConfigurationManager: mockPrivacyConfigurationManager, experimentManager: mockFreemiumDBPExperimentManager, subscriptionManager: mockSubscriptionManager, - accountManager: mockAccountManager, freemiumDBPUserStateManager: mockFreemiumDBPUserStateManagerManager, featureDisabler: mockFeatureDisabler) // When @@ -157,12 +141,10 @@ final class FreemiumDBPFeatureTests: XCTestCase { // Given mockPrivacyConfigurationManager.mockConfig.isSubfeatureKeyEnabled = { _, _ in true } mockSubscriptionManager.canPurchase = true - mockAccountManager.accessToken = nil mockFreemiumDBPExperimentManager.isTreatment = true sut = DefaultFreemiumDBPFeature(privacyConfigurationManager: mockPrivacyConfigurationManager, experimentManager: mockFreemiumDBPExperimentManager, subscriptionManager: mockSubscriptionManager, - accountManager: mockAccountManager, freemiumDBPUserStateManager: mockFreemiumDBPUserStateManagerManager, featureDisabler: mockFeatureDisabler) // When @@ -176,12 +158,10 @@ final class FreemiumDBPFeatureTests: XCTestCase { // Given mockPrivacyConfigurationManager.mockConfig.isSubfeatureKeyEnabled = { _, _ in true } mockSubscriptionManager.canPurchase = true - mockAccountManager.accessToken = nil mockFreemiumDBPExperimentManager.isTreatment = false sut = DefaultFreemiumDBPFeature(privacyConfigurationManager: mockPrivacyConfigurationManager, experimentManager: mockFreemiumDBPExperimentManager, subscriptionManager: mockSubscriptionManager, - accountManager: mockAccountManager, freemiumDBPUserStateManager: mockFreemiumDBPUserStateManagerManager, featureDisabler: mockFeatureDisabler) // When @@ -196,13 +176,11 @@ final class FreemiumDBPFeatureTests: XCTestCase { mockFreemiumDBPUserStateManagerManager.didActivate = false mockPrivacyConfigurationManager.mockConfig.isSubfeatureKeyEnabled = { _, _ in false } mockSubscriptionManager.canPurchase = true - mockAccountManager.accessToken = nil // When sut = DefaultFreemiumDBPFeature(privacyConfigurationManager: mockPrivacyConfigurationManager, experimentManager: mockFreemiumDBPExperimentManager, subscriptionManager: mockSubscriptionManager, - accountManager: mockAccountManager, freemiumDBPUserStateManager: mockFreemiumDBPUserStateManagerManager, featureDisabler: mockFeatureDisabler) @@ -215,12 +193,10 @@ final class FreemiumDBPFeatureTests: XCTestCase { mockFreemiumDBPUserStateManagerManager.didActivate = true mockPrivacyConfigurationManager.mockConfig.isSubfeatureKeyEnabled = { _, _ in false } mockSubscriptionManager.canPurchase = true - mockAccountManager.accessToken = nil sut = DefaultFreemiumDBPFeature(privacyConfigurationManager: mockPrivacyConfigurationManager, experimentManager: mockFreemiumDBPExperimentManager, subscriptionManager: mockSubscriptionManager, - accountManager: mockAccountManager, freemiumDBPUserStateManager: mockFreemiumDBPUserStateManagerManager, featureDisabler: mockFeatureDisabler) @@ -238,12 +214,11 @@ final class FreemiumDBPFeatureTests: XCTestCase { mockFreemiumDBPUserStateManagerManager.didActivate = true mockPrivacyConfigurationManager.mockConfig.isSubfeatureKeyEnabled = { _, _ in false } mockSubscriptionManager.canPurchase = true - mockAccountManager.accessToken = "some_token" + mockSubscriptionManager.resultTokenContainer = OAuthTokensFactory.makeValidTokenContainerWithEntitlements() sut = DefaultFreemiumDBPFeature(privacyConfigurationManager: mockPrivacyConfigurationManager, experimentManager: mockFreemiumDBPExperimentManager, subscriptionManager: mockSubscriptionManager, - accountManager: mockAccountManager, freemiumDBPUserStateManager: mockFreemiumDBPUserStateManagerManager, featureDisabler: mockFeatureDisabler) @@ -261,13 +236,11 @@ final class FreemiumDBPFeatureTests: XCTestCase { mockFreemiumDBPUserStateManagerManager.didActivate = true mockPrivacyConfigurationManager.mockConfig.isSubfeatureKeyEnabled = { _, _ in true } mockSubscriptionManager.canPurchase = true - mockAccountManager.accessToken = nil // When sut = DefaultFreemiumDBPFeature(privacyConfigurationManager: mockPrivacyConfigurationManager, experimentManager: mockFreemiumDBPExperimentManager, subscriptionManager: mockSubscriptionManager, - accountManager: mockAccountManager, freemiumDBPUserStateManager: mockFreemiumDBPUserStateManagerManager, featureDisabler: mockFeatureDisabler) @@ -281,13 +254,11 @@ final class FreemiumDBPFeatureTests: XCTestCase { mockFreemiumDBPUserStateManagerManager.didActivate = true mockPrivacyConfigurationManager.mockConfig.isSubfeatureKeyEnabled = { _, _ in true } mockSubscriptionManager.canPurchase = false - mockAccountManager.accessToken = nil // When sut = DefaultFreemiumDBPFeature(privacyConfigurationManager: mockPrivacyConfigurationManager, experimentManager: mockFreemiumDBPExperimentManager, subscriptionManager: mockSubscriptionManager, - accountManager: mockAccountManager, freemiumDBPUserStateManager: mockFreemiumDBPUserStateManagerManager, featureDisabler: mockFeatureDisabler) @@ -301,14 +272,12 @@ final class FreemiumDBPFeatureTests: XCTestCase { mockFreemiumDBPUserStateManagerManager.didActivate = false mockPrivacyConfigurationManager.mockConfig.isSubfeatureKeyEnabled = { _, _ in false } mockSubscriptionManager.canPurchase = true - mockAccountManager.accessToken = nil mockFreemiumDBPExperimentManager.isTreatment = true let expectation = XCTestExpectation(description: "isAvailablePublisher emits values") sut = DefaultFreemiumDBPFeature(privacyConfigurationManager: mockPrivacyConfigurationManager, experimentManager: mockFreemiumDBPExperimentManager, subscriptionManager: mockSubscriptionManager, - accountManager: mockAccountManager, freemiumDBPUserStateManager: mockFreemiumDBPUserStateManagerManager, featureDisabler: mockFeatureDisabler) @@ -337,14 +306,12 @@ final class FreemiumDBPFeatureTests: XCTestCase { mockFreemiumDBPUserStateManagerManager.didActivate = true mockPrivacyConfigurationManager.mockConfig.isSubfeatureKeyEnabled = { _, _ in true } mockSubscriptionManager.canPurchase = true - mockAccountManager.accessToken = nil mockFreemiumDBPExperimentManager.isTreatment = true let expectation = XCTestExpectation(description: "isAvailablePublisher emits values") sut = DefaultFreemiumDBPFeature(privacyConfigurationManager: mockPrivacyConfigurationManager, experimentManager: mockFreemiumDBPExperimentManager, subscriptionManager: mockSubscriptionManager, - accountManager: mockAccountManager, freemiumDBPUserStateManager: mockFreemiumDBPUserStateManagerManager, featureDisabler: mockFeatureDisabler) @@ -373,14 +340,12 @@ final class FreemiumDBPFeatureTests: XCTestCase { mockFreemiumDBPUserStateManagerManager.didActivate = true mockPrivacyConfigurationManager.mockConfig.isSubfeatureKeyEnabled = { _, _ in true } mockSubscriptionManager.canPurchase = true - mockAccountManager.accessToken = nil mockFreemiumDBPExperimentManager.isTreatment = true let expectation = XCTestExpectation(description: "isAvailablePublisher emits values") sut = DefaultFreemiumDBPFeature(privacyConfigurationManager: mockPrivacyConfigurationManager, experimentManager: mockFreemiumDBPExperimentManager, subscriptionManager: mockSubscriptionManager, - accountManager: mockAccountManager, freemiumDBPUserStateManager: mockFreemiumDBPUserStateManagerManager, featureDisabler: mockFeatureDisabler) @@ -396,7 +361,7 @@ final class FreemiumDBPFeatureTests: XCTestCase { // When sut.subscribeToDependencyUpdates() - mockAccountManager.accessToken = "some_token" + mockSubscriptionManager.resultTokenContainer = OAuthTokensFactory.makeValidTokenContainerWithEntitlements() NotificationCenter.default.post(name: .subscriptionDidChange, object: nil) // Then @@ -409,14 +374,13 @@ final class FreemiumDBPFeatureTests: XCTestCase { mockFreemiumDBPUserStateManagerManager.didActivate = true mockPrivacyConfigurationManager.mockConfig.isSubfeatureKeyEnabled = { _, _ in true } mockSubscriptionManager.canPurchase = true - mockAccountManager.accessToken = "some_token" + mockSubscriptionManager.resultTokenContainer = OAuthTokensFactory.makeValidTokenContainerWithEntitlements() mockFreemiumDBPExperimentManager.isTreatment = true let expectation = XCTestExpectation(description: "isAvailablePublisher emits values") sut = DefaultFreemiumDBPFeature(privacyConfigurationManager: mockPrivacyConfigurationManager, experimentManager: mockFreemiumDBPExperimentManager, subscriptionManager: mockSubscriptionManager, - accountManager: mockAccountManager, freemiumDBPUserStateManager: mockFreemiumDBPUserStateManagerManager, featureDisabler: mockFeatureDisabler) @@ -432,7 +396,7 @@ final class FreemiumDBPFeatureTests: XCTestCase { // When sut.subscribeToDependencyUpdates() - mockAccountManager.accessToken = nil + mockSubscriptionManager.resultTokenContainer = nil NotificationCenter.default.post(name: .subscriptionDidChange, object: nil) // Then diff --git a/UnitTests/Freemium/DBP/FreemiumDBPFirstProfileSavedNotifierTests.swift b/UnitTests/Freemium/DBP/FreemiumDBPFirstProfileSavedNotifierTests.swift index 1056485bb2..74077e12f3 100644 --- a/UnitTests/Freemium/DBP/FreemiumDBPFirstProfileSavedNotifierTests.swift +++ b/UnitTests/Freemium/DBP/FreemiumDBPFirstProfileSavedNotifierTests.swift @@ -18,26 +18,28 @@ import XCTest @testable import DuckDuckGo_Privacy_Browser +import Networking +import NetworkingTestingUtils +import SubscriptionTestingUtilities final class FreemiumDBPFirstProfileSavedNotifierTests: XCTestCase { private var mockFreemiumDBPUserStateManager: MockFreemiumDBPUserStateManager! - private var mockAccountManager: MockAccountManager! private var mockNotificationCenter: MockNotificationCenter! + private var mockSubscriptionManager: SubscriptionManagerMock! private var sut: FreemiumDBPFirstProfileSavedNotifier! override func setUpWithError() throws { mockFreemiumDBPUserStateManager = MockFreemiumDBPUserStateManager() - mockAccountManager = MockAccountManager() mockNotificationCenter = MockNotificationCenter() + mockSubscriptionManager = SubscriptionManagerMock() sut = FreemiumDBPFirstProfileSavedNotifier(freemiumDBPUserStateManager: mockFreemiumDBPUserStateManager, - accountManager: mockAccountManager, + subscriptionManager: mockSubscriptionManager, notificationCenter: mockNotificationCenter) } func testWhenAllCriteriaSatisfied_thenNotificationShouldBePosted() { // Given - mockAccountManager.accessToken = nil mockFreemiumDBPUserStateManager.didActivate = true mockFreemiumDBPUserStateManager.didPostFirstProfileSavedNotification = false @@ -52,7 +54,7 @@ final class FreemiumDBPFirstProfileSavedNotifierTests: XCTestCase { func testWhenUserIsAuthenticated_thenNotificationShouldNotBePosted() { // Given - mockAccountManager.accessToken = "some_token" + mockSubscriptionManager.resultTokenContainer = OAuthTokensFactory.makeValidTokenContainerWithEntitlements() mockFreemiumDBPUserStateManager.didActivate = true mockFreemiumDBPUserStateManager.didPostFirstProfileSavedNotification = false @@ -65,7 +67,6 @@ final class FreemiumDBPFirstProfileSavedNotifierTests: XCTestCase { func testWhenUserHasNotActivated_thenNotificationShouldNotBePosted() { // Given - mockAccountManager.accessToken = nil mockFreemiumDBPUserStateManager.didActivate = false mockFreemiumDBPUserStateManager.didPostFirstProfileSavedNotification = false @@ -78,7 +79,6 @@ final class FreemiumDBPFirstProfileSavedNotifierTests: XCTestCase { func testWhenNotificationAlreadyPosted_thenShouldNotPostAgain() { // Given - mockAccountManager.accessToken = nil mockFreemiumDBPUserStateManager.didActivate = true mockFreemiumDBPUserStateManager.didPostFirstProfileSavedNotification = true @@ -91,7 +91,6 @@ final class FreemiumDBPFirstProfileSavedNotifierTests: XCTestCase { func testWhenNotificationIsPosted_thenStateShouldBeUpdated() { // Given - mockAccountManager.accessToken = nil mockFreemiumDBPUserStateManager.didActivate = true mockFreemiumDBPUserStateManager.didPostFirstProfileSavedNotification = false diff --git a/UnitTests/MaliciousSiteProtection/MaliciousSiteProtectionTests.swift b/UnitTests/MaliciousSiteProtection/MaliciousSiteProtectionTests.swift index 14385f6555..e0996324ca 100644 --- a/UnitTests/MaliciousSiteProtection/MaliciousSiteProtectionTests.swift +++ b/UnitTests/MaliciousSiteProtection/MaliciousSiteProtectionTests.swift @@ -20,7 +20,8 @@ import BrowserServicesKit import Combine import Foundation import MaliciousSiteProtection -import TestUtils +import Networking +import NetworkingTestingUtils import XCTest @testable import DuckDuckGo_Privacy_Browser @@ -35,7 +36,7 @@ final class MaliciousSiteProtectionTests: XCTestCase { var dataManager: MaliciousSiteProtection.DataManager! override func setUp() async throws { - apiService = MockAPIService(apiResponse: .failure(CancellationError())) + apiService = MockAPIService(requestHandler: { request in .failure(CancellationError()) }) let mockFileStore = MockMaliciousSiteFileStore() mockDataProvider = MockMaliciousSiteDataProvider() dataManager = MaliciousSiteProtection.DataManager(fileStore: mockFileStore, embeddedDataProvider: mockDataProvider, fileNameProvider: { _ in "file.json" }) diff --git a/UnitTests/Menus/MoreOptionsMenuTests.swift b/UnitTests/Menus/MoreOptionsMenuTests.swift index c5487ae8b2..ae54ba741a 100644 --- a/UnitTests/Menus/MoreOptionsMenuTests.swift +++ b/UnitTests/Menus/MoreOptionsMenuTests.swift @@ -58,20 +58,9 @@ final class MoreOptionsMenuTests: XCTestCase { internalUserDecider = InternalUserDeciderMock() defaultBrowserProvider = DefaultBrowserProviderMock() defaultBrowserProvider.isDefault = true - storePurchaseManager = StorePurchaseManagerMock() - - subscriptionManager = SubscriptionManagerMock(accountManager: AccountManagerMock(), - subscriptionEndpointService: SubscriptionEndpointServiceMock(), - authEndpointService: AuthEndpointServiceMock(), - storePurchaseManager: storePurchaseManager, - currentEnvironment: SubscriptionEnvironment(serviceEnvironment: .production, - purchasePlatform: .appStore), - canPurchase: false, - subscriptionFeatureMappingCache: SubscriptionFeatureMappingCacheMock()) - + subscriptionManager = SubscriptionManagerMock() mockFreemiumDBPFeature = MockFreemiumDBPFeature() - mockNotificationCenter = MockNotificationCenter() mockPixelHandler = MockFreemiumDBPExperimentPixelHandler() mockFreemiumDBPUserStateManager = MockFreemiumDBPUserStateManager() @@ -109,11 +98,6 @@ final class MoreOptionsMenuTests: XCTestCase { // MARK: - Subscription & Freemium - private func mockAuthentication() { - subscriptionManager.accountManager.storeAuthToken(token: "") - subscriptionManager.accountManager.storeAccount(token: "", email: "", externalID: "") - } - @MainActor func testThatPrivacyProIsNotPresentWhenUnauthenticatedAndPurchaseNotAllowedOnAppStore () { subscriptionManager.canPurchase = false @@ -121,7 +105,7 @@ final class MoreOptionsMenuTests: XCTestCase { setupMoreOptionsMenu() - XCTAssertFalse(subscriptionManager.accountManager.isUserAuthenticated) + XCTAssertFalse(subscriptionManager.isUserAuthenticated) XCTAssertFalse(moreOptionsMenu.items.map { $0.title }.contains(UserText.subscriptionOptionsMenuItem)) } @@ -132,7 +116,7 @@ final class MoreOptionsMenuTests: XCTestCase { setupMoreOptionsMenu() - XCTAssertFalse(subscriptionManager.accountManager.isUserAuthenticated) + XCTAssertFalse(subscriptionManager.isUserAuthenticated) XCTAssertTrue(moreOptionsMenu.items.map { $0.title }.contains(UserText.subscriptionOptionsMenuItem)) } @@ -143,7 +127,7 @@ final class MoreOptionsMenuTests: XCTestCase { setupMoreOptionsMenu() - XCTAssertFalse(subscriptionManager.accountManager.isUserAuthenticated) + XCTAssertFalse(subscriptionManager.isUserAuthenticated) XCTAssertTrue(moreOptionsMenu.items.map { $0.title }.contains(UserText.subscriptionOptionsMenuItem)) } @@ -155,7 +139,7 @@ final class MoreOptionsMenuTests: XCTestCase { setupMoreOptionsMenu() - XCTAssertFalse(subscriptionManager.accountManager.isUserAuthenticated) + XCTAssertFalse(subscriptionManager.isUserAuthenticated) XCTAssertTrue(subscriptionManager.canPurchase) XCTAssertEqual(moreOptionsMenu.items[0].title, UserText.sendFeedback) @@ -187,7 +171,7 @@ final class MoreOptionsMenuTests: XCTestCase { setupMoreOptionsMenu() - XCTAssertFalse(subscriptionManager.accountManager.isUserAuthenticated) + XCTAssertFalse(subscriptionManager.isUserAuthenticated) XCTAssertTrue(subscriptionManager.canPurchase) XCTAssertEqual(moreOptionsMenu.items[0].title, UserText.sendFeedback) @@ -343,7 +327,7 @@ final class NetworkProtectionVisibilityMock: VPNFeatureGatekeeper { return !visible } - func canStartVPN() async throws -> Bool { + func canStartVPN() -> Bool { return false } diff --git a/UnitTests/NavigationBar/LocalPinningManagerTests.swift b/UnitTests/NavigationBar/LocalPinningManagerTests.swift index 5cf5cf7962..254ab6c8ac 100644 --- a/UnitTests/NavigationBar/LocalPinningManagerTests.swift +++ b/UnitTests/NavigationBar/LocalPinningManagerTests.swift @@ -21,15 +21,6 @@ import NetworkProtection @testable import DuckDuckGo_Privacy_Browser -private struct NetworkProtectionFeatureActivationMock: NetworkProtectionFeatureActivation { - - let activated: Bool = true - - var isFeatureActivated: Bool { - activated - } -} - final class LocalPinningManagerTests: XCTestCase { override func setUp() { @@ -43,7 +34,7 @@ final class LocalPinningManagerTests: XCTestCase { } private func createManager() -> LocalPinningManager { - return LocalPinningManager(networkProtectionFeatureActivation: NetworkProtectionFeatureActivationMock()) + return LocalPinningManager() } func testWhenTogglingPinningForAView_AndViewIsNotPinned_ThenViewBecomesPinned() { diff --git a/UnitTests/Subscription/SubscriptionAppStoreRestorerTests.swift b/UnitTests/Subscription/SubscriptionAppStoreRestorerTests.swift index bd26652802..24ab20c4d3 100644 --- a/UnitTests/Subscription/SubscriptionAppStoreRestorerTests.swift +++ b/UnitTests/Subscription/SubscriptionAppStoreRestorerTests.swift @@ -40,10 +40,6 @@ final class SubscriptionAppStoreRestorerTests: XCTestCase { var userDefaults: UserDefaults! var pixelKit: PixelKit! var uiHandler: SubscriptionUIHandlerMock! - - var accountManager: AccountManagerMock! - var subscriptionService: SubscriptionEndpointServiceMock! - var authService: AuthEndpointServiceMock! var storePurchaseManager: StorePurchaseManagerMock! var subscriptionFeatureMappingCache: SubscriptionFeatureMappingCacheMock! var subscriptionEnvironment: SubscriptionEnvironment! @@ -72,24 +68,12 @@ final class SubscriptionAppStoreRestorerTests: XCTestCase { self.uiEventsHappened.append(action) }) - accountManager = AccountManagerMock() - subscriptionService = SubscriptionEndpointServiceMock() - authService = AuthEndpointServiceMock() storePurchaseManager = StorePurchaseManagerMock() - subscriptionFeatureMappingCache = SubscriptionFeatureMappingCacheMock() - - subscriptionEnvironment = SubscriptionEnvironment(serviceEnvironment: .production, - purchasePlatform: .appStore) - - subscriptionManager = SubscriptionManagerMock(accountManager: accountManager, - subscriptionEndpointService: subscriptionService, - authEndpointService: authService, - storePurchaseManager: storePurchaseManager, - currentEnvironment: subscriptionEnvironment, - canPurchase: true, - subscriptionFeatureMappingCache: subscriptionFeatureMappingCache) + subscriptionEnvironment = SubscriptionEnvironment(serviceEnvironment: .production, purchasePlatform: .appStore) + subscriptionManager = SubscriptionManagerMock() + subscriptionManager.currentEnvironment = subscriptionEnvironment + subscriptionManager.resultStorePurchaseManager = storePurchaseManager appStoreRestoreFlow = AppStoreRestoreFlowMock() - subscriptionAppStoreRestorer = DefaultSubscriptionAppStoreRestorer(subscriptionManager: subscriptionManager, appStoreRestoreFlow: appStoreRestoreFlow, uiHandler: uiHandler) @@ -97,23 +81,15 @@ final class SubscriptionAppStoreRestorerTests: XCTestCase { override func tearDown() async throws { userDefaults = nil - PixelKit.tearDown() pixelKit.clearFrequencyHistoryForAllPixels() - pixelsFired.removeAll() uiEventsHappened.removeAll() - - accountManager = nil - subscriptionService = nil - authService = nil storePurchaseManager = nil subscriptionEnvironment = nil - subscriptionManager = nil appStoreRestoreFlow = nil uiHandler = nil - subscriptionAppStoreRestorer = nil } @@ -121,7 +97,7 @@ final class SubscriptionAppStoreRestorerTests: XCTestCase { func testRestoreAppStoreSubscriptionSuccess() async throws { // Given - appStoreRestoreFlow.restoreAccountFromPastPurchaseResult = .success(()) + appStoreRestoreFlow.restoreAccountFromPastPurchaseResult = .success("") // When await subscriptionAppStoreRestorer.restoreAppStoreSubscription() @@ -155,7 +131,7 @@ final class SubscriptionAppStoreRestorerTests: XCTestCase { // Given storePurchaseManager.syncAppleIDAccountResultError = StoreKitError.unknown await uiHandler.setAlertResponse(alertResponse: .alertFirstButtonReturn) - appStoreRestoreFlow.restoreAccountFromPastPurchaseResult = .success(()) + appStoreRestoreFlow.restoreAccountFromPastPurchaseResult = .success("") // When await subscriptionAppStoreRestorer.restoreAppStoreSubscription() @@ -180,6 +156,7 @@ final class SubscriptionAppStoreRestorerTests: XCTestCase { // Given appStoreRestoreFlow.restoreAccountFromPastPurchaseResult = .failure(.missingAccountOrTransactions) await uiHandler.setAlertResponse(alertResponse: .alertFirstButtonReturn) + subscriptionManager.resultURL = URL(string: "https://www.duckduckgo.com") // When await subscriptionAppStoreRestorer.restoreAppStoreSubscription() @@ -290,11 +267,9 @@ final class SubscriptionAppStoreRestorerTests: XCTestCase { func testRestoreAppStoreSubscriptionWhenRestoreFailsDueToSubscriptionBeingExpired() async throws { // Given - appStoreRestoreFlow.restoreAccountFromPastPurchaseResult = .failure(.subscriptionExpired(accountDetails: .init(authToken: Constants.authToken, - accessToken: Constants.accessToken, - externalID: Constants.externalID, - email: Constants.email)) ) + appStoreRestoreFlow.restoreAccountFromPastPurchaseResult = .failure(AppStoreRestoreFlowError.subscriptionExpired) await uiHandler.setAlertResponse(alertResponse: .alertFirstButtonReturn) + subscriptionManager.resultURL = URL(string: "https://www.duckduckgo.com") // When await subscriptionAppStoreRestorer.restoreAppStoreSubscription() diff --git a/UnitTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTestsForStripe.swift b/UnitTests/Subscription/SubscriptionPagesUseSubscriptionFeatureForStripeTests.swift similarity index 60% rename from UnitTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTestsForStripe.swift rename to UnitTests/Subscription/SubscriptionPagesUseSubscriptionFeatureForStripeTests.swift index a9530000da..c44b2ec129 100644 --- a/UnitTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTestsForStripe.swift +++ b/UnitTests/Subscription/SubscriptionPagesUseSubscriptionFeatureForStripeTests.swift @@ -1,5 +1,5 @@ // -// SubscriptionPagesUseSubscriptionFeatureTestsForStripe.swift +// SubscriptionPagesUseSubscriptionFeatureForStripeTests.swift // // Copyright © 2024 DuckDuckGo. All rights reserved. // @@ -26,9 +26,11 @@ import UserScript @testable import PixelKit import PixelKitTestingUtilities import os.log +import Networking +import NetworkingTestingUtils @available(macOS 12.0, *) -final class SubscriptionPagesUseSubscriptionFeatureTestsForStripe: XCTestCase { +final class SubscriptionPagesUseSubscriptionFeatureForStripeTests: XCTestCase { private struct Constants { static let userDefaultsSuiteName = "SubscriptionPagesUseSubscriptionFeatureTests" @@ -39,9 +41,9 @@ final class SubscriptionPagesUseSubscriptionFeatureTestsForStripe: XCTestCase { static let email = "dax@duck.com" - static let entitlements = [Entitlement(product: .dataBrokerProtection), - Entitlement(product: .identityTheftRestoration), - Entitlement(product: .networkProtection)] + static let entitlements: [SubscriptionEntitlement] = [.dataBrokerProtection, + .identityTheftRestoration, + .networkProtection] static let mostRecentTransactionJWS = "dGhpcyBpcyBub3QgYSByZWFsIEFw(...)cCBTdG9yZSB0cmFuc2FjdGlvbiBKV1M=" @@ -56,27 +58,15 @@ final class SubscriptionPagesUseSubscriptionFeatureTestsForStripe: XCTestCase { price: "99", currency: "USD")] - static let subscriptionOptions = SubscriptionOptions(platform: SubscriptionPlatformName.stripe, - options: [ - SubscriptionOption(id: "1", - cost: SubscriptionOptionCost(displayPrice: "$9.00", recurrence: "monthly")), - SubscriptionOption(id: "2", - cost: SubscriptionOptionCost(displayPrice: "$99.00", recurrence: "yearly")) - ], - features: [ - SubscriptionFeature(name: .networkProtection), - SubscriptionFeature(name: .dataBrokerProtection), - SubscriptionFeature(name: .identityTheftRestoration) - ]) - - static let validateTokenResponse = ValidateTokenResponse(account: ValidateTokenResponse.Account(email: Constants.email, - entitlements: Constants.entitlements, - externalID: Constants.externalID)) - + static let subscriptionOptions = SubscriptionOptions( + platform: SubscriptionPlatformName.stripe, + options: [ + SubscriptionOption(id: "1", cost: SubscriptionOptionCost(displayPrice: "$9.00", recurrence: "monthly")), + SubscriptionOption(id: "2", cost: SubscriptionOptionCost(displayPrice: "$99.00", recurrence: "yearly")) + ], + availableEntitlements: [.networkProtection, .dataBrokerProtection, .identityTheftRestoration]) static let mockParams: [String: String] = [:] @MainActor static let mockScriptMessage = MockWKScriptMessage(name: "", body: "", webView: WKWebView() ) - - static let invalidTokenError = APIServiceError.serverError(statusCode: 401, error: "invalid_token") } var userDefaults: UserDefaults! @@ -84,32 +74,16 @@ final class SubscriptionPagesUseSubscriptionFeatureTestsForStripe: XCTestCase { var uiHandler: SubscriptionUIHandlerMock! var pixelKit: PixelKit! - var accountStorage: AccountKeychainStorageMock! - var accessTokenStorage: SubscriptionTokenKeychainStorageMock! - var entitlementsCache: UserDefaultsCache<[Entitlement]>! - - var subscriptionService: SubscriptionEndpointServiceMock! - var authService: AuthEndpointServiceMock! + var subscriptionManager: SubscriptionManagerMock! var storePurchaseManager: StorePurchaseManagerMock! var subscriptionEnvironment: SubscriptionEnvironment! - - var subscriptionFeatureMappingCache: SubscriptionFeatureMappingCacheMock! - var subscriptionFeatureFlagger: FeatureFlaggerMapping! - var appStorePurchaseFlow: AppStorePurchaseFlow! var appStoreRestoreFlow: AppStoreRestoreFlow! - var appStoreAccountManagementFlow: AppStoreAccountManagementFlow! var stripePurchaseFlow: StripePurchaseFlow! - var subscriptionFeatureAvailability: SubscriptionFeatureAvailabilityMock! - - var accountManager: AccountManager! - var subscriptionManager: SubscriptionManager! var mockFreemiumDBPExperimentManager: MockFreemiumDBPExperimentManager! - var feature: SubscriptionPagesUseSubscriptionFeature! - var pixelsFired: [String] = [] var uiEventsHappened: [SubscriptionUIHandlerMock.UIHandlerMockPerformedAction] = [] @@ -130,61 +104,22 @@ final class SubscriptionPagesUseSubscriptionFeatureTestsForStripe: XCTestCase { uiHandler = SubscriptionUIHandlerMock { action in self.uiEventsHappened.append(action) } - - subscriptionService = SubscriptionEndpointServiceMock() - authService = AuthEndpointServiceMock() - + subscriptionManager = SubscriptionManagerMock() + subscriptionManager.resultURL = URL(string: "https://example.com") storePurchaseManager = StorePurchaseManagerMock() - subscriptionEnvironment = SubscriptionEnvironment(serviceEnvironment: .production, + subscriptionManager.resultStorePurchaseManager = storePurchaseManager + subscriptionEnvironment = SubscriptionEnvironment(serviceEnvironment: .staging, purchasePlatform: .stripe) - accountStorage = AccountKeychainStorageMock() - accessTokenStorage = SubscriptionTokenKeychainStorageMock() - - entitlementsCache = UserDefaultsCache<[Entitlement]>(userDefaults: userDefaults, - key: UserDefaultsCacheKey.subscriptionEntitlements, - settings: UserDefaultsCacheSettings(defaultExpirationInterval: .minutes(20))) - - subscriptionFeatureMappingCache = SubscriptionFeatureMappingCacheMock() - subscriptionFeatureFlagger = FeatureFlaggerMapping(mapping: { $0.defaultState }) - - // Real AccountManager - accountManager = DefaultAccountManager(storage: accountStorage, - accessTokenStorage: accessTokenStorage, - entitlementsCache: entitlementsCache, - subscriptionEndpointService: subscriptionService, - authEndpointService: authService) - - // Real Flows - appStoreRestoreFlow = DefaultAppStoreRestoreFlow(accountManager: accountManager, - storePurchaseManager: storePurchaseManager, - subscriptionEndpointService: subscriptionService, - authEndpointService: authService) - - appStorePurchaseFlow = DefaultAppStorePurchaseFlow(subscriptionEndpointService: subscriptionService, - storePurchaseManager: storePurchaseManager, - accountManager: accountManager, - appStoreRestoreFlow: appStoreRestoreFlow, - authEndpointService: authService) - - appStoreAccountManagementFlow = DefaultAppStoreAccountManagementFlow(authEndpointService: authService, - storePurchaseManager: storePurchaseManager, - accountManager: accountManager) - - stripePurchaseFlow = DefaultStripePurchaseFlow(subscriptionEndpointService: subscriptionService, - authEndpointService: authService, - accountManager: accountManager) + subscriptionManager.currentEnvironment = subscriptionEnvironment + appStoreRestoreFlow = DefaultAppStoreRestoreFlow(subscriptionManager: subscriptionManager, + storePurchaseManager: storePurchaseManager) + appStorePurchaseFlow = DefaultAppStorePurchaseFlow(subscriptionManager: subscriptionManager, + storePurchaseManager: storePurchaseManager, + appStoreRestoreFlow: appStoreRestoreFlow) + stripePurchaseFlow = DefaultStripePurchaseFlow(subscriptionManager: subscriptionManager) subscriptionFeatureAvailability = SubscriptionFeatureAvailabilityMock(isSubscriptionPurchaseAllowed: true, usesUnifiedFeedbackForm: false) - - // Real SubscriptionManager - subscriptionManager = DefaultSubscriptionManager(storePurchaseManager: storePurchaseManager, - accountManager: accountManager, - subscriptionEndpointService: subscriptionService, - authEndpointService: authService, - subscriptionFeatureMappingCache: subscriptionFeatureMappingCache, - subscriptionEnvironment: subscriptionEnvironment) - mockFreemiumDBPExperimentManager = MockFreemiumDBPExperimentManager() feature = SubscriptionPagesUseSubscriptionFeature(subscriptionManager: subscriptionManager, @@ -199,30 +134,13 @@ final class SubscriptionPagesUseSubscriptionFeatureTestsForStripe: XCTestCase { userDefaults = nil pixelsFired.removeAll() uiEventsHappened.removeAll() - - subscriptionService = nil - authService = nil storePurchaseManager = nil subscriptionEnvironment = nil - - accountStorage = nil - accessTokenStorage = nil - - entitlementsCache.reset() - entitlementsCache = nil - - accountManager = nil - - // Real Flows appStorePurchaseFlow = nil appStoreRestoreFlow = nil - appStoreAccountManagementFlow = nil stripePurchaseFlow = nil - subscriptionFeatureAvailability = nil - subscriptionManager = nil - feature = nil } @@ -231,7 +149,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTestsForStripe: XCTestCase { func testGetSubscriptionOptionsSuccess() async throws { // Given XCTAssertEqual(subscriptionEnvironment.purchasePlatform, .stripe) - subscriptionService.getProductsResult = .success(Constants.productItems) + subscriptionManager.productsResponse = .success(Constants.productItems) // When let result = try await feature.getSubscriptionOptions(params: Constants.mockParams, original: Constants.mockScriptMessage) @@ -246,7 +164,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTestsForStripe: XCTestCase { func testGetSubscriptionOptionsReturnsEmptyOptionsWhenNoSubscriptionOptions() async throws { // Given XCTAssertEqual(subscriptionEnvironment.purchasePlatform, .stripe) - subscriptionService.getProductsResult = .success([]) + subscriptionManager.productsResponse = .success([]) storePurchaseManager.subscriptionOptionsResult = nil // When @@ -261,7 +179,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTestsForStripe: XCTestCase { func testGetSubscriptionOptionsReturnsEmptyOptionsWhenSubscriptionOptionsDidNotFetch() async throws { // Given XCTAssertEqual(subscriptionEnvironment.purchasePlatform, .stripe) - subscriptionService.getProductsResult = .failure(Constants.invalidTokenError) + subscriptionManager.productsResponse = .failure(Subscription.SubscriptionManagerError.tokenUnavailable(error: nil)) storePurchaseManager.subscriptionOptionsResult = nil // When @@ -279,14 +197,15 @@ final class SubscriptionPagesUseSubscriptionFeatureTestsForStripe: XCTestCase { // Given ensureUserUnauthenticatedState() XCTAssertEqual(subscriptionEnvironment.purchasePlatform, .stripe) - XCTAssertFalse(accountManager.isUserAuthenticated) - - authService.createAccountResult = .success(CreateAccountResponse(authToken: Constants.authToken, - externalID: Constants.externalID, - status: "created")) + XCTAssertFalse(subscriptionManager.isUserAuthenticated) + subscriptionManager.resultCreateAccountTokenContainer = OAuthTokensFactory.makeValidTokenContainerWithEntitlements() + let subscriptionID = "some-subscription-id" + subscriptionManager.resultSubscription = SubscriptionMockFactory.subscription + storePurchaseManager.purchaseSubscriptionResult = .success(subscriptionID) + subscriptionManager.confirmPurchaseResponse = .success(SubscriptionMockFactory.subscription) // When - let subscriptionSelectedParams = ["id": "some-subscription-id"] + let subscriptionSelectedParams = ["id": subscriptionID] let result = try await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) // Then @@ -300,16 +219,16 @@ final class SubscriptionPagesUseSubscriptionFeatureTestsForStripe: XCTestCase { // Given ensureUserAuthenticatedState() XCTAssertEqual(subscriptionEnvironment.purchasePlatform, .stripe) - XCTAssertTrue(accountManager.isUserAuthenticated) - - subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredSubscription) + XCTAssertTrue(subscriptionManager.isUserAuthenticated) + await uiHandler.setAlertResponse(alertResponse: .alertFirstButtonReturn) + subscriptionManager.resultSubscription = SubscriptionMockFactory.expiredSubscription // When let subscriptionSelectedParams = ["id": "some-subscription-id"] let result = try await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) // Then - XCTAssertFalse(authService.createAccountCalled) +// XCTAssertFalse(authService.createAccountCalled) XCTAssertEqual(uiEventsHappened, [.didDismissProgressViewController]) XCTAssertNil(result) XCTAssertPrivacyPixelsFired([PrivacyProPixel.privacyProPurchaseAttempt.name + "_d", @@ -320,8 +239,9 @@ final class SubscriptionPagesUseSubscriptionFeatureTestsForStripe: XCTestCase { // Given ensureUserUnauthenticatedState() XCTAssertEqual(subscriptionEnvironment.purchasePlatform, .stripe) - - authService.createAccountResult = .failure(Constants.invalidTokenError) + subscriptionManager.resultCreateAccountTokenContainer = nil + storePurchaseManager.purchaseSubscriptionResult = .success(Constants.mostRecentTransactionJWS) + subscriptionManager.confirmPurchaseResponse = .success(SubscriptionMockFactory.subscription) await uiHandler.setAlertResponse(alertResponse: .alertFirstButtonReturn) // When @@ -330,10 +250,11 @@ final class SubscriptionPagesUseSubscriptionFeatureTestsForStripe: XCTestCase { // Then XCTAssertFalse(storePurchaseManager.purchaseSubscriptionCalled) - XCTAssertEqual(uiEventsHappened, [.didDismissProgressViewController, - .didShowAlert(.somethingWentWrong), - .didShowTab(.subscription(subscriptionManager.url(for: .purchase))), - .didDismissProgressViewController]) + XCTAssertTrue(uiEventsHappened.count == 4) + XCTAssertTrue(uiEventsHappened.contains(.didDismissProgressViewController)) + XCTAssertTrue(uiEventsHappened.contains(.didShowAlert(.somethingWentWrong))) + XCTAssertTrue(uiEventsHappened.contains(.didShowTab(.subscription(subscriptionManager.url(for: .purchase))))) + XCTAssertTrue(uiEventsHappened.contains(.didDismissProgressViewController)) XCTAssertNil(result) XCTAssertPrivacyPixelsFired([PrivacyProPixel.privacyProPurchaseAttempt.name + "_d", PrivacyProPixel.privacyProPurchaseAttempt.name + "_c", @@ -350,8 +271,10 @@ final class SubscriptionPagesUseSubscriptionFeatureTestsForStripe: XCTestCase { // Given ensureUserAuthenticatedState() - authService.getAccessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) - authService.validateTokenResult = .success(Constants.validateTokenResponse) +// authService.getAccessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) +// authService.validateTokenResult = .success(Constants.validateTokenResponse) +// subscriptionManager.resultSubscription = SubscriptionMockFactory.subscription +// subscriptionManager.resultExchangeTokenContainer = OAuthTokensFactory.makeValidTokenContainerWithEntitlements() // When let result = try await feature.completeStripePayment(params: Constants.mockParams, original: Constants.mockScriptMessage) @@ -369,18 +292,14 @@ final class SubscriptionPagesUseSubscriptionFeatureTestsForStripe: XCTestCase { } @available(macOS 12.0, *) -extension SubscriptionPagesUseSubscriptionFeatureTestsForStripe { +extension SubscriptionPagesUseSubscriptionFeatureForStripeTests { func ensureUserAuthenticatedState() { - accountStorage.authToken = Constants.authToken - accountStorage.email = Constants.email - accountStorage.externalID = Constants.externalID - accessTokenStorage.accessToken = Constants.accessToken + subscriptionManager.resultTokenContainer = OAuthTokensFactory.makeValidTokenContainerWithEntitlements() } func ensureUserUnauthenticatedState() { - try? accessTokenStorage.removeAccessToken() - try? accountStorage.clearAuthenticationState() + subscriptionManager.resultTokenContainer = nil } public func XCTAssertPrivacyPixelsFired(_ pixels: [String], file: StaticString = #file, line: UInt = #line) { diff --git a/UnitTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift b/UnitTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift index 2046934d1f..dbc762d070 100644 --- a/UnitTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift +++ b/UnitTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift @@ -27,86 +27,52 @@ import UserScript import PixelKitTestingUtilities import os.log import DataBrokerProtection +import Networking +import NetworkingTestingUtils @available(macOS 12.0, *) final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { private struct Constants { static let userDefaultsSuiteName = "SubscriptionPagesUseSubscriptionFeatureTests" - - static let authToken = UUID().uuidString - static let accessToken = UUID().uuidString static let externalID = UUID().uuidString - static let email = "dax@duck.com" - - static let entitlements = [Entitlement(product: .dataBrokerProtection), - Entitlement(product: .identityTheftRestoration), - Entitlement(product: .networkProtection)] + static let entitlements: [SubscriptionEntitlement] = [.dataBrokerProtection, + .identityTheftRestoration, + .networkProtection] static let mostRecentTransactionJWS = "dGhpcyBpcyBub3QgYSByZWFsIEFw(...)cCBTdG9yZSB0cmFuc2FjdGlvbiBKV1M=" - - static let subscriptionOptions = SubscriptionOptions(platform: SubscriptionPlatformName.macos, - options: [ - SubscriptionOption(id: "1", - cost: SubscriptionOptionCost(displayPrice: "9 USD", recurrence: "monthly")), - SubscriptionOption(id: "2", - cost: SubscriptionOptionCost(displayPrice: "99 USD", recurrence: "yearly")) - ], - features: [ - SubscriptionFeature(name: .networkProtection), - SubscriptionFeature(name: .dataBrokerProtection), - SubscriptionFeature(name: .identityTheftRestoration) - ]) - - static let validateTokenResponse = ValidateTokenResponse(account: ValidateTokenResponse.Account(email: Constants.email, - entitlements: Constants.entitlements, - externalID: Constants.externalID)) - + static let subscriptionOptions = SubscriptionOptions( + platform: SubscriptionPlatformName.macos, + options: [ + SubscriptionOption(id: "1", cost: SubscriptionOptionCost(displayPrice: "9 USD", recurrence: "monthly")), + SubscriptionOption(id: "2", cost: SubscriptionOptionCost(displayPrice: "99 USD", recurrence: "yearly")) + ], + availableEntitlements: [.networkProtection, .dataBrokerProtection, .identityTheftRestoration]) static let mockParams: [String: String] = [:] @MainActor static let mockScriptMessage = MockWKScriptMessage(name: "", body: "", webView: WKWebView() ) - - static let invalidTokenError = APIServiceError.serverError(statusCode: 401, error: "invalid_token") } var userDefaults: UserDefaults! var broker: UserScriptMessageBroker = UserScriptMessageBroker(context: "testBroker") var uiHandler: SubscriptionUIHandlerMock! var pixelKit: PixelKit! - - var accountStorage: AccountKeychainStorageMock! - var accessTokenStorage: SubscriptionTokenKeychainStorageMock! - var entitlementsCache: UserDefaultsCache<[Entitlement]>! - - var subscriptionService: SubscriptionEndpointServiceMock! - var authService: AuthEndpointServiceMock! - var storePurchaseManager: StorePurchaseManagerMock! var subscriptionEnvironment: SubscriptionEnvironment! - - var subscriptionFeatureMappingCache: SubscriptionFeatureMappingCacheMock! - var subscriptionFeatureFlagger: FeatureFlaggerMapping! - var appStorePurchaseFlow: AppStorePurchaseFlow! var appStoreRestoreFlow: AppStoreRestoreFlow! - var appStoreAccountManagementFlow: AppStoreAccountManagementFlow! var stripePurchaseFlow: StripePurchaseFlow! - var subscriptionAttributionPixelHandler: SubscriptionAttributionPixelHandler! - var subscriptionFeatureAvailability: SubscriptionFeatureAvailabilityMock! - - var accountManager: AccountManager! - var subscriptionManager: SubscriptionManager! + var subscriptionManager: SubscriptionManagerMock! var mockFreemiumDBPExperimentManager: MockFreemiumDBPExperimentManager! private var mockPixelHandler: MockFreemiumDBPExperimentPixelHandler! private var mockFreemiumDBPUserStateManager: MockFreemiumDBPUserStateManager! - - var feature: SubscriptionPagesUseSubscriptionFeature! - var pixelsFired: [String] = [] var uiEventsHappened: [SubscriptionUIHandlerMock.UIHandlerMockPerformedAction] = [] + var feature: SubscriptionPagesUseSubscriptionFeature! + @MainActor override func setUpWithError() throws { // Mocks userDefaults = UserDefaults(suiteName: Constants.userDefaultsSuiteName)! @@ -125,66 +91,24 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { self.uiEventsHappened.append(action) } - subscriptionService = SubscriptionEndpointServiceMock() - authService = AuthEndpointServiceMock() - storePurchaseManager = StorePurchaseManagerMock() - subscriptionEnvironment = SubscriptionEnvironment(serviceEnvironment: .production, - purchasePlatform: .appStore) - accountStorage = AccountKeychainStorageMock() - accessTokenStorage = SubscriptionTokenKeychainStorageMock() - - entitlementsCache = UserDefaultsCache<[Entitlement]>(userDefaults: userDefaults, - key: UserDefaultsCacheKey.subscriptionEntitlements, - settings: UserDefaultsCacheSettings(defaultExpirationInterval: .minutes(20))) - - subscriptionFeatureMappingCache = SubscriptionFeatureMappingCacheMock() - subscriptionFeatureFlagger = FeatureFlaggerMapping(mapping: { $0.defaultState }) - - // Real AccountManager - accountManager = DefaultAccountManager(storage: accountStorage, - accessTokenStorage: accessTokenStorage, - entitlementsCache: entitlementsCache, - subscriptionEndpointService: subscriptionService, - authEndpointService: authService) - - // Real Flows - appStoreRestoreFlow = DefaultAppStoreRestoreFlow(accountManager: accountManager, - storePurchaseManager: storePurchaseManager, - subscriptionEndpointService: subscriptionService, - authEndpointService: authService) - - appStorePurchaseFlow = DefaultAppStorePurchaseFlow(subscriptionEndpointService: subscriptionService, + subscriptionEnvironment = SubscriptionEnvironment(serviceEnvironment: .production, purchasePlatform: .appStore) + subscriptionManager = SubscriptionManagerMock() + subscriptionManager.resultStorePurchaseManager = storePurchaseManager + subscriptionManager.resultURL = URL(string: "https://example.com") + subscriptionManager.currentEnvironment = subscriptionEnvironment + appStoreRestoreFlow = DefaultAppStoreRestoreFlow(subscriptionManager: subscriptionManager, + storePurchaseManager: storePurchaseManager) + appStorePurchaseFlow = DefaultAppStorePurchaseFlow(subscriptionManager: subscriptionManager, storePurchaseManager: storePurchaseManager, - accountManager: accountManager, - appStoreRestoreFlow: appStoreRestoreFlow, - authEndpointService: authService) - - appStoreAccountManagementFlow = DefaultAppStoreAccountManagementFlow(authEndpointService: authService, - storePurchaseManager: storePurchaseManager, - accountManager: accountManager) - - stripePurchaseFlow = DefaultStripePurchaseFlow(subscriptionEndpointService: subscriptionService, - authEndpointService: authService, - accountManager: accountManager) - + appStoreRestoreFlow: appStoreRestoreFlow) + stripePurchaseFlow = DefaultStripePurchaseFlow(subscriptionManager: subscriptionManager) subscriptionAttributionPixelHandler = PrivacyProSubscriptionAttributionPixelHandler() - subscriptionFeatureAvailability = SubscriptionFeatureAvailabilityMock(isSubscriptionPurchaseAllowed: true, usesUnifiedFeedbackForm: false) - - // Real SubscriptionManager - subscriptionManager = DefaultSubscriptionManager(storePurchaseManager: storePurchaseManager, - accountManager: accountManager, - subscriptionEndpointService: subscriptionService, - authEndpointService: authService, - subscriptionFeatureMappingCache: subscriptionFeatureMappingCache, - subscriptionEnvironment: subscriptionEnvironment) - mockFreemiumDBPExperimentManager = MockFreemiumDBPExperimentManager() mockPixelHandler = MockFreemiumDBPExperimentPixelHandler() mockFreemiumDBPUserStateManager = MockFreemiumDBPUserStateManager() - feature = SubscriptionPagesUseSubscriptionFeature(subscriptionManager: subscriptionManager, subscriptionSuccessPixelHandler: subscriptionAttributionPixelHandler, stripePurchaseFlow: stripePurchaseFlow, @@ -200,30 +124,13 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { userDefaults = nil pixelsFired.removeAll() uiEventsHappened.removeAll() - - subscriptionService = nil - authService = nil storePurchaseManager = nil subscriptionEnvironment = nil - - accountStorage = nil - accessTokenStorage = nil - - entitlementsCache.reset() - entitlementsCache = nil - - accountManager = nil - - // Real Flows appStorePurchaseFlow = nil appStoreRestoreFlow = nil - appStoreAccountManagementFlow = nil stripePurchaseFlow = nil - subscriptionFeatureAvailability = nil - subscriptionManager = nil - feature = nil } @@ -232,16 +139,14 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { func testGetSubscriptionSuccessWithoutRefreshingAuthToken() async throws { // Given ensureUserAuthenticatedState() - - authService.validateTokenResult = .success(Constants.validateTokenResponse) + subscriptionManager.resultTokenContainer = OAuthTokensFactory.makeValidTokenContainerWithEntitlements() // When let result = try await feature.getSubscription(params: Constants.mockParams, original: Constants.mockScriptMessage) // Then let subscription = try XCTUnwrap(result as? SubscriptionPagesUseSubscriptionFeature.Subscription) - XCTAssertEqual(subscription.token, Constants.authToken) - XCTAssertEqual(accountManager.authToken, Constants.authToken) + XCTAssertEqual(subscription.token, subscriptionManager.resultTokenContainer?.accessToken) XCTAssertPrivacyPixelsFired([]) } @@ -249,7 +154,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { // Given ensureUserUnauthenticatedState() - authService.validateTokenResult = .failure(Constants.invalidTokenError) + subscriptionManager.resultTokenContainer = nil storePurchaseManager.mostRecentTransactionResult = nil // When @@ -258,7 +163,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { // Then let subscription = try XCTUnwrap(result as? SubscriptionPagesUseSubscriptionFeature.Subscription) XCTAssertEqual(subscription.token, "") - XCTAssertFalse(accountManager.isUserAuthenticated) + XCTAssertFalse(subscriptionManager.isUserAuthenticated) XCTAssertPrivacyPixelsFired([]) } @@ -267,19 +172,15 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { func testSetSubscriptionSuccess() async throws { // Given ensureUserUnauthenticatedState() - - authService.getAccessTokenResult = .success(.init(accessToken: Constants.accessToken)) - authService.validateTokenResult = .success(Constants.validateTokenResponse) + subscriptionManager.resultExchangeTokenContainer = OAuthTokensFactory.makeValidTokenContainerWithEntitlements() // When - let setSubscriptionParams = ["token": Constants.authToken] + let setSubscriptionParams = ["token": subscriptionManager.resultExchangeTokenContainer!.accessToken] let result = try await feature.setSubscription(params: setSubscriptionParams, original: Constants.mockScriptMessage) // Then - XCTAssertEqual(accountManager.authToken, Constants.authToken) - XCTAssertEqual(accountManager.accessToken, Constants.accessToken) - XCTAssertEqual(accountManager.email, Constants.email) - XCTAssertEqual(accountManager.externalID, Constants.externalID) + let tokens = try await subscriptionManager.getTokenContainer(policy: .local) + XCTAssertEqual(tokens, subscriptionManager.resultExchangeTokenContainer) XCTAssertNil(result) XCTAssertPrivacyPixelsFired([PrivacyProPixel.privacyProRestorePurchaseEmailSuccess.name + "_d", PrivacyProPixel.privacyProRestorePurchaseEmailSuccess.name + "_c"]) @@ -288,35 +189,16 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { func testSetSubscriptionErrorWhenFailedToExchangeToken() async throws { // Given ensureUserUnauthenticatedState() - - authService.getAccessTokenResult = .failure(Constants.invalidTokenError) - - // When - let setSubscriptionParams = ["token": Constants.authToken] - let result = try await feature.setSubscription(params: setSubscriptionParams, original: Constants.mockScriptMessage) - - // Then - XCTAssertNil(accountManager.authToken) - XCTAssertFalse(accountManager.isUserAuthenticated) - XCTAssertNil(result) - XCTAssertPrivacyPixelsFired([PrivacyProPixel.privacyProRestorePurchaseEmailSuccess.name + "_d", - PrivacyProPixel.privacyProRestorePurchaseEmailSuccess.name + "_c"]) - } - - func testSetSubscriptionErrorWhenFailedToFetchAccountDetails() async throws { - // Given - ensureUserUnauthenticatedState() - - authService.getAccessTokenResult = .success(.init(accessToken: Constants.accessToken)) - authService.validateTokenResult = .failure(Constants.invalidTokenError) + subscriptionManager.resultExchangeTokenContainer = nil // When - let setSubscriptionParams = ["token": Constants.authToken] + let setSubscriptionParams = ["token": "sometoken"] let result = try await feature.setSubscription(params: setSubscriptionParams, original: Constants.mockScriptMessage) // Then - XCTAssertNil(accountManager.authToken) - XCTAssertFalse(accountManager.isUserAuthenticated) + let tokens = try? await subscriptionManager.getTokenContainer(policy: .local) + XCTAssertNil(tokens) + XCTAssertFalse(subscriptionManager.isUserAuthenticated) XCTAssertNil(result) XCTAssertPrivacyPixelsFired([PrivacyProPixel.privacyProRestorePurchaseEmailSuccess.name + "_d", PrivacyProPixel.privacyProRestorePurchaseEmailSuccess.name + "_c"]) @@ -327,32 +209,25 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { func testBackToSettingsSuccess() async throws { // Given ensureUserAuthenticatedState() - accountStorage.email = nil - - XCTAssertNil(accountManager.email) + XCTAssertNil(subscriptionManager.userEmail) let notificationPostedExpectation = expectation(forNotification: .subscriptionPageCloseAndOpenPreferences, object: nil) - authService.validateTokenResult = .success(Constants.validateTokenResponse) - // When let result = try await feature.backToSettings(params: Constants.mockParams, original: Constants.mockScriptMessage) // Then await fulfillment(of: [notificationPostedExpectation], timeout: 1) - XCTAssertEqual(accountManager.email, Constants.email) XCTAssertNil(result) XCTAssertPrivacyPixelsFired([]) } func testBackToSettingsErrorOnFetchingAccountDetails() async throws { // Given - ensureUserAuthenticatedState() + ensureUserUnauthenticatedState() let notificationPostedExpectation = expectation(forNotification: .subscriptionPageCloseAndOpenPreferences, object: nil) - authService.validateTokenResult = .failure(Constants.invalidTokenError) - // When let result = try await feature.backToSettings(params: Constants.mockParams, original: Constants.mockScriptMessage) @@ -414,20 +289,14 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { // Given ensureUserUnauthenticatedState() XCTAssertEqual(subscriptionEnvironment.purchasePlatform, .appStore) - XCTAssertFalse(accountManager.isUserAuthenticated) + XCTAssertFalse(subscriptionManager.isUserAuthenticated) storePurchaseManager.hasActiveSubscriptionResult = false storePurchaseManager.mostRecentTransactionResult = nil - authService.createAccountResult = .success(CreateAccountResponse(authToken: Constants.authToken, - externalID: Constants.externalID, - status: "created")) - authService.getAccessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) - authService.validateTokenResult = .success(Constants.validateTokenResponse) + subscriptionManager.resultCreateAccountTokenContainer = OAuthTokensFactory.makeValidTokenContainerWithEntitlements() storePurchaseManager.purchaseSubscriptionResult = .success(Constants.mostRecentTransactionJWS) - subscriptionService.confirmPurchaseResult = .success(ConfirmPurchaseResponse(email: Constants.email, - entitlements: Constants.entitlements, - subscription: SubscriptionMockFactory.subscription)) + subscriptionManager.confirmPurchaseResponse = .success(SubscriptionMockFactory.subscription) // When let subscriptionSelectedParams = ["id": "some-subscription-id"] @@ -451,20 +320,17 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { mockFreemiumDBPExperimentManager.pixelParameters = ["daysEnrolled": "1"] ensureUserUnauthenticatedState() XCTAssertEqual(subscriptionEnvironment.purchasePlatform, .appStore) - XCTAssertFalse(accountManager.isUserAuthenticated) + XCTAssertFalse(subscriptionManager.isUserAuthenticated) storePurchaseManager.hasActiveSubscriptionResult = false storePurchaseManager.mostRecentTransactionResult = nil - authService.createAccountResult = .success(CreateAccountResponse(authToken: Constants.authToken, - externalID: Constants.externalID, - status: "created")) - authService.getAccessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) - authService.validateTokenResult = .success(Constants.validateTokenResponse) + await uiHandler.setAlertResponse(alertResponse: .alertFirstButtonReturn) + + subscriptionManager.resultCreateAccountTokenContainer = OAuthTokensFactory.makeValidTokenContainerWithEntitlements() + subscriptionManager.resultSubscription = SubscriptionMockFactory.subscription storePurchaseManager.purchaseSubscriptionResult = .success(Constants.mostRecentTransactionJWS) - subscriptionService.confirmPurchaseResult = .success(ConfirmPurchaseResponse(email: Constants.email, - entitlements: Constants.entitlements, - subscription: SubscriptionMockFactory.subscription)) + subscriptionManager.confirmPurchaseResponse = .success(SubscriptionMockFactory.subscription) // When let subscriptionSelectedParams = ["id": "some-subscription-id"] @@ -488,30 +354,22 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { // Given ensureUserAuthenticatedState() XCTAssertEqual(subscriptionEnvironment.purchasePlatform, .appStore) - XCTAssertTrue(accountManager.isUserAuthenticated) storePurchaseManager.hasActiveSubscriptionResult = false storePurchaseManager.mostRecentTransactionResult = Constants.mostRecentTransactionJWS - subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredSubscription) - - authService.storeLoginResult = .success(StoreLoginResponse(authToken: Constants.authToken, - email: Constants.email, - externalID: Constants.externalID, - id: 1, - status: "authenticated")) - authService.getAccessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) - authService.validateTokenResult = .success(Constants.validateTokenResponse) + await uiHandler.setAlertResponse(alertResponse: .alertFirstButtonReturn) storePurchaseManager.purchaseSubscriptionResult = .success(Constants.mostRecentTransactionJWS) - subscriptionService.confirmPurchaseResult = .success(ConfirmPurchaseResponse(email: Constants.email, - entitlements: Constants.entitlements, - subscription: SubscriptionMockFactory.subscription)) + subscriptionManager.resultCreateAccountTokenContainer = OAuthTokensFactory.makeValidTokenContainerWithEntitlements() + subscriptionManager.resultSubscription = SubscriptionMockFactory.expiredSubscription + let newSub = SubscriptionMockFactory.subscription + subscriptionManager.confirmPurchaseResponse = .success(newSub) // When let subscriptionSelectedParams = ["id": "some-subscription-id"] let result = try await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) // Then - XCTAssertFalse(authService.createAccountCalled) +// XCTAssertFalse(authService.createAccountCalled) XCTAssertEqual(uiEventsHappened, [.didPresentProgressViewController, .didUpdateProgressViewController, .didDismissProgressViewController]) @@ -528,21 +386,19 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { // Given ensureUserAuthenticatedState() XCTAssertEqual(subscriptionEnvironment.purchasePlatform, .appStore) - XCTAssertTrue(accountManager.isUserAuthenticated) - + await uiHandler.setAlertResponse(alertResponse: .alertFirstButtonReturn) storePurchaseManager.hasActiveSubscriptionResult = false - subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) + await uiHandler.setAlertResponse(alertResponse: .alertFirstButtonReturn) + subscriptionManager.resultSubscription = SubscriptionMockFactory.expiredSubscription storePurchaseManager.purchaseSubscriptionResult = .success(Constants.mostRecentTransactionJWS) - subscriptionService.confirmPurchaseResult = .success(ConfirmPurchaseResponse(email: Constants.email, - entitlements: Constants.entitlements, - subscription: SubscriptionMockFactory.subscription)) + subscriptionManager.confirmPurchaseResponse = .success(SubscriptionMockFactory.subscription) + subscriptionManager.resultCreateAccountTokenContainer = OAuthTokensFactory.makeValidTokenContainerWithEntitlements() // When let subscriptionSelectedParams = ["id": "some-subscription-id"] let result = try await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) // Then - XCTAssertFalse(authService.createAccountCalled) XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) XCTAssertEqual(uiEventsHappened, [.didPresentProgressViewController, .didUpdateProgressViewController, @@ -590,14 +446,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { storePurchaseManager.hasActiveSubscriptionResult = true await uiHandler.setAlertResponse(alertResponse: .alertFirstButtonReturn) storePurchaseManager.mostRecentTransactionResult = Constants.mostRecentTransactionJWS - authService.storeLoginResult = .success(StoreLoginResponse(authToken: Constants.authToken, - email: Constants.email, - externalID: Constants.externalID, - id: 1, - status: "authenticated")) - authService.getAccessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) - authService.validateTokenResult = .success(Constants.validateTokenResponse) - subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.subscription) + subscriptionManager.resultSubscription = SubscriptionMockFactory.subscription // When let subscriptionSelectedParams = ["id": "some-subscription-id"] @@ -628,7 +477,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { storePurchaseManager.hasActiveSubscriptionResult = false storePurchaseManager.mostRecentTransactionResult = nil - authService.createAccountResult = .failure(Constants.invalidTokenError) +// authService.createAccountResult = .failure(Constants.invalidTokenError) await uiHandler.setAlertResponse(alertResponse: .alertFirstButtonReturn) // When @@ -657,9 +506,11 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(subscriptionEnvironment.purchasePlatform, .appStore) storePurchaseManager.hasActiveSubscriptionResult = false - subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) + subscriptionManager.resultSubscription = SubscriptionMockFactory.expiredStripeSubscription storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.purchaseCancelledByUser) + await uiHandler.setAlertResponse(alertResponse: .abort) + let subscriptionSelectedParams = ["id": "some-subscription-id"] let result = try await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) @@ -678,7 +529,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(subscriptionEnvironment.purchasePlatform, .appStore) storePurchaseManager.hasActiveSubscriptionResult = false - subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) + subscriptionManager.resultSubscription = SubscriptionMockFactory.expiredStripeSubscription storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.productNotFound) await uiHandler.setAlertResponse(alertResponse: .alertFirstButtonReturn) @@ -708,7 +559,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(subscriptionEnvironment.purchasePlatform, .appStore) storePurchaseManager.hasActiveSubscriptionResult = false - subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) + subscriptionManager.resultSubscription = SubscriptionMockFactory.expiredStripeSubscription storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.externalIDisNotAValidUUID) await uiHandler.setAlertResponse(alertResponse: .alertFirstButtonReturn) @@ -738,7 +589,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(subscriptionEnvironment.purchasePlatform, .appStore) storePurchaseManager.hasActiveSubscriptionResult = false - subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) + subscriptionManager.resultSubscription = SubscriptionMockFactory.expiredStripeSubscription storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.purchaseFailed) await uiHandler.setAlertResponse(alertResponse: .alertFirstButtonReturn) @@ -768,7 +619,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(subscriptionEnvironment.purchasePlatform, .appStore) storePurchaseManager.hasActiveSubscriptionResult = false - subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) + subscriptionManager.resultSubscription = SubscriptionMockFactory.expiredStripeSubscription storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.transactionCannotBeVerified) await uiHandler.setAlertResponse(alertResponse: .alertFirstButtonReturn) @@ -798,7 +649,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(subscriptionEnvironment.purchasePlatform, .appStore) storePurchaseManager.hasActiveSubscriptionResult = false - subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) + subscriptionManager.resultSubscription = SubscriptionMockFactory.expiredStripeSubscription storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.transactionPendingAuthentication) await uiHandler.setAlertResponse(alertResponse: .alertFirstButtonReturn) @@ -828,7 +679,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(subscriptionEnvironment.purchasePlatform, .appStore) storePurchaseManager.hasActiveSubscriptionResult = false - subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) + subscriptionManager.resultSubscription = SubscriptionMockFactory.expiredStripeSubscription storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.unknownError) await uiHandler.setAlertResponse(alertResponse: .alertFirstButtonReturn) @@ -879,7 +730,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { func testFeatureSelectedSuccessForNetworkProtection() async throws { // Given ensureUserAuthenticatedState() - let selectedFeature = Entitlement.ProductName.networkProtection + let selectedFeature = SubscriptionEntitlement.networkProtection let notificationPostedExpectation = expectation(forNotification: .ToggleNetworkProtectionInMainWindow, object: nil) @@ -896,7 +747,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { func testFeatureSelectedSuccessForPersonalInformationRemoval() async throws { // Given ensureUserAuthenticatedState() - let selectedFeature = Entitlement.ProductName.dataBrokerProtection + let selectedFeature = SubscriptionEntitlement.dataBrokerProtection let notificationPostedExpectation = expectation(forNotification: .openPersonalInformationRemoval, object: nil) let uiHandlerCalledExpectation = expectation(description: "uiHandlerCalled") @@ -920,7 +771,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { func testFeatureSelectedSuccessForIdentityTheftRestoration() async throws { // Given ensureUserAuthenticatedState() - let selectedFeature = Entitlement.ProductName.identityTheftRestoration + let selectedFeature = SubscriptionEntitlement.identityTheftRestoration let uiHandlerCalledExpectation = expectation(description: "uiHandlerCalled") @@ -953,14 +804,14 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { // Then let tokenResponse = try XCTUnwrap(result as? [String: String]) - XCTAssertEqual(tokenResponse["token"], Constants.accessToken) + XCTAssertEqual(tokenResponse["token"], subscriptionManager.resultTokenContainer!.accessToken) XCTAssertPrivacyPixelsFired([]) } func testGetAccessTokenEmptyOnMissingToken() async throws { // Given ensureUserUnauthenticatedState() - XCTAssertNil(accountManager.accessToken) + XCTAssertFalse(subscriptionManager.isUserAuthenticated) // When let result = try await feature.getAccessToken(params: Constants.mockParams, original: Constants.mockScriptMessage) @@ -975,20 +826,14 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { // Given ensureUserUnauthenticatedState() XCTAssertEqual(subscriptionEnvironment.purchasePlatform, .appStore) - XCTAssertFalse(accountManager.isUserAuthenticated) + XCTAssertFalse(subscriptionManager.isUserAuthenticated) + subscriptionManager.resultCreateAccountTokenContainer = OAuthTokensFactory.makeValidTokenContainerWithEntitlements() storePurchaseManager.hasActiveSubscriptionResult = false storePurchaseManager.mostRecentTransactionResult = nil - - authService.createAccountResult = .success(CreateAccountResponse(authToken: Constants.authToken, - externalID: Constants.externalID, - status: "created")) - authService.getAccessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) - authService.validateTokenResult = .success(Constants.validateTokenResponse) + subscriptionManager.confirmPurchaseResponse = .success(SubscriptionMockFactory.subscription) + subscriptionManager.resultCreateAccountTokenContainer = OAuthTokensFactory.makeValidTokenContainerWithEntitlements() storePurchaseManager.purchaseSubscriptionResult = .success(Constants.mostRecentTransactionJWS) - subscriptionService.confirmPurchaseResult = .success(ConfirmPurchaseResponse(email: Constants.email, - entitlements: Constants.entitlements, - subscription: SubscriptionMockFactory.subscription)) mockFreemiumDBPUserStateManager.didActivate = true feature.with(broker: broker) @@ -1008,20 +853,14 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { // Given ensureUserUnauthenticatedState() XCTAssertEqual(subscriptionEnvironment.purchasePlatform, .appStore) - XCTAssertFalse(accountManager.isUserAuthenticated) + XCTAssertFalse(subscriptionManager.isUserAuthenticated) storePurchaseManager.hasActiveSubscriptionResult = false storePurchaseManager.mostRecentTransactionResult = nil - authService.createAccountResult = .success(CreateAccountResponse(authToken: Constants.authToken, - externalID: Constants.externalID, - status: "created")) - authService.getAccessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) - authService.validateTokenResult = .success(Constants.validateTokenResponse) + subscriptionManager.resultCreateAccountTokenContainer = OAuthTokensFactory.makeValidTokenContainerWithEntitlements() storePurchaseManager.purchaseSubscriptionResult = .success(Constants.mostRecentTransactionJWS) - subscriptionService.confirmPurchaseResult = .success(ConfirmPurchaseResponse(email: Constants.email, - entitlements: Constants.entitlements, - subscription: SubscriptionMockFactory.subscription)) + subscriptionManager.confirmPurchaseResponse = .success(SubscriptionMockFactory.subscription) mockFreemiumDBPUserStateManager.didActivate = false feature.with(broker: broker) @@ -1042,20 +881,17 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { // Given ensureUserUnauthenticatedState() XCTAssertEqual(subscriptionEnvironment.purchasePlatform, .appStore) - XCTAssertFalse(accountManager.isUserAuthenticated) + XCTAssertFalse(subscriptionManager.isUserAuthenticated) storePurchaseManager.hasActiveSubscriptionResult = false storePurchaseManager.mostRecentTransactionResult = nil - authService.createAccountResult = .success(CreateAccountResponse(authToken: Constants.authToken, - externalID: Constants.externalID, - status: "created")) - authService.getAccessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) - authService.validateTokenResult = .success(Constants.validateTokenResponse) + await uiHandler.setAlertResponse(alertResponse: .alertFirstButtonReturn) + + subscriptionManager.resultCreateAccountTokenContainer = OAuthTokensFactory.makeValidTokenContainerWithEntitlements() + subscriptionManager.confirmPurchaseResponse = .success(SubscriptionMockFactory.subscription) + storePurchaseManager.purchaseSubscriptionResult = .success(Constants.mostRecentTransactionJWS) - subscriptionService.confirmPurchaseResult = .success(ConfirmPurchaseResponse(email: Constants.email, - entitlements: Constants.entitlements, - subscription: SubscriptionMockFactory.subscription)) mockFreemiumDBPUserStateManager.didPostFirstProfileSavedNotification = true feature.with(broker: broker) @@ -1074,20 +910,15 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { // Given ensureUserUnauthenticatedState() XCTAssertEqual(subscriptionEnvironment.purchasePlatform, .appStore) - XCTAssertFalse(accountManager.isUserAuthenticated) + XCTAssertFalse(subscriptionManager.isUserAuthenticated) + await uiHandler.setAlertResponse(alertResponse: .alertFirstButtonReturn) storePurchaseManager.hasActiveSubscriptionResult = false storePurchaseManager.mostRecentTransactionResult = nil - authService.createAccountResult = .success(CreateAccountResponse(authToken: Constants.authToken, - externalID: Constants.externalID, - status: "created")) - authService.getAccessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) - authService.validateTokenResult = .success(Constants.validateTokenResponse) + subscriptionManager.resultTokenContainer = OAuthTokensFactory.makeValidTokenContainerWithEntitlements() storePurchaseManager.purchaseSubscriptionResult = .success(Constants.mostRecentTransactionJWS) - subscriptionService.confirmPurchaseResult = .success(ConfirmPurchaseResponse(email: Constants.email, - entitlements: Constants.entitlements, - subscription: SubscriptionMockFactory.subscription)) + subscriptionManager.confirmPurchaseResponse = .success(SubscriptionMockFactory.subscription) mockFreemiumDBPUserStateManager.didPostFirstProfileSavedNotification = false feature.with(broker: broker) @@ -1107,15 +938,13 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { extension SubscriptionPagesUseSubscriptionFeatureTests { func ensureUserAuthenticatedState() { - accountStorage.authToken = Constants.authToken - accountStorage.email = Constants.email - accountStorage.externalID = Constants.externalID - accessTokenStorage.accessToken = Constants.accessToken + subscriptionManager.resultTokenContainer = OAuthTokensFactory.makeValidTokenContainerWithEntitlements() + XCTAssertTrue(subscriptionManager.isUserAuthenticated) } func ensureUserUnauthenticatedState() { - try? accessTokenStorage.removeAccessToken() - try? accountStorage.clearAuthenticationState() + subscriptionManager.resultTokenContainer = nil + XCTAssertFalse(subscriptionManager.isUserAuthenticated) } public func XCTAssertPrivacyPixelsFired(_ pixels: [String], file: StaticString = #file, line: UInt = #line) { diff --git a/UnitTests/Sync/Mocks/MockDDGSyncing.swift b/UnitTests/Sync/Mocks/MockDDGSyncing.swift index e400ec9a8a..a1f23043e8 100644 --- a/UnitTests/Sync/Mocks/MockDDGSyncing.swift +++ b/UnitTests/Sync/Mocks/MockDDGSyncing.swift @@ -18,7 +18,7 @@ import Foundation import Combine -import TestUtils +import PersistenceTestingUtils @testable import DuckDuckGo_Privacy_Browser @testable import DDGSync diff --git a/UnitTests/Sync/SyncPreferencesTests.swift b/UnitTests/Sync/SyncPreferencesTests.swift index f1b1d40fa5..54400565dd 100644 --- a/UnitTests/Sync/SyncPreferencesTests.swift +++ b/UnitTests/Sync/SyncPreferencesTests.swift @@ -21,7 +21,7 @@ import Combine import Persistence import SyncUI import XCTest -import TestUtils +import PersistenceTestingUtils @testable import BrowserServicesKit @testable import DDGSync @testable import DuckDuckGo_Privacy_Browser diff --git a/UnitTests/UnifiedFeedbackForm/UnifiedFeedbackFormViewModelTests.swift b/UnitTests/UnifiedFeedbackForm/UnifiedFeedbackFormViewModelTests.swift index 1aafbcb795..3d71623387 100644 --- a/UnitTests/UnifiedFeedbackForm/UnifiedFeedbackFormViewModelTests.swift +++ b/UnitTests/UnifiedFeedbackForm/UnifiedFeedbackFormViewModelTests.swift @@ -16,15 +16,29 @@ // limitations under the License. // +import XCTest import Subscription import SubscriptionTestingUtilities -import XCTest - @testable import DuckDuckGo_Privacy_Browser +@testable import PersistenceTestingUtils @testable import Networking -@testable import TestUtils +import NetworkingTestingUtils final class UnifiedFeedbackFormViewModelTests: XCTestCase { + + var subscriptionManager: SubscriptionManagerMock! + var apiService: MockAPIService! + + override func setUpWithError() throws { + subscriptionManager = SubscriptionManagerMock() + apiService = MockAPIService() + } + + override func tearDownWithError() throws { + subscriptionManager = nil + apiService = nil + } + enum Error: String, Swift.Error { case generic } @@ -32,8 +46,9 @@ final class UnifiedFeedbackFormViewModelTests: XCTestCase { func testWhenCreatingViewModel_ThenInitialStateIsFeedbackPending() throws { let collector = MockVPNMetadataCollector() let sender = MockVPNFeedbackSender() - let viewModel = UnifiedFeedbackFormViewModel(subscriptionManager: SubscriptionManagerMock(), - apiService: MockAPIService(apiResponse: .failure(Error.generic)), + + let viewModel = UnifiedFeedbackFormViewModel(subscriptionManager: subscriptionManager, + apiService: apiService, vpnMetadataCollector: collector, feedbackSender: sender) @@ -43,8 +58,8 @@ final class UnifiedFeedbackFormViewModelTests: XCTestCase { func testGivenNoEmail_WhenSendingFeedbackSucceeds_ThenFeedbackIsSent() async throws { let collector = MockVPNMetadataCollector() let sender = MockVPNFeedbackSender() - let viewModel = UnifiedFeedbackFormViewModel(subscriptionManager: SubscriptionManagerMock(), - apiService: MockAPIService(apiResponse: .failure(Error.generic)), + let viewModel = UnifiedFeedbackFormViewModel(subscriptionManager: subscriptionManager, + apiService: apiService, vpnMetadataCollector: collector, feedbackSender: sender) viewModel.selectedReportType = UnifiedFeedbackReportType.reportIssue.rawValue @@ -60,10 +75,16 @@ final class UnifiedFeedbackFormViewModelTests: XCTestCase { func testGivenEmail_WhenSendingFeedbackSucceeds_ThenFeedbackIsSent() async throws { let collector = MockVPNMetadataCollector() let sender = MockVPNFeedbackSender() + + // API request to mock let payload = UnifiedFeedbackFormViewModel.Response(message: "something", error: nil) let response = APIResponseV2(data: try! JSONEncoder().encode(payload), httpResponse: HTTPURLResponse()) - let viewModel = UnifiedFeedbackFormViewModel(subscriptionManager: SubscriptionManagerMock(), - apiService: MockAPIService(apiResponse: .success(response)), + + apiService.set(response: response, forRequestURL: UnifiedFeedbackFormViewModel.feedbackEndpoint) + + subscriptionManager.resultTokenContainer = OAuthTokensFactory.makeValidTokenContainerWithEntitlements() + let viewModel = UnifiedFeedbackFormViewModel(subscriptionManager: subscriptionManager, + apiService: apiService, vpnMetadataCollector: collector, feedbackSender: sender) viewModel.selectedReportType = UnifiedFeedbackReportType.reportIssue.rawValue @@ -80,8 +101,8 @@ final class UnifiedFeedbackFormViewModelTests: XCTestCase { func testWhenSendingFeedbackFails_ThenFeedbackIsNotSent() async throws { let collector = MockVPNMetadataCollector() let sender = MockVPNFeedbackSender() - let viewModel = UnifiedFeedbackFormViewModel(subscriptionManager: SubscriptionManagerMock(), - apiService: MockAPIService(apiResponse: .failure(Error.generic)), + let viewModel = UnifiedFeedbackFormViewModel(subscriptionManager: subscriptionManager, + apiService: apiService, vpnMetadataCollector: collector, feedbackSender: sender) viewModel.selectedReportType = UnifiedFeedbackReportType.reportIssue.rawValue @@ -98,8 +119,8 @@ final class UnifiedFeedbackFormViewModelTests: XCTestCase { func testGivenInvalidEmail_WhenSendingFeedbackFails_ThenFeedbackIsNotSent() async throws { let collector = MockVPNMetadataCollector() let sender = MockVPNFeedbackSender() - let viewModel = UnifiedFeedbackFormViewModel(subscriptionManager: SubscriptionManagerMock(), - apiService: MockAPIService(apiResponse: .failure(Error.generic)), + let viewModel = UnifiedFeedbackFormViewModel(subscriptionManager: subscriptionManager, + apiService: apiService, vpnMetadataCollector: collector, feedbackSender: sender) viewModel.selectedReportType = UnifiedFeedbackReportType.reportIssue.rawValue @@ -117,8 +138,8 @@ final class UnifiedFeedbackFormViewModelTests: XCTestCase { func testGivenValidEmail_WhenSendingFeedbackFails_ThenFeedbackIsNotSent() async throws { let collector = MockVPNMetadataCollector() let sender = MockVPNFeedbackSender() - let viewModel = UnifiedFeedbackFormViewModel(subscriptionManager: SubscriptionManagerMock(), - apiService: MockAPIService(apiResponse: .failure(Error.generic)), + let viewModel = UnifiedFeedbackFormViewModel(subscriptionManager: subscriptionManager, + apiService: apiService, vpnMetadataCollector: collector, feedbackSender: sender) viewModel.selectedReportType = UnifiedFeedbackReportType.reportIssue.rawValue @@ -137,8 +158,8 @@ final class UnifiedFeedbackFormViewModelTests: XCTestCase { let collector = MockVPNMetadataCollector() let sender = MockVPNFeedbackSender() let delegate = MockVPNFeedbackFormViewModelDelegate() - let viewModel = UnifiedFeedbackFormViewModel(subscriptionManager: SubscriptionManagerMock(), - apiService: MockAPIService(apiResponse: .failure(Error.generic)), + let viewModel = UnifiedFeedbackFormViewModel(subscriptionManager: subscriptionManager, + apiService: apiService, vpnMetadataCollector: collector, feedbackSender: sender) viewModel.delegate = delegate @@ -271,25 +292,3 @@ private class MockVPNFeedbackFormViewModelDelegate: UnifiedFeedbackFormViewModel } } - -extension MockAPIService { - convenience init(apiResponse: Result) { - self.init { _ in apiResponse } - } -} - -extension SubscriptionManagerMock { - - convenience init() { - let accountManager = AccountManagerMock() - accountManager.accessToken = "token" - self.init(accountManager: accountManager, - subscriptionEndpointService: SubscriptionEndpointServiceMock(), - authEndpointService: AuthEndpointServiceMock(), - storePurchaseManager: StorePurchaseManagerMock(), - currentEnvironment: SubscriptionEnvironment(serviceEnvironment: .production, - purchasePlatform: .appStore), - canPurchase: false, - subscriptionFeatureMappingCache: SubscriptionFeatureMappingCacheMock()) - } -} diff --git a/fastlane/README.md b/fastlane/README.md index 95cba385c1..1d4f0ea94c 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -69,7 +69,7 @@ Makes App Store release build and uploads it to TestFlight [bundle exec] fastlane mac promote_latest_testflight_to_appstore ``` -Promotes the latest testflight build to appstore without submitting for review +Promotes the latest TestFlight build to App Store without submitting for review ### mac release_appstore @@ -135,6 +135,14 @@ Updates embedded files and pushes to remote. Executes the release preparation work in the repository +### mac create_keychain_ui_tests + +```sh +[bundle exec] fastlane mac create_keychain_ui_tests +``` + +Creates a new Kechain to use on UI tests + ---- This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.