diff --git a/Core/URLOpener.swift b/Core/URLOpener.swift new file mode 100644 index 0000000000..d107c8a805 --- /dev/null +++ b/Core/URLOpener.swift @@ -0,0 +1,33 @@ +// +// URLOpener.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +public protocol URLOpener: AnyObject { + func canOpenURL(_ url: URL) -> Bool + func open(_ url: URL, options: [UIApplication.OpenExternalURLOptionsKey: Any], completionHandler completion: ((Bool) -> Void)?) +} + +public extension URLOpener { + func open(_ url: URL) { + open(url, options: [:], completionHandler: nil) + } +} + +extension UIApplication: URLOpener {} diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index c6959cb514..ecc121692d 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -601,6 +601,10 @@ 98F3A1D8217B37010011A0D4 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F3A1D7217B37010011A0D4 /* Theme.swift */; }; 98F6EA472863124100720957 /* ContentBlockerRulesLists.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F6EA462863124100720957 /* ContentBlockerRulesLists.swift */; }; 98F78B8E22419093007CACF4 /* ThemableNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F78B8D22419093007CACF4 /* ThemableNavigationController.swift */; }; + 9F23B8012C2BC94400950875 /* OnboardingBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8002C2BC94400950875 /* OnboardingBackground.swift */; }; + 9F23B8032C2BCD0000950875 /* DaxDialogStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8022C2BCD0000950875 /* DaxDialogStyles.swift */; }; + 9F23B8062C2BE22700950875 /* OnboardingIntroViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8052C2BE22700950875 /* OnboardingIntroViewModelTests.swift */; }; + 9F23B8092C2BE9B700950875 /* MockURLOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8082C2BE9B700950875 /* MockURLOpener.swift */; }; 9F2510142BF5809E0096DB16 /* SubscriptionFlowViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2510132BF5809E0096DB16 /* SubscriptionFlowViewModelTests.swift */; }; 9F8FE9492BAE50E50071E372 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 9F8FE9482BAE50E50071E372 /* Lottie */; }; 9FA5E44B2BF1AF3400BDEF02 /* SubscriptionContainerViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA5E44A2BF1AF3400BDEF02 /* SubscriptionContainerViewFactory.swift */; }; @@ -608,6 +612,12 @@ 9FB027122C2526DD009EA190 /* DaxDialogIntroView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB027112C2526DD009EA190 /* DaxDialogIntroView.swift */; }; 9FB027142C252E0C009EA190 /* DaxDialogBrowsersComparisonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB027132C252E0C009EA190 /* DaxDialogBrowsersComparisonView.swift */; }; 9FB027192C26BC29009EA190 /* BrowsersComparisonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB027182C26BC29009EA190 /* BrowsersComparisonModel.swift */; }; + 9FB0271B2C2927D0009EA190 /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB0271A2C2927D0009EA190 /* OnboardingView.swift */; }; + 9FB0271D2C293619009EA190 /* OnboardingIntroViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB0271C2C293619009EA190 /* OnboardingIntroViewModel.swift */; }; + 9FB0271F2C294985009EA190 /* OnboardingDefaultBrowserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB0271E2C294985009EA190 /* OnboardingDefaultBrowserView.swift */; }; + 9FE08BD32C2A5B88001D5EBC /* OnboardingTextStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE08BD22C2A5B88001D5EBC /* OnboardingTextStyles.swift */; }; + 9FE08BD62C2A60CD001D5EBC /* MetricBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE08BD52C2A60CD001D5EBC /* MetricBuilder.swift */; }; + 9FE08BDA2C2A86D0001D5EBC /* URLOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE08BD92C2A86D0001D5EBC /* URLOpener.swift */; }; 9FF7E9822C22A1F100902BE5 /* DaxDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF7E9812C22A1F100902BE5 /* DaxDialogView.swift */; }; 9FF7E9862C23D10300902BE5 /* BrowsersComparisonChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF7E9852C23D10300902BE5 /* BrowsersComparisonChart.swift */; }; AA3D854523D9942200788410 /* AppIconSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3D854423D9942200788410 /* AppIconSettingsViewController.swift */; }; @@ -2239,12 +2249,22 @@ 98F3A1D7217B37010011A0D4 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; }; 98F6EA462863124100720957 /* ContentBlockerRulesLists.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentBlockerRulesLists.swift; sourceTree = ""; }; 98F78B8D22419093007CACF4 /* ThemableNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemableNavigationController.swift; sourceTree = ""; }; + 9F23B8002C2BC94400950875 /* OnboardingBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingBackground.swift; sourceTree = ""; }; + 9F23B8022C2BCD0000950875 /* DaxDialogStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaxDialogStyles.swift; sourceTree = ""; }; + 9F23B8052C2BE22700950875 /* OnboardingIntroViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingIntroViewModelTests.swift; sourceTree = ""; }; + 9F23B8082C2BE9B700950875 /* MockURLOpener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLOpener.swift; sourceTree = ""; }; 9F2510132BF5809E0096DB16 /* SubscriptionFlowViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionFlowViewModelTests.swift; sourceTree = ""; }; 9FA5E44A2BF1AF3400BDEF02 /* SubscriptionContainerViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionContainerViewFactory.swift; sourceTree = ""; }; 9FA5E44D2BF1B16400BDEF02 /* SubscriptionContainerViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionContainerViewModelTests.swift; sourceTree = ""; }; 9FB027112C2526DD009EA190 /* DaxDialogIntroView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaxDialogIntroView.swift; sourceTree = ""; }; 9FB027132C252E0C009EA190 /* DaxDialogBrowsersComparisonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaxDialogBrowsersComparisonView.swift; sourceTree = ""; }; 9FB027182C26BC29009EA190 /* BrowsersComparisonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowsersComparisonModel.swift; sourceTree = ""; }; + 9FB0271A2C2927D0009EA190 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = ""; }; + 9FB0271C2C293619009EA190 /* OnboardingIntroViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingIntroViewModel.swift; sourceTree = ""; }; + 9FB0271E2C294985009EA190 /* OnboardingDefaultBrowserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingDefaultBrowserView.swift; sourceTree = ""; }; + 9FE08BD22C2A5B88001D5EBC /* OnboardingTextStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingTextStyles.swift; sourceTree = ""; }; + 9FE08BD52C2A60CD001D5EBC /* MetricBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricBuilder.swift; sourceTree = ""; }; + 9FE08BD92C2A86D0001D5EBC /* URLOpener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLOpener.swift; sourceTree = ""; }; 9FF7E9812C22A1F100902BE5 /* DaxDialogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaxDialogView.swift; sourceTree = ""; }; 9FF7E9852C23D10300902BE5 /* BrowsersComparisonChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowsersComparisonChart.swift; sourceTree = ""; }; AA3D854423D9942200788410 /* AppIconSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconSettingsViewController.swift; sourceTree = ""; }; @@ -4189,6 +4209,24 @@ name = Themes; sourceTree = ""; }; + 9F23B7FF2C2BABE000950875 /* OnboardingIntro */ = { + isa = PBXGroup; + children = ( + 9FB0271C2C293619009EA190 /* OnboardingIntroViewModel.swift */, + 9FB0271A2C2927D0009EA190 /* OnboardingView.swift */, + 9FB0271E2C294985009EA190 /* OnboardingDefaultBrowserView.swift */, + ); + path = OnboardingIntro; + sourceTree = ""; + }; + 9F23B8042C2BE20500950875 /* Onboarding */ = { + isa = PBXGroup; + children = ( + 9F23B8052C2BE22700950875 /* OnboardingIntroViewModelTests.swift */, + ); + name = Onboarding; + sourceTree = ""; + }; 9FA5E44C2BF1B14100BDEF02 /* Subscription */ = { isa = PBXGroup; children = ( @@ -4217,11 +4255,32 @@ path = BrowsersComparison; sourceTree = ""; }; + 9FE08BD12C2A5B77001D5EBC /* Styles */ = { + isa = PBXGroup; + children = ( + 9FE08BD22C2A5B88001D5EBC /* OnboardingTextStyles.swift */, + 9F23B8022C2BCD0000950875 /* DaxDialogStyles.swift */, + ); + path = Styles; + sourceTree = ""; + }; + 9FE08BD42C2A60BD001D5EBC /* MetricBuilder */ = { + isa = PBXGroup; + children = ( + 9FE08BD52C2A60CD001D5EBC /* MetricBuilder.swift */, + ); + path = MetricBuilder; + sourceTree = ""; + }; 9FF7E9802C22A19800902BE5 /* OnboardingExperiment */ = { isa = PBXGroup; children = ( + 9FE08BD42C2A60BD001D5EBC /* MetricBuilder */, + 9FE08BD12C2A5B77001D5EBC /* Styles */, 9FB027172C26BC0F009EA190 /* BrowsersComparison */, 9FB027102C2526A8009EA190 /* DaxDialogs */, + 9F23B7FF2C2BABE000950875 /* OnboardingIntro */, + 9F23B8002C2BC94400950875 /* OnboardingBackground.swift */, ); path = OnboardingExperiment; sourceTree = ""; @@ -4963,6 +5022,7 @@ 851DFD88212C5ED600D95F20 /* Main */, EE56DE3A2A6038F500375C41 /* NetworkProtection */, F1D477C71F2139210031ED49 /* OmniBar */, + 9F23B8042C2BE20500950875 /* Onboarding */, 98EA2C3F218BB5140023E1DC /* Settings */, 9FA5E44C2BF1B14100BDEF02 /* Subscription */, F13B4BF71F18C9E800814661 /* Tabs */, @@ -5132,6 +5192,7 @@ 1EE411F22857C4A30003FE64 /* CollectionExtension.swift */, 1E6A4D682984208800A371D3 /* LocaleExtension.swift */, 56D855692BEA9169009F9698 /* CurrentDateProviding.swift */, + 9FE08BD92C2A86D0001D5EBC /* URLOpener.swift */, ); name = Utilities; sourceTree = ""; @@ -5208,6 +5269,7 @@ CBDD5DE029A6741300832877 /* MockBundle.swift */, C1B0F6412AB08BE9001EAF05 /* MockPrivacyConfiguration.swift */, C185ED652BD43A5500BAE9DC /* MockDDGSyncing.swift */, + 9F23B8082C2BE9B700950875 /* MockURLOpener.swift */, ); name = Mocks; sourceTree = ""; @@ -6547,6 +6609,7 @@ 6F8496412BC3D8EE00ADA54E /* OnboardingButtonsView.swift in Sources */, 1EA513782866039400493C6A /* TrackerAnimationLogic.swift in Sources */, 854A01332A558B3A00FCC628 /* UIView+Constraints.swift in Sources */, + 9FB0271B2C2927D0009EA190 /* OnboardingView.swift in Sources */, C12726EE2A5FF88C00215B02 /* EmailSignupPromptView.swift in Sources */, CB2283F32BD79FC20057DD0A /* BrokenSitePromptView.swift in Sources */, 83134D7D20E2D725006CE65D /* FeedbackSender.swift in Sources */, @@ -6596,6 +6659,8 @@ 6FBF0F8B2BD7C0A900136CF0 /* AllProtectedCell.swift in Sources */, 1E4F4A5A297193DE00625985 /* MainViewController+CookiesManaged.swift in Sources */, 8586A10D24CBA7070049720E /* FindInPageActivity.swift in Sources */, + 9FB0271D2C293619009EA190 /* OnboardingIntroViewModel.swift in Sources */, + 9FE08BD62C2A60CD001D5EBC /* MetricBuilder.swift in Sources */, 1E1626072968413B0004127F /* ViewExtension.swift in Sources */, 31A42566285A0A6300049386 /* FaviconViewModel.swift in Sources */, D65625952C22D382006EF297 /* TabViewController.swift in Sources */, @@ -6668,6 +6733,7 @@ 83BE9BC3215D69C1009844D9 /* AppConfigurationFetch.swift in Sources */, 37CF91622BB474AA00BADCAE /* CrashCollectionOnboardingView.swift in Sources */, 1EEC460627A9499600E75FCB /* DownloadsList.swift in Sources */, + 9F23B8032C2BCD0000950875 /* DaxDialogStyles.swift in Sources */, C1641EAF2BC2F5140012607A /* ImportPasswordsViewController.swift in Sources */, D63FF8982C1B6A45006DE24D /* DuckPlayer.swift in Sources */, 85B9CB8921AEBDD5009001F1 /* FavoriteHomeCell.swift in Sources */, @@ -6717,6 +6783,7 @@ 85DE681A2B6A8BB000DED4FE /* MainViewCoordinator.swift in Sources */, F1617C151E57336D00DEDCAF /* TabManager.swift in Sources */, 85449EF523FDA02800512AAF /* KeyboardSettingsViewController.swift in Sources */, + 9FB0271F2C294985009EA190 /* OnboardingDefaultBrowserView.swift in Sources */, 85C11E4C2090888C00BFFEB4 /* HomeRowReminder.swift in Sources */, 31B2F11F287846320040427A /* NoMicPermissionAlert.swift in Sources */, 310C4B45281B5A9A00BA79A9 /* AutofillLoginDetailsView.swift in Sources */, @@ -6958,8 +7025,10 @@ 1E865AF0272042DB001C74F3 /* TextSizeSettingsViewController.swift in Sources */, D6E0C1892B7A2E0D00D5E1E9 /* DesktopDownloadViewModel.swift in Sources */, 8524CC9A246DA81700E59D45 /* FullscreenDaxDialogViewController.swift in Sources */, + 9F23B8012C2BC94400950875 /* OnboardingBackground.swift in Sources */, CBBB9A192BED441400BEAC71 /* PixelExperimentForBrokenSites.swift in Sources */, 6FE018402C25CB3F001F680D /* FavoritesSectionHeader.swift in Sources */, + 9FE08BD32C2A5B88001D5EBC /* OnboardingTextStyles.swift in Sources */, F17669D71E43401C003D3222 /* MainViewController.swift in Sources */, 6FE127462C2054A900EB5724 /* NewTabPageViewController.swift in Sources */, 984D60B2222A1284003B9E3B /* FeedbackFormViewController.swift in Sources */, @@ -7063,6 +7132,7 @@ 314A3EFC293905EC00D3D4C8 /* BrokenSiteReportingTests.swift in Sources */, 851B1283221FE65E004781BC /* ImproveOnboardingExperiment1Tests.swift in Sources */, F194FAFB1F14E622009B4DF8 /* UIFontExtensionTests.swift in Sources */, + 9F23B8092C2BE9B700950875 /* MockURLOpener.swift in Sources */, F40F843728C939760081AE75 /* AutofillLoginListViewModelTests.swift in Sources */, C14882E827F20DAB00D59F0C /* TestDataLoader.swift in Sources */, C14882EA27F20DD000D59F0C /* MockBookmarksCoreDataStorage.swift in Sources */, @@ -7087,6 +7157,7 @@ C174CE602BD6A6CE00AED2EA /* MockDDGSyncing.swift in Sources */, 8521FDE6238D414B00A44CC3 /* FileStoreTests.swift in Sources */, F14E491F1E391CE900DC037C /* URLExtensionTests.swift in Sources */, + 9F23B8062C2BE22700950875 /* OnboardingIntroViewModelTests.swift in Sources */, 85D2187424BF25CD004373D2 /* FaviconsTests.swift in Sources */, 85AD49EE2B6149110085D2D1 /* CookieStorageTests.swift in Sources */, 569437242BDD405400C0881B /* SyncBookmarksAdapterTests.swift in Sources */, @@ -7265,6 +7336,7 @@ F1134EB01F40AC6300B73467 /* AtbParser.swift in Sources */, EE50052E29C369D300AE0773 /* FeatureFlag.swift in Sources */, BD15DB852B959CFD00821457 /* BundleExtension.swift in Sources */, + 9FE08BDA2C2A86D0001D5EBC /* URLOpener.swift in Sources */, 37DF000F29F9D635002B7D3E /* SyncBookmarksAdapter.swift in Sources */, B652DF10287C2C1600C12A9C /* ContentBlocking.swift in Sources */, 4BE2756827304F57006B20B0 /* URLRequestExtension.swift in Sources */, diff --git a/DuckDuckGo/DaxOnboarding.xcassets/DDGDefaultBrowser.imageset/Contents.json b/DuckDuckGo/DaxOnboarding.xcassets/DDGDefaultBrowser.imageset/Contents.json new file mode 100644 index 0000000000..75a35d08cb --- /dev/null +++ b/DuckDuckGo/DaxOnboarding.xcassets/DDGDefaultBrowser.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "DDGDefaultBrowser.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/DaxOnboarding.xcassets/DDGDefaultBrowser.imageset/DDGDefaultBrowser.pdf b/DuckDuckGo/DaxOnboarding.xcassets/DDGDefaultBrowser.imageset/DDGDefaultBrowser.pdf new file mode 100644 index 0000000000..5e1e9d83af Binary files /dev/null and b/DuckDuckGo/DaxOnboarding.xcassets/DDGDefaultBrowser.imageset/DDGDefaultBrowser.pdf differ diff --git a/DuckDuckGo/DaxOnboarding.xcassets/DaxOnboardingBackground.imageset/Contents.json b/DuckDuckGo/DaxOnboarding.xcassets/DaxOnboardingBackground.imageset/Contents.json new file mode 100644 index 0000000000..cab1647e9c --- /dev/null +++ b/DuckDuckGo/DaxOnboarding.xcassets/DaxOnboardingBackground.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "DaxOnboardingBackground.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/DaxOnboarding.xcassets/DaxOnboardingBackground.imageset/DaxOnboardingBackground.pdf b/DuckDuckGo/DaxOnboarding.xcassets/DaxOnboardingBackground.imageset/DaxOnboardingBackground.pdf new file mode 100644 index 0000000000..412cf26233 Binary files /dev/null and b/DuckDuckGo/DaxOnboarding.xcassets/DaxOnboardingBackground.imageset/DaxOnboardingBackground.pdf differ diff --git a/DuckDuckGo/DaxOnboarding.xcassets/Hiker.imageset/Contents.json b/DuckDuckGo/DaxOnboarding.xcassets/Hiker.imageset/Contents.json new file mode 100644 index 0000000000..09319cba51 --- /dev/null +++ b/DuckDuckGo/DaxOnboarding.xcassets/Hiker.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "Hiker.pdf", + "idiom" : "iphone" + }, + { + "filename" : "HikerLarge.pdf", + "idiom" : "ipad" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/DaxOnboarding.xcassets/Hiker.imageset/Hiker.pdf b/DuckDuckGo/DaxOnboarding.xcassets/Hiker.imageset/Hiker.pdf new file mode 100644 index 0000000000..36d5fd8d5f Binary files /dev/null and b/DuckDuckGo/DaxOnboarding.xcassets/Hiker.imageset/Hiker.pdf differ diff --git a/DuckDuckGo/DaxOnboarding.xcassets/Hiker.imageset/HikerLarge.pdf b/DuckDuckGo/DaxOnboarding.xcassets/Hiker.imageset/HikerLarge.pdf new file mode 100644 index 0000000000..296e5696fb Binary files /dev/null and b/DuckDuckGo/DaxOnboarding.xcassets/Hiker.imageset/HikerLarge.pdf differ diff --git a/DuckDuckGo/DaxOnboarding.xcassets/HikerSmall.imageset/Contents.json b/DuckDuckGo/DaxOnboarding.xcassets/HikerSmall.imageset/Contents.json new file mode 100644 index 0000000000..7509061caa --- /dev/null +++ b/DuckDuckGo/DaxOnboarding.xcassets/HikerSmall.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "HikerSmall.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/DaxOnboarding.xcassets/HikerSmall.imageset/HikerSmall.pdf b/DuckDuckGo/DaxOnboarding.xcassets/HikerSmall.imageset/HikerSmall.pdf new file mode 100644 index 0000000000..ca97f45b11 Binary files /dev/null and b/DuckDuckGo/DaxOnboarding.xcassets/HikerSmall.imageset/HikerSmall.pdf differ diff --git a/DuckDuckGo/OnboardingButtonsView.swift b/DuckDuckGo/OnboardingButtonsView.swift index 259e6c8dc8..d674397f5e 100644 --- a/DuckDuckGo/OnboardingButtonsView.swift +++ b/DuckDuckGo/OnboardingButtonsView.swift @@ -51,8 +51,14 @@ struct OnboardingActions: View { extension OnboardingActions { class Model: ObservableObject { - @Published var primaryButtonTitle = "" - @Published var secondaryButtonTitle = "" - @Published var isContinueEnabled = true + @Published var primaryButtonTitle: String + @Published var secondaryButtonTitle: String + @Published var isContinueEnabled: Bool + + init(primaryButtonTitle: String = "", secondaryButtonTitle: String = "", isContinueEnabled: Bool = true) { + self.primaryButtonTitle = primaryButtonTitle + self.secondaryButtonTitle = secondaryButtonTitle + self.isContinueEnabled = isContinueEnabled + } } } diff --git a/DuckDuckGo/OnboardingExperiment/MetricBuilder/MetricBuilder.swift b/DuckDuckGo/OnboardingExperiment/MetricBuilder/MetricBuilder.swift new file mode 100644 index 0000000000..32af0ab644 --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/MetricBuilder/MetricBuilder.swift @@ -0,0 +1,69 @@ +// +// MetricBuilder.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 +import class UIKit.UIScreen + +final class MetricBuilder { + private let iPhoneValue: T + private let iPadValue: T + private var iPhoneSmallScreen: T? + + init(iPhone: T, iPad: T) { + iPhoneValue = iPhone + iPadValue = iPad + } + + convenience init(value: T) { + self.init(iPhone: value, iPad: value) + } + + func smallIphone(_ value: T) -> Self { + iPhoneSmallScreen = value + return self + } + + func build(v: UserInterfaceSizeClass?, h: UserInterfaceSizeClass?) -> T { + if isIPad(v, h) { + iPadValue + } else { + if isIPhoneSmallScreen(UIScreen.main.bounds.size) { + iPhoneSmallScreen ?? iPhoneValue + } else { + iPhoneValue + } + } + } + + private func isIPhonePortrait(_ verticalSizeClass: UserInterfaceSizeClass?, _ horizontalSizeClass: UserInterfaceSizeClass?) -> Bool { + verticalSizeClass == .regular && horizontalSizeClass == .compact + } + + private func isIPhoneLandscape(_ verticalSizeClass: UserInterfaceSizeClass?) -> Bool { + verticalSizeClass == .compact + } + + private func isIPhoneSmallScreen(_ frame: CGSize) -> Bool { + frame.height > 0 && frame.height <= 667 // iPhone SE + } + + private func isIPad(_ verticalSizeClass: UserInterfaceSizeClass?, _ horizontalSizeClass: UserInterfaceSizeClass?) -> Bool { + verticalSizeClass == .regular && horizontalSizeClass == .regular + } +} diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingBackground.swift b/DuckDuckGo/OnboardingExperiment/OnboardingBackground.swift new file mode 100644 index 0000000000..ec2c3b2620 --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/OnboardingBackground.swift @@ -0,0 +1,95 @@ +// +// OnboardingBackground.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 + +struct OnboardingBackground: View { + + var body: some View { + ZStack { + Gradient() + + Image(.daxOnboardingBackground) + .resizable() + .aspectRatio(contentMode: .fill) + } + .ignoresSafeArea() + } +} + +// MARK: - Gradient + +private extension OnboardingBackground { + + struct Gradient: View { + @Environment(\.colorScheme) private var colorScheme + + var body: some View { + switch colorScheme { + case .light: + lightGradient + case .dark: + darkGradient + @unknown default: + lightGradient + } + } + + private var lightGradient: some View { + gradient(colorStops: [ + .init(color: Color(red: 1, green: 0.9, blue: 0.87), location: 0.00), + .init(color: Color(red: 0.99, green: 0.89, blue: 0.87), location: 0.28), + .init(color: Color(red: 0.99, green: 0.89, blue: 0.87), location: 0.46), + .init(color: Color(red: 0.96, green: 0.87, blue: 0.87), location: 0.72), + .init(color: Color(red: 0.9, green: 0.84, blue: 0.92), location: 1.00), + ]) + } + + private var darkGradient: some View { + gradient(colorStops: [ + .init(color: Color(red: 0.29, green: 0.19, blue: 0.25), location: 0.00), + .init(color: Color(red: 0.35, green: 0.23, blue: 0.32), location: 0.28), + .init(color: Color(red: 0.37, green: 0.25, blue: 0.38), location: 0.46), + .init(color: Color(red: 0.2, green: 0.15, blue: 0.32), location: 0.72), + .init(color: Color(red: 0.16, green: 0.15, blue: 0.34), location: 1.00), + ]) + } + + private func gradient(colorStops: [SwiftUI.Gradient.Stop]) -> some View { + LinearGradient( + stops: colorStops, + startPoint: UnitPoint(x: 0, y: 0.5), + endPoint: UnitPoint(x: 1, y: 0.5) + ) + .rotationEffect(Angle(degrees: 90)) + } + + } + +} + +#Preview("Light Mode") { + OnboardingBackground() + .preferredColorScheme(.light) +} + +#Preview("Dark Mode") { + OnboardingBackground() + .preferredColorScheme(.dark) +} diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingDefaultBrowserView.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingDefaultBrowserView.swift new file mode 100644 index 0000000000..50530d61f9 --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingDefaultBrowserView.swift @@ -0,0 +1,80 @@ +// +// OnboardingDefaultBrowserView.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 + +struct OnboardingDefaultBrowserView: View { + @Environment(\.verticalSizeClass) private var verticalSizeClass + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + + let setAsDefaultBrowserAction: () -> Void + let cancelAction: () -> Void + + var body: some View { + ZStack { + Color(designSystemColor: .surface) + .ignoresSafeArea() + + VStack(spacing: Metrics.verticalSpacing) { + Text(UserText.onboardingDefaultBrowserTitle) + .onboardingTitleStyle() + .padding([.top, .horizontal]) + + Text(UserText.DaxOnboardingExperiment.DefaultBrowser.message) + .font(.system(size: 16.0)) + .multilineTextAlignment(.center) + .foregroundColor(.secondary) + + Image(.ddgDefaultBrowser) + .resizable() + .aspectRatio(contentMode: .fit) + .padding(.top, 48) + + Spacer() + .frame(height: Metrics.spacerHeight.build(v: verticalSizeClass, h: horizontalSizeClass)) + + OnboardingActions( + viewModel: .init( + primaryButtonTitle: UserText.onboardingSetAsDefaultBrowser, + secondaryButtonTitle: UserText.onboardingDefaultBrowserMaybeLater + ), + primaryAction: setAsDefaultBrowserAction, + secondaryAction: cancelAction + ) + + } + .padding(.top) + .frame(maxWidth: Metrics.viewWidth, maxHeight: .infinity, alignment: .center) + } + } +} + +// MARK: - Metrics + +private enum Metrics { + static let verticalSpacing: CGFloat = 16.0 + static let spacerHeight = MetricBuilder(iPhone: 142, iPad: 142).smallIphone(10) + static let viewWidth: CGFloat = 325.0 +} + +// MARK: - Preview + +#Preview { + OnboardingDefaultBrowserView(setAsDefaultBrowserAction: {}, cancelAction: {}) +} diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift new file mode 100644 index 0000000000..d6e2d6f2cb --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift @@ -0,0 +1,56 @@ +// +// OnboardingIntroViewModel.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Core +import class UIKit.UIApplication + +final class OnboardingIntroViewModel: ObservableObject { + @Published private(set) var state: OnboardingView.ViewState = .landing + + var onCompletingOnboardingIntro: (() -> Void)? + private let urlOpener: URLOpener + + init(urlOpener: URLOpener = UIApplication.shared) { + self.urlOpener = urlOpener + } + + func onAppear() { + state = .onboarding(.startOnboardingDialog) + } + + func startOnboardingAction() { + state = .onboarding(.browsersComparisonDialog) + } + + func chooseBrowserAction() { + state = .chooseBrowser + } + + func setDefaultBrowserAction() { + if let url = URL(string: UIApplication.openSettingsURLString) { + urlOpener.open(url) + } + onCompletingOnboardingIntro?() + } + + func cancelSetDefaultBrowserAction() { + onCompletingOnboardingIntro?() + } +} diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift new file mode 100644 index 0000000000..a2049c29c8 --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift @@ -0,0 +1,194 @@ +// +// OnboardingView.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 + +// MARK: - OnboardingView + +struct OnboardingView: View { + + @Environment(\.verticalSizeClass) private var verticalSizeClass + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + @ObservedObject private var model: OnboardingIntroViewModel + + init(model: OnboardingIntroViewModel) { + self.model = model + } + + var body: some View { + Group { + switch model.state { + case .landing: + backgroundWrapped(view: landingView) + case let .onboarding(viewState): + backgroundWrapped(view: mainView(state: viewState)) + case .chooseBrowser: + chooseBrowserView + } + } + .transition(.opacity) + } + + private func backgroundWrapped(view: some View) -> some View { + GeometryReader { proxy in + ZStack { + OnboardingBackground() + .frame(width: proxy.size.width, height: proxy.size.height) + + view + } + } + } + + private func mainView(state: ViewState.Intro) -> some View { + GeometryReader { geometry in + VStack(alignment: .center) { + switch state { + case .startOnboardingDialog: + introView + .frame(width: geometry.size.width) + case .browsersComparisonDialog: + browsersComparisonView + .frame(width: geometry.size.width) + } + } + .offset(y: geometry.size.height * Metrics.dialogVerticalOffsetPercentage.build(v: verticalSizeClass, h: horizontalSizeClass)) + .transition(.opacity) + .animation(.easeInOut(duration: 0.5)) + } + } + + private var chooseBrowserView: some View { + OnboardingDefaultBrowserView( + setAsDefaultBrowserAction: { + model.setDefaultBrowserAction() + }, + cancelAction: { + model.cancelSetDefaultBrowserAction() + } + ) + } + + private var landingView: some View { + return LandingView() + .ignoresSafeArea(edges: .bottom) + .frame(maxHeight: .infinity, alignment: .bottom) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + Metrics.daxDialogDelay) { + withAnimation { + model.onAppear() + } + } + } + } + + private var introView: some View { + DaxDialogIntroView { + withAnimation { + model.startOnboardingAction() + } + } + .onboardingDaxDialogStyle() + .padding() + } + + private var browsersComparisonView: some View { + DaxDialogBrowsersComparisonView { + withAnimation { + model.chooseBrowserAction() + } + } + .onboardingDaxDialogStyle() + .padding() + } +} + +// MARK: - View State + +extension OnboardingView { + + enum ViewState: Equatable { + case landing + case onboarding(Intro) + case chooseBrowser + } + +} + +extension OnboardingView.ViewState { + + enum Intro: Equatable { + case startOnboardingDialog + case browsersComparisonDialog + } + +} + +// MARK: - Landing View + +extension OnboardingView { + + struct LandingView: View { + @Environment(\.verticalSizeClass) private var verticalSizeClass + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + + var body: some View { + GeometryReader { proxy in + VStack { + Spacer() + + Image(.daxIcon) + .resizable() + .frame(width: Metrics.iconSize.width, height: Metrics.iconSize.height) + + Text(UserText.onboardingWelcomeHeader) + .onboardingTitleStyle() + .frame(width: Metrics.titleWidth.build(v: verticalSizeClass, h: horizontalSizeClass), alignment: .top) + + Spacer() + + Image(Metrics.hikerImage.build(v: verticalSizeClass, h: horizontalSizeClass)) + } + .frame(width: proxy.size.width, height: proxy.size.height, alignment: .center) + } + } + } +} + +// MARK: - Metrics + +private enum Metrics { + static let iconSize = CGSize(width: 70, height: 70) + static let titleWidth = MetricBuilder(iPhone: 252, iPad: nil) + static let hikerImage = MetricBuilder(value: .hiker).smallIphone(.hikerSmall) + static let daxDialogDelay: TimeInterval = 2.0 + static let dialogVerticalOffsetPercentage = MetricBuilder(iPhone: 0.1, iPad: 0.2).smallIphone(0.05) +} + +// MARK: - Preview + +#Preview("Onboarding - Light") { + OnboardingView(model: .init()) + .preferredColorScheme(.light) +} + +#Preview("Onboarding - Dark") { + OnboardingView(model: .init()) + .preferredColorScheme(.dark) +} diff --git a/DuckDuckGo/OnboardingExperiment/Styles/DaxDialogStyles.swift b/DuckDuckGo/OnboardingExperiment/Styles/DaxDialogStyles.swift new file mode 100644 index 0000000000..147955a73a --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/Styles/DaxDialogStyles.swift @@ -0,0 +1,46 @@ +// +// DaxDialogStyles.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 OnboardingStyles { + + struct DaxDialogStyle: ViewModifier { + @Environment(\.verticalSizeClass) private var verticalSizeClass + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + + func body(content: Content) -> some View { + content + .frame(maxWidth: Metrics.daxDialogMaxWidth.build(v: verticalSizeClass, h: horizontalSizeClass)) + } + + } + +} + +private enum Metrics { + static let daxDialogMaxWidth = MetricBuilder(iPhone: nil, iPad: 480) +} + +extension View { + + func onboardingDaxDialogStyle() -> some View { + modifier(OnboardingStyles.DaxDialogStyle()) + } +} diff --git a/DuckDuckGo/OnboardingExperiment/Styles/OnboardingTextStyles.swift b/DuckDuckGo/OnboardingExperiment/Styles/OnboardingTextStyles.swift new file mode 100644 index 0000000000..a0fe295316 --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/Styles/OnboardingTextStyles.swift @@ -0,0 +1,51 @@ +// +// OnboardingTextStyles.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 + +enum OnboardingStyles {} + +extension OnboardingStyles { + + struct TitleStyle: ViewModifier { + + func body(content: Content) -> some View { + let view = content + .font(.system(size: 28, weight: .bold)) + .foregroundColor(.primary) + .multilineTextAlignment(.center) + + if #available(iOS 16, *) { + return view.kerning(0.38) + } else { + return view + } + } + + } + +} + +extension View { + + func onboardingTitleStyle() -> some View { + modifier(OnboardingStyles.TitleStyle()) + } + +} diff --git a/DuckDuckGoTests/MockURLOpener.swift b/DuckDuckGoTests/MockURLOpener.swift new file mode 100644 index 0000000000..79a5b9fe6e --- /dev/null +++ b/DuckDuckGoTests/MockURLOpener.swift @@ -0,0 +1,41 @@ +// +// MockURLOpener.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 +@testable import Core + +final class MockURLOpener: URLOpener { + private(set) var didCallCanOpenURL = false + private(set) var didCallOpenURL = false + private(set) var capturedURL: URL? + + var canOpenURL = false + + func canOpenURL(_ url: URL) -> Bool { + didCallCanOpenURL = true + capturedURL = url + return canOpenURL + } + + func open(_ url: URL, options: [UIApplication.OpenExternalURLOptionsKey: Any], completionHandler completion: ((Bool) -> Void)?) { + didCallOpenURL = true + capturedURL = url + } + +} diff --git a/DuckDuckGoTests/OnboardingIntroViewModelTests.swift b/DuckDuckGoTests/OnboardingIntroViewModelTests.swift new file mode 100644 index 0000000000..faa7b46c29 --- /dev/null +++ b/DuckDuckGoTests/OnboardingIntroViewModelTests.swift @@ -0,0 +1,119 @@ +// +// OnboardingIntroViewModelTests.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import DuckDuckGo + +final class OnboardingIntroViewModelTests: XCTestCase { + + func testWhenSubscribeToViewStateThenShouldSendLanding() { + // GIVEN + let sut = OnboardingIntroViewModel() + + // WHEN + let result = sut.state + + // THEN + XCTAssertEqual(result, .landing) + } + + func testWhenOnAppearIsCalledIsCalledThenViewStateChangesToStartOnboardingDialog() { + // GIVEN + let sut = OnboardingIntroViewModel() + XCTAssertEqual(sut.state, .landing) + + // WHEN + sut.onAppear() + + // THEN + XCTAssertEqual(sut.state, .onboarding(.startOnboardingDialog)) + } + + func testWhenStartOnboardingActionIsCalledThenViewStateChangesToBrowsersComparisonDialog() { + // GIVEN + let sut = OnboardingIntroViewModel() + XCTAssertEqual(sut.state, .landing) + + // WHEN + sut.startOnboardingAction() + + // THEN + XCTAssertEqual(sut.state, .onboarding(.browsersComparisonDialog)) + } + + func testWhenChooseBrowserActionIsCalledThenViewStateChangesToChooseBrowser() { + // GIVEN + let sut = OnboardingIntroViewModel() + XCTAssertEqual(sut.state, .landing) + + // WHEN + sut.chooseBrowserAction() + + // THEN + XCTAssertEqual(sut.state, .chooseBrowser) + } + + func testWhenSetDefaultBrowserActionIsCalledThenURLOpenerOpensSettingsURL() { + // GIVEN + let urlOpenerMock = MockURLOpener() + let sut = OnboardingIntroViewModel(urlOpener: urlOpenerMock) + XCTAssertFalse(urlOpenerMock.didCallOpenURL) + XCTAssertNil(urlOpenerMock.capturedURL) + + // WHEN + sut.setDefaultBrowserAction() + + // THEN + XCTAssertTrue(urlOpenerMock.didCallOpenURL) + XCTAssertEqual(urlOpenerMock.capturedURL?.absoluteString, UIApplication.openSettingsURLString) + } + + func testWhenSetDefaultBrowserActionIsCalledThenOnCompletingOnboardingIntroIsCalled() { + // GIVEN + var didCallOnCompletingOnboardingIntro = false + let sut = OnboardingIntroViewModel(urlOpener: MockURLOpener()) + sut.onCompletingOnboardingIntro = { + didCallOnCompletingOnboardingIntro = true + } + XCTAssertFalse(didCallOnCompletingOnboardingIntro) + + // WHEN + sut.setDefaultBrowserAction() + + // THEN + XCTAssertTrue(didCallOnCompletingOnboardingIntro) + } + + func testWhenCancelSetDefaultBrowserActionIsCalledThenOnCompletingOnboardingIntroIsCalled() { + // GIVEN + var didCallOnCompletingOnboardingIntro = false + let sut = OnboardingIntroViewModel(urlOpener: MockURLOpener()) + sut.onCompletingOnboardingIntro = { + didCallOnCompletingOnboardingIntro = true + } + XCTAssertFalse(didCallOnCompletingOnboardingIntro) + + // WHEN + sut.cancelSetDefaultBrowserAction() + + // THEN + XCTAssertTrue(didCallOnCompletingOnboardingIntro) + } + +}