diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index a3f3ca0348..8a62e8e494 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -778,17 +778,24 @@ D64648AD2B59936B0033090B /* SubscriptionEmailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64648AC2B59936B0033090B /* SubscriptionEmailView.swift */; }; D64648AF2B5993890033090B /* SubscriptionEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64648AE2B5993890033090B /* SubscriptionEmailViewModel.swift */; }; D652498E2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D652498D2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift */; }; + D65CEA702B6AC6C9008A759B /* Subscription.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D65CEA6F2B6AC6C9008A759B /* Subscription.xcassets */; }; D664C7B62B289AA200CBFA76 /* SubscriptionFlowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664C7942B289AA000CBFA76 /* SubscriptionFlowViewModel.swift */; }; D664C7B72B289AA200CBFA76 /* Subscription.storekit in Resources */ = {isa = PBXBuildFile; fileRef = D664C7952B289AA000CBFA76 /* Subscription.storekit */; }; D664C7B92B289AA200CBFA76 /* WKUserContentController+Handler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664C7982B289AA000CBFA76 /* WKUserContentController+Handler.swift */; }; D664C7C72B289AA200CBFA76 /* PurchaseInProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664C7AD2B289AA000CBFA76 /* PurchaseInProgressView.swift */; }; D664C7C82B289AA200CBFA76 /* SubscriptionFlowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664C7AE2B289AA000CBFA76 /* SubscriptionFlowView.swift */; }; - D664C7C92B289AA200CBFA76 /* HeadlessWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664C7AF2B289AA000CBFA76 /* HeadlessWebView.swift */; }; + D664C7C92B289AA200CBFA76 /* AsyncHeadlessWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664C7AF2B289AA000CBFA76 /* AsyncHeadlessWebView.swift */; }; D664C7CC2B289AA200CBFA76 /* SubscriptionPagesUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664C7B32B289AA000CBFA76 /* SubscriptionPagesUserScript.swift */; }; D664C7CE2B289AA200CBFA76 /* SubscriptionPagesUseSubscriptionFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664C7B52B289AA000CBFA76 /* SubscriptionPagesUseSubscriptionFeature.swift */; }; D664C7DD2B28A02800CBFA76 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D664C7DC2B28A02800CBFA76 /* StoreKit.framework */; }; + D668D9252B693778008E2FF2 /* SubscriptionITPView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D668D9242B693778008E2FF2 /* SubscriptionITPView.swift */; }; + D668D9272B6937D2008E2FF2 /* SubscriptionITPViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D668D9262B6937D2008E2FF2 /* SubscriptionITPViewModel.swift */; }; + D668D9292B69681C008E2FF2 /* IdentityTheftRestorationPagesUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = D668D9282B69681C008E2FF2 /* IdentityTheftRestorationPagesUserScript.swift */; }; + D668D92B2B696840008E2FF2 /* IdentityTheftRestorationPagesFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = D668D92A2B696840008E2FF2 /* IdentityTheftRestorationPagesFeature.swift */; }; + D668D92D2B696945008E2FF2 /* Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = D668D92C2B696945008E2FF2 /* Subscription.swift */; }; D68DF81C2B58302E0023DBEA /* SubscriptionRestoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68DF81B2B58302E0023DBEA /* SubscriptionRestoreView.swift */; }; D68DF81E2B5830380023DBEA /* SubscriptionRestoreViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68DF81D2B5830380023DBEA /* SubscriptionRestoreViewModel.swift */; }; + D69DBB502B72B1D300156310 /* View+TopMostController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69DBB4F2B72B1D200156310 /* View+TopMostController.swift */; }; D69FBF762B28BE3600B505F1 /* SettingsSubscriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69FBF752B28BE3600B505F1 /* SettingsSubscriptionView.swift */; }; D6D12C9F2B291CA90054390C /* URL+Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12C8B2B291CA90054390C /* URL+Subscription.swift */; }; D6D12CA02B291CA90054390C /* SubscriptionPurchaseEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12C8C2B291CA90054390C /* SubscriptionPurchaseEnvironment.swift */; }; @@ -804,6 +811,8 @@ D6D12CAB2B291CAA0054390C /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12C9C2B291CA90054390C /* APIService.swift */; }; D6D12CAC2B291CAA0054390C /* AuthService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12C9D2B291CA90054390C /* AuthService.swift */; }; D6D12CAD2B291CAA0054390C /* PurchaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12C9E2B291CA90054390C /* PurchaseManager.swift */; }; + D6D95CE12B6D52DA00960317 /* RootPresentationMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D95CE02B6D52DA00960317 /* RootPresentationMode.swift */; }; + D6D95CE32B6D9F8800960317 /* AsyncHeadlessWebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D95CE22B6D9F8800960317 /* AsyncHeadlessWebViewModel.swift */; }; D6E83C122B1E6AB3006C8AFB /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E83C112B1E6AB3006C8AFB /* SettingsView.swift */; }; D6E83C2E2B1EA06E006C8AFB /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E83C2D2B1EA06E006C8AFB /* SettingsViewModel.swift */; }; D6E83C312B1EA309006C8AFB /* SettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E83C302B1EA309006C8AFB /* SettingsCell.swift */; }; @@ -822,6 +831,9 @@ D6E83C682B23B6A3006C8AFB /* FontSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E83C672B23B6A3006C8AFB /* FontSettings.swift */; }; D6F93E3C2B4FFA97004C268D /* SubscriptionDebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F93E3B2B4FFA97004C268D /* SubscriptionDebugViewController.swift */; }; D6F93E3E2B50A8A0004C268D /* SubscriptionSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F93E3D2B50A8A0004C268D /* SubscriptionSettingsView.swift */; }; + D6FEB8B12B7498A300C3615F /* HeadlessWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6FEB8B02B7498A300C3615F /* HeadlessWebView.swift */; }; + D6FEB8B32B74990D00C3615F /* HeadlessWebViewNavCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6FEB8B22B74990D00C3615F /* HeadlessWebViewNavCoordinator.swift */; }; + D6FEB8B52B74994000C3615F /* HeadlessWebViewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6FEB8B42B74994000C3615F /* HeadlessWebViewCoordinator.swift */; }; EA39B7E2268A1A35000C62CD /* privacy-reference-tests in Resources */ = {isa = PBXBuildFile; fileRef = EA39B7E1268A1A35000C62CD /* privacy-reference-tests */; }; EAB19EDA268963510015D3EA /* DomainMatchingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB19ED9268963510015D3EA /* DomainMatchingTests.swift */; }; EE0153E12A6EABE0002A8B26 /* NetworkProtectionConvenienceInitialisers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0153E02A6EABE0002A8B26 /* NetworkProtectionConvenienceInitialisers.swift */; }; @@ -2430,17 +2442,24 @@ D64648AC2B59936B0033090B /* SubscriptionEmailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionEmailView.swift; sourceTree = ""; }; D64648AE2B5993890033090B /* SubscriptionEmailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionEmailViewModel.swift; sourceTree = ""; }; D652498D2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionSettingsViewModel.swift; sourceTree = ""; }; + D65CEA6F2B6AC6C9008A759B /* Subscription.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Subscription.xcassets; sourceTree = ""; }; D664C7942B289AA000CBFA76 /* SubscriptionFlowViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionFlowViewModel.swift; sourceTree = ""; }; D664C7952B289AA000CBFA76 /* Subscription.storekit */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Subscription.storekit; sourceTree = ""; }; D664C7982B289AA000CBFA76 /* WKUserContentController+Handler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "WKUserContentController+Handler.swift"; sourceTree = ""; }; D664C7AD2B289AA000CBFA76 /* PurchaseInProgressView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PurchaseInProgressView.swift; sourceTree = ""; }; D664C7AE2B289AA000CBFA76 /* SubscriptionFlowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionFlowView.swift; sourceTree = ""; }; - D664C7AF2B289AA000CBFA76 /* HeadlessWebView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeadlessWebView.swift; sourceTree = ""; }; + D664C7AF2B289AA000CBFA76 /* AsyncHeadlessWebView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AsyncHeadlessWebView.swift; sourceTree = ""; }; D664C7B32B289AA000CBFA76 /* SubscriptionPagesUserScript.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionPagesUserScript.swift; sourceTree = ""; }; D664C7B52B289AA000CBFA76 /* SubscriptionPagesUseSubscriptionFeature.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionPagesUseSubscriptionFeature.swift; sourceTree = ""; }; D664C7DC2B28A02800CBFA76 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; + D668D9242B693778008E2FF2 /* SubscriptionITPView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionITPView.swift; sourceTree = ""; }; + D668D9262B6937D2008E2FF2 /* SubscriptionITPViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionITPViewModel.swift; sourceTree = ""; }; + D668D9282B69681C008E2FF2 /* IdentityTheftRestorationPagesUserScript.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentityTheftRestorationPagesUserScript.swift; sourceTree = ""; }; + D668D92A2B696840008E2FF2 /* IdentityTheftRestorationPagesFeature.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentityTheftRestorationPagesFeature.swift; sourceTree = ""; }; + D668D92C2B696945008E2FF2 /* Subscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Subscription.swift; sourceTree = ""; }; D68DF81B2B58302E0023DBEA /* SubscriptionRestoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionRestoreView.swift; sourceTree = ""; }; D68DF81D2B5830380023DBEA /* SubscriptionRestoreViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionRestoreViewModel.swift; sourceTree = ""; }; + D69DBB4F2B72B1D200156310 /* View+TopMostController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+TopMostController.swift"; sourceTree = ""; }; D69FBF752B28BE3600B505F1 /* SettingsSubscriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSubscriptionView.swift; sourceTree = ""; }; D6D12C8B2B291CA90054390C /* URL+Subscription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URL+Subscription.swift"; sourceTree = ""; }; D6D12C8C2B291CA90054390C /* SubscriptionPurchaseEnvironment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionPurchaseEnvironment.swift; sourceTree = ""; }; @@ -2456,6 +2475,8 @@ D6D12C9C2B291CA90054390C /* APIService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = ""; }; D6D12C9D2B291CA90054390C /* AuthService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthService.swift; sourceTree = ""; }; D6D12C9E2B291CA90054390C /* PurchaseManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PurchaseManager.swift; sourceTree = ""; }; + D6D95CE02B6D52DA00960317 /* RootPresentationMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootPresentationMode.swift; sourceTree = ""; }; + D6D95CE22B6D9F8800960317 /* AsyncHeadlessWebViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncHeadlessWebViewModel.swift; sourceTree = ""; }; D6E83C112B1E6AB3006C8AFB /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; D6E83C2D2B1EA06E006C8AFB /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; D6E83C302B1EA309006C8AFB /* SettingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsCell.swift; sourceTree = ""; }; @@ -2474,6 +2495,9 @@ D6E83C672B23B6A3006C8AFB /* FontSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontSettings.swift; sourceTree = ""; }; D6F93E3B2B4FFA97004C268D /* SubscriptionDebugViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionDebugViewController.swift; sourceTree = ""; }; D6F93E3D2B50A8A0004C268D /* SubscriptionSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionSettingsView.swift; sourceTree = ""; }; + D6FEB8B02B7498A300C3615F /* HeadlessWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlessWebView.swift; sourceTree = ""; }; + D6FEB8B22B74990D00C3615F /* HeadlessWebViewNavCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlessWebViewNavCoordinator.swift; sourceTree = ""; }; + D6FEB8B42B74994000C3615F /* HeadlessWebViewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlessWebViewCoordinator.swift; sourceTree = ""; }; EA39B7E1268A1A35000C62CD /* privacy-reference-tests */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = "privacy-reference-tests"; path = "submodules/privacy-reference-tests"; sourceTree = SOURCE_ROOT; }; EAB19ED9268963510015D3EA /* DomainMatchingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DomainMatchingTests.swift; sourceTree = ""; }; EE0153E02A6EABE0002A8B26 /* NetworkProtectionConvenienceInitialisers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionConvenienceInitialisers.swift; sourceTree = ""; }; @@ -4519,12 +4543,14 @@ D664C7922B289AA000CBFA76 /* Subscription */ = { isa = PBXGroup; children = ( + D6D95CE42B6DA3F200960317 /* AsyncHeadlessWebview */, D664C7952B289AA000CBFA76 /* Subscription.storekit */, D664C7932B289AA000CBFA76 /* ViewModel */, D664C7AC2B289AA000CBFA76 /* Views */, D664C7B02B289AA000CBFA76 /* UserScripts */, D664C7962B289AA000CBFA76 /* Extensions */, D6D12C8A2B291CA90054390C /* Subscription */, + D65CEA6F2B6AC6C9008A759B /* Subscription.xcassets */, ); path = Subscription; sourceTree = ""; @@ -4534,6 +4560,7 @@ children = ( D664C7942B289AA000CBFA76 /* SubscriptionFlowViewModel.swift */, D68DF81D2B5830380023DBEA /* SubscriptionRestoreViewModel.swift */, + D668D9262B6937D2008E2FF2 /* SubscriptionITPViewModel.swift */, D64648AE2B5993890033090B /* SubscriptionEmailViewModel.swift */, D652498D2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift */, ); @@ -4544,6 +4571,7 @@ isa = PBXGroup; children = ( D664C7982B289AA000CBFA76 /* WKUserContentController+Handler.swift */, + D69DBB4F2B72B1D200156310 /* View+TopMostController.swift */, ); path = Extensions; sourceTree = ""; @@ -4551,12 +4579,13 @@ D664C7AC2B289AA000CBFA76 /* Views */ = { isa = PBXGroup; children = ( - D664C7AF2B289AA000CBFA76 /* HeadlessWebView.swift */, D664C7AD2B289AA000CBFA76 /* PurchaseInProgressView.swift */, D664C7AE2B289AA000CBFA76 /* SubscriptionFlowView.swift */, - D6F93E3D2B50A8A0004C268D /* SubscriptionSettingsView.swift */, D68DF81B2B58302E0023DBEA /* SubscriptionRestoreView.swift */, D64648AC2B59936B0033090B /* SubscriptionEmailView.swift */, + D668D9242B693778008E2FF2 /* SubscriptionITPView.swift */, + D6F93E3D2B50A8A0004C268D /* SubscriptionSettingsView.swift */, + D6D95CE02B6D52DA00960317 /* RootPresentationMode.swift */, ); path = Views; sourceTree = ""; @@ -4564,7 +4593,10 @@ D664C7B02B289AA000CBFA76 /* UserScripts */ = { isa = PBXGroup; children = ( + D668D92C2B696945008E2FF2 /* Subscription.swift */, + D668D9282B69681C008E2FF2 /* IdentityTheftRestorationPagesUserScript.swift */, D664C7B32B289AA000CBFA76 /* SubscriptionPagesUserScript.swift */, + D668D92A2B696840008E2FF2 /* IdentityTheftRestorationPagesFeature.swift */, D664C7B52B289AA000CBFA76 /* SubscriptionPagesUseSubscriptionFeature.swift */, ); path = UserScripts; @@ -4623,6 +4655,18 @@ path = Services; sourceTree = ""; }; + D6D95CE42B6DA3F200960317 /* AsyncHeadlessWebview */ = { + isa = PBXGroup; + children = ( + D6D95CE22B6D9F8800960317 /* AsyncHeadlessWebViewModel.swift */, + D664C7AF2B289AA000CBFA76 /* AsyncHeadlessWebView.swift */, + D6FEB8B02B7498A300C3615F /* HeadlessWebView.swift */, + D6FEB8B22B74990D00C3615F /* HeadlessWebViewNavCoordinator.swift */, + D6FEB8B42B74994000C3615F /* HeadlessWebViewCoordinator.swift */, + ); + path = AsyncHeadlessWebview; + sourceTree = ""; + }; D6E83C322B1F1279006C8AFB /* Sections */ = { isa = PBXGroup; children = ( @@ -6095,6 +6139,7 @@ AA4D6A9423DE49A5007E8790 /* AppIconBlack29x29@2x.png in Resources */, 98B001B3251EABB40090EC07 /* InfoPlist.strings in Resources */, AA4D6ACE23DE4D27007E8790 /* AppIconPurple60x60@3x.png in Resources */, + D65CEA702B6AC6C9008A759B /* Subscription.xcassets in Resources */, F1E4A4451EE89460006F2EAE /* Bookmarks.storyboard in Resources */, AA4D6ABB23DE4D15007E8790 /* AppIconYellow40x40@2x.png in Resources */, 84E341A01E2F7EFB00BDBA6F /* LaunchScreen.storyboard in Resources */, @@ -6475,6 +6520,7 @@ files = ( EE4FB1862A28CE7200E5CBA7 /* NetworkProtectionStatusView.swift in Sources */, C17B59592A03AAD30055F2D1 /* PasswordGenerationPromptViewModel.swift in Sources */, + D6FEB8B52B74994000C3615F /* HeadlessWebViewCoordinator.swift in Sources */, 8528AE81212F15D600D0BD74 /* AppRatingPrompt.xcdatamodeld in Sources */, 1E24295E293F57FA00584836 /* LottieView.swift in Sources */, 8577A1C5255D2C0D00D43FCD /* HitTestingToolbar.swift in Sources */, @@ -6505,6 +6551,7 @@ 8590CB69268A4E190089F6BF /* DebugEtagStorage.swift in Sources */, D6D12CA62B291CAA0054390C /* AppStoreRestoreFlow.swift in Sources */, C1CDA3162AFB9C7F006D1476 /* AutofillNeverPromptWebsitesManager.swift in Sources */, + D668D9272B6937D2008E2FF2 /* SubscriptionITPViewModel.swift in Sources */, F1CA3C371F045878005FADB3 /* PrivacyStore.swift in Sources */, 37FCAAC029930E26000E420A /* FailedAssertionView.swift in Sources */, 4BBBBA922B03291700D965DA /* VPNWaitlistUserText.swift in Sources */, @@ -6516,6 +6563,7 @@ 9874F9EE2187AFCE00CAF33D /* Themable.swift in Sources */, F44D279E27F331BB0037F371 /* AutofillLoginPromptViewModel.swift in Sources */, 3151F0F02735802800226F58 /* VoiceSearchViewController.swift in Sources */, + D6FEB8B32B74990D00C3615F /* HeadlessWebViewNavCoordinator.swift in Sources */, 85BDC310243359040053DB07 /* FindInPageUserScript.swift in Sources */, F1DE78581E5CAE350058895A /* TabViewGridCell.swift in Sources */, 984D035824ACCC6F0066CFB8 /* TabViewListCell.swift in Sources */, @@ -6545,7 +6593,7 @@ 85047C752A0D3C2900D2FF3F /* SyncSettingsViewController+Themable.swift in Sources */, F44D279F27F331BB0037F371 /* AutofillLoginPromptViewController.swift in Sources */, C1BF0BA529B63D7200482B73 /* AutofillLoginPromptHelper.swift in Sources */, - D664C7C92B289AA200CBFA76 /* HeadlessWebView.swift in Sources */, + D664C7C92B289AA200CBFA76 /* AsyncHeadlessWebView.swift in Sources */, F1F5337C1F26A9EF00D80D4F /* UserText.swift in Sources */, D6E83C5E2B224676006C8AFB /* SettingsCustomizeView.swift in Sources */, 1E8AD1C727BE9B2900ABA377 /* DownloadsListDataSource.swift in Sources */, @@ -6586,6 +6634,7 @@ EE276BEA2A77F823009167B6 /* NetworkProtectionRootViewController.swift in Sources */, 310ECFDD282A8BB0005029B3 /* EnableAutofillSettingsTableViewCell.swift in Sources */, 1E908BF329827C480008C8F3 /* AutoconsentManagement.swift in Sources */, + D6D95CE32B6D9F8800960317 /* AsyncHeadlessWebViewModel.swift in Sources */, CB9B8739278C8E72001F4906 /* WidgetEducationViewController.swift in Sources */, F4D9C4FA25117A0F00814B71 /* HomeMessageStorage.swift in Sources */, D69FBF762B28BE3600B505F1 /* SettingsSubscriptionView.swift in Sources */, @@ -6602,6 +6651,7 @@ CB258D1329A4F24E00DEBA24 /* ConfigurationStore.swift in Sources */, 85058370219F424500ED4EDB /* SearchBarExtension.swift in Sources */, 310D09212799FD1A00DC0060 /* MIMEType.swift in Sources */, + D668D9292B69681C008E2FF2 /* IdentityTheftRestorationPagesUserScript.swift in Sources */, D64648AD2B59936B0033090B /* SubscriptionEmailView.swift in Sources */, BD862E032B30DA170073E2EE /* VPNFeedbackFormViewModel.swift in Sources */, F4147354283BF834004AA7A5 /* AutofillContentScopeFeatureToggles.swift in Sources */, @@ -6609,6 +6659,7 @@ 31B524572715BB23002225AB /* WebJSAlert.swift in Sources */, 8536A1FD2ACF114B003AC5BA /* Theme+DesignSystem.swift in Sources */, F114C55B1E66EB020018F95F /* NibLoading.swift in Sources */, + D668D9252B693778008E2FF2 /* SubscriptionITPView.swift in Sources */, C10CB5F32A1A5BDF0048E503 /* AutofillViews.swift in Sources */, 982E5630222C3D5B008D861B /* FeedbackPickerViewController.swift in Sources */, 37FCAABC2992F592000E420A /* MultilineScrollableTextFix.swift in Sources */, @@ -6649,6 +6700,7 @@ F4F6DFB426E6B63700ED7E12 /* BookmarkFolderCell.swift in Sources */, D6F93E3E2B50A8A0004C268D /* SubscriptionSettingsView.swift in Sources */, 851B12CC22369931004781BC /* AtbAndVariantCleanup.swift in Sources */, + D668D92B2B696840008E2FF2 /* IdentityTheftRestorationPagesFeature.swift in Sources */, 85F2FFCF2211F8E5006BB258 /* TabSwitcherViewController+KeyCommands.swift in Sources */, 3157B43327F497E90042D3D7 /* SaveLoginView.swift in Sources */, F17922E01E71BB59006E3D97 /* AutocompleteViewControllerDelegate.swift in Sources */, @@ -6701,6 +6753,7 @@ 31C70B5528045E3500FB6AD1 /* SecureVaultErrorReporter.swift in Sources */, F4CE6D1B257EA33C00D0A6AA /* FireButtonAnimator.swift in Sources */, 85582E0029D7409700E9AE35 /* SyncSettingsViewController+PDFRendering.swift in Sources */, + D69DBB502B72B1D300156310 /* View+TopMostController.swift in Sources */, EE0153EF2A70021E002A8B26 /* NetworkProtectionInviteView.swift in Sources */, 9888F77B2224980500C46159 /* FeedbackViewController.swift in Sources */, D6E83C662B23936F006C8AFB /* SettingsDebugView.swift in Sources */, @@ -6783,6 +6836,7 @@ 980891A222369ADB00313A70 /* FeedbackUserText.swift in Sources */, 4BCD14692B05BDD5000B1E4C /* AppDelegate+Waitlists.swift in Sources */, 988F3DD3237DE8D900AEE34C /* ForgetDataAlert.swift in Sources */, + D6FEB8B12B7498A300C3615F /* HeadlessWebView.swift in Sources */, 850ABD012AC3961100A733DF /* MainViewController+Segues.swift in Sources */, 9817C9C321EF594700884F65 /* AutoClear.swift in Sources */, 9821234E2B6D0A6300F08C57 /* UserAuthenticator.swift in Sources */, @@ -6883,11 +6937,13 @@ D6E83C682B23B6A3006C8AFB /* FontSettings.swift in Sources */, 31EF52E1281B3BDC0034796E /* AutofillLoginListItemViewModel.swift in Sources */, 1E4FAA6627D8DFC800ADC5B3 /* CompleteDownloadRowViewModel.swift in Sources */, + D6D95CE12B6D52DA00960317 /* RootPresentationMode.swift in Sources */, 83004E862193E5ED00DA013C /* TabViewControllerBrowsingMenuExtension.swift in Sources */, EE01EB402AFBD0000096AAC9 /* NetworkProtectionVPNSettingsViewModel.swift in Sources */, EE72CA852A862D000043B5B3 /* NetworkProtectionDebugViewController.swift in Sources */, C18ED43A2AB6F77600BF3805 /* AutofillSettingsEnableFooterView.swift in Sources */, CB84C7BD29A3EF530088A5B8 /* AppConfigurationURLProvider.swift in Sources */, + D668D92D2B696945008E2FF2 /* Subscription.swift in Sources */, AA3D854723D9E88E00788410 /* AppIconSettingsCell.swift in Sources */, 316931D927BD22A80095F5ED /* DownloadActionMessageViewHelper.swift in Sources */, 9838059F2228208E00385F1A /* PositiveFeedbackViewController.swift in Sources */, diff --git a/DuckDuckGo/MainViewController+Segues.swift b/DuckDuckGo/MainViewController+Segues.swift index 0d994f5699..94b4be4591 100644 --- a/DuckDuckGo/MainViewController+Segues.swift +++ b/DuckDuckGo/MainViewController+Segues.swift @@ -244,8 +244,6 @@ extension MainViewController { let navController = UINavigationController(rootViewController: settingsController) navController.applyTheme(ThemeManager.shared.currentTheme) settingsController.modalPresentationStyle = .automatic - - settingsController.isModalInPresentation = true present(navController, animated: true) { completion?(settingsViewModel) diff --git a/DuckDuckGo/SettingsSubscriptionView.swift b/DuckDuckGo/SettingsSubscriptionView.swift index 11e681147d..b74e57c6b9 100644 --- a/DuckDuckGo/SettingsSubscriptionView.swift +++ b/DuckDuckGo/SettingsSubscriptionView.swift @@ -25,6 +25,10 @@ import UIKit struct SettingsSubscriptionView: View { @EnvironmentObject var viewModel: SettingsViewModel + @StateObject var subscriptionFlowViewModel = SubscriptionFlowViewModel() + @State var isShowingsubScriptionFlow = false + @State var isShowingDBP = false + @State var isShowingITP = false private var subscriptionDescriptionView: some View { VStack(alignment: .leading) { @@ -51,11 +55,11 @@ struct SettingsSubscriptionView: View { private var purchaseSubscriptionView: some View { return Group { SettingsCustomCell(content: { subscriptionDescriptionView }) - let viewModel = SubscriptionFlowViewModel(onFeatureSelected: { value in - self.viewModel.onAppearNavigationTarget = value - }) - NavigationLink(destination: SubscriptionFlowView(viewModel: viewModel)) { - SettingsCustomCell(content: { learnMoreView }) + SettingsCustomCell(content: { learnMoreView }, + action: { isShowingsubScriptionFlow = true }, + isButton: true ) + .sheet(isPresented: $isShowingsubScriptionFlow) { + SubscriptionFlowView(viewModel: subscriptionFlowViewModel).interactiveDismissDisabled() } } } @@ -68,17 +72,24 @@ struct SettingsSubscriptionView: View { disclosureIndicator: true, isButton: true) + /* NavigationLink(destination: Text("Data Broker Protection"), isActive: $viewModel.shouldNavigateToDBP) { SettingsCellView(label: UserText.settingsPProDBPTitle, subtitle: UserText.settingsPProDBPSubTitle) } + */ - NavigationLink(destination: Text("Identity Theft Restoration"), isActive: $viewModel.shouldNavigateToITP) { - SettingsCellView(label: UserText.settingsPProITRTitle, subtitle: UserText.settingsPProITRSubTitle) + SettingsCellView(label: UserText.settingsPProITRTitle, + subtitle: UserText.settingsPProITRSubTitle, + action: { isShowingITP.toggle() }, isButton: true) + .sheet(isPresented: $isShowingITP) { + SubscriptionITPView() } + - NavigationLink(destination: SubscriptionSettingsView(viewModel: SubscriptionSettingsViewModel())) { + NavigationLink(destination: SubscriptionSettingsView()) { SettingsCustomCell(content: { manageSubscriptionView }) } + } } @@ -91,6 +102,18 @@ struct SettingsSubscriptionView: View { } else { purchaseSubscriptionView } + + } + // Refresh subscription when dismissing the Subscription Flow + .onChange(of: isShowingsubScriptionFlow, perform: { value in + if !value { + Task { viewModel.onAppear() } + } + }) + + .onReceive(subscriptionFlowViewModel.$selectedFeature) { value in + guard let value else { return } + viewModel.onAppearNavigationTarget = value } } } diff --git a/DuckDuckGo/SettingsViewModel.swift b/DuckDuckGo/SettingsViewModel.swift index 899b4eef6f..c379365941 100644 --- a/DuckDuckGo/SettingsViewModel.swift +++ b/DuckDuckGo/SettingsViewModel.swift @@ -91,7 +91,7 @@ final class SettingsViewModel: ObservableObject { // Used to automatically navigate on Appear to a specific section enum SettingsSection: String { - case none, netP, dbp, itp + case none, netP, dbp, itr } @Published var onAppearNavigationTarget: SettingsSection @@ -422,13 +422,13 @@ extension SettingsViewModel { private func navigateOnAppear() { // We need a short delay to let the SwifttUI view lifecycle complete // Otherwise the transition can be inconsistent - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) { switch self.onAppearNavigationTarget { case .netP: self.presentLegacyView(.netP) case .dbp: self.shouldNavigateToDBP = true - case .itp: + case .itr: self.shouldNavigateToITP = true default: break diff --git a/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebView.swift b/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebView.swift new file mode 100644 index 0000000000..74b5224f9d --- /dev/null +++ b/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebView.swift @@ -0,0 +1,64 @@ +// +// AsyncHeadlessWebView.swift +// DuckDuckGo +// +// Copyright © 2023 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 WebKit +import UserScript +import SwiftUI +import DesignResourcesKit +import Core + +struct AsyncHeadlessWebViewSettings { + let bounces: Bool + + init(bounces: Bool = false) { + self.bounces = bounces + } +} + +struct AsyncHeadlessWebView: View { + @StateObject var viewModel: AsyncHeadlessWebViewViewModel + + var body: some View { + GeometryReader { geometry in + HeadlessWebView( + userScript: viewModel.userScript, + subFeature: viewModel.subFeature, + settings: viewModel.settings, + onScroll: { newPosition in + viewModel.updateScrollPosition(newPosition) + }, + onURLChange: { newURL in + viewModel.url = newURL + }, + onCanGoBack: { value in + viewModel.canGoBack = value + }, + onCanGoForward: { value in + viewModel.canGoForward = value + }, + onContentType: { value in + viewModel.contentType = value + }, + navigationCoordinator: viewModel.navigationCoordinator + ) + .frame(width: geometry.size.width, height: geometry.size.height) + } + } +} diff --git a/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebViewModel.swift b/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebViewModel.swift new file mode 100644 index 0000000000..304908891c --- /dev/null +++ b/DuckDuckGo/Subscription/AsyncHeadlessWebview/AsyncHeadlessWebViewModel.swift @@ -0,0 +1,66 @@ +// +// AsyncHeadlessWebViewModel.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UserScript +import Core +import Combine + +final class AsyncHeadlessWebViewViewModel: ObservableObject { + let userScript: UserScriptMessaging? + let subFeature: Subfeature? + let settings: AsyncHeadlessWebViewSettings + + private var initialScrollPositionSubject = PassthroughSubject() + private var subsequentScrollPositionSubject = PassthroughSubject() + private var cancellables = Set() + private var isFirstUpdate = true + private var initialDelay = 1 + + @Published var scrollPosition: CGPoint = .zero + @Published var url: URL? + @Published var canGoBack: Bool = false + @Published var canGoForward: Bool = false + @Published var contentType: String = "" + + var navigationCoordinator = HeadlessWebViewNavCoordinator(webView: nil) + + init(userScript: UserScriptMessaging?, subFeature: Subfeature?, settings: AsyncHeadlessWebViewSettings) { + self.userScript = userScript + self.subFeature = subFeature + self.settings = settings + + // Delayed publishing first update for scrollPosition + // To avoid publishing events on view updates + initialScrollPositionSubject + .delay(for: .seconds(initialDelay), scheduler: RunLoop.main) + .merge(with: subsequentScrollPositionSubject) + .assign(to: &$scrollPosition) + } + + func updateScrollPosition(_ newPosition: CGPoint) { + if isFirstUpdate { + initialScrollPositionSubject.send(newPosition) + isFirstUpdate = false + } else { + subsequentScrollPositionSubject.send(newPosition) + } + } + + +} diff --git a/DuckDuckGo/Subscription/AsyncHeadlessWebview/HeadlessWebView.swift b/DuckDuckGo/Subscription/AsyncHeadlessWebview/HeadlessWebView.swift new file mode 100644 index 0000000000..5aa39ae262 --- /dev/null +++ b/DuckDuckGo/Subscription/AsyncHeadlessWebview/HeadlessWebView.swift @@ -0,0 +1,81 @@ +// +// HeadlessWebView.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftUI +import WebKit +import UserScript + +struct HeadlessWebView: UIViewRepresentable { + let userScript: UserScriptMessaging? + let subFeature: Subfeature? + let settings: AsyncHeadlessWebViewSettings + var onScroll: ((CGPoint) -> Void)? + var onURLChange: ((URL) -> Void)? + var onCanGoBack: ((Bool) -> Void)? + var onCanGoForward: ((Bool) -> Void)? + var onContentType: ((String) -> Void)? + var navigationCoordinator: HeadlessWebViewNavCoordinator + + + func makeUIView(context: Context) -> WKWebView { + let configuration = WKWebViewConfiguration() + configuration.userContentController = makeUserContentController() + + let webView = WKWebView(frame: .zero, configuration: configuration) + + navigationCoordinator.webView = webView + webView.uiDelegate = context.coordinator + webView.scrollView.delegate = context.coordinator + webView.scrollView.bounces = settings.bounces + webView.navigationDelegate = context.coordinator + +#if DEBUG + if #available(iOS 16.4, *) { + webView.isInspectable = true + } +#endif + + context.coordinator.setupWebViewObservation(webView) + return webView + } + + func updateUIView(_ uiView: WKWebView, context: Context) {} + + func makeCoordinator() -> HeadlessWebViewCoordinator { + HeadlessWebViewCoordinator(self, + onScroll: onScroll, + onURLChange: onURLChange, + onCanGoBack: onCanGoBack, + onCanGoForward: onCanGoForward, + onContentType: onContentType) + } + + @MainActor + private func makeUserContentController() -> WKUserContentController { + let userContentController = WKUserContentController() + if let userScript, let subFeature { + userContentController.addUserScript(userScript.makeWKUserScriptSync()) + userContentController.addHandler(userScript) + userScript.registerSubfeature(delegate: subFeature) + } + return userContentController + } + +} diff --git a/DuckDuckGo/Subscription/AsyncHeadlessWebview/HeadlessWebViewCoordinator.swift b/DuckDuckGo/Subscription/AsyncHeadlessWebview/HeadlessWebViewCoordinator.swift new file mode 100644 index 0000000000..a2fb375dc7 --- /dev/null +++ b/DuckDuckGo/Subscription/AsyncHeadlessWebview/HeadlessWebViewCoordinator.swift @@ -0,0 +1,138 @@ +// +// HeadlessWebViewCoordinator.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import WebKit + +final class HeadlessWebViewCoordinator: NSObject { + var parent: HeadlessWebView + var onScroll: ((CGPoint) -> Void)? + var onURLChange: ((URL) -> Void)? + var onCanGoBack: ((Bool) -> Void)? + var onCanGoForward: ((Bool) -> Void)? + var onContentType: ((String) -> Void)? + + private var lastURL: URL? + + enum Constants { + static let contentTypeJS = "document.contentType" + static let externalSchemes = ["tel", "sms", "facetime"] + } + + private var webViewURLObservation: NSKeyValueObservation? + private var webViewCanGoBackObservation: NSKeyValueObservation? + private var webViewCanGoForwardObservation: NSKeyValueObservation? + + init(_ parent: HeadlessWebView, + onScroll: ((CGPoint) -> Void)?, + onURLChange: ((URL) -> Void)?, + onCanGoBack: ((Bool) -> Void)?, + onCanGoForward: ((Bool) -> Void)?, + onContentType: ((String) -> Void)?) { + self.parent = parent + self.onScroll = onScroll + self.onURLChange = onURLChange + self.onCanGoBack = onCanGoBack + self.onCanGoForward = onCanGoForward + self.onContentType = onContentType + } + + func setupWebViewObservation(_ webView: WKWebView) { + webViewURLObservation = webView.observe(\.url, options: [.new]) { [weak self] _, change in + if let newURL = change.newValue as? URL { + self?.onURLChange?(newURL) + self?.onCanGoBack?(webView.canGoBack) + } + } + + webViewCanGoBackObservation = webView.observe(\.canGoBack, options: [.new]) { [weak self] _, change in + if let canGoBack = change.newValue { + self?.onCanGoBack?(canGoBack) + } + } + + webViewCanGoForwardObservation = webView.observe(\.canGoForward, options: [.new]) { [weak self] _, change in + if let onCanGoForward = change.newValue { + self?.onCanGoForward?(onCanGoForward) + } + } + } + +} + +extension HeadlessWebViewCoordinator: WKUIDelegate {} + +extension HeadlessWebViewCoordinator: UIScrollViewDelegate { + func scrollViewDidScroll(_ scrollView: UIScrollView) { + let contentOffset = scrollView.contentOffset + onScroll?(contentOffset) + } +} + +extension HeadlessWebViewCoordinator: WKNavigationDelegate { + + func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? { + // Force all requests for new windows or frame to be loaded in the View Itself (No popups or new windows) + webView.load(navigationAction.request) + return nil + } + + func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { + if let url = webView.url, url != lastURL { + onURLChange?(url) + lastURL = url + if let onCanGoBack { + onCanGoBack(webView.canGoBack) + } + if let onCanGoForward { + onCanGoForward(webView.canGoForward) + } + } + } + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + webView.evaluateJavaScript(Constants.contentTypeJS) { result, error in + guard error == nil, let contentType = result as? String else { + return + } + self.onContentType?(contentType) + } + } + + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + guard let url = navigationAction.request.url else { + + decisionHandler(.allow) + return + } + + guard let scheme = url.scheme else { + decisionHandler(.cancel) + return + } + + if Constants.externalSchemes.contains(scheme) && UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url, options: [:], completionHandler: nil) + decisionHandler(.cancel) + } else { + decisionHandler(.allow) + } + } + +} diff --git a/DuckDuckGo/Subscription/AsyncHeadlessWebview/HeadlessWebViewNavCoordinator.swift b/DuckDuckGo/Subscription/AsyncHeadlessWebview/HeadlessWebViewNavCoordinator.swift new file mode 100644 index 0000000000..94f7695be9 --- /dev/null +++ b/DuckDuckGo/Subscription/AsyncHeadlessWebview/HeadlessWebViewNavCoordinator.swift @@ -0,0 +1,58 @@ +// +// HeadlessWebViewNavCoordinator.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import WebKit +import Core + +final class HeadlessWebViewNavCoordinator { + weak var webView: WKWebView? + + init(webView: WKWebView?) { + self.webView = webView + } + + func reload() async { + _ = await MainActor.run { + self.webView?.reload() + } + } + + func navigateTo(url: URL) { + guard let webView else { return } + DispatchQueue.main.asyncAfter(deadline: .now() + 0) { + DefaultUserAgentManager.shared.update(webView: webView, isDesktop: false, url: url) + webView.load(URLRequest(url: url)) + } + } + + func goBack() async { + guard await webView?.canGoBack == true else { return } + _ = await MainActor.run { + self.webView?.goBack() + } + } + + func goForward() async { + guard await webView?.canGoForward == true else { return } + _ = await MainActor.run { + self.webView?.goForward() + } + } +} diff --git a/DuckDuckGo/Subscription/Extensions/View+TopMostController.swift b/DuckDuckGo/Subscription/Extensions/View+TopMostController.swift new file mode 100644 index 0000000000..2d26949767 --- /dev/null +++ b/DuckDuckGo/Subscription/Extensions/View+TopMostController.swift @@ -0,0 +1,40 @@ +// +// View+TopMostController.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +extension View { + + // Grabs the topMost controller so we can properly present sheets anywhere + func topMostViewController() -> UIViewController? { + guard let keyWindow = UIApplication.shared.connectedScenes + .filter({ $0.activationState == .foregroundActive }) + .compactMap({ $0 as? UIWindowScene }) + .first?.windows + .filter({ $0.isKeyWindow }).first else { + return nil + } + + var topController = keyWindow.rootViewController + while let presentedController = topController?.presentedViewController { + topController = presentedController + } + return topController + } +} diff --git a/DuckDuckGo/Subscription/Subscription.xcassets/Contents.json b/DuckDuckGo/Subscription/Subscription.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/DuckDuckGo/Subscription/Subscription.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Subscription/Subscription.xcassets/ManageSubscriptionHero.imageset/Contents.json b/DuckDuckGo/Subscription/Subscription.xcassets/ManageSubscriptionHero.imageset/Contents.json new file mode 100644 index 0000000000..49070f6b76 --- /dev/null +++ b/DuckDuckGo/Subscription/Subscription.xcassets/ManageSubscriptionHero.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Sync-128.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/DuckDuckGo/Subscription/Subscription.xcassets/ManageSubscriptionHero.imageset/Sync-128.svg b/DuckDuckGo/Subscription/Subscription.xcassets/ManageSubscriptionHero.imageset/Sync-128.svg new file mode 100644 index 0000000000..c00e6ee0ed --- /dev/null +++ b/DuckDuckGo/Subscription/Subscription.xcassets/ManageSubscriptionHero.imageset/Sync-128.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/DuckDuckGo/Subscription/Subscription.xcassets/SubscriptionShareIcon.imageset/Contents.json b/DuckDuckGo/Subscription/Subscription.xcassets/SubscriptionShareIcon.imageset/Contents.json new file mode 100644 index 0000000000..9c8f34e6e8 --- /dev/null +++ b/DuckDuckGo/Subscription/Subscription.xcassets/SubscriptionShareIcon.imageset/Contents.json @@ -0,0 +1,25 @@ +{ + "images" : [ + { + "filename" : "share.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "share-dark.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/DuckDuckGo/Subscription/Subscription.xcassets/SubscriptionShareIcon.imageset/share-dark.pdf b/DuckDuckGo/Subscription/Subscription.xcassets/SubscriptionShareIcon.imageset/share-dark.pdf new file mode 100644 index 0000000000..4d647e1794 Binary files /dev/null and b/DuckDuckGo/Subscription/Subscription.xcassets/SubscriptionShareIcon.imageset/share-dark.pdf differ diff --git a/DuckDuckGo/Subscription/Subscription.xcassets/SubscriptionShareIcon.imageset/share.pdf b/DuckDuckGo/Subscription/Subscription.xcassets/SubscriptionShareIcon.imageset/share.pdf new file mode 100644 index 0000000000..cd5ff551f6 Binary files /dev/null and b/DuckDuckGo/Subscription/Subscription.xcassets/SubscriptionShareIcon.imageset/share.pdf differ diff --git a/DuckDuckGo/Subscription/Subscription/URL+Subscription.swift b/DuckDuckGo/Subscription/Subscription/URL+Subscription.swift index 2451ab5055..d8f35b7312 100644 --- a/DuckDuckGo/Subscription/Subscription/URL+Subscription.swift +++ b/DuckDuckGo/Subscription/Subscription/URL+Subscription.swift @@ -51,4 +51,9 @@ public extension URL { static var manageSubscriptionsIniOSAppStoreAppURL: URL { URL(string: "https://apps.apple.com/account/subscriptions")! } + + // MARK: - Identity Theft Protection + static var manageITP: URL { + URL(string: "https://abrown.duckduckgo.com/identity-theft-restoration")! + } } diff --git a/DuckDuckGo/Subscription/UserScripts/IdentityTheftRestorationPagesFeature.swift b/DuckDuckGo/Subscription/UserScripts/IdentityTheftRestorationPagesFeature.swift new file mode 100644 index 0000000000..28aa9881f0 --- /dev/null +++ b/DuckDuckGo/Subscription/UserScripts/IdentityTheftRestorationPagesFeature.swift @@ -0,0 +1,74 @@ +// +// IdentityTheftRestorationPagesFeature.swift +// DuckDuckGo +// +// Copyright © 2023 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. +// + +#if SUBSCRIPTION +import BrowserServicesKit +import Common +import Foundation +import WebKit +import UserScript +import Combine + +@available(iOS 15.0, *) +final class IdentityTheftRestorationPagesFeature: Subfeature, ObservableObject { + + struct Constants { + static let featureName = "useIdentityTheftRestoration" + static let os = "ios" + } + + struct OriginDomains { + static let duckduckgo = "duckduckgo.com" + static let abrown = "abrown.duckduckgo.com" + } + + struct Handlers { + static let getAccessToken = "getAccessToken" + } + + + var broker: UserScriptMessageBroker? + var featureName: String = Constants.featureName + + var messageOriginPolicy: MessageOriginPolicy = .only(rules: [ + .exact(hostname: OriginDomains.duckduckgo), + .exact(hostname: OriginDomains.abrown) + ]) + + var originalMessage: WKScriptMessage? + + func with(broker: UserScriptMessageBroker) { + self.broker = broker + } + + func handler(forMethodNamed methodName: String) -> Subfeature.Handler? { + switch methodName { + case Handlers.getAccessToken: return getAccessToken + default: + return nil + } + } + + func getAccessToken(params: Any, original: WKScriptMessage) async throws -> Encodable? { + let authToken = AccountManager().authToken ?? "" + return Subscription(token: authToken) + } + +} +#endif diff --git a/DuckDuckGo/Subscription/UserScripts/IdentityTheftRestorationPagesUserScript.swift b/DuckDuckGo/Subscription/UserScripts/IdentityTheftRestorationPagesUserScript.swift new file mode 100644 index 0000000000..cfbafec477 --- /dev/null +++ b/DuckDuckGo/Subscription/UserScripts/IdentityTheftRestorationPagesUserScript.swift @@ -0,0 +1,70 @@ +// +// IdentityTheftRestorationPagesUserScript.swift +// DuckDuckGo +// +// Copyright © 2023 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. +// + +#if SUBSCRIPTION + +import BrowserServicesKit +import Common +import Combine +import Foundation +import WebKit +import UserScript + +/// +/// The user script that will be the broker for all identity theft protection features +/// +public final class IdentityTheftRestorationPagesUserScript: NSObject, UserScript, UserScriptMessaging { + public var source: String = "" + + public static let context = "identityTheftRestorationPages" + + // special pages messaging cannot be isolated as we'll want regular page-scripts to be able to communicate + public let broker = UserScriptMessageBroker(context: IdentityTheftRestorationPagesUserScript.context, requiresRunInPageContentWorld: false ) + + public let messageNames: [String] = [ + IdentityTheftRestorationPagesUserScript.context + ] + + public let injectionTime: WKUserScriptInjectionTime = .atDocumentStart + public let forMainFrameOnly = true + public let requiresRunInPageContentWorld = true +} + +extension IdentityTheftRestorationPagesUserScript: WKScriptMessageHandlerWithReply { + @MainActor + public func userContentController(_ userContentController: WKUserContentController, + didReceive message: WKScriptMessage) async -> (Any?, String?) { + let action = broker.messageHandlerFor(message) + do { + let json = try await broker.execute(action: action, original: message) + return (json, nil) + } catch { + // forward uncaught errors to the client + return (nil, error.localizedDescription) + } + } +} + +extension IdentityTheftRestorationPagesUserScript: WKScriptMessageHandler { + public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + // unsupported + } +} + +#endif diff --git a/DuckDuckGo/Subscription/UserScripts/Subscription.swift b/DuckDuckGo/Subscription/UserScripts/Subscription.swift new file mode 100644 index 0000000000..9306a531c5 --- /dev/null +++ b/DuckDuckGo/Subscription/UserScripts/Subscription.swift @@ -0,0 +1,24 @@ +// +// Subscription.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +struct Subscription: Encodable { + let token: String +} diff --git a/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift b/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift index 3b85bd54b5..780b317fbe 100644 --- a/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift +++ b/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift @@ -103,10 +103,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec } } - struct Subscription: Encodable { - let token: String - } - + /// Values that the Frontend can use to determine the current state. // swiftlint:disable nesting struct SubscriptionValues: Codable { diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift index 7e883f8e06..abe47f3fc5 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift @@ -36,15 +36,19 @@ final class SubscriptionEmailViewModel: ObservableObject { @Published var shouldReloadWebView = false @Published var activateSubscription = false @Published var managingSubscriptionEmail = false + @Published var webViewModel: AsyncHeadlessWebViewViewModel private var cancellables = Set() - init(userScript: SubscriptionPagesUserScript, - subFeature: SubscriptionPagesUseSubscriptionFeature, - accountManager: AccountManager) { + init(userScript: SubscriptionPagesUserScript = SubscriptionPagesUserScript(), + subFeature: SubscriptionPagesUseSubscriptionFeature = SubscriptionPagesUseSubscriptionFeature(), + accountManager: AccountManager = AccountManager()) { self.userScript = userScript self.subFeature = subFeature self.accountManager = accountManager + self.webViewModel = AsyncHeadlessWebViewViewModel(userScript: userScript, + subFeature: subFeature, + settings: AsyncHeadlessWebViewSettings(bounces: false)) initializeView() setupTransactionObservers() } @@ -76,5 +80,9 @@ final class SubscriptionEmailViewModel: ObservableObject { activateSubscription = true } + func loadURL() { + webViewModel.navigationCoordinator.navigateTo(url: emailURL ) + } + } #endif diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift index 89cd53837b..084717c1b3 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift @@ -31,36 +31,43 @@ final class SubscriptionFlowViewModel: ObservableObject { let purchaseManager: PurchaseManager let viewTitle = UserText.settingsPProSection + enum Constants { + static let navigationBarHideThreshold = 40.0 + } + private var cancellables = Set() + private var canGoBackCancellable: AnyCancellable? // State variables var purchaseURL = URL.purchaseSubscription - // Closure passed to navigate to a specific section - // after returning to settings - var onFeatureSelected: ((SettingsViewModel.SettingsSection) -> Void) - enum FeatureName { static let netP = "vpn" - static let itp = "identity-theft-restoration" + static let itr = "identity-theft-restoration" static let dbp = "personal-information-removal" } // Published properties @Published var hasActiveSubscription = false @Published var transactionStatus: SubscriptionPagesUseSubscriptionFeature.TransactionStatus = .idle - @Published var shouldReloadWebView = false @Published var activatingSubscription = false @Published var shouldDismissView = false + @Published var webViewModel: AsyncHeadlessWebViewViewModel + @Published var shouldShowNavigationBar: Bool = false + @Published var selectedFeature: SettingsViewModel.SettingsSection? + @Published var canNavigateBack: Bool = false init(userScript: SubscriptionPagesUserScript = SubscriptionPagesUserScript(), subFeature: SubscriptionPagesUseSubscriptionFeature = SubscriptionPagesUseSubscriptionFeature(), purchaseManager: PurchaseManager = PurchaseManager.shared, - onFeatureSelected: @escaping ((SettingsViewModel.SettingsSection) -> Void)) { + selectedFeature: SettingsViewModel.SettingsSection? = nil) { self.userScript = userScript self.subFeature = subFeature self.purchaseManager = purchaseManager - self.onFeatureSelected = onFeatureSelected + self.selectedFeature = selectedFeature + self.webViewModel = AsyncHeadlessWebViewViewModel(userScript: userScript, + subFeature: subFeature, + settings: AsyncHeadlessWebViewSettings(bounces: false)) } // Observe transaction status @@ -95,37 +102,71 @@ final class SubscriptionFlowViewModel: ObservableObject { .receive(on: DispatchQueue.main) .sink { [weak self] value in if value != nil { - self?.shouldDismissView = true switch value?.feature { case FeatureName.netP: - self?.onFeatureSelected(.netP) - case FeatureName.itp: - self?.onFeatureSelected(.itp) + self?.selectedFeature = .netP + case FeatureName.itr: + self?.selectedFeature = .itr case FeatureName.dbp: - self?.onFeatureSelected(.dbp) + self?.selectedFeature = .dbp default: - return + break } + self?.finalizeSubscriptionFlow() } + + } + .store(in: &cancellables) + + webViewModel.$scrollPosition + .receive(on: DispatchQueue.main) + .sink { [weak self] value in + self?.shouldShowNavigationBar = value.y > Constants.navigationBarHideThreshold } .store(in: &cancellables) - + + canGoBackCancellable = webViewModel.$canGoBack + .receive(on: DispatchQueue.main) + .sink { [weak self] value in + self?.canNavigateBack = value + } } @MainActor private func setTransactionStatus(_ status: SubscriptionPagesUseSubscriptionFeature.TransactionStatus) { self.transactionStatus = status } + + @MainActor + private func disableGoBack() { + canGoBackCancellable?.cancel() + canNavigateBack = false + } func initializeViewData() async { await self.setupTransactionObserver() - await MainActor.run { shouldReloadWebView = true } + await self.updateSubscriptionStatus() + webViewModel.navigationCoordinator.navigateTo(url: purchaseURL ) } + func finalizeSubscriptionFlow() { + canGoBackCancellable?.cancel() + cancellables.removeAll() + subFeature.selectedFeature = nil + hasActiveSubscription = false + transactionStatus = .idle + activatingSubscription = false + shouldShowNavigationBar = false + selectedFeature = nil + canNavigateBack = false + shouldDismissView = true + } + func restoreAppstoreTransaction() { Task { if await subFeature.restoreAccountFromAppStorePurchase() { - await MainActor.run { shouldReloadWebView = true } + await disableGoBack() + await webViewModel.navigationCoordinator.reload() } else { await MainActor.run { } @@ -133,5 +174,17 @@ final class SubscriptionFlowViewModel: ObservableObject { } } + func updateSubscriptionStatus() async { + if AccountManager().isUserAuthenticated && hasActiveSubscription == false { + await disableGoBack() + await webViewModel.navigationCoordinator.reload() + } + } + + @MainActor + func navigateBack() async { + await webViewModel.navigationCoordinator.goBack() + } + } #endif diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionITPViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionITPViewModel.swift new file mode 100644 index 0000000000..a076f08341 --- /dev/null +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionITPViewModel.swift @@ -0,0 +1,144 @@ +// +// SubscriptionITPViewModel.swift +// DuckDuckGo +// +// Copyright © 2023 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 UserScript +import Combine +import Core + +#if SUBSCRIPTION +@available(iOS 15.0, *) +final class SubscriptionITPViewModel: ObservableObject { + + let userScript: IdentityTheftRestorationPagesUserScript + let subFeature: IdentityTheftRestorationPagesFeature + var manageITPURL = URL.manageITP + var viewTitle = UserText.settingsPProITRTitle + + enum Constants { + static let navigationBarHideThreshold = 40.0 + static let downloadableContent = ["application/pdf"] + } + + // State variables + var itpURL = URL.manageITP + @Published var webViewModel: AsyncHeadlessWebViewViewModel + @Published var shouldShowNavigationBar: Bool = false + @Published var canNavigateBack: Bool = false + @Published var isDownloadableContent: Bool = false + @Published var activityItems: [Any] = [] + @Published var attachmentURL: URL? + private var currentURL: URL? + + private var cancellables = Set() + private var canGoBackCancellable: AnyCancellable? + + init(userScript: IdentityTheftRestorationPagesUserScript = IdentityTheftRestorationPagesUserScript(), + subFeature: IdentityTheftRestorationPagesFeature = IdentityTheftRestorationPagesFeature()) { + self.userScript = userScript + self.subFeature = subFeature + self.webViewModel = AsyncHeadlessWebViewViewModel(userScript: userScript, + subFeature: subFeature, + settings: AsyncHeadlessWebViewSettings(bounces: false)) + } + + // Observe transaction status + private func setupSubscribers() async { + + webViewModel.$scrollPosition + .receive(on: DispatchQueue.main) + .sink { [weak self] value in + self?.shouldShowNavigationBar = value.y > Constants.navigationBarHideThreshold + } + .store(in: &cancellables) + + webViewModel.$contentType + .receive(on: DispatchQueue.main) + .sink { [weak self] value in + guard let strongSelf = self else { return } + + if Constants.downloadableContent.contains(value) { + strongSelf.isDownloadableContent = true + guard let url = strongSelf.currentURL else { return } + Task { + // We are using a dummy PDF for testing, as the real PDF's are behind the internal user login + if let downloadURL = URL(string: "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf") { + await strongSelf.downloadAttachment(from: downloadURL) + } + // if let downloadURL = url { + // await strongSelf.downloadAttachment(from: downloadURL) + } + } + } + .store(in: &cancellables) + + webViewModel.$url + .receive(on: DispatchQueue.main) + .sink { [weak self] value in + self?.isDownloadableContent = false + self?.currentURL = value + } + .store(in: &cancellables) + + + canGoBackCancellable = webViewModel.$canGoBack + .receive(on: DispatchQueue.main) + .sink { [weak self] value in + self?.canNavigateBack = value + } + } + + func initializeView() { + webViewModel.navigationCoordinator.navigateTo(url: manageITPURL ) + Task { await setupSubscribers() } + } + + private func downloadAttachment(from url: URL) async { + if let (temporaryURL, _) = try? await URLSession.shared.download(from: url) { + let fileManager = FileManager.default + + let fileName = url.lastPathComponent + + let tempDirectory = fileManager.temporaryDirectory + let tempFileURL = tempDirectory.appendingPathComponent(fileName) + + if fileManager.fileExists(atPath: tempFileURL.path) { + try? fileManager.removeItem(at: tempFileURL) + } + try? fileManager.moveItem(at: temporaryURL, to: tempFileURL) + DispatchQueue.main.async { + self.attachmentURL = tempFileURL + } + } + } + + + @MainActor + private func disableGoBack() { + canGoBackCancellable?.cancel() + canNavigateBack = false + } + + @MainActor + func navigateBack() async { + await webViewModel.navigationCoordinator.goBack() + } + +} +#endif diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift index 458be8fad4..0733cd2e83 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift @@ -39,7 +39,6 @@ final class SubscriptionRestoreViewModel: ObservableObject { @Published var transactionStatus: SubscriptionPagesUseSubscriptionFeature.TransactionStatus = .idle @Published var activationResult: SubscriptionActivationResult = .unknown @Published var subscriptionEmail: String? - @Published var isManagingEmailSubscription: Bool = false init(userScript: SubscriptionPagesUserScript = SubscriptionPagesUserScript(), subFeature: SubscriptionPagesUseSubscriptionFeature = SubscriptionPagesUseSubscriptionFeature(), @@ -80,9 +79,5 @@ final class SubscriptionRestoreViewModel: ObservableObject { } } - func manageEmailSubscription() { - isManagingEmailSubscription = true - } - } #endif diff --git a/DuckDuckGo/Subscription/Views/HeadlessWebView.swift b/DuckDuckGo/Subscription/Views/HeadlessWebView.swift deleted file mode 100644 index 116e34f771..0000000000 --- a/DuckDuckGo/Subscription/Views/HeadlessWebView.swift +++ /dev/null @@ -1,131 +0,0 @@ -// -// HeadlessWebView.swift -// DuckDuckGo -// -// Copyright © 2023 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 WebKit -import UserScript -import SwiftUI -import DesignResourcesKit -import Core - -struct HeadlessWebview: UIViewRepresentable { - let userScript: UserScriptMessaging - let subFeature: Subfeature - @Binding var url: URL - @Binding var shouldReload: Bool - - func makeUIView(context: Context) -> WKWebView { - let configuration = WKWebViewConfiguration() - configuration.userContentController = makeUserContentController() - - let webView = WKWebView(frame: .zero, configuration: configuration) - DefaultUserAgentManager.shared.update(webView: webView, isDesktop: false, url: url) - - // Just add time if you need to hook the WebView inspector - DispatchQueue.main.asyncAfter(deadline: .now() + 0) { - webView.load(URLRequest(url: url)) - } - - webView.uiDelegate = context.coordinator - - -#if DEBUG - if #available(iOS 16.4, *) { - webView.isInspectable = true - } -#endif - return webView - } - - func updateUIView(_ uiView: WKWebView, context: Context) { - if shouldReload { - reloadView(uiView: uiView) - } - } - - @MainActor - func reloadView(uiView: WKWebView) { - uiView.reload() - DispatchQueue.main.async { - shouldReload = false - } - } - - func makeCoordinator() -> Coordinator { - Coordinator() - } - - @MainActor - private func makeUserContentController() -> WKUserContentController { - let userContentController = WKUserContentController() - userContentController.addUserScript(userScript.makeWKUserScriptSync()) - userContentController.addHandler(userScript) - userScript.registerSubfeature(delegate: subFeature) - return userContentController - } - - class Coordinator: NSObject, WKUIDelegate { - var webView: WKWebView? - - private func topMostViewController() -> UIViewController? { - var topController: UIViewController? = UIApplication.shared.windows.filter { $0.isKeyWindow } - .first? - .rootViewController - while let presentedViewController = topController?.presentedViewController { - topController = presentedViewController - } - return topController - } - - // MARK: WKUIDelegate - - // Enables presenting Javascript alerts via the native layer (window.confirm()) - func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, - initiatedByFrame frame: WKFrameInfo, - completionHandler: @escaping (Bool) -> Void) { - let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert) - alertController.addAction(UIAlertAction(title: UserText.actionCancel, style: .cancel, handler: { _ in completionHandler(false) })) - alertController.addAction(UIAlertAction(title: UserText.actionOK, style: .default, handler: { _ in completionHandler(true) })) - - if let topController = topMostViewController() { - topController.present(alertController, animated: true, completion: nil) - } else { - completionHandler(false) - } - } - } -} - -struct AsyncHeadlessWebView: View { - @Binding var url: URL - let userScript: UserScriptMessaging - let subFeature: Subfeature - @Binding var shouldReload: Bool - - var body: some View { - GeometryReader { geometry in - HeadlessWebview(userScript: userScript, - subFeature: subFeature, - url: $url, - shouldReload: $shouldReload) - .frame(width: geometry.size.width, height: geometry.size.height) - } - } - -} diff --git a/DuckDuckGo/Subscription/Views/RootPresentationMode.swift b/DuckDuckGo/Subscription/Views/RootPresentationMode.swift new file mode 100644 index 0000000000..e43fc4bc08 --- /dev/null +++ b/DuckDuckGo/Subscription/Views/RootPresentationMode.swift @@ -0,0 +1,45 @@ +// +// RootPresentationMode.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +/* + iOS15 does not support NavigationStack navigation so this creates a 'RootPresentationMode' + environment that views can use to create a binding for dismissal of the whole stack of views + See: https://stackoverflow.com/questions/57334455/how-can-i-pop-to-the-root-view-using-swiftui + */ +struct RootPresentationModeKey: EnvironmentKey { + static let defaultValue: Binding = .constant(RootPresentationMode()) +} + +extension EnvironmentValues { + var rootPresentationMode: Binding { + get { return self[RootPresentationModeKey.self] } + set { self[RootPresentationModeKey.self] = newValue } + } +} + +typealias RootPresentationMode = Bool + +extension RootPresentationMode { + + public mutating func dismiss() { + self.toggle() + } +} diff --git a/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift b/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift index 714e9696ea..50811f81a6 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift @@ -24,34 +24,35 @@ import Foundation @available(iOS 15.0, *) struct SubscriptionEmailView: View { - @ObservedObject var viewModel: SubscriptionEmailViewModel - @Binding var isActivatingSubscription: Bool + @StateObject var viewModel = SubscriptionEmailViewModel() @Environment(\.dismiss) var dismiss + @Environment(\.rootPresentationMode) private var rootPresentationMode: Binding + @State private var isActive: Bool = false + @State var isAddingDevice = false var body: some View { ZStack { VStack { - AsyncHeadlessWebView(url: $viewModel.emailURL, - userScript: viewModel.userScript, - subFeature: viewModel.subFeature, - shouldReload: $viewModel.shouldReloadWebView).background() + AsyncHeadlessWebView(viewModel: viewModel.webViewModel) + .background() } } + .onAppear { + viewModel.loadURL() + } + .onChange(of: viewModel.activateSubscription) { active in if active { - // We just need to dismiss the current view - if viewModel.managingSubscriptionEmail { + // If updating email, just go back + if isAddingDevice { dismiss() } else { - // Update the binding to tear down the entire view stack - // This dismisses all views in between and takes you back to the welcome page - isActivatingSubscription = false + // Pop to Root view + self.rootPresentationMode.wrappedValue.dismiss() } } } .navigationTitle(viewModel.viewTitle) } - - } #endif diff --git a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift index e9b06406ac..d8a2064a8d 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift @@ -20,13 +20,72 @@ #if SUBSCRIPTION import SwiftUI import Foundation +import DesignResourcesKit @available(iOS 15.0, *) struct SubscriptionFlowView: View { - + @Environment(\.dismiss) var dismiss - @ObservedObject var viewModel: SubscriptionFlowViewModel + @StateObject var viewModel = SubscriptionFlowViewModel() @State private var isAlertVisible = false + @State private var shouldShowNavigationBar = false + @State private var isActive: Bool = false + + enum Constants { + static let daxLogo = "Home" + static let daxLogoSize: CGFloat = 24.0 + static let empty = "" + static let navButtonPadding: CGFloat = 20.0 + static let backButtonImage = "chevron.left" + } + + var body: some View { + NavigationView { + baseView + .toolbar { + ToolbarItemGroup(placement: .navigationBarLeading) { + backButton + } + ToolbarItem(placement: .principal) { + HStack { + Image(Constants.daxLogo) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: Constants.daxLogoSize, height: Constants.daxLogoSize) + Text(viewModel.viewTitle).daxBodyRegular() + } + } + } + .edgesIgnoringSafeArea(.top) + .navigationBarTitleDisplayMode(.inline) + .navigationBarHidden(!viewModel.shouldShowNavigationBar).animation(.easeOut) + } + .tint(Color(designSystemColor: .textPrimary)) + .environment(\.rootPresentationMode, self.$isActive) + } + + @ViewBuilder + private var dismissButton: some View { + Button(action: { viewModel.finalizeSubscriptionFlow() }, label: { Text(UserText.subscriptionCloseButton) }) + .padding(Constants.navButtonPadding) + .contentShape(Rectangle()) + .tint(Color(designSystemColor: .textPrimary)) + } + + @ViewBuilder + private var backButton: some View { + if viewModel.canNavigateBack { + Button(action: { + Task { await viewModel.navigateBack() } + }, label: { + HStack(spacing: 0) { + Image(systemName: Constants.backButtonImage) + Text(UserText.backButtonTitle) + } + + }) + } + } private func getTransactionStatus() -> String { switch viewModel.transactionStatus { @@ -41,48 +100,49 @@ struct SubscriptionFlowView: View { } } - var body: some View { - ZStack { - AsyncHeadlessWebView(url: $viewModel.purchaseURL, - userScript: viewModel.userScript, - subFeature: viewModel.subFeature, - shouldReload: $viewModel.shouldReloadWebView).background() - - // Overlay that appears when transaction is in progress - if viewModel.transactionStatus != .idle { - PurchaseInProgressView(status: getTransactionStatus()) - } - - // Activation View - NavigationLink(destination: SubscriptionRestoreView(viewModel: SubscriptionRestoreViewModel(), - isActivatingSubscription: $viewModel.activatingSubscription), - isActive: $viewModel.activatingSubscription) { - EmptyView() - } - } - .onChange(of: viewModel.shouldReloadWebView) { shouldReload in - if shouldReload { - viewModel.shouldReloadWebView = false + + @ViewBuilder + private var baseView: some View { + ZStack(alignment: .top) { + webView + + // Show a dismiss button while the bar is not visible + // But it should be hidden while performing a transaction + if !shouldShowNavigationBar && viewModel.transactionStatus == .idle { + HStack { + backButton.padding(.leading, Constants.navButtonPadding) + Spacer() + dismissButton + } } } + .onChange(of: viewModel.hasActiveSubscription) { result in if result { isAlertVisible = true } } + .onChange(of: viewModel.shouldDismissView) { result in if result { dismiss() + viewModel.shouldDismissView = false + } + } + + .onChange(of: viewModel.activatingSubscription) { value in + if value { + isActive = true + viewModel.activatingSubscription = false } } .onAppear(perform: { + setUpAppearances() Task { await viewModel.initializeViewData() } + }) - .navigationTitle(viewModel.viewTitle) - .navigationBarBackButtonHidden(viewModel.transactionStatus != .idle) - // Active subscription found Alert .alert(isPresented: $isAlertVisible) { Alert( title: Text(UserText.subscriptionFoundTitle), @@ -94,7 +154,38 @@ struct SubscriptionFlowView: View { } ) } - .navigationBarBackButtonHidden(viewModel.transactionStatus != .idle) + // The trailing close button should be hidden when a transaction is in progress + .navigationBarItems(trailing: viewModel.transactionStatus == .idle + ? Button(UserText.subscriptionCloseButton) { viewModel.finalizeSubscriptionFlow() } + : nil) } + + @ViewBuilder + private var webView: some View { + + ZStack(alignment: .top) { + // Restore View Hidden Link + NavigationLink(destination: SubscriptionRestoreView(), isActive: $isActive) { + EmptyView() + }.isDetailLink(false) + + AsyncHeadlessWebView(viewModel: viewModel.webViewModel) + .background() + + if viewModel.transactionStatus != .idle { + PurchaseInProgressView(status: getTransactionStatus()) + } + + } + } + + private func setUpAppearances() { + let navAppearance = UINavigationBar.appearance() + navAppearance.backgroundColor = UIColor(designSystemColor: .surface) + navAppearance.barTintColor = UIColor(designSystemColor: .surface) + navAppearance.shadowImage = UIImage() + navAppearance.tintColor = UIColor(designSystemColor: .textPrimary) + } + } #endif diff --git a/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift b/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift new file mode 100644 index 0000000000..7d8c83c7b6 --- /dev/null +++ b/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift @@ -0,0 +1,155 @@ +// +// SubscriptionITPView.swift +// DuckDuckGo +// +// Copyright © 2023 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. +// + +#if SUBSCRIPTION +import SwiftUI +import Foundation +import DesignResourcesKit + +struct SubscriptionActivityViewController: UIViewControllerRepresentable { + var activityItems: [Any] + var applicationActivities: [UIActivity]? + + func makeUIViewController(context: Context) -> UIActivityViewController { + return UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities) + } + + func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {} +} + +@available(iOS 15.0, *) +struct SubscriptionITPView: View { + + @Environment(\.dismiss) var dismiss + @StateObject var viewModel = SubscriptionITPViewModel() + @State private var shouldShowNavigationBar = false + @State private var isShowingActivityView = false + + enum Constants { + static let daxLogo = "Home" + static let daxLogoSize: CGFloat = 24.0 + static let empty = "" + static let navButtonPadding: CGFloat = 20.0 + static let backButtonImage = "chevron.left" + static let shareImage = "SubscriptionShareIcon" + } + + var body: some View { + NavigationView { + baseView + .toolbar { + ToolbarItemGroup(placement: .navigationBarLeading) { + backButton + } + ToolbarItem(placement: .principal) { + HStack { + Image(Constants.daxLogo) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: Constants.daxLogoSize, height: Constants.daxLogoSize) + Text(viewModel.viewTitle).daxBodyRegular() + } + } + ToolbarItem(placement: .navigationBarTrailing) { + shareButton + } + ToolbarItem(placement: .navigationBarTrailing) { + Button(UserText.subscriptionCloseButton) { dismiss() } + } + } + .edgesIgnoringSafeArea(.top) + .navigationBarTitleDisplayMode(.inline) + .navigationBarHidden(!viewModel.shouldShowNavigationBar && !viewModel.isDownloadableContent).animation(.easeOut) + + .onAppear(perform: { + setUpAppearances() + viewModel.initializeView() + }) + }.tint(Color(designSystemColor: .textPrimary)) + } + + private var baseView: some View { + ZStack(alignment: .top) { + webView + + // Show a dismiss button while the bar is not visible + // But it should be hidden while performing a transaction + if !shouldShowNavigationBar { + HStack { + backButton.padding(.leading, Constants.navButtonPadding) + Spacer() + dismissButton + } + } + + } + } + + @ViewBuilder + private var webView: some View { + + ZStack(alignment: .top) { + AsyncHeadlessWebView(viewModel: viewModel.webViewModel) + .background() + } + } + + @ViewBuilder + private var backButton: some View { + if viewModel.canNavigateBack { + Button(action: { + Task { await viewModel.navigateBack() } + }, label: { + HStack(spacing: 0) { + Image(systemName: Constants.backButtonImage) + Text(UserText.backButtonTitle) + } + + }) + } + } + + @ViewBuilder + private var shareButton: some View { + if viewModel.isDownloadableContent { + Button(action: { isShowingActivityView = true }, label: { Image(Constants.shareImage) }) + .popover(isPresented: $isShowingActivityView, arrowEdge: .bottom) { + SubscriptionActivityViewController(activityItems: [viewModel.attachmentURL], applicationActivities: nil) + } + } + } + + @ViewBuilder + private var dismissButton: some View { + Button(action: { dismiss() }, label: { Text(UserText.subscriptionCloseButton) }) + .padding(Constants.navButtonPadding) + .contentShape(Rectangle()) + .tint(Color(designSystemColor: .textPrimary)) + } + + + private func setUpAppearances() { + let navAppearance = UINavigationBar.appearance() + navAppearance.backgroundColor = UIColor(designSystemColor: .surface) + navAppearance.barTintColor = UIColor(designSystemColor: .surface) + navAppearance.shadowImage = UIImage() + navAppearance.tintColor = UIColor(designSystemColor: .textPrimary) + } +} +#endif diff --git a/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift b/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift index 3006cf4ace..2301cf7daa 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift @@ -26,15 +26,14 @@ import DesignResourcesKit struct SubscriptionRestoreView: View { @Environment(\.dismiss) var dismiss - @StateObject var viewModel: SubscriptionRestoreViewModel + @Environment(\.rootPresentationMode) private var rootPresentationMode: Binding + @StateObject var viewModel = SubscriptionRestoreViewModel() @State private var expandedItemId: Int = 0 @State private var isAlertVisible = false - - // Binding used to dismiss the entire stack (Go back to settings from several levels down) - @Binding var isActivatingSubscription: Bool + @State private var isActive: Bool = false private enum Constants { - static let heroImage = "SyncTurnOnSyncHero" + static let heroImage = "ManageSubscriptionHero" static let appleIDIcon = "Platform-Apple-16" static let emailIcon = "Email-16" static let headerLineSpacing = 10.0 @@ -50,6 +49,12 @@ struct SubscriptionRestoreView: View { var body: some View { ZStack { VStack { + + // Email Activation View Hidden link + NavigationLink(destination: SubscriptionEmailView(isAddingDevice: viewModel.isAddingDevice), isActive: $isActive) { + EmptyView() + }.isDetailLink(false) + headerView listView } @@ -58,6 +63,7 @@ struct SubscriptionRestoreView: View { .navigationBarBackButtonHidden(viewModel.transactionStatus != .idle) .applyInsetGroupedListStyle() .alert(isPresented: $isAlertVisible) { getAlert() } + .onChange(of: viewModel.activationResult) { result in if result != .unknown { isAlertVisible = true @@ -72,16 +78,6 @@ struct SubscriptionRestoreView: View { } } - // Activation View - NavigationLink(destination: SubscriptionEmailView( - viewModel: SubscriptionEmailViewModel( - userScript: viewModel.userScript, - subFeature: viewModel.subFeature, - accountManager: viewModel.accountManager), - isActivatingSubscription: $isActivatingSubscription), - isActive: $viewModel.isManagingEmailSubscription) { - EmptyView() - } } private var listItems: [ListItem] { @@ -93,7 +89,7 @@ struct SubscriptionRestoreView: View { .init(id: 1, content: getCellTitle(icon: Constants.emailIcon, text: UserText.subscriptionActivateEmail), - expandedContent: getEmailCellContent(buttonAction: viewModel.manageEmailSubscription )) + expandedContent: getEmailCellContent(buttonAction: { isActive = true })) ] } @@ -138,12 +134,6 @@ struct SubscriptionRestoreView: View { HStack { getCellButton(buttonText: UserText.subscriptionManageEmailButton, action: buttonAction) - /* TO BE IMPLEMENTED ?? - Spacer() - Button(action: {}, label: { - Text(UserText.subscriptionManageEmailResendInstructions).daxButton().daxBodyBold() - }) - */ } } } diff --git a/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift b/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift index 8e44ce451c..49b2b76aa3 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift @@ -29,10 +29,9 @@ class SceneEnvironment: ObservableObject { @available(iOS 15.0, *) struct SubscriptionSettingsView: View { - @ObservedObject var viewModel: SubscriptionSettingsViewModel @Environment(\.presentationMode) var presentationMode + @StateObject var viewModel = SubscriptionSettingsViewModel() @StateObject var sceneEnvironment = SceneEnvironment() - @State private var isActivatingSubscription = false var body: some View { List { @@ -46,9 +45,8 @@ struct SubscriptionSettingsView: View { }.textCase(nil) Section(header: Text(UserText.subscriptionManageDevices)) { - NavigationLink(destination: SubscriptionRestoreView( - viewModel: SubscriptionRestoreViewModel(isAddingDevice: true), - isActivatingSubscription: $isActivatingSubscription)) { + + NavigationLink(destination: SubscriptionRestoreView()) { SettingsCustomCell(content: { Text(UserText.subscriptionAddDeviceButton) .daxBodyRegular() @@ -56,6 +54,7 @@ struct SubscriptionSettingsView: View { }) } + SettingsCustomCell(content: { Text(UserText.subscriptionRemoveFromDevice) .daxBodyRegular() diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index 5b9b476aac..9c24c77556 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -1033,6 +1033,9 @@ But if you *do* want a peek under the hood, you can find more information about static let subscriptionCompletingPurchaseTitle = NSLocalizedString("subscription.progress.view.completing.purchase", value: "Completing purchase...", comment: "Progress view title when completing the purchase") // Subscription Settings + public static let subscriptionTitle = NSLocalizedString("subscription.title", value: "Privacy Pro", comment: "Navigation bar Title for subscriptions") + public static let subscriptionCloseButton = NSLocalizedString("subscription.close", value: "Close", comment: "Navigation Button for closing subscription view") + static func subscriptionInfo(expiration: String) -> String { let localized = NSLocalizedString("subscription.subscription.active.caption", value: "Your Privacy Pro subscription renews on %@", comment: "Subscription Expiration Data") return String(format: localized, expiration) diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index 19241e7e48..69faa3d347 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -1980,6 +1980,9 @@ But if you *do* want a peek under the hood, you can find more information about /* Change plan or billing title */ "subscription.change.plan" = "Change Plan Or Billing"; +/* Navigation Button for closing subscription view */ +"subscription.close" = "Close"; + /* FAQ Button */ "subscription.faq" = "Privacy Pro FAQ"; @@ -2055,6 +2058,9 @@ But if you *do* want a peek under the hood, you can find more information about /* Subscription Expiration Data */ "subscription.subscription.active.caption" = "Your Privacy Pro subscription renews on %@"; +/* Navigation bar Title for subscriptions */ +"subscription.title" = "Privacy Pro"; + /* Message confirming that recovery code was copied to clipboard */ "sync.code.copied" = "Recovery code copied to clipboard";