diff --git a/.github/workflows/promote_testflight.yml b/.github/workflows/promote_testflight.yml new file mode 100644 index 0000000000..641c945ad7 --- /dev/null +++ b/.github/workflows/promote_testflight.yml @@ -0,0 +1,33 @@ +name: Promote TestFlight to App Store + +on: + workflow_dispatch: {} + +jobs: + promote-testflight-to-appstore: + runs-on: macos-14 + + steps: + - name: Check out the code + uses: actions/checkout@v4 + with: + ref: ${{ github.ref_name }} + sparse-checkout: | + .github + Gemfile + Gemfile.lock + fastlane + scripts + + - name: Set up fastlane + run: bundle install + + - name: Promote TestFlight to App Store + env: + APPLE_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_BASE64 }} + APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} + APPLE_API_KEY_ISSUER: ${{ secrets.APPLE_API_KEY_ISSUER }} + run: | + git config --global user.name "Dax the Duck" + git config --global user.email "dax@duckduckgo.com" + bundle exec fastlane promote_latest_testflight_to_appstore diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 525a57ddcf..50a4fefb6f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,7 +6,7 @@ on: destination: description: "Upload destination (App Store or TestFlight)" required: true - default: appstore + default: testflight type: choice options: - appstore @@ -36,10 +36,9 @@ jobs: id: destination run: | INPUT_DESTINATION=${{ github.event.inputs.destination }} - echo "destination=${INPUT_DESTINATION:-"appstore"}" >> $GITHUB_OUTPUT + echo "destination=${INPUT_DESTINATION:-"testflight"}" >> $GITHUB_OUTPUT - name: Assert release branch - if: steps.destination.outputs.destination == 'appstore' run: | case "${{ github.ref }}" in *release/*) ;; diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index 3b2b7dc2b3..e99711a2c3 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 7.141.0 +MARKETING_VERSION = 7.143.0 diff --git a/Core/AppPrivacyConfigurationDataProvider.swift b/Core/AppPrivacyConfigurationDataProvider.swift index e26b5f00c5..15f2c36375 100644 --- a/Core/AppPrivacyConfigurationDataProvider.swift +++ b/Core/AppPrivacyConfigurationDataProvider.swift @@ -23,8 +23,8 @@ import BrowserServicesKit final public class AppPrivacyConfigurationDataProvider: EmbeddedDataProvider { public struct Constants { - public static let embeddedDataETag = "\"4ffe7d2b6c8e252d0289b1398cc2685d\"" - public static let embeddedDataSHA = "9795ade4fdbc474688250f2ecfa097e917feea21a54fd97c524b851245d170e8" + public static let embeddedDataETag = "\"f8b9cfd5f1eb7b77c21d4476f85bd177\"" + public static let embeddedDataSHA = "c26c97714d73a9e1e99dbd341d5890da42b49d34a296672be3d3cea00bdd37a0" } public var embeddedDataEtag: String { diff --git a/Core/AppTrackerDataSetProvider.swift b/Core/AppTrackerDataSetProvider.swift index 29f59c5818..57696ef15b 100644 --- a/Core/AppTrackerDataSetProvider.swift +++ b/Core/AppTrackerDataSetProvider.swift @@ -23,8 +23,8 @@ import BrowserServicesKit final public class AppTrackerDataSetProvider: EmbeddedDataProvider { public struct Constants { - public static let embeddedDataETag = "\"f5dad10599a8d6088ec1908da3eeb3cc\"" - public static let embeddedDataSHA = "bb4c80f41383971693b241b4580fa74bbec1378ca6c2f8da8745dc6044fa1af9" + public static let embeddedDataETag = "\"0b9329cf89ed936dab485072c32814c4\"" + public static let embeddedDataSHA = "09b3b38cb556f93ac3d7c45e2535677285f5beef4cb4c0af2aaaa00b25f311d4" } public var embeddedDataEtag: String { diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index 4ba9c88b32..0a021242ea 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -708,6 +708,11 @@ extension Pixel { case privacyProTransactionProgressNotHiddenAfter60s case privacyProSuccessfulSubscriptionAttribution case privacyProKeychainAccessError + case privacyProSubscriptionCookieMissingTokenOnSignIn + case privacyProSubscriptionCookieMissingCookieOnSignOut + case privacyProSubscriptionCookieRefreshedWithUpdate + case privacyProSubscriptionCookieRefreshedWithDelete + case privacyProSubscriptionCookieFailedToSetSubscriptionCookie // MARK: Pixel Experiment case pixelExperimentEnrollment @@ -827,6 +832,9 @@ extension Pixel { case duckplayerExperimentDailySearch case duckplayerExperimentWeeklySearch case duckplayerExperimentYoutubePageView + + // MARK: WebView Error Page Shown + case webViewErrorPageShown } } @@ -1517,6 +1525,11 @@ extension Pixel.Event { case .privacyProTransactionProgressNotHiddenAfter60s: return "m_privacy-pro_progress_not_hidden_after_60s" case .privacyProSuccessfulSubscriptionAttribution: return "m_subscribe" case .privacyProKeychainAccessError: return "m_privacy-pro_keychain_access_error" + case .privacyProSubscriptionCookieMissingTokenOnSignIn: return "m_privacy-pro_subscription-cookie-missing_token_on_sign_in" + case .privacyProSubscriptionCookieMissingCookieOnSignOut: return "m_privacy-pro_subscription-cookie-missing_cookie_on_sign_out" + case .privacyProSubscriptionCookieRefreshedWithUpdate: return "m_privacy-pro_subscription-cookie-refreshed_with_update" + case .privacyProSubscriptionCookieRefreshedWithDelete: return "m_privacy-pro_subscription-cookie-refreshed_with_delete" + case .privacyProSubscriptionCookieFailedToSetSubscriptionCookie: return "m_privacy-pro_subscription-cookie-failed_to_set_subscription_cookie" // MARK: Pixel Experiment case .pixelExperimentEnrollment: return "pixel_experiment_enrollment" @@ -1649,6 +1662,8 @@ extension Pixel.Event { case .duckplayerExperimentWeeklySearch: return "duckplayer_experiment_weekly_search_v2" case .duckplayerExperimentYoutubePageView: return "duckplayer_experiment_youtube_page_view_v2" + // MARK: - WebView Error Page shown + case .webViewErrorPageShown: return "m_errorpageshown" } } } diff --git a/Core/SchemeHandler.swift b/Core/SchemeHandler.swift index 059e1fee45..535b0686e9 100644 --- a/Core/SchemeHandler.swift +++ b/Core/SchemeHandler.swift @@ -29,6 +29,7 @@ public class SchemeHandler { } public enum SchemeType: Equatable { + case allow case navigational case external(Action) case blob @@ -51,6 +52,7 @@ public class SchemeHandler { case shortcuts case shortcutsProduction = "shortcuts-production" case workflow + case marketplaceKit = "marketplace-kit" } private enum BlockedScheme: String { @@ -74,6 +76,13 @@ public class SchemeHandler { } switch PlatformScheme(rawValue: schemeString) { + case .marketplaceKit: + // marketplaceKit urls have to be allowed through without interference + if #available(iOS 17.4, *) { + return .allow + } else { + return .unknown + } case .sms, .mailto, .itms, .itmss, .itunes, .itmsApps, .itmsAppss, .shortcuts, .shortcutsProduction, .workflow: return .external(.askForConfirmation) case .none: diff --git a/Core/UserDefaultsPropertyWrapper.swift b/Core/UserDefaultsPropertyWrapper.swift index 2e119e09df..9fbffdcd53 100644 --- a/Core/UserDefaultsPropertyWrapper.swift +++ b/Core/UserDefaultsPropertyWrapper.swift @@ -180,7 +180,9 @@ public struct UserDefaultsWrapper { case duckPlayerPixelExperimentLastDayPixelFired = "com.duckduckgo.ios.duckplayer.pixel.experiment.last.day.pixel.fired.v2" case duckPlayerPixelExperimentLastVideoIDRendered = "com.duckduckgo.ios.duckplayer.pixel.experiment.last.videoID.rendered.v2" case duckPlayerPixelExperimentOverride = "com.duckduckgo.ios.duckplayer.pixel.experiment.override.v2" - + + // TipKit + case resetTipKitOnNextLaunch = "com.duckduckgo.ios.tipKit.resetOnNextLaunch" } private let key: Key diff --git a/Core/ios-config.json b/Core/ios-config.json index 161c16b811..edd16be36c 100644 --- a/Core/ios-config.json +++ b/Core/ios-config.json @@ -1,6 +1,6 @@ { "readme": "https://github.com/duckduckgo/privacy-configuration", - "version": 1728900024972, + "version": 1730109523334, "features": { "adClickAttribution": { "readme": "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#3rd-party-tracker-loading-protection", @@ -60,6 +60,11 @@ }, "hash": "b813ade8472a097ffbd43a3331116fe1" }, + "aiChat": { + "state": "disabled", + "exceptions": [], + "hash": "c292bb627849854515cebbded288ef5a" + }, "ampLinks": { "exceptions": [ { @@ -88,6 +93,9 @@ }, { "domain": "instructure.com" + }, + { + "domain": "centerwellpharmacy.com" } ], "settings": { @@ -114,7 +122,7 @@ ] }, "state": "enabled", - "hash": "fa5f86bac5946c528cd6bc7449a2718a" + "hash": "d9703d9553194bc54e66db1f2a4ec1f8" }, "androidBrowserConfig": { "exceptions": [], @@ -352,6 +360,9 @@ { "domain": "speedweek.com" }, + { + "domain": "la-becanerie.com" + }, { "domain": "marvel.com" }, @@ -366,6 +377,9 @@ }, { "domain": "instructure.com" + }, + { + "domain": "centerwellpharmacy.com" } ], "settings": { @@ -393,7 +407,7 @@ } } }, - "hash": "ea2fef42c1666e9d9e4cabd49e57764d" + "hash": "8392e127a3bcaee5c2913df355a7d254" }, "autofillBreakageReporter": { "state": "enabled", @@ -488,12 +502,15 @@ "steps": [ { "percent": 10 + }, + { + "percent": 50 } ] } } }, - "hash": "75ff21cf9a4181783c06965edd6fe746" + "hash": "9de8e4b066aa23f7c20ca638ee0d9f1a" }, "bookmarks": { "state": "enabled", @@ -517,9 +534,12 @@ }, { "domain": "instructure.com" + }, + { + "domain": "centerwellpharmacy.com" } ], - "hash": "37e0cf88badfc8b01b6394f0884502f6" + "hash": "3766f6af346d3fffdf1e8ffce682c66e" }, "brokenSitePrompt": { "state": "enabled", @@ -1223,6 +1243,9 @@ }, { "domain": "instructure.com" + }, + { + "domain": "centerwellpharmacy.com" } ], "settings": { @@ -1240,7 +1263,7 @@ } }, "state": "disabled", - "hash": "cb1f114a9e0314393b2a0f789cba163f" + "hash": "3973e9d924c9a054df7f5dffad1f1d19" }, "clickToPlay": { "exceptions": [ @@ -1258,6 +1281,9 @@ }, { "domain": "instructure.com" + }, + { + "domain": "centerwellpharmacy.com" } ], "settings": { @@ -1270,7 +1296,7 @@ } }, "state": "disabled", - "hash": "894fb86c1f058aee9db47cfcdf3637de" + "hash": "31a06101df1dc362bfcef2d7a6320f80" }, "clientBrandHint": { "exceptions": [], @@ -1313,11 +1339,22 @@ { "domain": "flexmls.com" }, + { + "domain": "humana.com" + }, { "domain": "instructure.com" + }, + { + "domain": "centerwellpharmacy.com" } ], - "hash": "96b2f778bab196aa424e9c859ddea778" + "hash": "980bf875526f3cc7892c001a7d2e5a74" + }, + "contextualOnboarding": { + "exceptions": [], + "state": "disabled", + "hash": "728493ef7a1488e4781656d3f9db84aa" }, "cookie": { "settings": { @@ -1382,10 +1419,13 @@ }, { "domain": "instructure.com" + }, + { + "domain": "centerwellpharmacy.com" } ], "state": "disabled", - "hash": "6b4d2cef180104c5c84f5687479b8492" + "hash": "cef2b67a9df0d36b0875e7b54d33a4d0" }, "customUserAgent": { "settings": { @@ -1402,6 +1442,10 @@ { "domain": "xfinity.com", "reason": "https://github.com/duckduckgo/privacy-configuration/pull/2149" + }, + { + "domain": "ihg.com", + "reason": "https://github.com/duckduckgo/privacy-configuration/pull/2383" } ], "ddgDefaultSites": [ @@ -1422,13 +1466,16 @@ }, { "domain": "xfinity.com" + }, + { + "domain": "ihg.com" } ], "omitVersionSites": [] }, "exceptions": [], "state": "enabled", - "hash": "2ed8c3ccd40db2d9dca1e7ecc4231045" + "hash": "e577ccb473bdb7ada49c4d3c6e79cf01" }, "dbp": { "state": "disabled", @@ -1456,7 +1503,7 @@ "state": "disabled" }, "openInNewTab": { - "state": "disabled" + "state": "internal" }, "enableDuckPlayer": { "state": "enabled", @@ -1549,7 +1596,7 @@ ] }, "state": "enabled", - "hash": "7f82d68f07b3e2aaac1b89725c1d379e" + "hash": "c21895584fc5a38e4290c7941ec7d5f8" }, "elementHiding": { "exceptions": [ @@ -1570,6 +1617,9 @@ }, { "domain": "instructure.com" + }, + { + "domain": "centerwellpharmacy.com" } ], "settings": { @@ -3526,6 +3576,23 @@ } ] }, + { + "domain": "mantan-web.jp", + "rules": [ + { + "selector": "center > div", + "type": "hide-empty" + }, + { + "selector": ".ad-rec", + "type": "hide-empty" + }, + { + "selector": ".conteiner__head", + "type": "hide-empty" + } + ] + }, { "domain": "marketwatch.com", "rules": [ @@ -4375,6 +4442,10 @@ { "selector": "[data-aa-adunit]", "type": "hide" + }, + { + "selector": "[data-adpath]", + "type": "hide-empty" } ] }, @@ -4868,7 +4939,7 @@ ] }, "state": "enabled", - "hash": "91a3dbe67daa37762247c415766a2634" + "hash": "9518158b11d290809536a99f637f467e" }, "exceptionHandler": { "exceptions": [ @@ -4886,10 +4957,13 @@ }, { "domain": "instructure.com" + }, + { + "domain": "centerwellpharmacy.com" } ], "state": "disabled", - "hash": "be6751fe0307a7e1b9476f4d8b8d0aaf" + "hash": "a214254da3cc914ed5bfc0a2d893b589" }, "extendedOnboarding": { "exceptions": [], @@ -4916,9 +4990,12 @@ }, { "domain": "instructure.com" + }, + { + "domain": "centerwellpharmacy.com" } ], - "hash": "7f042650922da2636492e77ed1101bce" + "hash": "008c61cd03c28287a7f86b598c37078b" }, "fingerprintingBattery": { "exceptions": [ @@ -4939,10 +5016,13 @@ }, { "domain": "instructure.com" + }, + { + "domain": "centerwellpharmacy.com" } ], "state": "enabled", - "hash": "fcc2138fa97c35ded544b39708fda919" + "hash": "d05606a02ffd6ce5e223bc26e748a203" }, "fingerprintingCanvas": { "settings": { @@ -5047,10 +5127,13 @@ }, { "domain": "instructure.com" + }, + { + "domain": "centerwellpharmacy.com" } ], "state": "disabled", - "hash": "49a3d497835bf5715aaaa73f87dd974f" + "hash": "b0eef1a098ab8c6cc9d6da35a9cfb7ad" }, "fingerprintingHardware": { "settings": { @@ -5116,10 +5199,13 @@ }, { "domain": "instructure.com" + }, + { + "domain": "centerwellpharmacy.com" } ], "state": "enabled", - "hash": "cd4a8461973d1c1648dd20e6d1f532a7" + "hash": "25a38bd7ccbca83ce0899548608235a7" }, "fingerprintingScreenSize": { "settings": { @@ -5173,10 +5259,13 @@ }, { "domain": "instructure.com" + }, + { + "domain": "centerwellpharmacy.com" } ], "state": "enabled", - "hash": "046340bb9287a20efed6189525ec5fed" + "hash": "c22a6e9f1c03693516589c47970d7a04" }, "fingerprintingTemporaryStorage": { "exceptions": [ @@ -5203,10 +5292,13 @@ }, { "domain": "instructure.com" + }, + { + "domain": "centerwellpharmacy.com" } ], "state": "enabled", - "hash": "14b7fe3d276b52109c59f0c71aee4f71" + "hash": "48b1d8e96ee94825378d12a8d5a66895" }, "googleRejected": { "exceptions": [ @@ -5224,41 +5316,26 @@ }, { "domain": "instructure.com" + }, + { + "domain": "centerwellpharmacy.com" } ], "state": "disabled", - "hash": "be6751fe0307a7e1b9476f4d8b8d0aaf" + "hash": "a214254da3cc914ed5bfc0a2d893b589" }, "gpc": { "state": "enabled", "exceptions": [ - { - "domain": "allegiantair.com" - }, { "domain": "boston.com" }, { "domain": "costco.com" }, - { - "domain": "crunchyroll.com" - }, { "domain": "espn.com" }, - { - "domain": "eventbrite.com" - }, - { - "domain": "duluthtrading.com" - }, - { - "domain": "web.whatsapp.com" - }, - { - "domain": "verizon.com" - }, { "domain": "chime.com" }, @@ -5266,13 +5343,13 @@ "domain": "tirerack.com" }, { - "domain": "milesplit.live" + "domain": "dollargeneral.com" }, { - "domain": "dollargeneral.com" + "domain": "milesplit.live" }, { - "domain": "monstergear.monsterenergy.com" + "domain": "monsterenergy.com" }, { "domain": "npr.org" @@ -5280,12 +5357,6 @@ { "domain": "norton.com" }, - { - "domain": "jcrew.com" - }, - { - "domain": "pgealerts.alerts.pge.com" - }, { "domain": "marvel.com" }, @@ -5299,10 +5370,10 @@ "domain": "flexmls.com" }, { - "domain": "oreillyauto.com" + "domain": "instructure.com" }, { - "domain": "instructure.com" + "domain": "centerwellpharmacy.com" } ], "settings": { @@ -5314,7 +5385,7 @@ "privacy-test-pages.site" ] }, - "hash": "fb031f27a1e1c2b143505c4f2e7b3ba7" + "hash": "37630ab090682ee7d004120a42031281" }, "harmfulApis": { "settings": { @@ -5430,10 +5501,13 @@ }, { "domain": "instructure.com" + }, + { + "domain": "centerwellpharmacy.com" } ], "state": "disabled", - "hash": "fb598c4167ff166d85dd49c701cc5579" + "hash": "f255e336420119584b7000846be6d456" }, "history": { "state": "enabled", @@ -5484,9 +5558,12 @@ }, { "domain": "instructure.com" + }, + { + "domain": "centerwellpharmacy.com" } ], - "hash": "b47d255c6f836ecb7ae0b3e61cc2c025" + "hash": "7407fc43cbd260f9aaca7cb7dab15bf4" }, "incontextSignup": { "exceptions": [], @@ -5545,6 +5622,9 @@ }, { "domain": "instructure.com" + }, + { + "domain": "centerwellpharmacy.com" } ], "settings": { @@ -5555,7 +5635,7 @@ ] }, "state": "enabled", - "hash": "d14f6e3a9aa4139ee1d517016b59691e" + "hash": "a1100eac5ecca0a11501df9f4dafa31a" }, "networkProtection": { "state": "enabled", @@ -5602,10 +5682,13 @@ }, { "domain": "instructure.com" + }, + { + "domain": "centerwellpharmacy.com" } ], "state": "disabled", - "hash": "82088db85ca7f64418fbfd57db25ade1" + "hash": "5646a778c1cb6ec6e9c0da2c7dbd4bdb" }, "performanceMetrics": { "state": "enabled", @@ -5624,9 +5707,12 @@ }, { "domain": "instructure.com" + }, + { + "domain": "centerwellpharmacy.com" } ], - "hash": "6792064606a5a72c5cd44addb4d40bda" + "hash": "60c3c3eed29e1e0c092fad8775483210" }, "phishingDetection": { "state": "disabled", @@ -5645,9 +5731,12 @@ }, { "domain": "instructure.com" + }, + { + "domain": "centerwellpharmacy.com" } ], - "hash": "37e0cf88badfc8b01b6394f0884502f6" + "hash": "3766f6af346d3fffdf1e8ffce682c66e" }, "pluginPointFocusedViewPlugin": { "state": "disabled", @@ -5764,10 +5853,13 @@ }, { "domain": "instructure.com" + }, + { + "domain": "centerwellpharmacy.com" } ], "state": "disabled", - "hash": "138c3b2409f6b3bf967b804ab9bf2ce2" + "hash": "68eb25a9461b134838100eecb0271905" }, "remoteMessaging": { "state": "enabled", @@ -5791,12 +5883,15 @@ }, { "domain": "instructure.com" + }, + { + "domain": "centerwellpharmacy.com" } ], "settings": { "windowInMs": 0 }, - "hash": "baf19d9e0f506ed09f46c95b1849adee" + "hash": "13d2723b0c33943f086acb8c239e22e8" }, "runtimeChecks": { "state": "disabled", @@ -5815,10 +5910,13 @@ }, { "domain": "instructure.com" + }, + { + "domain": "centerwellpharmacy.com" } ], "settings": {}, - "hash": "dfede9f06b9e322e198736703d013d15" + "hash": "568cf394681d38683d1aeb8f0d0e6a7c" }, "sendFullPackageInstallSource": { "state": "enabled", @@ -5842,10 +5940,13 @@ }, { "domain": "instructure.com" + }, + { + "domain": "centerwellpharmacy.com" } ], "state": "disabled", - "hash": "be6751fe0307a7e1b9476f4d8b8d0aaf" + "hash": "a214254da3cc914ed5bfc0a2d893b589" }, "sslCertificates": { "state": "enabled", @@ -7320,6 +7421,7 @@ "bodyelectricvitality.com.au", "cosmicbook.news", "eatroyo.com", + "experian.com", "piedmontng.com", "thesimsresource.com", "tradersync.com", @@ -8766,6 +8868,16 @@ } ] }, + "svonm.com": { + "rules": [ + { + "rule": "hgc-cf-cache-1.svonm.com", + "domains": [ + "t-online.de" + ] + } + ] + }, "taboola.com": { "rules": [ { @@ -9251,9 +9363,12 @@ }, { "domain": "instructure.com" + }, + { + "domain": "centerwellpharmacy.com" } ], - "hash": "0692c3f8cc6179433b9f23f00c7dd0ac" + "hash": "434130223ee6493827d477d0171521da" }, "trackingCookies1p": { "settings": { @@ -9277,10 +9392,13 @@ }, { "domain": "instructure.com" + }, + { + "domain": "centerwellpharmacy.com" } ], "state": "disabled", - "hash": "763f56424b0827b5731927a043219912" + "hash": "a5c95510cb55fbe69cbff10e55a982dd" }, "trackingCookies3p": { "settings": { @@ -9301,10 +9419,13 @@ }, { "domain": "instructure.com" + }, + { + "domain": "centerwellpharmacy.com" } ], "state": "disabled", - "hash": "82088db85ca7f64418fbfd57db25ade1" + "hash": "5646a778c1cb6ec6e9c0da2c7dbd4bdb" }, "trackingParameters": { "exceptions": [ @@ -9328,6 +9449,9 @@ }, { "domain": "instructure.com" + }, + { + "domain": "centerwellpharmacy.com" } ], "settings": { @@ -9360,7 +9484,7 @@ ] }, "state": "enabled", - "hash": "3805ecfb8a129f70a99e73a364b38f38" + "hash": "e530308726226930ff9a058fa064a39f" }, "userAgentRotation": { "settings": { @@ -9381,10 +9505,13 @@ }, { "domain": "instructure.com" + }, + { + "domain": "centerwellpharmacy.com" } ], "state": "disabled", - "hash": "9225b8785d6973db37abde99d81d219c" + "hash": "dd373ef0993c7ca9d9fa949db6d6aca0" }, "voiceSearch": { "exceptions": [], @@ -9415,6 +9542,9 @@ }, { "domain": "instructure.com" + }, + { + "domain": "centerwellpharmacy.com" } ], "state": "enabled", @@ -9487,7 +9617,7 @@ } ] }, - "hash": "2853748f3ebb813d59f4db4a7bb13c83" + "hash": "ed17f6ff342f200305eb4bbe544efec0" }, "webViewBlobDownload": { "exceptions": [], @@ -9509,6 +9639,11 @@ "state": "disabled", "hash": "728493ef7a1488e4781656d3f9db84aa" }, + "windowsNewTabPageExperiment": { + "state": "disabled", + "exceptions": [], + "hash": "c292bb627849854515cebbded288ef5a" + }, "windowsPermissionUsage": { "exceptions": [], "state": "disabled", diff --git a/Core/trackerData.json b/Core/trackerData.json index fafcb8a1a9..72548a65e2 100644 --- a/Core/trackerData.json +++ b/Core/trackerData.json @@ -1,6 +1,6 @@ { "_builtWith": { - "tracker-radar": "377ccbb6904d70981db82b125f927d0aab7cbc1101fa5daf7774eee9b18e6668-4013b4e91930c643394cb31c6c745356f133b04f", + "tracker-radar": "115cad5b09c64b18754dbfb462f35cb96a8b016d6ef8b6238e792f053a2a69b7-4013b4e91930c643394cb31c6c745356f133b04f", "tracker-surrogates": "0528e3226df15b1a3e319ad68ef76612a8f26623" }, "readme": "https://github.com/duckduckgo/tracker-blocklists", @@ -32940,6 +32940,17 @@ "cookies": 0.01, "default": "block" }, + "alertarithmetic.com": { + "domain": "alertarithmetic.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral (alertarithmetic.com)", + "displayName": "Admiral" + }, + "prevalence": 0.0118, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "aliasanvil.com": { "domain": "aliasanvil.com", "owner": { @@ -35074,6 +35085,17 @@ "cookies": 0.01, "default": "block" }, + "concernedchange.com": { + "domain": "concernedchange.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral (concernedchange.com)", + "displayName": "Admiral" + }, + "prevalence": 0.0118, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "concernedchickens.com": { "domain": "concernedchickens.com", "owner": { @@ -35624,17 +35646,6 @@ "cookies": 0.01, "default": "block" }, - "cumbersomecloud.com": { - "domain": "cumbersomecloud.com", - "owner": { - "name": "Leven Labs, Inc. DBA Admiral (cumbersomecloud.com)", - "displayName": "Admiral" - }, - "prevalence": 0.0118, - "fingerprinting": 1, - "cookies": 0.01, - "default": "block" - }, "curiouschalk.com": { "domain": "curiouschalk.com", "owner": { @@ -36350,17 +36361,6 @@ "cookies": 0.01, "default": "block" }, - "dreamycanyon.com": { - "domain": "dreamycanyon.com", - "owner": { - "name": "Leven Labs, Inc. DBA Admiral (dreamycanyon.com)", - "displayName": "Admiral" - }, - "prevalence": 0.0118, - "fingerprinting": 1, - "cookies": 0.01, - "default": "block" - }, "driftpizza.com": { "domain": "driftpizza.com", "owner": { @@ -37472,6 +37472,17 @@ "cookies": 0.01, "default": "block" }, + "fertilefeeling.com": { + "domain": "fertilefeeling.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral (fertilefeeling.com)", + "displayName": "Admiral" + }, + "prevalence": 0.0118, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "fewjuice.com": { "domain": "fewjuice.com", "owner": { @@ -40112,6 +40123,17 @@ "cookies": 0.01, "default": "block" }, + "lonelybulb.com": { + "domain": "lonelybulb.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral (lonelybulb.com)", + "displayName": "Admiral" + }, + "prevalence": 0.0118, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "lonelyflavor.com": { "domain": "lonelyflavor.com", "owner": { @@ -40486,6 +40508,17 @@ "cookies": 0.01, "default": "block" }, + "marrowleaves.com": { + "domain": "marrowleaves.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral (marrowleaves.com)", + "displayName": "Admiral" + }, + "prevalence": 0.0118, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "massivemark.com": { "domain": "massivemark.com", "owner": { @@ -41355,6 +41388,17 @@ "cookies": 0.01, "default": "block" }, + "numberlessring.com": { + "domain": "numberlessring.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral (numberlessring.com)", + "displayName": "Admiral" + }, + "prevalence": 0.0118, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "numerousnest.com": { "domain": "numerousnest.com", "owner": { @@ -41399,6 +41443,17 @@ "cookies": 0.01, "default": "block" }, + "objecthero.com": { + "domain": "objecthero.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral (objecthero.com)", + "displayName": "Admiral" + }, + "prevalence": 0.0118, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "obscenesidewalk.com": { "domain": "obscenesidewalk.com", "owner": { @@ -42774,6 +42829,17 @@ "cookies": 0.01, "default": "block" }, + "rarestcandy.com": { + "domain": "rarestcandy.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral (rarestcandy.com)", + "displayName": "Admiral" + }, + "prevalence": 0.0118, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "raresummer.com": { "domain": "raresummer.com", "owner": { @@ -42939,6 +43005,17 @@ "cookies": 0.01, "default": "block" }, + "reflectivestatement.com": { + "domain": "reflectivestatement.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral (reflectivestatement.com)", + "displayName": "Admiral" + }, + "prevalence": 0.0118, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "refundradar.com": { "domain": "refundradar.com", "owner": { @@ -42950,6 +43027,17 @@ "cookies": 0.01, "default": "block" }, + "regexmail.com": { + "domain": "regexmail.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral (regexmail.com)", + "displayName": "Admiral" + }, + "prevalence": 0.0118, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "regularplants.com": { "domain": "regularplants.com", "owner": { @@ -43676,6 +43764,17 @@ "cookies": 0.01, "default": "block" }, + "scribbleson.com": { + "domain": "scribbleson.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral (scribbleson.com)", + "displayName": "Admiral" + }, + "prevalence": 0.0118, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "scrollservice.com": { "domain": "scrollservice.com", "owner": { @@ -43764,6 +43863,17 @@ "cookies": 0.01, "default": "block" }, + "seemlysuggestion.com": { + "domain": "seemlysuggestion.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral (seemlysuggestion.com)", + "displayName": "Admiral" + }, + "prevalence": 0.0118, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "selfishsea.com": { "domain": "selfishsea.com", "owner": { @@ -45073,6 +45183,17 @@ "cookies": 0.01, "default": "block" }, + "steepscale.com": { + "domain": "steepscale.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral (steepscale.com)", + "displayName": "Admiral" + }, + "prevalence": 0.0118, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "steepsister.com": { "domain": "steepsister.com", "owner": { @@ -45491,17 +45612,6 @@ "cookies": 0.01, "default": "block" }, - "superviseshoes.com": { - "domain": "superviseshoes.com", - "owner": { - "name": "Leven Labs, Inc. DBA Admiral (superviseshoes.com)", - "displayName": "Admiral" - }, - "prevalence": 0.0118, - "fingerprinting": 1, - "cookies": 0.01, - "default": "block" - }, "supportwaves.com": { "domain": "supportwaves.com", "owner": { @@ -46162,17 +46272,6 @@ "cookies": 0.01, "default": "block" }, - "tranquilarchipelago.com": { - "domain": "tranquilarchipelago.com", - "owner": { - "name": "Leven Labs, Inc. DBA Admiral (tranquilarchipelago.com)", - "displayName": "Admiral" - }, - "prevalence": 0.0118, - "fingerprinting": 1, - "cookies": 0.01, - "default": "block" - }, "tranquilcan.com": { "domain": "tranquilcan.com", "owner": { @@ -46184,17 +46283,6 @@ "cookies": 0.01, "default": "block" }, - "tranquilcanyon.com": { - "domain": "tranquilcanyon.com", - "owner": { - "name": "Leven Labs, Inc. DBA Admiral (tranquilcanyon.com)", - "displayName": "Admiral" - }, - "prevalence": 0.0118, - "fingerprinting": 1, - "cookies": 0.01, - "default": "block" - }, "tranquilplume.com": { "domain": "tranquilplume.com", "owner": { @@ -57867,6 +57955,13 @@ "prevalence": 0.0118, "displayName": "Admiral" }, + "Leven Labs, Inc. DBA Admiral (alertarithmetic.com)": { + "domains": [ + "alertarithmetic.com" + ], + "prevalence": 0.0118, + "displayName": "Admiral" + }, "Leven Labs, Inc. DBA Admiral (aliasanvil.com)": { "domains": [ "aliasanvil.com" @@ -59470,6 +59565,13 @@ "prevalence": 0.0118, "displayName": "Admiral" }, + "Leven Labs, Inc. DBA Admiral (concernedchange.com)": { + "domains": [ + "concernedchange.com" + ], + "prevalence": 0.0118, + "displayName": "Admiral" + }, "Leven Labs, Inc. DBA Admiral (concernedchickens.com)": { "domains": [ "concernedchickens.com" @@ -59855,13 +59957,6 @@ "prevalence": 0.0118, "displayName": "Admiral" }, - "Leven Labs, Inc. DBA Admiral (cumbersomecloud.com)": { - "domains": [ - "cumbersomecloud.com" - ], - "prevalence": 0.0118, - "displayName": "Admiral" - }, "Leven Labs, Inc. DBA Admiral (curiouschalk.com)": { "domains": [ "curiouschalk.com" @@ -60415,13 +60510,6 @@ "prevalence": 0.0118, "displayName": "Admiral" }, - "Leven Labs, Inc. DBA Admiral (dreamycanyon.com)": { - "domains": [ - "dreamycanyon.com" - ], - "prevalence": 0.0118, - "displayName": "Admiral" - }, "Leven Labs, Inc. DBA Admiral (driftpizza.com)": { "domains": [ "driftpizza.com" @@ -61185,6 +61273,13 @@ "prevalence": 0.0118, "displayName": "Admiral" }, + "Leven Labs, Inc. DBA Admiral (fertilefeeling.com)": { + "domains": [ + "fertilefeeling.com" + ], + "prevalence": 0.0118, + "displayName": "Admiral" + }, "Leven Labs, Inc. DBA Admiral (fewjuice.com)": { "domains": [ "fewjuice.com" @@ -63068,6 +63163,13 @@ "prevalence": 0.0118, "displayName": "Admiral" }, + "Leven Labs, Inc. DBA Admiral (lonelybulb.com)": { + "domains": [ + "lonelybulb.com" + ], + "prevalence": 0.0118, + "displayName": "Admiral" + }, "Leven Labs, Inc. DBA Admiral (lonelyflavor.com)": { "domains": [ "lonelyflavor.com" @@ -63341,6 +63443,13 @@ "prevalence": 0.0118, "displayName": "Admiral" }, + "Leven Labs, Inc. DBA Admiral (marrowleaves.com)": { + "domains": [ + "marrowleaves.com" + ], + "prevalence": 0.0118, + "displayName": "Admiral" + }, "Leven Labs, Inc. DBA Admiral (massivemark.com)": { "domains": [ "massivemark.com" @@ -63957,6 +64066,13 @@ "prevalence": 0.0118, "displayName": "Admiral" }, + "Leven Labs, Inc. DBA Admiral (numberlessring.com)": { + "domains": [ + "numberlessring.com" + ], + "prevalence": 0.0118, + "displayName": "Admiral" + }, "Leven Labs, Inc. DBA Admiral (numerousnest.com)": { "domains": [ "numerousnest.com" @@ -63992,6 +64108,13 @@ "prevalence": 0.0118, "displayName": "Admiral" }, + "Leven Labs, Inc. DBA Admiral (objecthero.com)": { + "domains": [ + "objecthero.com" + ], + "prevalence": 0.0118, + "displayName": "Admiral" + }, "Leven Labs, Inc. DBA Admiral (obscenesidewalk.com)": { "domains": [ "obscenesidewalk.com" @@ -65056,6 +65179,13 @@ "prevalence": 0.0118, "displayName": "Admiral" }, + "Leven Labs, Inc. DBA Admiral (rarestcandy.com)": { + "domains": [ + "rarestcandy.com" + ], + "prevalence": 0.0118, + "displayName": "Admiral" + }, "Leven Labs, Inc. DBA Admiral (raresummer.com)": { "domains": [ "raresummer.com" @@ -65182,6 +65312,13 @@ "prevalence": 0.0118, "displayName": "Admiral" }, + "Leven Labs, Inc. DBA Admiral (reflectivestatement.com)": { + "domains": [ + "reflectivestatement.com" + ], + "prevalence": 0.0118, + "displayName": "Admiral" + }, "Leven Labs, Inc. DBA Admiral (refundradar.com)": { "domains": [ "refundradar.com" @@ -65189,6 +65326,13 @@ "prevalence": 0.0118, "displayName": "Admiral" }, + "Leven Labs, Inc. DBA Admiral (regexmail.com)": { + "domains": [ + "regexmail.com" + ], + "prevalence": 0.0118, + "displayName": "Admiral" + }, "Leven Labs, Inc. DBA Admiral (regularplants.com)": { "domains": [ "regularplants.com" @@ -65784,6 +65928,13 @@ "prevalence": 0.0118, "displayName": "Admiral" }, + "Leven Labs, Inc. DBA Admiral (scribbleson.com)": { + "domains": [ + "scribbleson.com" + ], + "prevalence": 0.0118, + "displayName": "Admiral" + }, "Leven Labs, Inc. DBA Admiral (scribblestring.com)": { "domains": [ "scribblestring.com" @@ -65854,6 +66005,13 @@ "prevalence": 0.0118, "displayName": "Admiral" }, + "Leven Labs, Inc. DBA Admiral (seemlysuggestion.com)": { + "domains": [ + "seemlysuggestion.com" + ], + "prevalence": 0.0118, + "displayName": "Admiral" + }, "Leven Labs, Inc. DBA Admiral (selectivesummer.com)": { "domains": [ "selectivesummer.com" @@ -66862,6 +67020,13 @@ "prevalence": 0.0118, "displayName": "Admiral" }, + "Leven Labs, Inc. DBA Admiral (steepscale.com)": { + "domains": [ + "steepscale.com" + ], + "prevalence": 0.0118, + "displayName": "Admiral" + }, "Leven Labs, Inc. DBA Admiral (steepsister.com)": { "domains": [ "steepsister.com" @@ -67219,13 +67384,6 @@ "prevalence": 0.0118, "displayName": "Admiral" }, - "Leven Labs, Inc. DBA Admiral (superviseshoes.com)": { - "domains": [ - "superviseshoes.com" - ], - "prevalence": 0.0118, - "displayName": "Admiral" - }, "Leven Labs, Inc. DBA Admiral (supportwaves.com)": { "domains": [ "supportwaves.com" @@ -67702,13 +67860,6 @@ "prevalence": 0.0118, "displayName": "Admiral" }, - "Leven Labs, Inc. DBA Admiral (tranquilarchipelago.com)": { - "domains": [ - "tranquilarchipelago.com" - ], - "prevalence": 0.0118, - "displayName": "Admiral" - }, "Leven Labs, Inc. DBA Admiral (tranquilcan.com)": { "domains": [ "tranquilcan.com" @@ -67716,13 +67867,6 @@ "prevalence": 0.0118, "displayName": "Admiral" }, - "Leven Labs, Inc. DBA Admiral (tranquilcanyon.com)": { - "domains": [ - "tranquilcanyon.com" - ], - "prevalence": 0.0118, - "displayName": "Admiral" - }, "Leven Labs, Inc. DBA Admiral (tranquilplume.com)": { "domains": [ "tranquilplume.com" @@ -69436,6 +69580,7 @@ "aheadgrow.com": "Leven Labs, Inc. DBA Admiral (aheadgrow.com)", "aheadmachine.com": "Leven Labs, Inc. DBA Admiral (aheadmachine.com)", "ak0gsh40.com": "Leven Labs, Inc. DBA Admiral (ak0gsh40.com)", + "alertarithmetic.com": "Leven Labs, Inc. DBA Admiral (alertarithmetic.com)", "aliasanvil.com": "Leven Labs, Inc. DBA Admiral (aliasanvil.com)", "alikeaddition.com": "Leven Labs, Inc. DBA Admiral (alikeaddition.com)", "aliveachiever.com": "Leven Labs, Inc. DBA Admiral (aliveachiever.com)", @@ -69665,6 +69810,7 @@ "comfygoodness.com": "Leven Labs, Inc. DBA Admiral (comfygoodness.com)", "comparereaction.com": "Leven Labs, Inc. DBA Admiral (comparereaction.com)", "compiledoctor.com": "Leven Labs, Inc. DBA Admiral (compiledoctor.com)", + "concernedchange.com": "Leven Labs, Inc. DBA Admiral (concernedchange.com)", "concernedchickens.com": "Leven Labs, Inc. DBA Admiral (concernedchickens.com)", "condemnedcomb.com": "Leven Labs, Inc. DBA Admiral (condemnedcomb.com)", "conditionchange.com": "Leven Labs, Inc. DBA Admiral (conditionchange.com)", @@ -69720,7 +69866,6 @@ "culturedfeather.com": "Leven Labs, Inc. DBA Admiral (culturedfeather.com)", "cumbersomecar.com": "Leven Labs, Inc. DBA Admiral (cumbersomecar.com)", "cumbersomecarpenter.com": "Leven Labs, Inc. DBA Admiral (cumbersomecarpenter.com)", - "cumbersomecloud.com": "Leven Labs, Inc. DBA Admiral (cumbersomecloud.com)", "curiouschalk.com": "Leven Labs, Inc. DBA Admiral (curiouschalk.com)", "curioussuccess.com": "Leven Labs, Inc. DBA Admiral (curioussuccess.com)", "curlycannon.com": "Leven Labs, Inc. DBA Admiral (curlycannon.com)", @@ -69800,7 +69945,6 @@ "dq95d35.com": "Leven Labs, Inc. DBA Admiral (dq95d35.com)", "drainpaste.com": "Leven Labs, Inc. DBA Admiral (drainpaste.com)", "dramaticdirection.com": "Leven Labs, Inc. DBA Admiral (dramaticdirection.com)", - "dreamycanyon.com": "Leven Labs, Inc. DBA Admiral (dreamycanyon.com)", "driftpizza.com": "Leven Labs, Inc. DBA Admiral (driftpizza.com)", "drollwharf.com": "Leven Labs, Inc. DBA Admiral (drollwharf.com)", "drydrum.com": "Leven Labs, Inc. DBA Admiral (drydrum.com)", @@ -69910,6 +70054,7 @@ "feeblestamp.com": "Leven Labs, Inc. DBA Admiral (feeblestamp.com)", "feignedfaucet.com": "Leven Labs, Inc. DBA Admiral (feignedfaucet.com)", "fernwaycloud.com": "Leven Labs, Inc. DBA Admiral (fernwaycloud.com)", + "fertilefeeling.com": "Leven Labs, Inc. DBA Admiral (fertilefeeling.com)", "fewjuice.com": "Leven Labs, Inc. DBA Admiral (fewjuice.com)", "fewkittens.com": "Leven Labs, Inc. DBA Admiral (fewkittens.com)", "finalizeforce.com": "Leven Labs, Inc. DBA Admiral (finalizeforce.com)", @@ -70179,6 +70324,7 @@ "livingsleet.com": "Leven Labs, Inc. DBA Admiral (livingsleet.com)", "lizardslaugh.com": "Leven Labs, Inc. DBA Admiral (lizardslaugh.com)", "loadsurprise.com": "Leven Labs, Inc. DBA Admiral (loadsurprise.com)", + "lonelybulb.com": "Leven Labs, Inc. DBA Admiral (lonelybulb.com)", "lonelyflavor.com": "Leven Labs, Inc. DBA Admiral (lonelyflavor.com)", "longingtrees.com": "Leven Labs, Inc. DBA Admiral (longingtrees.com)", "looseloaf.com": "Leven Labs, Inc. DBA Admiral (looseloaf.com)", @@ -70218,6 +70364,7 @@ "marriedbelief.com": "Leven Labs, Inc. DBA Admiral (marriedbelief.com)", "marriedmailbox.com": "Leven Labs, Inc. DBA Admiral (marriedmailbox.com)", "marriedvalue.com": "Leven Labs, Inc. DBA Admiral (marriedvalue.com)", + "marrowleaves.com": "Leven Labs, Inc. DBA Admiral (marrowleaves.com)", "massivemark.com": "Leven Labs, Inc. DBA Admiral (massivemark.com)", "materialisticmoon.com": "Leven Labs, Inc. DBA Admiral (materialisticmoon.com)", "materialmilk.com": "Leven Labs, Inc. DBA Admiral (materialmilk.com)", @@ -70306,11 +70453,13 @@ "notifyglass.com": "Leven Labs, Inc. DBA Admiral (notifyglass.com)", "nudgeduck.com": "Leven Labs, Inc. DBA Admiral (nudgeduck.com)", "nullnorth.com": "Leven Labs, Inc. DBA Admiral (nullnorth.com)", + "numberlessring.com": "Leven Labs, Inc. DBA Admiral (numberlessring.com)", "numerousnest.com": "Leven Labs, Inc. DBA Admiral (numerousnest.com)", "nutritiousbean.com": "Leven Labs, Inc. DBA Admiral (nutritiousbean.com)", "nuttyorganization.com": "Leven Labs, Inc. DBA Admiral (nuttyorganization.com)", "oafishchance.com": "Leven Labs, Inc. DBA Admiral (oafishchance.com)", "oafishobservation.com": "Leven Labs, Inc. DBA Admiral (oafishobservation.com)", + "objecthero.com": "Leven Labs, Inc. DBA Admiral (objecthero.com)", "obscenesidewalk.com": "Leven Labs, Inc. DBA Admiral (obscenesidewalk.com)", "observantice.com": "Leven Labs, Inc. DBA Admiral (observantice.com)", "oldfashionedoffer.com": "Leven Labs, Inc. DBA Admiral (oldfashionedoffer.com)", @@ -70463,6 +70612,7 @@ "rangecake.com": "Leven Labs, Inc. DBA Admiral (rangecake.com)", "rangeplayground.com": "Leven Labs, Inc. DBA Admiral (rangeplayground.com)", "rangergustav.com": "Leven Labs, Inc. DBA Admiral (rangergustav.com)", + "rarestcandy.com": "Leven Labs, Inc. DBA Admiral (rarestcandy.com)", "raresummer.com": "Leven Labs, Inc. DBA Admiral (raresummer.com)", "reactjspdf.com": "Leven Labs, Inc. DBA Admiral (reactjspdf.com)", "readingguilt.com": "Leven Labs, Inc. DBA Admiral (readingguilt.com)", @@ -70481,7 +70631,9 @@ "reconditeprison.com": "Leven Labs, Inc. DBA Admiral (reconditeprison.com)", "reconditerake.com": "Leven Labs, Inc. DBA Admiral (reconditerake.com)", "reconditerespect.com": "Leven Labs, Inc. DBA Admiral (reconditerespect.com)", + "reflectivestatement.com": "Leven Labs, Inc. DBA Admiral (reflectivestatement.com)", "refundradar.com": "Leven Labs, Inc. DBA Admiral (refundradar.com)", + "regexmail.com": "Leven Labs, Inc. DBA Admiral (regexmail.com)", "regularplants.com": "Leven Labs, Inc. DBA Admiral (regularplants.com)", "regulatesleet.com": "Leven Labs, Inc. DBA Admiral (regulatesleet.com)", "rehabilitatereason.com": "Leven Labs, Inc. DBA Admiral (rehabilitatereason.com)", @@ -70567,6 +70719,7 @@ "screechingfurniture.com": "Leven Labs, Inc. DBA Admiral (screechingfurniture.com)", "screechingstocking.com": "Leven Labs, Inc. DBA Admiral (screechingstocking.com)", "screechingstove.com": "Leven Labs, Inc. DBA Admiral (screechingstove.com)", + "scribbleson.com": "Leven Labs, Inc. DBA Admiral (scribbleson.com)", "scribblestring.com": "Leven Labs, Inc. DBA Admiral (scribblestring.com)", "scrollservice.com": "Leven Labs, Inc. DBA Admiral (scrollservice.com)", "scrubswim.com": "Leven Labs, Inc. DBA Admiral (scrubswim.com)", @@ -70577,6 +70730,7 @@ "secretspiders.com": "Leven Labs, Inc. DBA Admiral (secretspiders.com)", "secretturtle.com": "Leven Labs, Inc. DBA Admiral (secretturtle.com)", "seedscissors.com": "Leven Labs, Inc. DBA Admiral (seedscissors.com)", + "seemlysuggestion.com": "Leven Labs, Inc. DBA Admiral (seemlysuggestion.com)", "selectivesummer.com": "Leven Labs, Inc. DBA Admiral (selectivesummer.com)", "selfishsea.com": "Leven Labs, Inc. DBA Admiral (selfishsea.com)", "selfishsnake.com": "Leven Labs, Inc. DBA Admiral (selfishsnake.com)", @@ -70721,6 +70875,7 @@ "steadfastsystem.com": "Leven Labs, Inc. DBA Admiral (steadfastsystem.com)", "steadycopper.com": "Leven Labs, Inc. DBA Admiral (steadycopper.com)", "stealsteel.com": "Leven Labs, Inc. DBA Admiral (stealsteel.com)", + "steepscale.com": "Leven Labs, Inc. DBA Admiral (steepscale.com)", "steepsister.com": "Leven Labs, Inc. DBA Admiral (steepsister.com)", "steepsquirrel.com": "Leven Labs, Inc. DBA Admiral (steepsquirrel.com)", "stepcattle.com": "Leven Labs, Inc. DBA Admiral (stepcattle.com)", @@ -70772,7 +70927,6 @@ "superficialeyes.com": "Leven Labs, Inc. DBA Admiral (superficialeyes.com)", "superficialspring.com": "Leven Labs, Inc. DBA Admiral (superficialspring.com)", "superficialsquare.com": "Leven Labs, Inc. DBA Admiral (superficialsquare.com)", - "superviseshoes.com": "Leven Labs, Inc. DBA Admiral (superviseshoes.com)", "supportwaves.com": "Leven Labs, Inc. DBA Admiral (supportwaves.com)", "suspectmark.com": "Leven Labs, Inc. DBA Admiral (suspectmark.com)", "swankysquare.com": "Leven Labs, Inc. DBA Admiral (swankysquare.com)", @@ -70841,9 +70995,7 @@ "trackcaddie.com": "Leven Labs, Inc. DBA Admiral (trackcaddie.com)", "tradetooth.com": "Leven Labs, Inc. DBA Admiral (tradetooth.com)", "trafficviews.com": "Leven Labs, Inc. DBA Admiral (trafficviews.com)", - "tranquilarchipelago.com": "Leven Labs, Inc. DBA Admiral (tranquilarchipelago.com)", "tranquilcan.com": "Leven Labs, Inc. DBA Admiral (tranquilcan.com)", - "tranquilcanyon.com": "Leven Labs, Inc. DBA Admiral (tranquilcanyon.com)", "tranquilplume.com": "Leven Labs, Inc. DBA Admiral (tranquilplume.com)", "tranquilside.com": "Leven Labs, Inc. DBA Admiral (tranquilside.com)", "tranquilveil.com": "Leven Labs, Inc. DBA Admiral (tranquilveil.com)", diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 6d8c2a71c4..921e8a626f 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -63,6 +63,7 @@ 1E1D8B6C29953CE300C96994 /* autoconsent-test-page-banner.html in Resources */ = {isa = PBXBuildFile; fileRef = 1E1D8B6929953CE300C96994 /* autoconsent-test-page-banner.html */; }; 1E24295E293F57FA00584836 /* LottieView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E24295D293F57FA00584836 /* LottieView.swift */; }; 1E242960293F585300584836 /* cookie-icon-animated-40-light.json in Resources */ = {isa = PBXBuildFile; fileRef = 1E24295F293F585300584836 /* cookie-icon-animated-40-light.json */; }; + 1E39BEB02CC9477200496FBA /* SubscriptionCookieManageEventPixelMapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E39BEAF2CC9477200496FBA /* SubscriptionCookieManageEventPixelMapping.swift */; }; 1E4DCF4627B6A33600961E25 /* DownloadsListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4DCF4527B6A33600961E25 /* DownloadsListViewModel.swift */; }; 1E4DCF4827B6A35400961E25 /* DownloadsListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4DCF4727B6A35400961E25 /* DownloadsListModel.swift */; }; 1E4DCF4A27B6A38000961E25 /* DownloadListRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4DCF4927B6A38000961E25 /* DownloadListRepresentable.swift */; }; @@ -268,7 +269,7 @@ 560E990F2BEE2CB800507CE0 /* SyncErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560E990E2BEE2CB800507CE0 /* SyncErrorMessage.swift */; }; 564DE4532C3ED1B700D23241 /* NewTabDaxDialogFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564DE4522C3ED1B700D23241 /* NewTabDaxDialogFactory.swift */; }; 564DE4552C3EDEF200D23241 /* ContextualOnboardingNewTabDialogFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564DE4542C3EDEF200D23241 /* ContextualOnboardingNewTabDialogFactoryTests.swift */; }; - 564DE4572C4150E600D23241 /* HomeViewControllerDaxDialogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564DE4562C4150E600D23241 /* HomeViewControllerDaxDialogTests.swift */; }; + 564DE4572C4150E600D23241 /* NewTabPageControllerDaxDialogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564DE4562C4150E600D23241 /* NewTabPageControllerDaxDialogTests.swift */; }; 564DE45A2C450BE600D23241 /* DaxDialogsNewTabTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564DE4592C450BE600D23241 /* DaxDialogsNewTabTests.swift */; }; 564DE45E2C45218500D23241 /* OnboardingNavigationDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564DE45D2C45218500D23241 /* OnboardingNavigationDelegateTests.swift */; }; 564DE4602C4544CA00D23241 /* HomePageDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564DE45F2C4544CA00D23241 /* HomePageDependencies.swift */; }; @@ -306,7 +307,9 @@ 6F3537A42C4AC140009F8717 /* NewTabPageDaxLogoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F3537A32C4AC140009F8717 /* NewTabPageDaxLogoView.swift */; }; 6F40D15B2C34423800BF22F0 /* HomePageDisplayDailyPixelBucket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F40D15A2C34423800BF22F0 /* HomePageDisplayDailyPixelBucket.swift */; }; 6F40D15E2C34436500BF22F0 /* HomePageDisplayDailyPixelBucketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F40D15C2C34436200BF22F0 /* HomePageDisplayDailyPixelBucketTests.swift */; }; + 6F5041C92CC11A5100989E48 /* SimpleNewTabPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5041C82CC11A5100989E48 /* SimpleNewTabPageView.swift */; }; 6F5345AF2C53F2DE00424A43 /* NewTabPageSettingsPersistentStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5345AE2C53F2DE00424A43 /* NewTabPageSettingsPersistentStorage.swift */; }; + 6F5AA3EF2CC1588400685CB4 /* FavoritesListInteractingAdapterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5AA3EE2CC1588400685CB4 /* FavoritesListInteractingAdapterTests.swift */; }; 6F5CC0812C2AFFE400AFC840 /* ToggleExpandButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5CC0802C2AFFE400AFC840 /* ToggleExpandButtonStyle.swift */; }; 6F64AA532C47E92600CF4489 /* FavoritesFaviconLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F64AA522C47E92600CF4489 /* FavoritesFaviconLoader.swift */; }; 6F64AA592C4818D700CF4489 /* NewTabPageShortcut.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F64AA582C4818D700CF4489 /* NewTabPageShortcut.swift */; }; @@ -361,10 +364,19 @@ 6FE127462C2054A900EB5724 /* NewTabPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE127452C2054A900EB5724 /* NewTabPageViewController.swift */; }; 6FE1274B2C20943500EB5724 /* ShortcutItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE1274A2C20943500EB5724 /* ShortcutItemView.swift */; }; 6FEC0B852C999352006B4F6E /* FavoriteItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FEC0B842C999352006B4F6E /* FavoriteItem.swift */; }; - 6FEC0B882C999961006B4F6E /* FavoriteDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FEC0B872C999961006B4F6E /* FavoriteDataSource.swift */; }; + 6FEC0B882C999961006B4F6E /* FavoritesListInteractingAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FEC0B872C999961006B4F6E /* FavoritesListInteractingAdapter.swift */; }; 6FF915822B88E07A0042AC87 /* AdAttributionFetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF915802B88E0750042AC87 /* AdAttributionFetcherTests.swift */; }; + 7B1604E82CB685B400A44EC6 /* Logger+TipKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1604E72CB685B400A44EC6 /* Logger+TipKit.swift */; }; + 7B1604EC2CB68BDA00A44EC6 /* TipKitController+ConvenienceInitializers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1604EB2CB68BDA00A44EC6 /* TipKitController+ConvenienceInitializers.swift */; }; + 7B1604EE2CB68D2600A44EC6 /* TipKitDebugOptionsUIActionHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1604ED2CB68D2600A44EC6 /* TipKitDebugOptionsUIActionHandling.swift */; }; + 7B8E0EC62CC81B4900B2B722 /* TipKitController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8E0EC52CC81B4800B2B722 /* TipKitController.swift */; }; 7BC571202BDBB877003B0CCE /* VPNActivationDateStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC5711F2BDBB877003B0CCE /* VPNActivationDateStore.swift */; }; 7BC571212BDBB977003B0CCE /* VPNActivationDateStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC5711F2BDBB877003B0CCE /* VPNActivationDateStore.swift */; }; + 7BDBAD0E2CBFB3F1000379B7 /* VPN.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7BDBAD0D2CBFB3F1000379B7 /* VPN.xcassets */; }; + 7BF78E022CA2CC3E0026A1FC /* TipKitAppEventHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BF78E012CA2CC3E0026A1FC /* TipKitAppEventHandling.swift */; }; + 7BFD5FD52C9DA310000FF959 /* VPNAddWidgetTip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFD5FD42C9DA310000FF959 /* VPNAddWidgetTip.swift */; }; + 7BFD5FD72C9DB9D7000FF959 /* VPNGeoswitchingTip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFD5FD62C9DB9D7000FF959 /* VPNGeoswitchingTip.swift */; }; + 7BFD5FD92C9DBC24000FF959 /* VPNSnoozeTip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFD5FD82C9DBC24000FF959 /* VPNSnoozeTip.swift */; }; 83004E802193BB8200DA013C /* WKNavigationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83004E7F2193BB8200DA013C /* WKNavigationExtension.swift */; }; 83004E862193E5ED00DA013C /* TabViewControllerBrowsingMenuExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83004E852193E5ED00DA013C /* TabViewControllerBrowsingMenuExtension.swift */; }; 83004E882193E8C700DA013C /* TabViewControllerLongPressMenuExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83004E872193E8C700DA013C /* TabViewControllerLongPressMenuExtension.swift */; }; @@ -673,7 +685,6 @@ 98B001AA251EABB40090EC07 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 98B001A8251EABB40090EC07 /* Localizable.strings */; }; 98B001B0251EABB40090EC07 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 98B001AE251EABB40090EC07 /* InfoPlist.strings */; }; 98B001B3251EABB40090EC07 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 98B001B1251EABB40090EC07 /* InfoPlist.strings */; }; - 98B31290218CCB2200E54DE1 /* MockDependencyProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98B3128F218CCB2200E54DE1 /* MockDependencyProvider.swift */; }; 98B31292218CCB8C00E54DE1 /* AppDependencyProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98B31291218CCB8C00E54DE1 /* AppDependencyProvider.swift */; }; 98BFA911294A2086004EA636 /* bookmarks_3k.html in Resources */ = {isa = PBXBuildFile; fileRef = 98BFA910294A2086004EA636 /* bookmarks_3k.html */; }; 98BFA913294A3DDC004EA636 /* BookmarksEditModelPerformanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98BFA912294A3DDC004EA636 /* BookmarksEditModelPerformanceTests.swift */; }; @@ -1023,7 +1034,6 @@ EE01EB402AFBD0000096AAC9 /* NetworkProtectionVPNSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE01EB3F2AFBD0000096AAC9 /* NetworkProtectionVPNSettingsViewModel.swift */; }; EE01EB432AFC1E0A0096AAC9 /* NetworkProtectionVPNLocationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE01EB422AFC1E0A0096AAC9 /* NetworkProtectionVPNLocationView.swift */; }; EE0798C52B179936000A4F64 /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0798C42B179936000A4F64 /* NetworkProtectionVPNCountryLabelsModel.swift */; }; - EE0D1B9C2C8B41DB00AC0987 /* StubAutofillLoginImportStateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0D1B9B2C8B41DB00AC0987 /* StubAutofillLoginImportStateProvider.swift */; }; EE3766DE2AC5945500AAB575 /* NetworkProtectionUNNotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE3766DD2AC5945500AAB575 /* NetworkProtectionUNNotificationPresenter.swift */; }; EE3B226B29DE0F110082298A /* MockInternalUserStoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE3B226A29DE0F110082298A /* MockInternalUserStoring.swift */; }; EE3B226C29DE0FD30082298A /* MockInternalUserStoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE3B226A29DE0F110082298A /* MockInternalUserStoring.swift */; }; @@ -1369,6 +1379,7 @@ 1E24295D293F57FA00584836 /* LottieView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LottieView.swift; sourceTree = ""; }; 1E24295F293F585300584836 /* cookie-icon-animated-40-light.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "cookie-icon-animated-40-light.json"; sourceTree = ""; }; 1E25D5312C92126B004400F0 /* SubscriptionFeatureAvailabilityMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionFeatureAvailabilityMock.swift; sourceTree = ""; }; + 1E39BEAF2CC9477200496FBA /* SubscriptionCookieManageEventPixelMapping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionCookieManageEventPixelMapping.swift; sourceTree = ""; }; 1E4DCF4527B6A33600961E25 /* DownloadsListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsListViewModel.swift; sourceTree = ""; }; 1E4DCF4727B6A35400961E25 /* DownloadsListModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsListModel.swift; sourceTree = ""; }; 1E4DCF4927B6A38000961E25 /* DownloadListRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadListRepresentable.swift; sourceTree = ""; }; @@ -1553,7 +1564,7 @@ 560E990E2BEE2CB800507CE0 /* SyncErrorMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncErrorMessage.swift; sourceTree = ""; }; 564DE4522C3ED1B700D23241 /* NewTabDaxDialogFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabDaxDialogFactory.swift; sourceTree = ""; }; 564DE4542C3EDEF200D23241 /* ContextualOnboardingNewTabDialogFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextualOnboardingNewTabDialogFactoryTests.swift; sourceTree = ""; }; - 564DE4562C4150E600D23241 /* HomeViewControllerDaxDialogTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewControllerDaxDialogTests.swift; sourceTree = ""; }; + 564DE4562C4150E600D23241 /* NewTabPageControllerDaxDialogTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageControllerDaxDialogTests.swift; sourceTree = ""; }; 564DE4592C450BE600D23241 /* DaxDialogsNewTabTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaxDialogsNewTabTests.swift; sourceTree = ""; }; 564DE45D2C45218500D23241 /* OnboardingNavigationDelegateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingNavigationDelegateTests.swift; sourceTree = ""; }; 564DE45F2C4544CA00D23241 /* HomePageDependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePageDependencies.swift; sourceTree = ""; }; @@ -1591,7 +1602,9 @@ 6F3537A32C4AC140009F8717 /* NewTabPageDaxLogoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageDaxLogoView.swift; sourceTree = ""; }; 6F40D15A2C34423800BF22F0 /* HomePageDisplayDailyPixelBucket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePageDisplayDailyPixelBucket.swift; sourceTree = ""; }; 6F40D15C2C34436200BF22F0 /* HomePageDisplayDailyPixelBucketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePageDisplayDailyPixelBucketTests.swift; sourceTree = ""; }; + 6F5041C82CC11A5100989E48 /* SimpleNewTabPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleNewTabPageView.swift; sourceTree = ""; }; 6F5345AE2C53F2DE00424A43 /* NewTabPageSettingsPersistentStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSettingsPersistentStorage.swift; sourceTree = ""; }; + 6F5AA3EE2CC1588400685CB4 /* FavoritesListInteractingAdapterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesListInteractingAdapterTests.swift; sourceTree = ""; }; 6F5CC0802C2AFFE400AFC840 /* ToggleExpandButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleExpandButtonStyle.swift; sourceTree = ""; }; 6F64AA522C47E92600CF4489 /* FavoritesFaviconLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesFaviconLoader.swift; sourceTree = ""; }; 6F64AA582C4818D700CF4489 /* NewTabPageShortcut.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageShortcut.swift; sourceTree = ""; }; @@ -1647,9 +1660,18 @@ 6FE127452C2054A900EB5724 /* NewTabPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageViewController.swift; sourceTree = ""; }; 6FE1274A2C20943500EB5724 /* ShortcutItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutItemView.swift; sourceTree = ""; }; 6FEC0B842C999352006B4F6E /* FavoriteItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteItem.swift; sourceTree = ""; }; - 6FEC0B872C999961006B4F6E /* FavoriteDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteDataSource.swift; sourceTree = ""; }; + 6FEC0B872C999961006B4F6E /* FavoritesListInteractingAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesListInteractingAdapter.swift; sourceTree = ""; }; 6FF915802B88E0750042AC87 /* AdAttributionFetcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdAttributionFetcherTests.swift; sourceTree = ""; }; + 7B1604E72CB685B400A44EC6 /* Logger+TipKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+TipKit.swift"; sourceTree = ""; }; + 7B1604EB2CB68BDA00A44EC6 /* TipKitController+ConvenienceInitializers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TipKitController+ConvenienceInitializers.swift"; sourceTree = ""; }; + 7B1604ED2CB68D2600A44EC6 /* TipKitDebugOptionsUIActionHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipKitDebugOptionsUIActionHandling.swift; sourceTree = ""; }; + 7B8E0EC52CC81B4800B2B722 /* TipKitController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipKitController.swift; sourceTree = ""; }; 7BC5711F2BDBB877003B0CCE /* VPNActivationDateStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNActivationDateStore.swift; sourceTree = ""; }; + 7BDBAD0D2CBFB3F1000379B7 /* VPN.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = VPN.xcassets; sourceTree = ""; }; + 7BF78E012CA2CC3E0026A1FC /* TipKitAppEventHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipKitAppEventHandling.swift; sourceTree = ""; }; + 7BFD5FD42C9DA310000FF959 /* VPNAddWidgetTip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNAddWidgetTip.swift; sourceTree = ""; }; + 7BFD5FD62C9DB9D7000FF959 /* VPNGeoswitchingTip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNGeoswitchingTip.swift; sourceTree = ""; }; + 7BFD5FD82C9DBC24000FF959 /* VPNSnoozeTip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNSnoozeTip.swift; sourceTree = ""; }; 83004E7F2193BB8200DA013C /* WKNavigationExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKNavigationExtension.swift; sourceTree = ""; }; 83004E832193E14C00DA013C /* UIAlertControllerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = UIAlertControllerExtension.swift; path = ../Core/UIAlertControllerExtension.swift; sourceTree = ""; }; 83004E852193E5ED00DA013C /* TabViewControllerBrowsingMenuExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabViewControllerBrowsingMenuExtension.swift; sourceTree = ""; }; @@ -2455,7 +2477,6 @@ 98B12739251EABD5007473E4 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/InfoPlist.strings; sourceTree = ""; }; 98B1273B251EABD5007473E4 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/InfoPlist.strings; sourceTree = ""; }; 98B1273C251EABD5007473E4 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Localizable.strings; sourceTree = ""; }; - 98B3128F218CCB2200E54DE1 /* MockDependencyProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDependencyProvider.swift; sourceTree = ""; }; 98B31291218CCB8C00E54DE1 /* AppDependencyProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDependencyProvider.swift; sourceTree = ""; }; 98B4904D251EAC2200A1B398 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = ""; }; 98B4904E251EAC2200A1B398 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -2728,7 +2749,6 @@ CB18F2712AF6D4E400A0F8FE /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/InfoPlist.strings; sourceTree = ""; }; CB1AEFB02799AA940031AE3D /* SwiftUICollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUICollectionViewCell.swift; sourceTree = ""; }; CB1FAE472AF6D59B003F452F /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/InfoPlist.strings; sourceTree = ""; }; - CB2283F22BD79FC20057DD0A /* BrokenSitePromptView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrokenSitePromptView.swift; sourceTree = ""; }; CB24F70E29A3EB15006DCC58 /* AppConfigurationURLProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppConfigurationURLProvider.swift; path = ../Core/AppConfigurationURLProvider.swift; sourceTree = ""; }; CB258D0C29A4CD0500DEBA24 /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; CB258D0F29A4D0FD00DEBA24 /* ConfigurationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationManager.swift; sourceTree = ""; }; @@ -2742,7 +2762,6 @@ CB48D3352B90CECD00631D8B /* UserBehaviorMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserBehaviorMonitorTests.swift; sourceTree = ""; }; CB4FA44D2C78AACE00A16F5A /* SpecialErrorPageUserScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialErrorPageUserScript.swift; sourceTree = ""; }; CB5038622AF6D563007FD69F /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = ""; }; - CB5418622BD90CD000C2CD26 /* BrokenSitePromptViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrokenSitePromptViewModel.swift; sourceTree = ""; }; CB6ABD002AF6D52B004A8224 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/InfoPlist.strings; sourceTree = ""; }; CB6CE65B2AF6D4EE00119848 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = ""; }; CB7407BC2AF6D56D0090A41C /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -2846,7 +2865,6 @@ EE01EB3F2AFBD0000096AAC9 /* NetworkProtectionVPNSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionVPNSettingsViewModel.swift; sourceTree = ""; }; EE01EB422AFC1E0A0096AAC9 /* NetworkProtectionVPNLocationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionVPNLocationView.swift; sourceTree = ""; }; EE0798C42B179936000A4F64 /* NetworkProtectionVPNCountryLabelsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionVPNCountryLabelsModel.swift; sourceTree = ""; }; - EE0D1B9B2C8B41DB00AC0987 /* StubAutofillLoginImportStateProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubAutofillLoginImportStateProvider.swift; sourceTree = ""; }; EE3766DD2AC5945500AAB575 /* NetworkProtectionUNNotificationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionUNNotificationPresenter.swift; sourceTree = ""; }; EE3B226A29DE0F110082298A /* MockInternalUserStoring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockInternalUserStoring.swift; sourceTree = ""; }; EE3B98EA2A9634CC002F63A0 /* DuckDuckGoAlpha.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DuckDuckGoAlpha.entitlements; sourceTree = ""; }; @@ -3195,15 +3213,6 @@ path = FingerprintingUITests; sourceTree = ""; }; - 0283A1F82C6E3C3100508FBD /* Recovered References */ = { - isa = PBXGroup; - children = ( - CB2283F22BD79FC20057DD0A /* BrokenSitePromptView.swift */, - CB5418622BD90CD000C2CD26 /* BrokenSitePromptViewModel.swift */, - ); - name = "Recovered References"; - sourceTree = ""; - }; 0283A1F92C6E3D4200508FBD /* BrokenSitePrompt */ = { isa = PBXGroup; children = ( @@ -3739,6 +3748,7 @@ children = ( 4B37E04F2B928CA6009E81CA /* vpn-light-mode.json */, 4B6ED9442B992FE4007F5CAA /* vpn-dark-mode.json */, + 7BDBAD0D2CBFB3F1000379B7 /* VPN.xcassets */, ); name = Resources; sourceTree = ""; @@ -3856,8 +3866,9 @@ 6F7FB8DF2C660B1A00867DA7 /* NewTabPageFavoritesModelTests.swift */, 6F7FB8E42C66158D00867DA7 /* NewTabPageShortcutsSettingsModelTests.swift */, 6F7FB8E62C66197E00867DA7 /* NewTabPageSectionsSettingsModelTests.swift */, - 564DE4562C4150E600D23241 /* HomeViewControllerDaxDialogTests.swift */, + 564DE4562C4150E600D23241 /* NewTabPageControllerDaxDialogTests.swift */, 6FABAA682C6116FD003762EC /* NewTabPageShortcutsSettingsStorageTests.swift */, + 6F5AA3EE2CC1588400685CB4 /* FavoritesListInteractingAdapterTests.swift */, ); name = NewTabPage; sourceTree = ""; @@ -3911,7 +3922,7 @@ 6FD3F8122C3EFDA200DA5797 /* FavoritesPreviewDataSource.swift */, 6FA3438E2C3D3BC300470677 /* Favorite.swift */, 6FEC0B842C999352006B4F6E /* FavoriteItem.swift */, - 6FEC0B872C999961006B4F6E /* FavoriteDataSource.swift */, + 6FEC0B872C999961006B4F6E /* FavoritesListInteractingAdapter.swift */, ); name = Model; sourceTree = ""; @@ -3962,6 +3973,7 @@ 6F03CAF82C32C3AA004179A8 /* Messages */, 6FE127372C20492500EB5724 /* NewTabPage.swift */, 6FD8E51F2C5BA23200345670 /* NewTabPageViewModel.swift */, + 6F5041C82CC11A5100989E48 /* SimpleNewTabPageView.swift */, 6FE127392C204BD000EB5724 /* NewTabPageView.swift */, 6FE127452C2054A900EB5724 /* NewTabPageViewController.swift */, 6FD3F8182C41252900DA5797 /* NewTabPageControllerDelegate.swift */, @@ -4020,6 +4032,28 @@ name = AdAttribution; sourceTree = ""; }; + 7BF78E002CA2CC100026A1FC /* TipKit */ = { + isa = PBXGroup; + children = ( + 7B1604E72CB685B400A44EC6 /* Logger+TipKit.swift */, + 7BF78E012CA2CC3E0026A1FC /* TipKitAppEventHandling.swift */, + 7B8E0EC52CC81B4800B2B722 /* TipKitController.swift */, + 7B1604EB2CB68BDA00A44EC6 /* TipKitController+ConvenienceInitializers.swift */, + 7B1604ED2CB68D2600A44EC6 /* TipKitDebugOptionsUIActionHandling.swift */, + ); + path = TipKit; + sourceTree = ""; + }; + 7BFD5FD32C9DA235000FF959 /* TipKit */ = { + isa = PBXGroup; + children = ( + 7BFD5FD42C9DA310000FF959 /* VPNAddWidgetTip.swift */, + 7BFD5FD62C9DB9D7000FF959 /* VPNGeoswitchingTip.swift */, + 7BFD5FD82C9DBC24000FF959 /* VPNSnoozeTip.swift */, + ); + name = TipKit; + sourceTree = ""; + }; 830FA79B1F8E81FB00FCE105 /* ContentBlocker */ = { isa = PBXGroup; children = ( @@ -4180,7 +4214,6 @@ 83ED3B8D1FA8E63700B47556 /* README.md */, 83ED3B8C1FA8E61D00B47556 /* ManualTestsScript.md */, 85A313962028E78A00327D00 /* release_notes.txt */, - 0283A1F82C6E3C3100508FBD /* Recovered References */, ); sourceTree = ""; }; @@ -4248,6 +4281,7 @@ F13B4BF41F18C74500814661 /* Tabs */, F1386BA21E6846320062FC3C /* TabSwitcher */, 98F3A1D6217B36EE0011A0D4 /* Themes */, + 7BF78E002CA2CC100026A1FC /* TipKit */, F11CEF581EBB66C80088E4D7 /* Tutorials */, CB48D32F2B90CE8500631D8B /* UserBehaviorMonitor */, F1D796ED1E7AE4090019D451 /* UserInterface */, @@ -5337,6 +5371,7 @@ D664C7962B289AA000CBFA76 /* Extensions */, D65CEA6F2B6AC6C9008A759B /* Subscription.xcassets */, BDE219E52C406D19005D5884 /* PrivacyProDataReporting.swift */, + 1E39BEAF2CC9477200496FBA /* SubscriptionCookieManageEventPixelMapping.swift */, ); path = Subscription; sourceTree = ""; @@ -5561,6 +5596,7 @@ EECD94B22A28B8580085C66E /* NetworkProtection */ = { isa = PBXGroup; children = ( + 7BFD5FD32C9DA235000FF959 /* TipKit */, 4BD96E072C4DCCD1003BC32C /* LiveActivity */, 4B37E04E2B928C91009E81CA /* Resources */, EE01EB412AFC1DE10096AAC9 /* PreferredLocation */, @@ -5988,7 +6024,6 @@ children = ( 9F4CC51A2C48C0C7006A96EB /* MockTabDelegate.swift */, C14882E927F20DD000D59F0C /* MockBookmarksCoreDataStorage.swift */, - 98B3128F218CCB2200E54DE1 /* MockDependencyProvider.swift */, C158AC7A297AB5DC0008723A /* MockSecureVault.swift */, F1134EBA1F40D3D000B73467 /* MockStatisticsStore.swift */, 026DABA328242BC80089E0B5 /* MockUserAgent.swift */, @@ -6399,7 +6434,6 @@ 31951E9328230D8900CAF535 /* Shared */, F407605428131923006B1E0B /* SaveLogin */, EE5929612C5A8AF40029380B /* AutofillUsageMonitor.swift */, - EE0D1B9B2C8B41DB00AC0987 /* StubAutofillLoginImportStateProvider.swift */, ); name = Autofill; sourceTree = ""; @@ -6969,6 +7003,7 @@ 85F98F98296F4CB100742F4A /* SyncAssets.xcassets in Resources */, 31BC5F412C2B0B540004DF37 /* DuckPlayer.xcassets in Resources */, AA4D6A9423DE49A5007E8790 /* AppIconBlack29x29@2x.png in Resources */, + 7BDBAD0E2CBFB3F1000379B7 /* VPN.xcassets in Resources */, 98B001B3251EABB40090EC07 /* InfoPlist.strings in Resources */, AA4D6ACE23DE4D27007E8790 /* AppIconPurple60x60@3x.png in Resources */, D65CEA702B6AC6C9008A759B /* Subscription.xcassets in Resources */, @@ -7352,9 +7387,12 @@ EE4FB1882A28D11900E5CBA7 /* NetworkProtectionStatusViewModel.swift in Sources */, 6FB1FE9E2C24D41D0075B68B /* NewTabPageSectionsDebugView.swift in Sources */, 8540BD5623D9E9C20057FDD2 /* PreserveLoginsSettingsViewController.swift in Sources */, + 7B8E0EC62CC81B4900B2B722 /* TipKitController.swift in Sources */, + 7B1604EC2CB68BDA00A44EC6 /* TipKitController+ConvenienceInitializers.swift in Sources */, 851672D12BED1FC900592F24 /* AutocompleteView.swift in Sources */, 3161D13227AC161B00285CF6 /* DownloadMetadata.swift in Sources */, D664C7C72B289AA200CBFA76 /* PurchaseInProgressView.swift in Sources */, + 7BFD5FD72C9DB9D7000FF959 /* VPNGeoswitchingTip.swift in Sources */, F1668BCE1E798081008CBA04 /* BookmarksViewController.swift in Sources */, 8590CB69268A4E190089F6BF /* DebugEtagStorage.swift in Sources */, C1CDA3162AFB9C7F006D1476 /* AutofillNeverPromptWebsitesManager.swift in Sources */, @@ -7427,6 +7465,7 @@ 9F5E5AB02C3E4C6000165F54 /* ContextualOnboardingPresenter.swift in Sources */, 310D091B2799F54900DC0060 /* DownloadManager.swift in Sources */, 98D98A7425ED88D100D8E3DF /* BrowsingMenuEntryViewCell.swift in Sources */, + 7BFD5FD92C9DBC24000FF959 /* VPNSnoozeTip.swift in Sources */, 98F3A1D8217B37010011A0D4 /* Theme.swift in Sources */, 4B2C79612C5B27AC00A240CC /* VPNSnoozeActivityAttributes.swift in Sources */, CB9B873C278C8FEA001F4906 /* WidgetEducationView.swift in Sources */, @@ -7468,6 +7507,7 @@ C185ED612BD4329700BAE9DC /* ImportPasswordsStatusHandler.swift in Sources */, CB9B8739278C8E72001F4906 /* WidgetEducationViewController.swift in Sources */, F4D9C4FA25117A0F00814B71 /* HomeMessageStorage.swift in Sources */, + 6F5041C92CC11A5100989E48 /* SimpleNewTabPageView.swift in Sources */, D69FBF762B28BE3600B505F1 /* SettingsSubscriptionView.swift in Sources */, D664C7CC2B289AA200CBFA76 /* SubscriptionPagesUserScript.swift in Sources */, AA3D854523D9942200788410 /* AppIconSettingsViewController.swift in Sources */, @@ -7527,6 +7567,7 @@ 3157B43827F4C8490042D3D7 /* FaviconsHelper.swift in Sources */, 85F200042216F5D8006BB258 /* FindInPageView.swift in Sources */, D652498E2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift in Sources */, + 7B1604E82CB685B400A44EC6 /* Logger+TipKit.swift in Sources */, 8548D95E25262B1B005AAE49 /* ViewHighlighter.swift in Sources */, F4D7221026F29A70007D6193 /* BookmarkDetailsCell.swift in Sources */, F1617C131E572E0300DEDCAF /* TabSwitcherViewController.swift in Sources */, @@ -7538,7 +7579,9 @@ C1641EAF2BC2F5140012607A /* ImportPasswordsViewController.swift in Sources */, D63FF8982C1B6A45006DE24D /* DuckPlayer.swift in Sources */, 85B9CB8921AEBDD5009001F1 /* FavoriteHomeCell.swift in Sources */, + 1E39BEB02CC9477200496FBA /* SubscriptionCookieManageEventPixelMapping.swift in Sources */, C1935A102C88D131001AD72D /* AutofillSurveyManager.swift in Sources */, + 7B1604EE2CB68D2600A44EC6 /* TipKitDebugOptionsUIActionHandling.swift in Sources */, 98999D5922FDA41500CBBE1B /* BasicAuthenticationAlert.swift in Sources */, C13B32D22A0E750700A59236 /* AutofillSettingStatus.swift in Sources */, 1DDF40202BA049FA006850D9 /* SettingsRootView.swift in Sources */, @@ -7682,6 +7725,7 @@ 31CC224928369B38001654A4 /* AutofillLoginSettingsListViewController.swift in Sources */, F1D796EC1E7AB8930019D451 /* SaveBookmarkActivity.swift in Sources */, F4B0B78C252CAFF700830156 /* OnboardingWidgetsViewController.swift in Sources */, + 7BFD5FD52C9DA310000FF959 /* VPNAddWidgetTip.swift in Sources */, C17B595A2A03AAD30055F2D1 /* PasswordGenerationPromptViewController.swift in Sources */, 8531A08E1F9950E6000484F0 /* UnprotectedSitesViewController.swift in Sources */, CBD4F13C279EBF4A00B20FD7 /* HomeMessage.swift in Sources */, @@ -7778,7 +7822,6 @@ 0283A2012C6E46E300508FBD /* BrokenSitePromptLimiter.swift in Sources */, 85DFEDF924CF3D0E00973FE7 /* TabsBarCell.swift in Sources */, 851672D32BED23FE00592F24 /* AutocompleteViewModel.swift in Sources */, - EE0D1B9C2C8B41DB00AC0987 /* StubAutofillLoginImportStateProvider.swift in Sources */, 8562CE152B9B645C00E1D399 /* CachedBookmarkSuggestions.swift in Sources */, C13F3F682B7F88100083BE40 /* AuthConfirmationPromptView.swift in Sources */, F1617C191E573EA800DEDCAF /* TabSwitcherDelegate.swift in Sources */, @@ -7830,6 +7873,7 @@ F15D43201E706CC500BF2CDC /* AutocompleteViewController.swift in Sources */, BD862E092B30F63E0073E2EE /* VPNMetadataCollector.swift in Sources */, D6E83C682B23B6A3006C8AFB /* FontSettings.swift in Sources */, + 7BF78E022CA2CC3E0026A1FC /* TipKitAppEventHandling.swift in Sources */, 1DEAADF62BA4809400E25A97 /* CookiePopUpProtectionView.swift in Sources */, 31EF52E1281B3BDC0034796E /* AutofillLoginListItemViewModel.swift in Sources */, C1EA86602C74CB6C00E8604D /* SyncPromoView.swift in Sources */, @@ -7866,7 +7910,7 @@ 1E4DCF4627B6A33600961E25 /* DownloadsListViewModel.swift in Sources */, 37C696772C4957940073E131 /* RemoteMessagingDebugViewController.swift in Sources */, 31860A5B2C57ED2D005561F5 /* DuckPlayerStorage.swift in Sources */, - 6FEC0B882C999961006B4F6E /* FavoriteDataSource.swift in Sources */, + 6FEC0B882C999961006B4F6E /* FavoritesListInteractingAdapter.swift in Sources */, F4F6DFB626E6B71300ED7E12 /* BookmarkFoldersTableViewController.swift in Sources */, 8586A11024CCCD040049720E /* TabsBarViewController.swift in Sources */, 6FEC0B852C999352006B4F6E /* FavoriteItem.swift in Sources */, @@ -7922,7 +7966,7 @@ 98DA35C4268CC81E00159906 /* DomainMatchingReportTests.swift in Sources */, 8590CB632684F10F0089F6BF /* ContentBlockerProtectionStoreTests.swift in Sources */, 83EDCC411F86B89C005CDFCD /* StatisticsLoaderTests.swift in Sources */, - 564DE4572C4150E600D23241 /* HomeViewControllerDaxDialogTests.swift in Sources */, + 564DE4572C4150E600D23241 /* NewTabPageControllerDaxDialogTests.swift in Sources */, C14882E327F20D9A00D59F0C /* BookmarksExporterTests.swift in Sources */, 85C29708247BDD060063A335 /* DaxDialogsBrowsingSpecTests.swift in Sources */, 9FE05CF12C36468A00D9046B /* OnboardingPixelReporterTests.swift in Sources */, @@ -7938,7 +7982,6 @@ F13B4BF91F18CA0600814661 /* TabsModelTests.swift in Sources */, F1BDDBFD2C340D9C00459306 /* SubscriptionContainerViewModelTests.swift in Sources */, 987243142C5232B5007ECC76 /* BookmarksDatabaseSetupTests.swift in Sources */, - 98B31290218CCB2200E54DE1 /* MockDependencyProvider.swift in Sources */, CBDD5DDF29A6736A00832877 /* APIHeadersTests.swift in Sources */, D6F557BA2C8859040034444B /* DuckPlayerExperimentTests.swift in Sources */, 986B45D0299E30A50089D2D7 /* BookmarkEntityTests.swift in Sources */, @@ -8034,6 +8077,7 @@ C14882EA27F20DD000D59F0C /* MockBookmarksCoreDataStorage.swift in Sources */, 1E05D1DB29C47B3300BF9A1F /* DailyPixelTests.swift in Sources */, 564DE4552C3EDEF200D23241 /* ContextualOnboardingNewTabDialogFactoryTests.swift in Sources */, + 6F5AA3EF2CC1588400685CB4 /* FavoritesListInteractingAdapterTests.swift in Sources */, 981FED7422046017008488D7 /* AutoClearTests.swift in Sources */, 98DDF9F322C4029D00DE38DB /* InitHelpers.swift in Sources */, B6AD9E3628D4510A0019CDE9 /* ContentBlockerRulesManagerMock.swift in Sources */, @@ -9181,7 +9225,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProvider.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -9218,7 +9262,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9308,7 +9352,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9335,7 +9379,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9484,7 +9528,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9509,7 +9553,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGo/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9578,7 +9622,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -9612,7 +9656,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9645,7 +9689,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9675,7 +9719,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9985,7 +10029,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10016,7 +10060,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -10044,7 +10088,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -10077,7 +10121,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -10107,7 +10151,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProviderAlpha.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -10140,11 +10184,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 2; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10377,7 +10421,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10404,7 +10448,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10436,7 +10480,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10473,7 +10517,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10508,7 +10552,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10543,11 +10587,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 2; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10720,11 +10764,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 2; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10753,10 +10797,10 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 0; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 2; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0f92e79e4b..c8902a4ce6 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "1ed569676555d493c9c5575eaed22aa02569aac9", - "version" : "6.19.0" + "revision" : "b74549bd869fdecc16fad851f2f608b1724764df", + "version" : "6.25.0" } }, { @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/duckduckgo-autofill.git", "state" : { - "revision" : "1fee787458d13f8ed07f9fe81aecd6e59609339e", - "version" : "13.1.0" + "revision" : "945ac09a0189dc6736db617867fde193ea984b20", + "version" : "15.0.0" } }, { diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index 0442bb02d9..d5f34b1a3c 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -88,8 +88,13 @@ import os.log private var autofillUsageMonitor = AutofillUsageMonitor() private(set) var subscriptionFeatureAvailability: SubscriptionFeatureAvailability! + private var subscriptionCookieManager: SubscriptionCookieManaging! var privacyProDataReporter: PrivacyProDataReporting! + // MARK: - Feature specific app event handlers + + private let tipKitAppEventsHandler = TipKitAppEventHandler() + // MARK: lifecycle @UserDefaultsWrapper(key: .privacyConfigCustomURL, defaultValue: nil) @@ -118,7 +123,7 @@ import os.log } } - // swiftlint:disable:next function_body_length + // swiftlint:disable:next function_body_length cyclomatic_complexity func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { #if targetEnvironment(simulator) @@ -308,6 +313,17 @@ import os.log subscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability( privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager, purchasePlatform: .appStore) + + subscriptionCookieManager = SubscriptionCookieManager(subscriptionManager: AppDependencyProvider.shared.subscriptionManager, + currentCookieStore: { [weak self] in + guard self?.mainViewController?.tabManager.model.hasActiveTabs ?? false else { + // We shouldn't interact with WebKit's cookie store unless we have a WebView, + // eventually the subscription cookie will be refreshed on opening the first tab + return nil + } + + return WKWebsiteDataStore.current().httpCookieStore + }, eventMapping: SubscriptionCookieManageEventPixelMapping()) homePageConfiguration = HomePageConfiguration(variantManager: AppDependencyProvider.shared.variantManager, remoteMessagingClient: remoteMessagingClient, @@ -346,7 +362,8 @@ import os.log contextualOnboardingLogic: daxDialogs, contextualOnboardingPixelReporter: onboardingPixelReporter, subscriptionFeatureAvailability: subscriptionFeatureAvailability, - voiceSearchHelper: voiceSearchHelper) + voiceSearchHelper: voiceSearchHelper, + subscriptionCookieManager: subscriptionCookieManager) main.loadViewIfNeeded() syncErrorHandler.alertPresenter = main @@ -394,6 +411,8 @@ import os.log didCrashDuringCrashHandlersSetUp = false } + tipKitAppEventsHandler.appDidFinishLaunching() + return true } @@ -568,6 +587,10 @@ import os.log } } + Task { @MainActor in + await subscriptionCookieManager.refreshSubscriptionCookie() + } + let importPasswordsStatusHandler = ImportPasswordsStatusHandler(syncService: syncService) importPasswordsStatusHandler.checkSyncSuccessStatus() diff --git a/DuckDuckGo/AppDependencyProvider.swift b/DuckDuckGo/AppDependencyProvider.swift index 7460cb6bf4..4997f73e56 100644 --- a/DuckDuckGo/AppDependencyProvider.swift +++ b/DuckDuckGo/AppDependencyProvider.swift @@ -139,9 +139,4 @@ final class AppDependencyProvider: DependencyProvider { accountManager: accountManager) } - /// Only meant to be used for testing. - /// - static func makeTestingInstance() -> Self { - Self.init() - } } diff --git a/DuckDuckGo/BrowsingMenu/BrowsingMenuAnimator.swift b/DuckDuckGo/BrowsingMenu/BrowsingMenuAnimator.swift index 401993c4da..5fe45d94f5 100644 --- a/DuckDuckGo/BrowsingMenu/BrowsingMenuAnimator.swift +++ b/DuckDuckGo/BrowsingMenu/BrowsingMenuAnimator.swift @@ -106,7 +106,7 @@ final class BrowsingMenuAnimator: NSObject, UIViewControllerAnimatedTransitionin fromViewController.view.isHidden = true - if toViewController.homeViewController != nil { + if toViewController.newTabPageViewController != nil { toViewController.presentedMenuButton.setState(.bookmarksImage, animated: true) } else { toViewController.presentedMenuButton.setState(.menuImage, animated: true) diff --git a/DuckDuckGo/DaxDialogViewController.swift b/DuckDuckGo/DaxDialogViewController.swift index e231e6c566..78c524c2f5 100644 --- a/DuckDuckGo/DaxDialogViewController.swift +++ b/DuckDuckGo/DaxDialogViewController.swift @@ -42,7 +42,7 @@ class DaxDialogViewController: UIViewController { initCTA() } } - + func calculateHeight() -> CGFloat { guard let text = message ?? cta, !text.isEmpty else { return 370.0 } @@ -59,7 +59,7 @@ class DaxDialogViewController: UIViewController { let bottomMargin: CGFloat = 24.0 return iconHeight + topMargin + size.height + buttonHeight + bottomMargin } - + var onTapCta: (() -> Void)? private var position: Int = 0 @@ -188,3 +188,14 @@ extension DaxDialogViewController { } } } + +extension DaxDialogViewController { + static func loadFromStoryboard() -> DaxDialogViewController { + let storyboard = UIStoryboard(name: "DaxOnboarding", bundle: Bundle.main) + guard let controller = storyboard.instantiateViewController(identifier: "DaxDialog") as? DaxDialogViewController else { + fatalError("Failed to instantiate DaxDialogViewController from storyboard") + } + + return controller + } +} diff --git a/DuckDuckGo/Debug.storyboard b/DuckDuckGo/Debug.storyboard index c66b2130dc..6f8d490805 100644 --- a/DuckDuckGo/Debug.storyboard +++ b/DuckDuckGo/Debug.storyboard @@ -383,9 +383,18 @@ - + + + + + + + + + + @@ -393,7 +402,7 @@ - + @@ -1049,34 +1058,34 @@ - + - + - + - + diff --git a/DuckDuckGo/FavoritesFaviconLoader.swift b/DuckDuckGo/FavoritesFaviconLoader.swift index 7adc516ce9..4716a21a48 100644 --- a/DuckDuckGo/FavoritesFaviconLoader.swift +++ b/DuckDuckGo/FavoritesFaviconLoader.swift @@ -40,8 +40,12 @@ actor FavoritesFaviconLoader: FavoritesFaviconLoading { } tasks[domain] = newTask + let value = await newTask.value + if value == nil { + tasks[domain] = nil + } - return await newTask.value + return value } nonisolated func existingFavicon(for favorite: Favorite, size: CGFloat) -> Favicon? { diff --git a/DuckDuckGo/FavoriteDataSource.swift b/DuckDuckGo/FavoritesListInteractingAdapter.swift similarity index 72% rename from DuckDuckGo/FavoriteDataSource.swift rename to DuckDuckGo/FavoritesListInteractingAdapter.swift index 921938d532..2f4e3fac30 100644 --- a/DuckDuckGo/FavoriteDataSource.swift +++ b/DuckDuckGo/FavoritesListInteractingAdapter.swift @@ -1,5 +1,5 @@ // -// FavoriteDataSource.swift +// FavoritesListInteractingAdapter.swift // DuckDuckGo // // Copyright © 2024 DuckDuckGo. All rights reserved. @@ -24,12 +24,30 @@ import Bookmarks final class FavoritesListInteractingAdapter: NewTabPageFavoriteDataSource { let favoritesListInteracting: FavoritesListInteracting + let appSettings: AppSettings - init(favoritesListInteracting: FavoritesListInteracting) { + private var cancellables: Set = [] + + private var displayModeSubject = PassthroughSubject() + + init(favoritesListInteracting: FavoritesListInteracting, appSettings: AppSettings = AppDependencyProvider.shared.appSettings) { self.favoritesListInteracting = favoritesListInteracting + self.appSettings = appSettings + self.externalUpdates = favoritesListInteracting.externalUpdates.merge(with: displayModeSubject).eraseToAnyPublisher() + + NotificationCenter.default.publisher(for: AppUserDefaults.Notifications.favoritesDisplayModeChange) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + guard let self else { + return + } + favoritesListInteracting.favoritesDisplayMode = self.appSettings.favoritesDisplayMode + displayModeSubject.send() + } + .store(in: &cancellables) } - var externalUpdates: AnyPublisher { favoritesListInteracting.externalUpdates } + let externalUpdates: AnyPublisher var favorites: [Favorite] { (try? favoritesListInteracting.favorites.map(Favorite.init)) ?? [] diff --git a/DuckDuckGo/FavoritesViewModel.swift b/DuckDuckGo/FavoritesViewModel.swift index 781d77a044..babd8e20c2 100644 --- a/DuckDuckGo/FavoritesViewModel.swift +++ b/DuckDuckGo/FavoritesViewModel.swift @@ -54,18 +54,23 @@ class FavoritesViewModel: ObservableObject { private let favoriteDataSource: NewTabPageFavoriteDataSource private let pixelFiring: PixelFiring.Type private let dailyPixelFiring: DailyPixelFiring.Type + private let isNewTabPageCustomizationEnabled: Bool var isEmpty: Bool { allFavorites.filter(\.isFavorite).isEmpty } - init(favoriteDataSource: NewTabPageFavoriteDataSource, + init(isNewTabPageCustomizationEnabled: Bool = false, + favoriteDataSource: NewTabPageFavoriteDataSource, faviconLoader: FavoritesFaviconLoading, pixelFiring: PixelFiring.Type = Pixel.self, dailyPixelFiring: DailyPixelFiring.Type = DailyPixel.self) { self.favoriteDataSource = favoriteDataSource self.pixelFiring = pixelFiring self.dailyPixelFiring = dailyPixelFiring + self.isNewTabPageCustomizationEnabled = isNewTabPageCustomizationEnabled + self.isCollapsed = isNewTabPageCustomizationEnabled + self.faviconLoader = MissingFaviconWrapper(loader: faviconLoader, onFaviconMissing: { [weak self] in guard let self else { return } @@ -73,8 +78,7 @@ class FavoritesViewModel: ObservableObject { self.faviconMissing() } }) - - + favoriteDataSource.externalUpdates.sink { [weak self] _ in self?.updateData() }.store(in: &cancellables) @@ -93,6 +97,10 @@ class FavoritesViewModel: ObservableObject { } func prefixedFavorites(for columnsCount: Int) -> FavoritesSlice { + guard isNewTabPageCustomizationEnabled else { + return .init(items: allFavorites, isCollapsible: false) + } + let hasFavorites = allFavorites.contains(where: \.isFavorite) let maxCollapsedItemsCount = hasFavorites ? columnsCount * 2 : columnsCount let isCollapsible = allFavorites.count > maxCollapsedItemsCount @@ -170,7 +178,10 @@ class FavoritesViewModel: ObservableObject { var allFavorites = favoriteDataSource.favorites.map { FavoriteItem.favorite($0) } - allFavorites.append(.addFavorite) + + if isNewTabPageCustomizationEnabled { + allFavorites.append(.addFavorite) + } self.allFavorites = allFavorites } diff --git a/DuckDuckGo/HomeMessage.xcassets/WidgetEducation/WidgetEducationHomeScreen.imageset/iphone.png b/DuckDuckGo/HomeMessage.xcassets/WidgetEducation/WidgetEducationHomeScreen.imageset/iphone.png deleted file mode 100644 index 19ae85b0b9..0000000000 Binary files a/DuckDuckGo/HomeMessage.xcassets/WidgetEducation/WidgetEducationHomeScreen.imageset/iphone.png and /dev/null differ diff --git a/DuckDuckGo/HomeScreenTransition.swift b/DuckDuckGo/HomeScreenTransition.swift index 8de6d19b3d..6fb2e8518b 100644 --- a/DuckDuckGo/HomeScreenTransition.swift +++ b/DuckDuckGo/HomeScreenTransition.swift @@ -90,7 +90,7 @@ class FromHomeScreenTransition: HomeScreenTransition { tabSwitcherViewController.view.frame = transitionContext.finalFrame(for: tabSwitcherViewController) tabSwitcherViewController.prepareForPresentation() - guard let homeScreen = mainViewController.homeController, + guard let homeScreen = mainViewController.newTabPageViewController, let tab = mainViewController.tabManager.model.currentTab, let rowIndex = tabSwitcherViewController.tabsModel.indexOf(tab: tab), let layoutAttr = tabSwitcherViewController.collectionView.layoutAttributesForItem(at: IndexPath(row: rowIndex, section: 0)) @@ -163,7 +163,7 @@ class ToHomeScreenTransition: HomeScreenTransition { prepareSubviews(using: transitionContext) guard let mainViewController = transitionContext.viewController(forKey: .to) as? MainViewController, - let homeScreen = mainViewController.homeController, + let homeScreen = mainViewController.newTabPageViewController, let tab = mainViewController.tabManager.model.currentTab, let rowIndex = tabSwitcherViewController.tabsModel.indexOf(tab: tab), let layoutAttr = tabSwitcherViewController.collectionView.layoutAttributesForItem(at: IndexPath(row: rowIndex, section: 0)) diff --git a/DuckDuckGo/MainViewController+KeyCommands.swift b/DuckDuckGo/MainViewController+KeyCommands.swift index 45f499a9b6..3e6bb42e7c 100644 --- a/DuckDuckGo/MainViewController+KeyCommands.swift +++ b/DuckDuckGo/MainViewController+KeyCommands.swift @@ -33,7 +33,7 @@ extension MainViewController { } var browsingCommands = [UIKeyCommand]() - if homeController == nil { + if newTabPageViewController == nil { browsingCommands = [ UIKeyCommand(title: "", action: #selector(keyboardFind), input: "f", modifierFlags: [.command], discoverabilityTitle: UserText.keyCommandFind), @@ -140,7 +140,7 @@ extension MainViewController { @objc func keyboardLocation() { guard tabSwitcherController == nil else { return } - if let controller = homeController { + if let controller = newTabPageViewController { controller.launchNewSearch() } else { showBars() diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index 7e90bf0129..d1a2fd2e36 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -77,12 +77,8 @@ class MainViewController: UIViewController { emailManager.requestDelegate = self return emailManager }() - - var homeViewController: HomeViewController? + var newTabPageViewController: NewTabPageViewController? - var homeController: (NewTabPage & HomeScreenTransitionSource)? { - homeViewController ?? newTabPageViewController - } var tabsBarController: TabsBarViewController? var suggestionTrayController: SuggestionTrayViewController? @@ -127,6 +123,7 @@ class MainViewController: UIViewController { private var feedbackCancellable: AnyCancellable? let subscriptionFeatureAvailability: SubscriptionFeatureAvailability + private let subscriptionCookieManager: SubscriptionCookieManaging let privacyProDataReporter: PrivacyProDataReporting private lazy var featureFlagger = AppDependencyProvider.shared.featureFlagger @@ -199,7 +196,8 @@ class MainViewController: UIViewController { tutorialSettings: TutorialSettings = DefaultTutorialSettings(), statisticsStore: StatisticsStore = StatisticsUserDefaults(), subscriptionFeatureAvailability: SubscriptionFeatureAvailability, - voiceSearchHelper: VoiceSearchHelperProtocol + voiceSearchHelper: VoiceSearchHelperProtocol, + subscriptionCookieManager: SubscriptionCookieManaging ) { self.bookmarksDatabase = bookmarksDatabase self.bookmarksDatabaseCleaner = bookmarksDatabaseCleaner @@ -221,7 +219,8 @@ class MainViewController: UIViewController { privacyProDataReporter: privacyProDataReporter, contextualOnboardingPresenter: contextualOnboardingPresenter, contextualOnboardingLogic: contextualOnboardingLogic, - onboardingPixelReporter: contextualOnboardingPixelReporter) + onboardingPixelReporter: contextualOnboardingPixelReporter, + subscriptionCookieManager: subscriptionCookieManager) self.syncPausedStateManager = syncPausedStateManager self.privacyProDataReporter = privacyProDataReporter self.homeTabManager = NewTabPageManager() @@ -232,6 +231,7 @@ class MainViewController: UIViewController { self.statisticsStore = statisticsStore self.subscriptionFeatureAvailability = subscriptionFeatureAvailability self.voiceSearchHelper = voiceSearchHelper + self.subscriptionCookieManager = subscriptionCookieManager super.init(nibName: nil, bundle: nil) @@ -486,7 +486,7 @@ class MainViewController: UIViewController { @objc private func keyboardWillHide() { - if homeController?.isDragging == true, keyboardShowing { + if newTabPageViewController?.isDragging == true, keyboardShowing { Pixel.fire(pixel: .addressBarGestureDismiss) } } @@ -681,7 +681,6 @@ class MainViewController: UIViewController { } self.menuBookmarksViewModel.favoritesDisplayMode = self.appSettings.favoritesDisplayMode self.favoritesViewModel.favoritesDisplayMode = self.appSettings.favoritesDisplayMode - self.homeController?.reloadFavorites() WidgetCenter.shared.reloadAllTimelines() } } @@ -695,9 +694,6 @@ class MainViewController: UIViewController { syncUpdatesCancellable = syncDataProviders.bookmarksAdapter.syncDidCompletePublisher .sink { [weak self] _ in self?.favoritesViewModel.reloadData() - DispatchQueue.main.async { - self?.homeController?.reloadFavorites() - } } } @@ -796,53 +792,34 @@ class MainViewController: UIViewController { } let newTabDaxDialogFactory = NewTabDaxDialogFactory(delegate: self, contextualOnboardingLogic: DaxDialogs.shared, onboardingPixelReporter: contextualOnboardingPixelReporter) - if homeTabManager.isNewTabPageSectionsEnabled { - let controller = NewTabPageViewController(tab: tabModel, - interactionModel: favoritesViewModel, - syncService: syncService, - syncBookmarksAdapter: syncDataProviders.bookmarksAdapter, - homePageMessagesConfiguration: homePageConfiguration, - privacyProDataReporting: privacyProDataReporter, - variantManager: variantManager, - newTabDialogFactory: newTabDaxDialogFactory, - newTabDialogTypeProvider: DaxDialogs.shared, - faviconLoader: faviconLoader) - - controller.delegate = self - controller.shortcutsDelegate = self - controller.chromeDelegate = self - - newTabPageViewController = controller - addToContentContainer(controller: controller) - viewCoordinator.logoContainer.isHidden = true - adjustNewTabPageSafeAreaInsets(for: appSettings.currentAddressBarPosition) - } else { - let homePageDependencies = HomePageDependencies(homePageConfiguration: homePageConfiguration, - model: tabModel, - favoritesViewModel: favoritesViewModel, - appSettings: appSettings, - syncService: syncService, - syncDataProviders: syncDataProviders, - privacyProDataReporter: privacyProDataReporter, - variantManager: variantManager, - newTabDialogFactory: newTabDaxDialogFactory, - newTabDialogTypeProvider: DaxDialogs.shared) - let controller = HomeViewController.loadFromStoryboard(homePageDependecies: homePageDependencies) - - controller.delegate = self - controller.chromeDelegate = self - homeViewController = controller - addToContentContainer(controller: controller) - } + let controller = NewTabPageViewController(tab: tabModel, + isNewTabPageCustomizationEnabled: homeTabManager.isNewTabPageSectionsEnabled, + interactionModel: favoritesViewModel, + syncService: syncService, + syncBookmarksAdapter: syncDataProviders.bookmarksAdapter, + homePageMessagesConfiguration: homePageConfiguration, + privacyProDataReporting: privacyProDataReporter, + variantManager: variantManager, + newTabDialogFactory: newTabDaxDialogFactory, + newTabDialogTypeProvider: DaxDialogs.shared, + faviconLoader: faviconLoader) + + controller.delegate = self + controller.shortcutsDelegate = self + controller.chromeDelegate = self + + newTabPageViewController = controller + addToContentContainer(controller: controller) + viewCoordinator.logoContainer.isHidden = true + adjustNewTabPageSafeAreaInsets(for: appSettings.currentAddressBarPosition) refreshControls() syncService.scheduler.requestSyncImmediately() } fileprivate func removeHomeScreen() { - homeController?.willMove(toParent: nil) - homeController?.dismiss() - homeViewController = nil + newTabPageViewController?.willMove(toParent: nil) + newTabPageViewController?.dismiss() newTabPageViewController = nil } @@ -1193,7 +1170,7 @@ class MainViewController: UIViewController { func refreshMenuButtonState() { let expectedState: MenuButton.State - if homeViewController != nil { + if !homeTabManager.isNewTabPageSectionsEnabled && newTabPageViewController != nil { expectedState = .bookmarksImage viewCoordinator.lastToolbarButton.accessibilityLabel = UserText.bookmarksButtonHint viewCoordinator.omniBar.menuButton.accessibilityLabel = UserText.bookmarksButtonHint @@ -1432,18 +1409,7 @@ class MainViewController: UIViewController { attachHomeScreen() tabsBarController?.refresh(tabsModel: tabManager.model) swipeTabsCoordinator?.refresh(tabsModel: tabManager.model, scrollToSelected: true) - homeController?.openedAsNewTab(allowingKeyboard: allowingKeyboard) - } - - func animateLogoAppearance() { - viewCoordinator.logoContainer.alpha = 0 - viewCoordinator.logoContainer.transform = CGAffineTransform(scaleX: 0.5, y: 0.5) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - UIView.animate(withDuration: 0.2) { - self.viewCoordinator.logoContainer.alpha = 1 - self.viewCoordinator.logoContainer.transform = CGAffineTransform(scaleX: 1.0, y: 1.0) - } - } + newTabPageViewController?.openedAsNewTab(allowingKeyboard: allowingKeyboard) } func updateFindInPage() { @@ -1753,7 +1719,7 @@ extension MainViewController: BrowserChromeDelegate { updateBlock() } } - + func setNavigationBarHidden(_ hidden: Bool) { if hidden { hideKeyboard() } @@ -1823,7 +1789,7 @@ extension MainViewController: OmniBarDelegate { func onOmniQueryUpdated(_ updatedQuery: String) { if updatedQuery.isEmpty { - if homeController != nil { + if newTabPageViewController != nil { hideSuggestionTray() } else { let didShow = tryToShowSuggestionTray(.favorites) @@ -1892,7 +1858,7 @@ extension MainViewController: OmniBarDelegate { let menuEntries: [BrowsingMenuEntry] let headerEntries: [BrowsingMenuEntry] - if isNewTabPageVisible { + if homeTabManager.isNewTabPageSectionsEnabled && newTabPageViewController != nil { menuEntries = tab.buildShortcutsMenu() headerEntries = [] } else { @@ -1933,7 +1899,7 @@ extension MainViewController: OmniBarDelegate { } func fireControllerAwarePixel(ntp: Pixel.Event, serp: Pixel.Event, website: Pixel.Event) { - if homeController != nil { + if newTabPageViewController != nil { Pixel.fire(pixel: ntp) } else if let currentTab { if currentTab.url?.isDuckDuckGoSearch == true { @@ -2005,7 +1971,7 @@ extension MainViewController: OmniBarDelegate { fireControllerAwarePixel(ntp: .addressBarClickOnNTP, serp: .addressBarClickOnSERP, website: .addressBarClickOnWebsite) } - guard homeController == nil else { return } + guard newTabPageViewController == nil else { return } if !skipSERPFlow, isSERPPresented, let query = omniBar.textField.text { tryToShowSuggestionTray(.autocomplete(query: query)) @@ -2022,10 +1988,10 @@ extension MainViewController: OmniBarDelegate { if !DaxDialogs.shared.shouldShowFireButtonPulse { ViewHighlighter.hideAll() } - guard let homeController = homeController else { + guard let newTabPageViewController = newTabPageViewController else { return selectQueryText } - homeController.launchNewSearch() + newTabPageViewController.launchNewSearch() return selectQueryText } @@ -2065,7 +2031,7 @@ extension MainViewController: FavoritesOverlayDelegate { func favoritesOverlay(_ overlay: FavoritesOverlay, didSelect favorite: BookmarkEntity) { guard let url = favorite.urlObject else { return } Pixel.fire(pixel: .favoriteLaunchedWebsite) - homeViewController?.chromeDelegate = nil + newTabPageViewController?.chromeDelegate = nil dismissOmniBar() Favicons.shared.loadFavicon(forDomain: url.host, intoCache: .fireproof, fromCache: .tabs) if url.isBookmarklet() { @@ -2088,7 +2054,7 @@ extension MainViewController: AutocompleteViewControllerDelegate { } func autocomplete(selectedSuggestion suggestion: Suggestion) { - homeViewController?.chromeDelegate = nil + newTabPageViewController?.chromeDelegate = nil dismissOmniBar() viewCoordinator.omniBar.cancel() switch suggestion { @@ -2113,7 +2079,7 @@ extension MainViewController: AutocompleteViewControllerDelegate { loadUrl(url) case .openTab(title: _, url: let url): - if homeViewController != nil, let tab = tabManager.model.currentTab { + if newTabPageViewController != nil, let tab = tabManager.model.currentTab { self.closeTab(tab) } loadUrlInNewTab(url, reuseExisting: true, inheritedAttribution: .noAttribution) @@ -2194,49 +2160,6 @@ extension MainViewController { } } -extension MainViewController: HomeControllerDelegate { - - func home(_ home: HomeViewController, didRequestQuery query: String) { - loadQueryInNewTab(query) - } - - func home(_ home: HomeViewController, didRequestUrl url: URL) { - handleRequestedURL(url) - } - - func home(_ home: HomeViewController, didRequestEdit favorite: BookmarkEntity) { - segueToEditBookmark(favorite) - } - - func home(_ home: HomeViewController, didRequestContentOverflow shouldOverflow: Bool) -> CGFloat { - allowContentUnderflow = shouldOverflow - return contentUnderflow - } - - func homeDidDeactivateOmniBar(home: HomeViewController) { - hideSuggestionTray() - dismissOmniBar() - } - - func showSettings(_ home: HomeViewController) { - segueToSettings() - } - - func home(_ home: HomeViewController, didRequestHideLogo hidden: Bool) { - viewCoordinator.logoContainer.isHidden = hidden - } - - func homeDidRequestLogoContainer(_ home: HomeViewController) -> UIView { - return viewCoordinator.logoContainer - } - - func home(_ home: HomeViewController, searchTransitionUpdated percent: CGFloat) { - viewCoordinator.statusBackground.alpha = percent - viewCoordinator.navigationBarContainer.alpha = percent - } - -} - extension MainViewController: NewTabPageControllerDelegate { func newTabPageDidOpenFavoriteURL(_ controller: NewTabPageViewController, url: URL) { handleRequestedURL(url) @@ -2512,17 +2435,19 @@ extension MainViewController: TabDelegate { extension MainViewController: TabSwitcherDelegate { + private func animateLogoAppearance() { + newTabPageViewController?.view.transform = CGAffineTransform().scaledBy(x: 0.5, y: 0.5) + newTabPageViewController?.view.alpha = 0.0 + UIView.animate(withDuration: 0.2, delay: 0.1, options: [.curveEaseInOut, .beginFromCurrentState]) { + self.newTabPageViewController?.view.transform = .identity + self.newTabPageViewController?.view.alpha = 1.0 + } + } + func tabSwitcherDidRequestNewTab(tabSwitcher: TabSwitcherViewController) { newTab() - if homeViewController != nil { + if newTabPageViewController != nil { animateLogoAppearance() - } else if newTabPageViewController != nil { - newTabPageViewController?.view.transform = CGAffineTransform().scaledBy(x: 0.5, y: 0.5) - newTabPageViewController?.view.alpha = 0.0 - UIView.animate(withDuration: 0.2, delay: 0.1, options: [.curveEaseInOut, .beginFromCurrentState]) { - self.newTabPageViewController?.view.transform = .identity - self.newTabPageViewController?.view.alpha = 1.0 - } } } @@ -2540,7 +2465,7 @@ extension MainViewController: TabSwitcherDelegate { // switcher is still presented. DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { tabSwitcher.dismiss(animated: true) { - self.homeController?.viewDidAppear(true) + self.newTabPageViewController?.viewDidAppear(true) } } } @@ -2746,7 +2671,7 @@ extension MainViewController: AutoClearWorker { // Ideally this should happen once data clearing has finished AND the animation is finished if showNextDaxDialog { - self.homeController?.showNextDaxDialog() + self.newTabPageViewController?.showNextDaxDialog() } else if KeyboardSettings().onNewTab { let showKeyboardAfterFireButton = DispatchWorkItem { self.enterSearch() @@ -2840,7 +2765,7 @@ extension MainViewController: OnboardingDelegate { markOnboardingSeen() controller.modalTransitionStyle = .crossDissolve controller.dismiss(animated: true) - homeController?.onboardingCompleted() + newTabPageViewController?.onboardingCompleted() } func markOnboardingSeen() { diff --git a/DuckDuckGo/NetworkProtectionStatusView.swift b/DuckDuckGo/NetworkProtectionStatusView.swift index f8508a2482..21e79aa654 100644 --- a/DuckDuckGo/NetworkProtectionStatusView.swift +++ b/DuckDuckGo/NetworkProtectionStatusView.swift @@ -19,11 +19,61 @@ import SwiftUI import NetworkProtection +import TipKit struct NetworkProtectionStatusView: View { + + static let defaultImageSize = CGSize(width: 32, height: 32) + @Environment(\.colorScheme) var colorScheme - @StateObject public var statusModel: NetworkProtectionStatusViewModel + @ObservedObject + public var statusModel: NetworkProtectionStatusViewModel + + // MARK: - Tips + + let geoswitchingTip: VPNGeoswitchingTip = { + let tip = VPNGeoswitchingTip() + + if #available(iOS 17.0, *) { + if tip.shouldDisplay { + Task { + for await status in tip.statusUpdates { + if case .invalidated = status { + await VPNSnoozeTip.geolocationTipDismissedEvent.donate() + await VPNAddWidgetTip.geolocationTipDismissedEvent.donate() + } + } + } + } + } + + return tip + }() + + let snoozeTip: VPNSnoozeTip = { + let tip = VPNSnoozeTip() + + if #available(iOS 17.0, *) { + if tip.shouldDisplay { + Task { + for await status in tip.statusUpdates { + if case .invalidated = status { + await VPNAddWidgetTip.snoozeTipDismissedEvent.donate() + } + } + } + } + } + + return tip + }() + + let widgetTip: VPNAddWidgetTip = { + VPNAddWidgetTip() + }() + + // MARK: - View var body: some View { List { @@ -35,6 +85,7 @@ struct NetworkProtectionStatusView: View { } toggle() + locationDetails() if statusModel.isNetPEnabled && statusModel.hasServerInfo && !statusModel.isSnoozing { @@ -51,6 +102,9 @@ struct NetworkProtectionStatusView: View { .animation(.easeOut, value: statusModel.shouldShowError) }) .applyInsetGroupedListStyle() + .sheet(isPresented: $statusModel.showAddWidgetEducationView) { + widgetEducationSheet() + } } @ViewBuilder @@ -86,11 +140,27 @@ struct NetworkProtectionStatusView: View { .padding([.top, .bottom], 2) snooze() + } header: { header() } .increaseHeaderProminence() .listRowBackground(Color(designSystemColor: .surface)) + + Section { + if #available(iOS 17.0, *) { + widgetTipView() + .tipImageSize(Self.defaultImageSize) + .padding(.horizontal, 3) + } + + if #available(iOS 17.0, *) { + snoozeTipView() + .tipImageSize(Self.defaultImageSize) + .padding(.horizontal, 3) + } + } + .listRowBackground(Color(designSystemColor: .surface)) } @ViewBuilder @@ -151,8 +221,8 @@ struct NetworkProtectionStatusView: View { @ViewBuilder private func locationDetails() -> some View { - if !statusModel.isSnoozing, let location = statusModel.location { - Section { + Section { + if !statusModel.isSnoozing, let location = statusModel.location { var locationAttributedString: AttributedString { var attributedString = AttributedString( statusModel.preferredLocation.isNearest ? "\(location) \(UserText.netPVPNLocationNearest)" : location @@ -164,16 +234,10 @@ struct NetworkProtectionStatusView: View { return attributedString } - NavigationLink(destination: NetworkProtectionVPNLocationView()) { + NavigationLink(destination: locationView()) { NetworkProtectionLocationItemView(title: locationAttributedString, imageName: nil) } - } header: { - Text(statusModel.isNetPEnabled ? UserText.vpnLocationConnected : UserText.vpnLocationSelected) - .foregroundColor(.init(designSystemColor: .textSecondary)) - } - .listRowBackground(Color(designSystemColor: .surface)) - } else { - Section { + } else { let imageName = statusModel.preferredLocation.isNearest ? "VPNLocation" : nil var nearestLocationAttributedString: AttributedString { var attributedString = AttributedString(statusModel.preferredLocation.title) @@ -181,15 +245,32 @@ struct NetworkProtectionStatusView: View { return attributedString } - NavigationLink(destination: NetworkProtectionVPNLocationView()) { + NavigationLink(destination: locationView()) { NetworkProtectionLocationItemView(title: nearestLocationAttributedString, imageName: imageName) } - } header: { - Text(statusModel.isNetPEnabled ? UserText.vpnLocationConnected : UserText.vpnLocationSelected) - .foregroundColor(.init(designSystemColor: .textSecondary)) } - .listRowBackground(Color(designSystemColor: .surface)) + } header: { + Text(statusModel.isNetPEnabled ? UserText.vpnLocationConnected : UserText.vpnLocationSelected) + .foregroundColor(.init(designSystemColor: .textSecondary)) + } + .listRowBackground(Color(designSystemColor: .surface)) + + Section { + if #available(iOS 17.0, *) { + geoswitchingTipView() + .tipImageSize(Self.defaultImageSize) + .padding(.horizontal, 3) + } } + .listRowBackground(Color(designSystemColor: .surface)) + } + + @ViewBuilder + private func locationView() -> some View { + NetworkProtectionVPNLocationView() + .onAppear { + statusModel.handleUserOpenedVPNLocations() + } } @ViewBuilder @@ -267,6 +348,61 @@ struct NetworkProtectionStatusView: View { isAnimating: $statusModel.isNetPEnabled ) } + + // MARK: - Tips + + @available(iOS 17.0, *) + @ViewBuilder + private func geoswitchingTipView() -> some View { + if statusModel.canShowTips { + + TipView(geoswitchingTip) + .removeGroupedListStyleInsets() + .tipCornerRadius(0) + .tipBackground(Color(designSystemColor: .surface)) + } + } + + @available(iOS 17.0, *) + @ViewBuilder + private func snoozeTipView() -> some View { + if statusModel.canShowTips, + statusModel.hasServerInfo { + + TipView(snoozeTip, action: statusModel.snoozeActionHandler(action:)) + .removeGroupedListStyleInsets() + .tipCornerRadius(0) + .tipBackground(Color(designSystemColor: .surface)) + } + } + + @available(iOS 17.0, *) + @ViewBuilder + private func widgetTipView() -> some View { + if statusModel.canShowTips, + !statusModel.isNetPEnabled && !statusModel.isSnoozing { + + TipView(widgetTip, action: statusModel.widgetActionHandler(action:)) + .removeGroupedListStyleInsets() + .tipCornerRadius(0) + .tipBackground(Color(designSystemColor: .surface)) + } + } + + // MARK: - Sheets + + private func widgetEducationSheet() -> some View { + NavigationView { + WidgetEducationView() + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button(UserText.navigationTitleDone) { + statusModel.showAddWidgetEducationView = false + } + } + } + } + } } private struct NetworkProtectionErrorView: View { diff --git a/DuckDuckGo/NetworkProtectionStatusViewModel.swift b/DuckDuckGo/NetworkProtectionStatusViewModel.swift index 7e7e5dc51f..becccb79aa 100644 --- a/DuckDuckGo/NetworkProtectionStatusViewModel.swift +++ b/DuckDuckGo/NetworkProtectionStatusViewModel.swift @@ -24,6 +24,7 @@ import WidgetKit import BrowserServicesKit import Core import Subscription +import TipKit struct NetworkProtectionLocationStatusModel { enum LocationIcon { @@ -94,12 +95,24 @@ final class NetworkProtectionStatusViewModel: ObservableObject { return formatter }() + private let featureFlagger = AppDependencyProvider.shared.featureFlagger private let tunnelController: (TunnelController & TunnelSessionProvider) private let statusObserver: ConnectionStatusObserver private let serverInfoObserver: ConnectionServerInfoObserver private let errorObserver: ConnectionErrorObserver private var cancellables: Set = [] + // MARK: - Tips + + var canShowTips: Bool { + featureFlagger.isFeatureOn(.networkProtectionUserTips) + } + + /// Whether the "Add Widget" education sheet should be presented to the user. + /// + @Published + var showAddWidgetEducationView: Bool = false + // MARK: Error struct ErrorItem { @@ -121,7 +134,19 @@ final class NetworkProtectionStatusViewModel: ObservableObject { // MARK: Toggle Item - @Published public var isNetPEnabled = false + @Published public var isNetPEnabled = false { + didSet { + if #available(iOS 17.0, *) { + if isNetPEnabled { + VPNGeoswitchingTip.donateVPNConnectedEvent() + } + + VPNSnoozeTip.vpnEnabled = isNetPEnabled + VPNAddWidgetTip.vpnEnabled = isNetPEnabled + } + } + } + @Published public var isSnoozing = false { didSet { snoozeRequestPending = false @@ -452,6 +477,10 @@ final class NetworkProtectionStatusViewModel: ObservableObject { return } + if #available(iOS 17.0, *) { + VPNSnoozeTip().invalidate(reason: .actionPerformed) + } + let defaultDuration: TimeInterval = .minutes(20) snoozeRequestPending = true try? await activeSession.sendProviderMessage(.startSnooze(defaultDuration)) @@ -546,6 +575,35 @@ final class NetworkProtectionStatusViewModel: ObservableObject { self.downloadTotal = nil } + // MARK: - UI Events handling + + @available(iOS 17.0, *) + func snoozeActionHandler(action: Tips.Action) { + if action.id == VPNSnoozeTip.ActionIdentifiers.learnMore.rawValue { + let url = URL(string: "https://duckduckgo.com/duckduckgo-help-pages/privacy-pro/vpn/troubleshooting/")! + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } + } + + @available(iOS 17.0, *) + @MainActor + func widgetActionHandler(action: Tips.Action) { + if action.id == VPNAddWidgetTip.ActionIdentifiers.addWidget.rawValue { + showAddWidgetEducationView = true + + VPNAddWidgetTip().invalidate(reason: .actionPerformed) + } + } + + /// The user opened the VPN locations view + /// + func handleUserOpenedVPNLocations() { + if #available(iOS 17.0, *) { + Task { @MainActor in + VPNGeoswitchingTip().invalidate(reason: .actionPerformed) + } + } + } } private extension ConnectionStatus { diff --git a/DuckDuckGo/NewTabPage.swift b/DuckDuckGo/NewTabPage.swift index 7be2446e29..eb382f9b5e 100644 --- a/DuckDuckGo/NewTabPage.swift +++ b/DuckDuckGo/NewTabPage.swift @@ -21,8 +21,7 @@ import UIKit protocol NewTabPage: UIViewController { - var isDragging: Bool { get } // TODO: Mariusz, check if needed in both - func reloadFavorites() // TODO: Mariusz: check if needed with reactive approach + var isDragging: Bool { get } func launchNewSearch() func openedAsNewTab(allowingKeyboard: Bool) diff --git a/DuckDuckGo/NewTabPageView.swift b/DuckDuckGo/NewTabPageView.swift index aaed282ca5..219ddd9c27 100644 --- a/DuckDuckGo/NewTabPageView.swift +++ b/DuckDuckGo/NewTabPageView.swift @@ -34,6 +34,8 @@ struct NewTabPageView: View { @State private var customizeButtonShowedInline = false @State private var isAddingFavorite: Bool = false + @State var isDragging: Bool = false + init(viewModel: NewTabPageViewModel, messagesModel: NewTabPageMessagesModel, favoritesViewModel: FavoritesViewModel, @@ -71,6 +73,15 @@ struct NewTabPageView: View { sectionsSettingsModel: sectionsSettingsModel) } }) + .simultaneousGesture( + DragGesture() + .onChanged({ value in + if value.translation.height > 0 { + viewModel.beginDragging() + } + }) + .onEnded({ _ in viewModel.endDragging() }) + ) } } diff --git a/DuckDuckGo/NewTabPageViewController.swift b/DuckDuckGo/NewTabPageViewController.swift index ee3d0bb4a0..0f24599de6 100644 --- a/DuckDuckGo/NewTabPageViewController.swift +++ b/DuckDuckGo/NewTabPageViewController.swift @@ -23,7 +23,7 @@ import Bookmarks import BrowserServicesKit import Core -final class NewTabPageViewController: UIHostingController, NewTabPage { +final class NewTabPageViewController: UIHostingController, NewTabPage { private let syncService: DDGSyncing private let syncBookmarksAdapter: SyncBookmarksAdapter @@ -43,7 +43,15 @@ final class NewTabPageViewController: UIHostingController, NewTa private var hostingController: UIHostingController? + private weak var daxDialogViewController: DaxDialogViewController? + private var daxDialogHeightConstraint: NSLayoutConstraint? + + var isDaxDialogVisible: Bool { + daxDialogViewController?.view.isHidden == false + } + init(tab: Tab, + isNewTabPageCustomizationEnabled: Bool, interactionModel: FavoritesListInteracting, syncService: DDGSyncing, syncBookmarksAdapter: SyncBookmarksAdapter, @@ -64,23 +72,35 @@ final class NewTabPageViewController: UIHostingController, NewTa newTabPageViewModel = NewTabPageViewModel() shortcutsSettingsModel = NewTabPageShortcutsSettingsModel() sectionsSettingsModel = NewTabPageSectionsSettingsModel() - favoritesModel = FavoritesViewModel(favoriteDataSource: FavoritesListInteractingAdapter(favoritesListInteracting: interactionModel), faviconLoader: faviconLoader) + favoritesModel = FavoritesViewModel(isNewTabPageCustomizationEnabled: isNewTabPageCustomizationEnabled, + favoriteDataSource: FavoritesListInteractingAdapter(favoritesListInteracting: interactionModel), + faviconLoader: faviconLoader) shortcutsModel = ShortcutsModel() messagesModel = NewTabPageMessagesModel(homePageMessagesConfiguration: homePageMessagesConfiguration, privacyProDataReporter: privacyProDataReporting) - let newTabPageView = NewTabPageView(viewModel: newTabPageViewModel, - messagesModel: messagesModel, - favoritesViewModel: favoritesModel, - shortcutsModel: shortcutsModel, - shortcutsSettingsModel: shortcutsSettingsModel, - sectionsSettingsModel: sectionsSettingsModel) - - super.init(rootView: newTabPageView) + if isNewTabPageCustomizationEnabled { + super.init(rootView: AnyView(NewTabPageView(viewModel: self.newTabPageViewModel, + messagesModel: self.messagesModel, + favoritesViewModel: self.favoritesModel, + shortcutsModel: self.shortcutsModel, + shortcutsSettingsModel: self.shortcutsSettingsModel, + sectionsSettingsModel: self.sectionsSettingsModel))) + } else { + super.init(rootView: AnyView(SimpleNewTabPageView(viewModel: self.newTabPageViewModel, + messagesModel: self.messagesModel, + favoritesViewModel: self.favoritesModel))) + } assignFavoriteModelActions() assignShorcutsModelActions() } + override func viewDidLoad() { + super.viewDidLoad() + + setUpDaxDialog() + } + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) @@ -90,6 +110,34 @@ final class NewTabPageViewController: UIHostingController, NewTa Pixel.fire(pixel: .homeScreenShown) sendDailyDisplayPixel() + + view.backgroundColor = UIColor(designSystemColor: .background) + } + + private func setUpDaxDialog() { + let daxDialogController = DaxDialogViewController.loadFromStoryboard() + guard let dialogView = daxDialogController.view else { return } + + self.addChild(daxDialogController) + self.view.addSubview(dialogView) + + dialogView.translatesAutoresizingMaskIntoConstraints = false + dialogView.isHidden = true + + let widthConstraint = dialogView.widthAnchor.constraint(equalTo: view.safeAreaLayoutGuide.widthAnchor, multiplier: 1) + widthConstraint.priority = .defaultHigh + let heightConstraint = dialogView.heightAnchor.constraint(equalToConstant: 250) + daxDialogHeightConstraint = heightConstraint + NSLayoutConstraint.activate([ + dialogView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 44.0), + dialogView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + dialogView.widthAnchor.constraint(lessThanOrEqualToConstant: 375), + heightConstraint, + widthConstraint + ]) + + daxDialogController.didMove(toParent: self) + daxDialogViewController = daxDialogController } // MARK: - Private @@ -140,7 +188,7 @@ final class NewTabPageViewController: UIHostingController, NewTa // MARK: - NewTabPage - let isDragging: Bool = false + var isDragging: Bool { newTabPageViewModel.isDragging } weak var chromeDelegate: BrowserChromeDelegate? weak var delegate: NewTabPageControllerDelegate? @@ -151,12 +199,18 @@ final class NewTabPageViewController: UIHostingController, NewTa } func openedAsNewTab(allowingKeyboard: Bool) { - guard allowingKeyboard && KeyboardSettings().onNewTab else { return } + if allowingKeyboard && KeyboardSettings().onNewTab { - // The omnibar is inside a collection view so this needs a chance to do its thing - // which might also be async. Not great. - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - self.launchNewSearch() + // The omnibar is inside a collection view so this needs a chance to do its thing + // which might also be async. Not great. + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.launchNewSearch() + } + } + + if !variantManager.isContextualDaxDialogsEnabled { + // In the new onboarding this gets called twice (viewDidAppear in Tab) which then reset the spec to nil. + presentNextDaxDialog() } } @@ -165,7 +219,7 @@ final class NewTabPageViewController: UIHostingController, NewTa } func showNextDaxDialog() { - showNextDaxDialogNew(dialogProvider: newTabDialogTypeProvider, factory: newTabDialogFactory) + presentNextDaxDialog() } func onboardingCompleted() { @@ -181,6 +235,8 @@ final class NewTabPageViewController: UIHostingController, NewTa private func presentNextDaxDialog() { if variantManager.isContextualDaxDialogsEnabled { showNextDaxDialogNew(dialogProvider: newTabDialogTypeProvider, factory: newTabDialogFactory) + } else { + showNextDaxDialog(dialogProvider: newTabDialogTypeProvider) } } @@ -218,6 +274,37 @@ extension NewTabPageViewController: HomeScreenTransitionSource { extension NewTabPageViewController { + func showNextDaxDialog(dialogProvider: NewTabDialogSpecProvider) { + guard let spec = dialogProvider.nextHomeScreenMessage() else { return } + guard !isDaxDialogVisible else { return } + guard let daxDialogViewController = daxDialogViewController else { return } + + newTabPageViewModel.startOnboarding() + + daxDialogViewController.view.isHidden = false + daxDialogViewController.view.alpha = 0.0 + + daxDialogViewController.loadViewIfNeeded() + daxDialogViewController.message = spec.message + daxDialogViewController.accessibleMessage = spec.accessibilityLabel + + if spec == .initial { + UniquePixel.fire(pixel: .onboardingContextualTryVisitSiteUnique, includedParameters: [.appVersion, .atb]) + } + + view.addGestureRecognizer(daxDialogViewController.tapToCompleteGestureRecognizer) + + daxDialogHeightConstraint?.constant = daxDialogViewController.calculateHeight() + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + UIView.animate(withDuration: 0.4, animations: { + daxDialogViewController.view.alpha = 1.0 + }, completion: { _ in + daxDialogViewController.start() + }) + } + } + func showNextDaxDialogNew(dialogProvider: NewTabDialogSpecProvider, factory: any NewTabDaxDialogProvider) { dismissHostingController(didFinishNTPOnboarding: false) diff --git a/DuckDuckGo/NewTabPageViewModel.swift b/DuckDuckGo/NewTabPageViewModel.swift index b53a7625ea..6cb387b402 100644 --- a/DuckDuckGo/NewTabPageViewModel.swift +++ b/DuckDuckGo/NewTabPageViewModel.swift @@ -26,6 +26,8 @@ final class NewTabPageViewModel: ObservableObject { @Published private(set) var isOnboarding: Bool @Published var isShowingSettings: Bool + private(set) var isDragging: Bool = false + private var introDataStorage: NewTabPageIntroDataStoring private let pixelFiring: PixelFiring.Type @@ -67,4 +69,12 @@ final class NewTabPageViewModel: ObservableObject { func finishOnboarding() { isOnboarding = false } + + func beginDragging() { + isDragging = true + } + + func endDragging() { + isDragging = false + } } diff --git a/DuckDuckGo/PrivacyInfo.xcprivacy b/DuckDuckGo/PrivacyInfo.xcprivacy index 15d10330da..98e99b1d0f 100644 --- a/DuckDuckGo/PrivacyInfo.xcprivacy +++ b/DuckDuckGo/PrivacyInfo.xcprivacy @@ -22,7 +22,7 @@ NSPrivacyAccessedAPICategoryUserDefaults NSPrivacyAccessedAPITypeReasons - CA92.1 + 1C8F.1 diff --git a/DuckDuckGo/RootDebugViewController.swift b/DuckDuckGo/RootDebugViewController.swift index 23500ebf8c..08dea4d8ae 100644 --- a/DuckDuckGo/RootDebugViewController.swift +++ b/DuckDuckGo/RootDebugViewController.swift @@ -50,6 +50,7 @@ class RootDebugViewController: UITableViewController { case resetDuckPlayerExperiment = 678 case overrideDuckPlayerExperiment = 679 case overrideDuckPlayerExperimentControl = 680 + case resetTipKit = 681 } @IBOutlet weak var shareButton: UIBarButtonItem! @@ -64,6 +65,7 @@ class RootDebugViewController: UITableViewController { private var sync: DDGSyncing? private var internalUserDecider: DefaultInternalUserDecider? var tabManager: TabManager? + private var tipKitUIActionHandler: TipKitDebugOptionsUIActionHandling? @UserDefaultsWrapper(key: .lastConfigurationRefreshDate, defaultValue: .distantPast) private var lastConfigurationRefreshDate: Date @@ -72,24 +74,29 @@ class RootDebugViewController: UITableViewController { sync: DDGSyncing, bookmarksDatabase: CoreDataDatabase, internalUserDecider: InternalUserDecider, - tabManager: TabManager) { + tabManager: TabManager, + tipKitUIActionHandler: TipKitDebugOptionsUIActionHandling = TipKitDebugOptionsUIActionHandler()) { self.sync = sync self.bookmarksDatabase = bookmarksDatabase self.internalUserDecider = internalUserDecider as? DefaultInternalUserDecider self.tabManager = tabManager + self.tipKitUIActionHandler = tipKitUIActionHandler + + super.init(coder: coder) + } + + required init?(coder: NSCoder) { super.init(coder: coder) } - - func configure(sync: DDGSyncing, bookmarksDatabase: CoreDataDatabase, internalUserDecider: InternalUserDecider, tabManager: TabManager) { + + func configure(sync: DDGSyncing, bookmarksDatabase: CoreDataDatabase, internalUserDecider: InternalUserDecider, tabManager: TabManager, tipKitUIActionHandler: TipKitDebugOptionsUIActionHandling = TipKitDebugOptionsUIActionHandler()) { + self.sync = sync self.bookmarksDatabase = bookmarksDatabase self.internalUserDecider = internalUserDecider as? DefaultInternalUserDecider self.tabManager = tabManager - } - - required init?(coder: NSCoder) { - super.init(coder: coder) + self.tipKitUIActionHandler = tipKitUIActionHandler } @IBSegueAction func onCreateImageCacheDebugScreen(_ coder: NSCoder) -> ImageCacheDebugViewController? { @@ -187,6 +194,8 @@ class RootDebugViewController: UITableViewController { case .resetDuckPlayerExperiment: DuckPlayerLaunchExperiment().cleanup() ActionMessageView.present(message: "Experiment Settings deleted. You'll be assigned a random cohort") + case .resetTipKit: + tipKitUIActionHandler?.resetTipKitTapped() case .overrideDuckPlayerExperiment: DuckPlayerLaunchExperiment().override() ActionMessageView.present(message: "Overriding experiment. You are now in the 'experiment' group. Restart the app to complete") diff --git a/DuckDuckGo/Settings.bundle/Root.plist b/DuckDuckGo/Settings.bundle/Root.plist index 3d6a4bec95..a7f84961db 100644 --- a/DuckDuckGo/Settings.bundle/Root.plist +++ b/DuckDuckGo/Settings.bundle/Root.plist @@ -6,7 +6,7 @@ DefaultValue - 7.141.0 + 7.143.0 Key version Title diff --git a/DuckDuckGo/SimpleNewTabPageView.swift b/DuckDuckGo/SimpleNewTabPageView.swift new file mode 100644 index 0000000000..cf31697226 --- /dev/null +++ b/DuckDuckGo/SimpleNewTabPageView.swift @@ -0,0 +1,220 @@ +// +// SimpleNewTabPageView.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 DuckUI +import RemoteMessaging + +struct SimpleNewTabPageView: View { + @Environment(\.horizontalSizeClass) var horizontalSizeClass + + @ObservedObject private var viewModel: NewTabPageViewModel + @ObservedObject private var messagesModel: NewTabPageMessagesModel + @ObservedObject private var favoritesViewModel: FavoritesViewModel + + init(viewModel: NewTabPageViewModel, + messagesModel: NewTabPageMessagesModel, + favoritesViewModel: FavoritesViewModel) { + self.viewModel = viewModel + self.messagesModel = messagesModel + self.favoritesViewModel = favoritesViewModel + + self.messagesModel.load() + } + + private var isShowingSections: Bool { + !favoritesViewModel.allFavorites.isEmpty + } + + var body: some View { + if !viewModel.isOnboarding { + mainView + .background(Color(designSystemColor: .background)) + .simultaneousGesture( + DragGesture() + .onChanged({ value in + if value.translation.height > 0 { + viewModel.beginDragging() + } + }) + .onEnded({ _ in viewModel.endDragging() }) + ) + } + } + + @ViewBuilder + private var mainView: some View { + if isShowingSections { + sectionsView + } else { + emptyStateView + } + } +} + +private extension SimpleNewTabPageView { + // MARK: - Views + @ViewBuilder + private var sectionsView: some View { + GeometryReader { proxy in + ScrollView { + VStack(spacing: Metrics.sectionSpacing) { + + messagesSectionView + .padding(.top, Metrics.nonGridSectionTopPadding) + + favoritesSectionView(proxy: proxy) + } + .padding(Metrics.largePadding) + } + .withScrollKeyboardDismiss() + } + } + + @ViewBuilder + private var emptyStateView: some View { + ZStack { + NewTabPageDaxLogoView() + + VStack(spacing: Metrics.sectionSpacing) { + messagesSectionView + .padding(.top, Metrics.nonGridSectionTopPadding) + .frame(maxHeight: .infinity, alignment: .top) + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + } + .padding(Metrics.largePadding) + } + + private var messagesSectionView: some View { + ForEach(messagesModel.homeMessageViewModels, id: \.messageId) { messageModel in + HomeMessageView(viewModel: messageModel) + .frame(maxWidth: horizontalSizeClass == .regular ? Metrics.messageMaximumWidthPad : Metrics.messageMaximumWidth) + .transition(.scale.combined(with: .opacity)) + } + } + + private func favoritesSectionView(proxy: GeometryProxy) -> some View { + FavoritesView(model: favoritesViewModel, + isAddingFavorite: .constant(false), + geometry: proxy) + } +} + +private extension View { + @ViewBuilder + func withScrollKeyboardDismiss() -> some View { + if #available(iOS 16, *) { + scrollDismissesKeyboard(.immediately) + } else { + self + } + } +} + +private struct Metrics { + + static let regularPadding = 16.0 + static let largePadding = 24.0 + static let sectionSpacing = 32.0 + static let nonGridSectionTopPadding = -8.0 + + static let messageMaximumWidth: CGFloat = 380 + static let messageMaximumWidthPad: CGFloat = 455 +} + +// MARK: - Preview + +#Preview("Regular") { + SimpleNewTabPageView( + viewModel: NewTabPageViewModel(), + messagesModel: NewTabPageMessagesModel( + homePageMessagesConfiguration: PreviewMessagesConfiguration( + homeMessages: [] + ) + ), + favoritesViewModel: FavoritesPreviewModel() + ) +} + +#Preview("With message") { + SimpleNewTabPageView( + viewModel: NewTabPageViewModel(), + messagesModel: NewTabPageMessagesModel( + homePageMessagesConfiguration: PreviewMessagesConfiguration( + homeMessages: [ + HomeMessage.remoteMessage( + remoteMessage: RemoteMessageModel( + id: "0", + content: .small(titleText: "Title", descriptionText: "Description"), + matchingRules: [], + exclusionRules: [], + isMetricsEnabled: false + ) + ) + ] + ) + ), + favoritesViewModel: FavoritesPreviewModel() + ) +} + +#Preview("No favorites") { + SimpleNewTabPageView( + viewModel: NewTabPageViewModel(), + messagesModel: NewTabPageMessagesModel( + homePageMessagesConfiguration: PreviewMessagesConfiguration( + homeMessages: [] + ) + ), + favoritesViewModel: FavoritesPreviewModel(favorites: []) + ) +} + +#Preview("Empty") { + SimpleNewTabPageView( + viewModel: NewTabPageViewModel(), + messagesModel: NewTabPageMessagesModel( + homePageMessagesConfiguration: PreviewMessagesConfiguration( + homeMessages: [] + ) + ), + favoritesViewModel: FavoritesPreviewModel() + ) +} + +private final class PreviewMessagesConfiguration: HomePageMessagesConfiguration { + private(set) var homeMessages: [HomeMessage] + + init(homeMessages: [HomeMessage]) { + self.homeMessages = homeMessages + } + + func refresh() { + + } + + func didAppear(_ homeMessage: HomeMessage) { + // no-op + } + + func dismissHomeMessage(_ homeMessage: HomeMessage) { + homeMessages = homeMessages.dropLast() + } +} diff --git a/DuckDuckGo/Subscription/SubscriptionCookieManageEventPixelMapping.swift b/DuckDuckGo/Subscription/SubscriptionCookieManageEventPixelMapping.swift new file mode 100644 index 0000000000..19efdba9dd --- /dev/null +++ b/DuckDuckGo/Subscription/SubscriptionCookieManageEventPixelMapping.swift @@ -0,0 +1,52 @@ +// +// SubscriptionCookieManageEventPixelMapping.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 Common +import Core +import Subscription + +public final class SubscriptionCookieManageEventPixelMapping: EventMapping { + + public init() { + super.init { event, _, _, _ in + let pixel: Pixel.Event = { + switch event { + case .errorHandlingAccountDidSignInTokenIsMissing: + return .privacyProSubscriptionCookieMissingTokenOnSignIn + case .errorHandlingAccountDidSignOutCookieIsMissing: + return .privacyProSubscriptionCookieMissingCookieOnSignOut + case .subscriptionCookieRefreshedWithUpdate: + return .privacyProSubscriptionCookieRefreshedWithUpdate + case .subscriptionCookieRefreshedWithDelete: + return .privacyProSubscriptionCookieRefreshedWithDelete + case .failedToSetSubscriptionCookie: + return .privacyProSubscriptionCookieFailedToSetSubscriptionCookie + } + }() + + Pixel.fire(pixel: pixel) + + } + } + + override init(mapping: @escaping EventMapping.Mapping) { + fatalError("Use init()") + } +} diff --git a/DuckDuckGo/SwipeTabsCoordinator.swift b/DuckDuckGo/SwipeTabsCoordinator.swift index ea883d3dd7..8071d83ec9 100644 --- a/DuckDuckGo/SwipeTabsCoordinator.swift +++ b/DuckDuckGo/SwipeTabsCoordinator.swift @@ -314,8 +314,7 @@ extension SwipeTabsCoordinator: UICollectionViewDataSource { cell.omniBar = omniBar cell.omniBar?.translatesAutoresizingMaskIntoConstraints = false - cell.updateConstraints() - + cell.omniBar?.showSeparator() if self.appSettings.currentAddressBarPosition.isBottom { cell.omniBar?.moveSeparatorToTop() @@ -330,7 +329,9 @@ extension SwipeTabsCoordinator: UICollectionViewDataSource { } } - + + cell.setNeedsUpdateConstraints() + return cell } @@ -343,7 +344,7 @@ class OmniBarCell: UICollectionViewCell { subviews.forEach { $0.removeFromSuperview() } if let omniBar { addSubview(omniBar) - + NSLayoutConstraint.activate([ constrainView(omniBar, by: .leadingMargin), constrainView(omniBar, by: .trailingMargin), @@ -354,14 +355,14 @@ class OmniBarCell: UICollectionViewCell { } } } - + override func updateConstraints() { - super.updateConstraints() let left = superview?.safeAreaInsets.left ?? 0 let right = superview?.safeAreaInsets.right ?? 0 omniBar?.updateOmniBarPadding(left: left, right: right) + + super.updateConstraints() } - } extension TabsModel { diff --git a/DuckDuckGo/TabManager.swift b/DuckDuckGo/TabManager.swift index 24077c9454..ba67adae9b 100644 --- a/DuckDuckGo/TabManager.swift +++ b/DuckDuckGo/TabManager.swift @@ -24,6 +24,7 @@ import WebKit import BrowserServicesKit import Persistence import History +import Subscription import os.log class TabManager { @@ -41,6 +42,7 @@ class TabManager { private let contextualOnboardingPresenter: ContextualOnboardingPresenting private let contextualOnboardingLogic: ContextualOnboardingLogic private let onboardingPixelReporter: OnboardingPixelReporting + private let subscriptionCookieManager: SubscriptionCookieManaging weak var delegate: TabDelegate? @@ -57,7 +59,8 @@ class TabManager { privacyProDataReporter: PrivacyProDataReporting, contextualOnboardingPresenter: ContextualOnboardingPresenting, contextualOnboardingLogic: ContextualOnboardingLogic, - onboardingPixelReporter: OnboardingPixelReporting) { + onboardingPixelReporter: OnboardingPixelReporting, + subscriptionCookieManager: SubscriptionCookieManaging) { self.model = model self.previewsSource = previewsSource self.bookmarksDatabase = bookmarksDatabase @@ -68,6 +71,7 @@ class TabManager { self.contextualOnboardingPresenter = contextualOnboardingPresenter self.contextualOnboardingLogic = contextualOnboardingLogic self.onboardingPixelReporter = onboardingPixelReporter + self.subscriptionCookieManager = subscriptionCookieManager registerForNotifications() } @@ -89,7 +93,8 @@ class TabManager { contextualOnboardingPresenter: contextualOnboardingPresenter, contextualOnboardingLogic: contextualOnboardingLogic, onboardingPixelReporter: onboardingPixelReporter, - featureFlagger: AppDependencyProvider.shared.featureFlagger) + featureFlagger: AppDependencyProvider.shared.featureFlagger, + subscriptionCookieManager: subscriptionCookieManager) controller.applyInheritedAttribution(inheritedAttribution) controller.attachWebView(configuration: configuration, andLoadRequest: url == nil ? nil : URLRequest.userInitiated(url!), @@ -167,7 +172,8 @@ class TabManager { contextualOnboardingPresenter: contextualOnboardingPresenter, contextualOnboardingLogic: contextualOnboardingLogic, onboardingPixelReporter: onboardingPixelReporter, - featureFlagger: AppDependencyProvider.shared.featureFlagger) + featureFlagger: AppDependencyProvider.shared.featureFlagger, + subscriptionCookieManager: subscriptionCookieManager) controller.attachWebView(configuration: configCopy, andLoadRequest: request, consumeCookies: !model.hasActiveTabs, diff --git a/DuckDuckGo/TabSwitcherTransition.swift b/DuckDuckGo/TabSwitcherTransition.swift index 5aebcad997..ea098cd149 100644 --- a/DuckDuckGo/TabSwitcherTransition.swift +++ b/DuckDuckGo/TabSwitcherTransition.swift @@ -86,7 +86,7 @@ class TabSwitcherTransitionDelegate: NSObject, UIViewControllerTransitioningDele return nil } - if mainVC.homeController != nil { + if mainVC.newTabPageViewController != nil { return FromHomeScreenTransition(mainViewController: mainVC, tabSwitcherViewController: tabSwitcherVC) } diff --git a/DuckDuckGo/TabViewController.swift b/DuckDuckGo/TabViewController.swift index 772bc1e760..7f05eb30ba 100644 --- a/DuckDuckGo/TabViewController.swift +++ b/DuckDuckGo/TabViewController.swift @@ -41,6 +41,7 @@ import NetworkProtection import Onboarding import os.log import Navigation +import Subscription class TabViewController: UIViewController { @@ -92,6 +93,7 @@ class TabViewController: UIViewController { let appSettings: AppSettings var featureFlagger: FeatureFlagger + let subscriptionCookieManager: SubscriptionCookieManaging private lazy var internalUserDecider = AppDependencyProvider.shared.internalUserDecider private lazy var autofillNeverPromptWebsitesManager = AppDependencyProvider.shared.autofillNeverPromptWebsitesManager @@ -321,7 +323,8 @@ class TabViewController: UIViewController { contextualOnboardingLogic: ContextualOnboardingLogic, onboardingPixelReporter: OnboardingCustomInteractionPixelReporting, urlCredentialCreator: URLCredentialCreating = URLCredentialCreator(), - featureFlagger: FeatureFlagger) -> TabViewController { + featureFlagger: FeatureFlagger, + subscriptionCookieManager: SubscriptionCookieManaging) -> TabViewController { let storyboard = UIStoryboard(name: "Tab", bundle: nil) let controller = storyboard.instantiateViewController(identifier: "TabViewController", creator: { coder in TabViewController(coder: coder, @@ -336,7 +339,8 @@ class TabViewController: UIViewController { contextualOnboardingLogic: contextualOnboardingLogic, onboardingPixelReporter: onboardingPixelReporter, urlCredentialCreator: urlCredentialCreator, - featureFlagger: featureFlagger + featureFlagger: featureFlagger, + subscriptionCookieManager: subscriptionCookieManager ) }) return controller @@ -369,7 +373,8 @@ class TabViewController: UIViewController { contextualOnboardingLogic: ContextualOnboardingLogic, onboardingPixelReporter: OnboardingCustomInteractionPixelReporting, urlCredentialCreator: URLCredentialCreating = URLCredentialCreator(), - featureFlagger: FeatureFlagger) { + featureFlagger: FeatureFlagger, + subscriptionCookieManager: SubscriptionCookieManaging) { self.tabModel = tabModel self.appSettings = appSettings self.bookmarksDatabase = bookmarksDatabase @@ -388,6 +393,7 @@ class TabViewController: UIViewController { self.onboardingPixelReporter = onboardingPixelReporter self.urlCredentialCreator = urlCredentialCreator self.featureFlagger = featureFlagger + self.subscriptionCookieManager = subscriptionCookieManager super.init(coder: aDecoder) } @@ -636,6 +642,8 @@ class TabViewController: UIViewController { await webView.configuration.websiteDataStore.dataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes()) let cookieStore = webView.configuration.websiteDataStore.httpCookieStore await WebCacheManager.shared.consumeCookies(httpCookieStore: cookieStore) + subscriptionCookieManager.resetLastRefreshDate() + await subscriptionCookieManager.refreshSubscriptionCookie() doLoad() } } @@ -1325,6 +1333,7 @@ extension TabViewController: WKNavigationDelegate { let mimeType = MIMEType(from: navigationResponse.response.mimeType, fileExtension: navigationResponse.response.url?.pathExtension) let urlSchemeType = navigationResponse.response.url.map { SchemeHandler.schemeType(for: $0) } ?? .unknown + let urlNavigationalScheme = navigationResponse.response.url?.scheme.map { URL.NavigationalScheme(rawValue: $0) } let httpResponse = navigationResponse.response as? HTTPURLResponse let isSuccessfulResponse = httpResponse?.isSuccessfulResponse ?? false @@ -1355,7 +1364,13 @@ extension TabViewController: WKNavigationDelegate { withAdditionalParameters: [PixelParameters.canAutoPreviewMIMEType: "1"]) } else if shouldTriggerDownloadAction(for: navigationResponse), let downloadMetadata = AppDependencyProvider.shared.downloadManager.downloadMetaData(for: navigationResponse.response) { - // 3. We know the response should trigger the file download prompt + // 3a. We know it is a download, but allow WebKit handle the "data" scheme natively + if urlNavigationalScheme == .data { + decisionHandler(.download) + return + } + + // 3b. We know the response should trigger the file download prompt self.presentSaveToDownloadsAlert(with: downloadMetadata) { self.startDownload(with: navigationResponse, decisionHandler: decisionHandler) } cancelHandler: { @@ -1699,19 +1714,6 @@ extension TabViewController: WKNavigationDelegate { decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { - if #available(iOS 17.4, *), - navigationAction.request.url?.scheme == "marketplace-kit", - internalUserDecider.isInternalUser { - - decisionHandler(.allow) - let urlString = navigationAction.request.url?.absoluteString ?? "" - ActionMessageView.present(message: "Marketplace Kit URL detected", - actionTitle: "COPY", - presentationLocation: .withoutBottomBar, onAction: { - UIPasteboard.general.string = urlString - }) - return - } if let url = navigationAction.request.url { if !tabURLInterceptor.allowsNavigatingTo(url: url) { @@ -1902,6 +1904,10 @@ extension TabViewController: WKNavigationDelegate { let schemeType = SchemeHandler.schemeType(for: url) self.blobDownloadTargetFrame = nil switch schemeType { + case .allow: + completion(.allow) + return + case .navigational: performNavigationFor(url: url, navigationAction: navigationAction, @@ -2065,6 +2071,7 @@ extension TabViewController: WKNavigationDelegate { if !(error.failedUrl?.isCustomURLScheme() ?? false) { url = error.failedUrl showError(message: error.localizedDescription) + Pixel.fire(pixel: .webViewErrorPageShown) } webpageDidFailToLoad() @@ -2180,7 +2187,7 @@ extension TabViewController { return } - if self.shouldTriggerDownloadAction(for: navigationResponse) { + if self.shouldTriggerDownloadAction(for: navigationResponse) && !FilePreviewHelper.canAutoPreviewMIMEType(downloadMetadata.mimeType) { // Show alert to the file download self.presentSaveToDownloadsAlert(with: downloadMetadata) { callback(self.transfer(download, diff --git a/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift b/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift index 12ef1e53ee..8e8d715052 100644 --- a/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift +++ b/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift @@ -84,12 +84,14 @@ extension TabViewController { entries.append(self.buildToggleProtectionEntry(forDomain: domain)) } - let name = UserText.actionReportBrokenSite - entries.append(BrowsingMenuEntry.regular(name: name, - image: UIImage(named: "Feedback-16")!, - action: { [weak self] in - self?.onReportBrokenSiteAction() - })) + if link != nil { + let name = UserText.actionReportBrokenSite + entries.append(BrowsingMenuEntry.regular(name: name, + image: UIImage(named: "Feedback-16")!, + action: { [weak self] in + self?.onReportBrokenSiteAction() + })) + } entries.append(.separator) diff --git a/DuckDuckGo/TabViewControllerLongPressMenuExtension.swift b/DuckDuckGo/TabViewControllerLongPressMenuExtension.swift index 38770be4e7..8de6d3de6c 100644 --- a/DuckDuckGo/TabViewControllerLongPressMenuExtension.swift +++ b/DuckDuckGo/TabViewControllerLongPressMenuExtension.swift @@ -111,7 +111,8 @@ extension TabViewController { contextualOnboardingPresenter: contextualOnboardingPresenter, contextualOnboardingLogic: contextualOnboardingLogic, onboardingPixelReporter: onboardingPixelReporter, - featureFlagger: AppDependencyProvider.shared.featureFlagger) + featureFlagger: AppDependencyProvider.shared.featureFlagger, + subscriptionCookieManager: subscriptionCookieManager) tabController.isLinkPreview = true let configuration = WKWebViewConfiguration.nonPersistent() tabController.attachWebView(configuration: configuration, andLoadRequest: URLRequest.userInitiated(url), consumeCookies: false) diff --git a/DuckDuckGo/TipKit/Logger+TipKit.swift b/DuckDuckGo/TipKit/Logger+TipKit.swift new file mode 100644 index 0000000000..1d791692b4 --- /dev/null +++ b/DuckDuckGo/TipKit/Logger+TipKit.swift @@ -0,0 +1,28 @@ +// +// Logger+TipKit.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 os.log + +extension Logger { + + static var tipKit: Logger = { + Logger(subsystem: Bundle.main.bundleIdentifier ?? "DuckDuckGo", category: "TipKit") + }() +} diff --git a/DuckDuckGo/TipKit/TipKitAppEventHandling.swift b/DuckDuckGo/TipKit/TipKitAppEventHandling.swift new file mode 100644 index 0000000000..572427579a --- /dev/null +++ b/DuckDuckGo/TipKit/TipKitAppEventHandling.swift @@ -0,0 +1,50 @@ +// +// TipKitAppEventHandling.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 Core +import Foundation +import os.log + +protocol TipKitAppEventHandling { + func appDidFinishLaunching() +} + +struct TipKitAppEventHandler: TipKitAppEventHandling { + + private let controller: TipKitController + private let logger: Logger + + init(controller: TipKitController = .make(), + logger: Logger = .tipKit) { + + self.controller = controller + self.logger = logger + } + + func appDidFinishLaunching() { + if #available(iOS 17.0, *) { + controller.configureTipKit([ + .displayFrequency(.immediate), + .datastoreLocation(.applicationDefault) + ]) + } else { + logger.log("TipKit initialization skipped: iOS 17.0 or later is required.") + } + } +} diff --git a/DuckDuckGo/StubAutofillLoginImportStateProvider.swift b/DuckDuckGo/TipKit/TipKitController+ConvenienceInitializers.swift similarity index 53% rename from DuckDuckGo/StubAutofillLoginImportStateProvider.swift rename to DuckDuckGo/TipKit/TipKitController+ConvenienceInitializers.swift index 2b9c6382ab..8b39a4ea95 100644 --- a/DuckDuckGo/StubAutofillLoginImportStateProvider.swift +++ b/DuckDuckGo/TipKit/TipKitController+ConvenienceInitializers.swift @@ -1,5 +1,5 @@ // -// StubAutofillLoginImportStateProvider.swift +// TipKitController+ConvenienceInitializers.swift // DuckDuckGo // // Copyright © 2024 DuckDuckGo. All rights reserved. @@ -18,18 +18,13 @@ // import Foundation -import BrowserServicesKit +import os -struct StubAutofillLoginImportStateProvider: AutofillLoginImportStateProvider { - public var isNewDDGUser: Bool = false - public var hasImportedLogins: Bool = false - var credentialsImportPromptPresentationCount: Int = 0 +extension TipKitController { - var isAutofillEnabled: Bool { - AppDependencyProvider.shared.appSettings.autofillCredentialsEnabled - } + static func make(logger: Logger = .tipKit, + userDefaults: UserDefaults = .standard) -> Self { - func hasNeverPromptWebsitesFor(_ domain: String) -> Bool { - AppDependencyProvider.shared.autofillNeverPromptWebsitesManager.hasNeverPromptWebsitesFor(domain: domain) + self.init(logger: logger, userDefaults: userDefaults) } } diff --git a/DuckDuckGo/TipKit/TipKitController.swift b/DuckDuckGo/TipKit/TipKitController.swift new file mode 100644 index 0000000000..c91a0b534f --- /dev/null +++ b/DuckDuckGo/TipKit/TipKitController.swift @@ -0,0 +1,93 @@ +// +// TipKitController.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 os.log +import TipKit + +protocol TipKitControlling { + @available(iOS 17.0, *) + func configureTipKit() + + @available(iOS 17.0, *) + func resetTipKitOnNextAppLaunch() +} + +final class TipKitController { + + private let logger: Logger + private let userDefaults: UserDefaults + + private var resetTipKitOnNextLaunch: Bool { + get { + userDefaults.bool(forKey: "resetTipKitOnNextLaunch") + } + + set { + userDefaults.set(newValue, forKey: "resetTipKitOnNextLaunch") + } + } + + init(logger: Logger, + userDefaults: UserDefaults) { + + self.logger = logger + self.userDefaults = userDefaults + } + + @available(iOS 17.0, macOS 14.0, *) + func configureTipKit(_ configuration: [Tips.ConfigurationOption] = []) { + do { + if resetTipKitOnNextLaunch { + resetTipKit() + resetTipKitOnNextLaunch = false + } + + try Tips.configure(configuration) + + logger.debug("TipKit initialized") + } catch { + logger.error("Failed to initialize TipKit: \(error)") + } + } + + @available(iOS 17.0, macOS 14.0, *) + private func resetTipKit() { + do { + try Tips.resetDatastore() + + logger.debug("TipKit reset") + } catch { + logger.debug("Failed to reset TipKit: \(error)") + } + } + + /// Resets TipKit + /// + /// One thing that's not documented as of 2024-10-09 is that resetting TipKit must happen before it's configured. + /// When trying to reset it after it's configured we get `TipKit.TipKitError(value: TipKit.TipKitError.Value.tipsDatastoreAlreadyConfigured)`. + /// In order to make things work for us we set a user defaults value that ensures TipKit will be reset on next + /// app launch instead of directly trying to reset it here. + /// + @available(iOS 17.0, *) + func resetTipKitOnNextAppLaunch() { + resetTipKitOnNextLaunch = true + logger.debug("TipKit will reset on next app launch") + } +} diff --git a/DuckDuckGo/TipKit/TipKitDebugOptionsUIActionHandling.swift b/DuckDuckGo/TipKit/TipKitDebugOptionsUIActionHandling.swift new file mode 100644 index 0000000000..1a12054a98 --- /dev/null +++ b/DuckDuckGo/TipKit/TipKitDebugOptionsUIActionHandling.swift @@ -0,0 +1,49 @@ +// +// TipKitDebugOptionsUIActionHandling.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 os.log + +protocol TipKitDebugOptionsUIActionHandling { + /// Resets TipKit + func resetTipKitTapped() +} + +struct TipKitDebugOptionsUIActionHandler: TipKitDebugOptionsUIActionHandling { + + private let controller: TipKitController + private let logger: Logger + + init(controller: TipKitController = .make(), + logger: Logger = .tipKit) { + + self.controller = controller + self.logger = logger + } + + func resetTipKitTapped() { + if #available(iOS 17.0, *) { + controller.resetTipKitOnNextAppLaunch() + + ActionMessageView.present(message: "TipKit will reset on next app launch.") + } else { + logger.log("TipKit initialization skipped: iOS 17.0 or later is required.") + } + } +} diff --git a/DuckDuckGo/UserScripts.swift b/DuckDuckGo/UserScripts.swift index 30904c6324..4b003e0a57 100644 --- a/DuckDuckGo/UserScripts.swift +++ b/DuckDuckGo/UserScripts.swift @@ -56,7 +56,7 @@ final class UserScripts: UserScriptsProvider { init(with sourceProvider: ScriptSourceProviding) { contentBlockerUserScript = ContentBlockerRulesUserScript(configuration: sourceProvider.contentBlockerRulesConfig) surrogatesScript = SurrogatesUserScript(configuration: sourceProvider.surrogatesConfig) - autofillUserScript = AutofillUserScript(scriptSourceProvider: sourceProvider.autofillSourceProvider, loginImportStateProvider: StubAutofillLoginImportStateProvider()) + autofillUserScript = AutofillUserScript(scriptSourceProvider: sourceProvider.autofillSourceProvider) autofillUserScript.sessionKey = sourceProvider.contentScopeProperties.sessionKey loginFormDetectionScript = sourceProvider.loginDetectionEnabled ? LoginFormDetectionUserScript() : nil diff --git a/DuckDuckGo/VPN.xcassets/Contents.json b/DuckDuckGo/VPN.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/DuckDuckGo/VPN.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/VPN.xcassets/VPNAddWidgetTipIcon.imageset/Contents.json b/DuckDuckGo/VPN.xcassets/VPNAddWidgetTipIcon.imageset/Contents.json new file mode 100644 index 0000000000..1360a4b9fb --- /dev/null +++ b/DuckDuckGo/VPN.xcassets/VPNAddWidgetTipIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Widget-Add-32 2.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/VPN.xcassets/VPNAddWidgetTipIcon.imageset/Widget-Add-32 2.pdf b/DuckDuckGo/VPN.xcassets/VPNAddWidgetTipIcon.imageset/Widget-Add-32 2.pdf new file mode 100644 index 0000000000..be3e56356e Binary files /dev/null and b/DuckDuckGo/VPN.xcassets/VPNAddWidgetTipIcon.imageset/Widget-Add-32 2.pdf differ diff --git a/DuckDuckGo/VPN.xcassets/VPNChangeLocationTipIcon.imageset/Contents.json b/DuckDuckGo/VPN.xcassets/VPNChangeLocationTipIcon.imageset/Contents.json new file mode 100644 index 0000000000..cb612d2e7d --- /dev/null +++ b/DuckDuckGo/VPN.xcassets/VPNChangeLocationTipIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Location-32.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/VPN.xcassets/VPNChangeLocationTipIcon.imageset/Location-32.pdf b/DuckDuckGo/VPN.xcassets/VPNChangeLocationTipIcon.imageset/Location-32.pdf new file mode 100644 index 0000000000..25b89dda77 Binary files /dev/null and b/DuckDuckGo/VPN.xcassets/VPNChangeLocationTipIcon.imageset/Location-32.pdf differ diff --git a/DuckDuckGo/VPN.xcassets/VPNUseSnoozeTipIcon.imageset/Contents.json b/DuckDuckGo/VPN.xcassets/VPNUseSnoozeTipIcon.imageset/Contents.json new file mode 100644 index 0000000000..492a76420b --- /dev/null +++ b/DuckDuckGo/VPN.xcassets/VPNUseSnoozeTipIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Moon-Snooze-32.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/VPN.xcassets/VPNUseSnoozeTipIcon.imageset/Moon-Snooze-32.pdf b/DuckDuckGo/VPN.xcassets/VPNUseSnoozeTipIcon.imageset/Moon-Snooze-32.pdf new file mode 100644 index 0000000000..60ce9c4901 Binary files /dev/null and b/DuckDuckGo/VPN.xcassets/VPNUseSnoozeTipIcon.imageset/Moon-Snooze-32.pdf differ diff --git a/DuckDuckGo/VPNAddWidgetTip.swift b/DuckDuckGo/VPNAddWidgetTip.swift new file mode 100644 index 0000000000..fd6f652415 --- /dev/null +++ b/DuckDuckGo/VPNAddWidgetTip.swift @@ -0,0 +1,78 @@ +// +// VPNAddWidgetTip.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 TipKit + +/// A tip to suggest to the user that they add our VPN widget for quick access to the VPN +/// +struct VPNAddWidgetTip {} + +/// Necessary split to support older iOS versions. +/// +@available(iOS 17.0, *) +extension VPNAddWidgetTip: Tip { + + enum ActionIdentifiers: String { + case addWidget = "com.duckduckgo.vpn.tip.addWidget.action.addWidget" + } + + static let geolocationTipDismissedEvent = Tips.Event(id: "com.duckduckgo.vpn.tip.addWidget.geolocationTipDismissedEvent") + + static let snoozeTipDismissedEvent = Tips.Event(id: "com.duckduckgo.vpn.tip.addWidget.geolocationTipDismissedEvent") + + @Parameter(.transient) + static var vpnEnabled: Bool = false + + private static let vpnDisconnectedEvent = Tips.Event(id: "com.duckduckgo.vpn.tip.addWidget.vpnDisconnectedEvent") + + var id: String { + "com.duckduckgo.vpn.tip.addWidget" + } + + var title: Text { + Text("Add VPN Widget") + } + + var message: Text? { + Text("Turn the VPN on and off right from the Home Screen.") + } + + var image: Image? { + Image(.vpnAddWidgetTipIcon) + } + + var actions: [Action] { + [Action(id: ActionIdentifiers.addWidget.rawValue) { + Text("Add widget") + .foregroundStyle(Color(designSystemColor: .accent)) + }] + } + + var rules: [Rule] { + #Rule(Self.geolocationTipDismissedEvent) { + $0.donations.count > 0 + } + #Rule(Self.snoozeTipDismissedEvent) { + $0.donations.count > 0 + } + #Rule(Self.$vpnEnabled) { + $0 == false + } + } +} diff --git a/DuckDuckGo/VPNGeoswitchingTip.swift b/DuckDuckGo/VPNGeoswitchingTip.swift new file mode 100644 index 0000000000..e8110fa18c --- /dev/null +++ b/DuckDuckGo/VPNGeoswitchingTip.swift @@ -0,0 +1,58 @@ +// +// VPNGeoswitchingTip.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 TipKit + +/// A tip to suggest to the user to change their location using geo-switching +/// +struct VPNGeoswitchingTip {} + +@available(iOS 17.0, *) +extension VPNGeoswitchingTip: Tip { + + private static let vpnConnectedEvent = Tips.Event(id: "com.duckduckgo.vpn.tip.geoswitching.vpnConnectedEvent") + + var id: String { + "com.duckduckgo.vpn.tip.geoswitching" + } + + var title: Text { + Text("Change Your Location") + } + + var message: Text? { + Text("You can customize your VPN location by connecting to any of our servers worldwide.") + } + + var image: Image? { + Image(.vpnChangeLocationTipIcon) + } + + var rules: [Rule] { + #Rule(Self.vpnConnectedEvent) { + $0.donations.donatedWithin(.week).count > 0 + } + } + + static func donateVPNConnectedEvent() { + Task { + await vpnConnectedEvent.donate() + } + } +} diff --git a/DuckDuckGo/VPNSnoozeTip.swift b/DuckDuckGo/VPNSnoozeTip.swift new file mode 100644 index 0000000000..9baa7bacba --- /dev/null +++ b/DuckDuckGo/VPNSnoozeTip.swift @@ -0,0 +1,71 @@ +// +// VPNSnoozeTip.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 TipKit + +/// A tip to suggest to the user to use the snooze feature to momentarily disable the VPN +/// +struct VPNSnoozeTip {} + +/// Necessary split to support older iOS versions. +/// +@available(iOS 17.0, *) +extension VPNSnoozeTip: Tip { + + enum ActionIdentifiers: String { + case learnMore = "com.duckduckgo.vpn.tip.snooze.learnMoreId" + } + + static let geolocationTipDismissedEvent = Tips.Event(id: "com.duckduckgo.vpn.tip.snooze.geolocationTipDismissedEvent") + + @Parameter(.transient) + static var vpnEnabled: Bool = false + + var id: String { + "com.duckduckgo.vpn.tip.snooze" + } + + var title: Text { + Text("Avoid VPN Conflicts") + } + + var message: Text? { + Text("You can use sites or apps that block VPN traffic by snoozing the VPN connection.") + } + + var image: Image? { + Image(.vpnUseSnoozeTipIcon) + } + + var actions: [Action] { + [Action(id: ActionIdentifiers.learnMore.rawValue) { + Text("Learn more") + .foregroundStyle(Color(designSystemColor: .accent)) + }] + } + + var rules: [Rule] { + #Rule(Self.geolocationTipDismissedEvent) { + $0.donations.count > 0 + } + #Rule(Self.$vpnEnabled) { + $0 == true + } + } +} diff --git a/DuckDuckGo/ViewExtension.swift b/DuckDuckGo/ViewExtension.swift index 3c00ddf314..e322db6120 100644 --- a/DuckDuckGo/ViewExtension.swift +++ b/DuckDuckGo/ViewExtension.swift @@ -62,6 +62,12 @@ extension View { .applyBackground() } + /// Removes the grouped list style insets for a single row. + /// + func removeGroupedListStyleInsets() -> some View { + listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + } + @ViewBuilder func applyBackground() -> some View { hideScrollContentBackground() diff --git a/DuckDuckGoTests/DownloadManagerTests.swift b/DuckDuckGoTests/DownloadManagerTests.swift index 86367d7144..ec613dee5a 100644 --- a/DuckDuckGoTests/DownloadManagerTests.swift +++ b/DuckDuckGoTests/DownloadManagerTests.swift @@ -26,8 +26,6 @@ import WidgetKit class DownloadManagerTests: XCTestCase { private let downloadManagerTestsHelper = DownloadTestsHelper(downloadsDirectory: DownloadManager().downloadsDirectory) - var mockDependencyProvider: MockDependencyProvider! - override func tearDown() { super.tearDown() downloadManagerTestsHelper.deleteAllFiles() diff --git a/DuckDuckGoTests/FavoritesListInteractingAdapterTests.swift b/DuckDuckGoTests/FavoritesListInteractingAdapterTests.swift new file mode 100644 index 0000000000..dcdebd766c --- /dev/null +++ b/DuckDuckGoTests/FavoritesListInteractingAdapterTests.swift @@ -0,0 +1,91 @@ +// +// FavoritesListInteractingAdapterTests.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 +import Combine +import Bookmarks + +@testable import DuckDuckGo + +final class FavoritesListInteractingAdapterTests: XCTestCase { + + private var favoritesListInteracting: MockFavoritesListInteracting! + private var appSettings: AppSettingsMock! + + private var cancellables: Set = [] + + override func setUpWithError() throws { + favoritesListInteracting = MockFavoritesListInteracting() + appSettings = AppSettingsMock() + } + + override func tearDownWithError() throws { + cancellables.removeAll() + } + + func testPublishesUpdateWhenFavoritesDisplayModeChanges() { + let expectation = XCTestExpectation(description: #function) + let sut = createSUT() + + sut.externalUpdates.sink { + XCTAssertTrue(Thread.isMainThread) + expectation.fulfill() + } + .store(in: &cancellables) + + NotificationCenter.default.post(name: AppUserDefaults.Notifications.favoritesDisplayModeChange, object: nil) + + wait(for: [expectation], timeout: 0.1) + } + + func testPublishesUpdateOnExternalListUpdate() { + let expectation = XCTestExpectation(description: #function) + let publisher = PassthroughSubject() + favoritesListInteracting.externalUpdates = publisher.eraseToAnyPublisher() + + let sut = createSUT() + + sut.externalUpdates.sink { + XCTAssertTrue(Thread.isMainThread) + expectation.fulfill() + } + .store(in: &cancellables) + + publisher.send() + + wait(for: [expectation], timeout: 0.1) + } + + private func createSUT() -> FavoritesListInteractingAdapter { + return FavoritesListInteractingAdapter(favoritesListInteracting: favoritesListInteracting, appSettings: appSettings) + } +} + +private class FavoritesListInteractingMock: FavoritesListInteracting { + var favoritesDisplayMode: Bookmarks.FavoritesDisplayMode = .displayNative(.mobile) + var favorites: [Bookmarks.BookmarkEntity] = [] + func favorite(at index: Int) -> Bookmarks.BookmarkEntity? { + return nil + } + func removeFavorite(_ favorite: Bookmarks.BookmarkEntity) {} + func moveFavorite(_ favorite: Bookmarks.BookmarkEntity, fromIndex: Int, toIndex: Int) { } + var externalUpdates: AnyPublisher = PassthroughSubject().eraseToAnyPublisher() + var localUpdates: AnyPublisher = PassthroughSubject().eraseToAnyPublisher() + func reloadData() {} +} diff --git a/DuckDuckGoTests/MockDependencyProvider.swift b/DuckDuckGoTests/MockDependencyProvider.swift deleted file mode 100644 index 05e28803f7..0000000000 --- a/DuckDuckGoTests/MockDependencyProvider.swift +++ /dev/null @@ -1,92 +0,0 @@ -// -// MockDependencyProvider.swift -// DuckDuckGo -// -// Copyright © 2018 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 BrowserServicesKit -import DDGSync -import Subscription -import SubscriptionTestingUtilities -import NetworkProtection -import RemoteMessaging -@testable import DuckDuckGo - -class MockDependencyProvider: DependencyProvider { - var appSettings: AppSettings - var variantManager: VariantManager - var featureFlagger: FeatureFlagger - var internalUserDecider: InternalUserDecider - var storageCache: StorageCache - var downloadManager: DownloadManager - var autofillLoginSession: AutofillLoginSession - var autofillNeverPromptWebsitesManager: AutofillNeverPromptWebsitesManager - var configurationManager: ConfigurationManager - var configurationStore: ConfigurationStore - var userBehaviorMonitor: UserBehaviorMonitor - var subscriptionManager: SubscriptionManager - var accountManager: AccountManager - var vpnFeatureVisibility: DefaultNetworkProtectionVisibility - var networkProtectionKeychainTokenStore: NetworkProtectionKeychainTokenStore - var networkProtectionTunnelController: NetworkProtectionTunnelController - var connectionObserver: NetworkProtection.ConnectionStatusObserver - var serverInfoObserver: NetworkProtection.ConnectionServerInfoObserver - var vpnSettings: NetworkProtection.VPNSettings - var persistentPixel: PersistentPixelFiring - - init() { - let defaultProvider = AppDependencyProvider.makeTestingInstance() - appSettings = defaultProvider.appSettings - variantManager = defaultProvider.variantManager - featureFlagger = defaultProvider.featureFlagger - internalUserDecider = defaultProvider.internalUserDecider - storageCache = defaultProvider.storageCache - downloadManager = defaultProvider.downloadManager - autofillLoginSession = defaultProvider.autofillLoginSession - autofillNeverPromptWebsitesManager = defaultProvider.autofillNeverPromptWebsitesManager - configurationStore = defaultProvider.configurationStore - configurationManager = defaultProvider.configurationManager - userBehaviorMonitor = defaultProvider.userBehaviorMonitor - - accountManager = AccountManagerMock() - - let subscriptionService = DefaultSubscriptionEndpointService(currentServiceEnvironment: .production) - let authService = DefaultAuthEndpointService(currentServiceEnvironment: .production) - let storePurchaseManager = DefaultStorePurchaseManager() - subscriptionManager = SubscriptionManagerMock(accountManager: accountManager, - subscriptionEndpointService: subscriptionService, - authEndpointService: authService, - storePurchaseManager: storePurchaseManager, - currentEnvironment: SubscriptionEnvironment(serviceEnvironment: .production, - purchasePlatform: .appStore), - canPurchase: true) - - let accessTokenProvider: () -> String? = { { "sometoken" } }() - networkProtectionKeychainTokenStore = NetworkProtectionKeychainTokenStore(accessTokenProvider: accessTokenProvider) - networkProtectionTunnelController = NetworkProtectionTunnelController(accountManager: accountManager, - tokenStore: networkProtectionKeychainTokenStore, - persistentPixel: MockPersistentPixel()) - vpnFeatureVisibility = DefaultNetworkProtectionVisibility(userDefaults: .networkProtectionGroupDefaults, - accountManager: accountManager) - - connectionObserver = ConnectionStatusObserverThroughSession() - serverInfoObserver = ConnectionServerInfoObserverThroughSession() - vpnSettings = VPNSettings(defaults: .networkProtectionGroupDefaults) - persistentPixel = MockPersistentPixel() - } -} diff --git a/DuckDuckGoTests/MockTabDelegate.swift b/DuckDuckGoTests/MockTabDelegate.swift index 3cd9a7c74c..b37a63a332 100644 --- a/DuckDuckGoTests/MockTabDelegate.swift +++ b/DuckDuckGoTests/MockTabDelegate.swift @@ -24,6 +24,8 @@ import BrowserServicesKit import PrivacyDashboard import Core import Persistence +import Subscription +import SubscriptionTestingUtilities @testable import DuckDuckGo final class MockTabDelegate: TabDelegate { @@ -134,7 +136,8 @@ extension TabViewController { contextualOnboardingLogic: contextualOnboardingLogic, onboardingPixelReporter: contextualOnboardingPixelReporter, urlCredentialCreator: MockCredentialCreator(), - featureFlagger: featureFlagger + featureFlagger: featureFlagger, + subscriptionCookieManager: SubscriptionCookieManagerMock() ) tab.attachWebView(configuration: .nonPersistent(), andLoadRequest: nil, consumeCookies: false, customWebView: customWebView) return tab diff --git a/DuckDuckGoTests/HomeViewControllerDaxDialogTests.swift b/DuckDuckGoTests/NewTabPageControllerDaxDialogTests.swift similarity index 93% rename from DuckDuckGoTests/HomeViewControllerDaxDialogTests.swift rename to DuckDuckGoTests/NewTabPageControllerDaxDialogTests.swift index 66c9506142..440a2934df 100644 --- a/DuckDuckGoTests/HomeViewControllerDaxDialogTests.swift +++ b/DuckDuckGoTests/NewTabPageControllerDaxDialogTests.swift @@ -1,5 +1,5 @@ // -// HomeViewControllerDaxDialogTests.swift +// NewTabPageControllerDaxDialogTests.swift // DuckDuckGo // // Copyright © 2024 DuckDuckGo. All rights reserved. @@ -26,12 +26,12 @@ import SwiftUI import Persistence import BrowserServicesKit -final class HomeViewControllerDaxDialogTests: XCTestCase { +final class NewTabPageControllerDaxDialogTests: XCTestCase { var variantManager: CapturingVariantManager! var dialogFactory: CapturingNewTabDaxDialogProvider! var specProvider: MockNewTabDialogSpecProvider! - var hvc: HomeViewController! + var hvc: NewTabPageViewController! override func setUpWithError() throws { let db = CoreDataDatabase.bookmarksMock @@ -56,19 +56,18 @@ final class HomeViewControllerDaxDialogTests: XCTestCase { remoteMessagingAvailabilityProvider: MockRemoteMessagingAvailabilityProviding(), duckPlayerStorage: MockDuckPlayerStorage()) let homePageConfiguration = HomePageConfiguration(remoteMessagingClient: remoteMessagingClient, privacyProDataReporter: MockPrivacyProDataReporter()) - let dependencies = HomePageDependencies( - homePageConfiguration: homePageConfiguration, - model: Tab(), - favoritesViewModel: MockFavoritesListInteracting(), - appSettings: AppSettingsMock(), + hvc = NewTabPageViewController( + tab: Tab(), + isNewTabPageCustomizationEnabled: false, + interactionModel: MockFavoritesListInteracting(), syncService: MockDDGSyncing(authState: .active, isSyncInProgress: false), - syncDataProviders: dataProviders, - privacyProDataReporter: MockPrivacyProDataReporter(), + syncBookmarksAdapter: dataProviders.bookmarksAdapter, + homePageMessagesConfiguration: homePageConfiguration, variantManager: variantManager, newTabDialogFactory: dialogFactory, - newTabDialogTypeProvider: specProvider) - hvc = HomeViewController.loadFromStoryboard( - homePageDependecies: dependencies) + newTabDialogTypeProvider: specProvider, + faviconLoader: EmptyFaviconLoading() + ) let window = UIWindow(frame: UIScreen.main.bounds) window.rootViewController = UIViewController() diff --git a/DuckDuckGoTests/NewTabPageFavoritesModelTests.swift b/DuckDuckGoTests/NewTabPageFavoritesModelTests.swift index 09a02148ca..758ddde88e 100644 --- a/DuckDuckGoTests/NewTabPageFavoritesModelTests.swift +++ b/DuckDuckGoTests/NewTabPageFavoritesModelTests.swift @@ -50,6 +50,13 @@ final class NewTabPageFavoritesModelTests: XCTestCase { XCTAssertEqual(PixelFiringMock.lastPixelName, Pixel.Event.newTabPageFavoritesSeeLess.name) } + func testReturnsAllFavoritesWhenCustomizationDisabled() { + favoriteDataSource.favorites.append(contentsOf: Array(repeating: Favorite.stub(), count: 10)) + let sut = createSUT(isNewTabPageCustomizationEnabled: false) + + XCTAssertEqual(sut.prefixedFavorites(for: 1).items.count, 10) + } + func testFiresPixelsOnFavoriteSelected() { let sut = createSUT() @@ -99,6 +106,16 @@ final class NewTabPageFavoritesModelTests: XCTestCase { XCTAssertFalse(slice.isCollapsible) } + func testPrefixFavoritesDoesNotCreatePlaceholdersWhenCustomizationDisabled() { + let sut = createSUT(isNewTabPageCustomizationEnabled: false) + + let slice = sut.prefixedFavorites(for: 3) + + XCTAssertTrue(slice.items.filter(\.isPlaceholder).isEmpty) + XCTAssertTrue(slice.items.isEmpty) + XCTAssertFalse(slice.isCollapsible) + } + func testPrefixFavoritesLimitsToTwoRows() { favoriteDataSource.favorites.append(contentsOf: Array(repeating: Favorite.stub(), count: 10)) let sut = createSUT() @@ -109,6 +126,16 @@ final class NewTabPageFavoritesModelTests: XCTestCase { XCTAssertTrue(slice.isCollapsible) } + func testListNotCollapsibleWhenCustomizationDisabled() { + favoriteDataSource.favorites.append(contentsOf: Array(repeating: Favorite.stub(), count: 10)) + + let sut = createSUT(isNewTabPageCustomizationEnabled: false) + + let favorites = sut.prefixedFavorites(for: 1) + XCTAssertFalse(favorites.isCollapsible) + XCTAssertFalse(sut.isCollapsed) + } + func testAddItemIsLastWhenFavoritesPresent() throws { favoriteDataSource.favorites.append(contentsOf: Array(repeating: Favorite.stub(), count: 10)) let sut = createSUT() @@ -126,8 +153,19 @@ final class NewTabPageFavoritesModelTests: XCTestCase { XCTAssertTrue(firstItem == .addFavorite) } - private func createSUT() -> FavoritesViewModel { - FavoritesViewModel(favoriteDataSource: favoriteDataSource, + func testDoesNotAppendAddItemWhenCustomizationDisabled() { + let sut = createSUT(isNewTabPageCustomizationEnabled: false) + + XCTAssertNil(sut.allFavorites.first) + + favoriteDataSource.favorites.append(contentsOf: Array(repeating: Favorite.stub(), count: 10)) + + XCTAssertNil(sut.allFavorites.first(where: { $0 == .addFavorite })) + } + + private func createSUT(isNewTabPageCustomizationEnabled: Bool = true) -> FavoritesViewModel { + FavoritesViewModel(isNewTabPageCustomizationEnabled: isNewTabPageCustomizationEnabled, + favoriteDataSource: favoriteDataSource, faviconLoader: FavoritesFaviconLoader(), pixelFiring: PixelFiringMock.self, dailyPixelFiring: PixelFiringMock.self) diff --git a/DuckDuckGoTests/OnboardingDaxFavouritesTests.swift b/DuckDuckGoTests/OnboardingDaxFavouritesTests.swift index ad3c3be669..ff93a30198 100644 --- a/DuckDuckGoTests/OnboardingDaxFavouritesTests.swift +++ b/DuckDuckGoTests/OnboardingDaxFavouritesTests.swift @@ -26,6 +26,7 @@ import BrowserServicesKit import RemoteMessaging import Configuration import Core +import SubscriptionTestingUtilities @testable import DuckDuckGo final class OnboardingDaxFavouritesTests: XCTestCase { @@ -78,7 +79,8 @@ final class OnboardingDaxFavouritesTests: XCTestCase { contextualOnboardingPixelReporter: OnboardingPixelReporterMock(), tutorialSettings: tutorialSettingsMock, subscriptionFeatureAvailability: SubscriptionFeatureAvailabilityMock.enabled, - voiceSearchHelper: MockVoiceSearchHelper(isSpeechRecognizerAvailable: true, voiceSearchEnabled: true) + voiceSearchHelper: MockVoiceSearchHelper(isSpeechRecognizerAvailable: true, voiceSearchEnabled: true), + subscriptionCookieManager: SubscriptionCookieManagerMock() ) let window = UIWindow(frame: UIScreen.main.bounds) window.rootViewController = UIViewController() diff --git a/DuckDuckGoTests/OnboardingNavigationDelegateTests.swift b/DuckDuckGoTests/OnboardingNavigationDelegateTests.swift index ca052c5717..7ccd6d49b9 100644 --- a/DuckDuckGoTests/OnboardingNavigationDelegateTests.swift +++ b/DuckDuckGoTests/OnboardingNavigationDelegateTests.swift @@ -26,6 +26,7 @@ import BrowserServicesKit import RemoteMessaging import Configuration import Combine +import SubscriptionTestingUtilities @testable import DuckDuckGo @testable import Core @@ -76,7 +77,8 @@ final class OnboardingNavigationDelegateTests: XCTestCase { contextualOnboardingLogic: ContextualOnboardingLogicMock(), contextualOnboardingPixelReporter: onboardingPixelReporter, subscriptionFeatureAvailability: SubscriptionFeatureAvailabilityMock.enabled, - voiceSearchHelper: MockVoiceSearchHelper(isSpeechRecognizerAvailable: true, voiceSearchEnabled: true)) + voiceSearchHelper: MockVoiceSearchHelper(isSpeechRecognizerAvailable: true, voiceSearchEnabled: true), + subscriptionCookieManager: SubscriptionCookieManagerMock()) let window = UIWindow(frame: UIScreen.main.bounds) window.rootViewController = UIViewController() window.makeKeyAndVisible() diff --git a/DuckDuckGoTests/Subscription/SubscriptionContainerViewModelTests.swift b/DuckDuckGoTests/Subscription/SubscriptionContainerViewModelTests.swift index 49d41fd704..2d4979997f 100644 --- a/DuckDuckGoTests/Subscription/SubscriptionContainerViewModelTests.swift +++ b/DuckDuckGoTests/Subscription/SubscriptionContainerViewModelTests.swift @@ -24,7 +24,21 @@ import SubscriptionTestingUtilities final class SubscriptionContainerViewModelTests: XCTestCase { var sut: SubscriptionContainerViewModel! - let subscriptionManager = MockDependencyProvider().subscriptionManager + + let subscriptionManager: SubscriptionManager = { + let accountManager = AccountManagerMock() + let subscriptionService = SubscriptionEndpointServiceMock() + let authService = AuthEndpointServiceMock() + let storePurchaseManager = StorePurchaseManagerMock() + return SubscriptionManagerMock(accountManager: accountManager, + subscriptionEndpointService: subscriptionService, + authEndpointService: authService, + storePurchaseManager: storePurchaseManager, + currentEnvironment: SubscriptionEnvironment(serviceEnvironment: .production, + purchasePlatform: .appStore), + canPurchase: true) + }() + let subscriptionFeatureAvailability = SubscriptionFeatureAvailabilityMock.enabled func testWhenInitWithOriginThenSubscriptionFlowPurchaseURLHasOriginSet() { diff --git a/DuckDuckGoTests/Subscription/SubscriptionFlowViewModelTests.swift b/DuckDuckGoTests/Subscription/SubscriptionFlowViewModelTests.swift index 166657aa67..67235b541e 100644 --- a/DuckDuckGoTests/Subscription/SubscriptionFlowViewModelTests.swift +++ b/DuckDuckGoTests/Subscription/SubscriptionFlowViewModelTests.swift @@ -25,7 +25,20 @@ import SubscriptionTestingUtilities final class SubscriptionFlowViewModelTests: XCTestCase { private var sut: SubscriptionFlowViewModel! - let subscriptionManager = MockDependencyProvider().subscriptionManager + let subscriptionManager: SubscriptionManager = { + let accountManager = AccountManagerMock() + let subscriptionService = DefaultSubscriptionEndpointService(currentServiceEnvironment: .production) + let authService = DefaultAuthEndpointService(currentServiceEnvironment: .production) + let storePurchaseManager = DefaultStorePurchaseManager() + return SubscriptionManagerMock(accountManager: accountManager, + subscriptionEndpointService: subscriptionService, + authEndpointService: authService, + storePurchaseManager: storePurchaseManager, + currentEnvironment: SubscriptionEnvironment(serviceEnvironment: .production, + purchasePlatform: .appStore), + canPurchase: true) + }() + let subscriptionFeatureAvailability = SubscriptionFeatureAvailabilityMock.enabled func testWhenInitWithOriginThenSubscriptionFlowPurchaseURLHasOriginSet() { diff --git a/Gemfile b/Gemfile index d2fc002a76..610772ceae 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,3 @@ source 'https://rubygems.org' -gem 'fastlane', '2.222.0' +gem 'fastlane', '2.225.0' diff --git a/Gemfile.lock b/Gemfile.lock index a3d1c3461a..ca9c16ec9b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,20 +10,20 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.963.0) - aws-sdk-core (3.201.4) + aws-partitions (1.992.0) + aws-sdk-core (3.210.0) aws-eventstream (~> 1, >= 1.3.0) - aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.8) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.88.0) - aws-sdk-core (~> 3, >= 3.201.0) + aws-sdk-kms (1.95.0) + aws-sdk-core (~> 3, >= 3.210.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.157.0) - aws-sdk-core (~> 3, >= 3.201.0) + aws-sdk-s3 (1.169.0) + aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) - aws-sigv4 (1.9.1) + aws-sigv4 (1.10.0) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) base64 (0.2.0) @@ -38,8 +38,8 @@ GEM domain_name (0.6.20240107) dotenv (2.8.1) emoji_regex (3.2.3) - excon (0.111.0) - faraday (1.10.3) + excon (0.112.0) + faraday (1.10.4) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) @@ -65,10 +65,10 @@ GEM faraday-patron (1.0.0) faraday-rack (1.0.0) faraday-retry (1.0.3) - faraday_middleware (1.2.0) + faraday_middleware (1.2.1) faraday (~> 1.0) fastimage (2.3.1) - fastlane (2.222.0) + fastlane (2.225.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -84,6 +84,7 @@ GEM faraday-cookie_jar (~> 0.0.6) faraday_middleware (~> 1.0) fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.0.0) gh_inspector (>= 1.1.2, < 2.0.0) google-apis-androidpublisher_v3 (~> 0.3) google-apis-playcustomapp_v1 (~> 0.1) @@ -109,6 +110,8 @@ GEM xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) + fastlane-sirp (1.0.0) + sysrandom (~> 1.0) gh_inspector (1.1.3) google-apis-androidpublisher_v3 (0.54.0) google-apis-core (>= 0.11.0, < 2.a) @@ -147,12 +150,12 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.6) + http-cookie (1.0.7) domain_name (~> 0.5) httpclient (2.8.3) jmespath (1.6.2) json (2.7.2) - jwt (2.8.2) + jwt (2.9.3) base64 mini_magick (4.13.2) mini_mime (1.1.5) @@ -171,8 +174,7 @@ GEM trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.3.6) - strscan + rexml (3.3.8) rouge (2.0.7) ruby2_keywords (0.0.5) rubyzip (2.3.2) @@ -185,7 +187,7 @@ GEM simctl (1.6.10) CFPropertyList naturally - strscan (3.1.0) + sysrandom (1.0.5) terminal-notifier (2.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) @@ -195,15 +197,15 @@ GEM tty-spinner (0.9.3) tty-cursor (~> 0.7) uber (0.1.0) - unicode-display_width (2.5.0) + unicode-display_width (2.6.0) word_wrap (1.0.0) - xcodeproj (1.25.0) + xcodeproj (1.25.1) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) nanaimo (~> 0.3.0) - rexml (>= 3.3.2, < 4.0) + rexml (>= 3.3.6, < 4.0) xcpretty (0.3.0) rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.1) @@ -213,7 +215,7 @@ PLATFORMS ruby DEPENDENCIES - fastlane (= 2.222.0) + fastlane (= 2.225.0) BUNDLED WITH 2.3.26 diff --git a/fastlane/Fastfile b/fastlane/Fastfile index f18657fd83..0cadad0296 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -168,6 +168,32 @@ lane :alpha_adhoc do |options| end end + + +desc 'Promotes the latest TestFlight build to App Store without submitting for review' +lane :promote_latest_testflight_to_appstore do |options| + + app_identifier = options[:alpha] ? "com.duckduckgo.mobile.ios.alpha" : "com.duckduckgo.mobile.ios" + + latest_testflight_build_number( + api_key: get_api_key, + username: get_username(options), + platform: 'ios', + app_identifier: app_identifier + ) + + latest_build_number = lane_context[SharedValues::LATEST_TESTFLIGHT_BUILD_NUMBER] + latest_build_version = lane_context[SharedValues::LATEST_TESTFLIGHT_VERSION] + + UI.message("The latest build number #{latest_build_number} of the latest version: #{latest_build_version} for app identifier: #{app_identifier}") + + upload_metadata({ + build_number: latest_build_number.to_s, + app_version: latest_build_version.to_s, + app_identifier: app_identifier + }) +end + desc 'Makes App Store release build and uploads it to App Store Connect' lane :release_appstore do |options| build_release(options) @@ -194,7 +220,8 @@ desc 'Updates App Store metadata' lane :upload_metadata do |options| deliver(common_deliver_arguments.merge(options).merge({ skip_binary_upload: true, - skip_metadata: false + skip_metadata: false, + version_check_wait_retry_limit: 0 })) end diff --git a/scripts/prepare_release.sh b/scripts/prepare_release.sh index dca7095571..5fe9059474 100755 --- a/scripts/prepare_release.sh +++ b/scripts/prepare_release.sh @@ -3,6 +3,7 @@ set -eo pipefail mute=">/dev/null 2>&1" +is_subsequent_release=0 base_branch="main" build_number=0 @@ -78,24 +79,28 @@ read_command_line_arguments() { print_usage_and_exit "💥 Error: Missing argument" fi + shift 1 + + while [[ "$#" -gt 0 ]]; do + case "$1" in + -v) + mute= + ;; + -s|--subsequent) + is_subsequent_release=1 + ;; + *) + print_usage_and_exit "💥 Error: Unknown option '$1'" + ;; + esac + shift + done + if [[ $input =~ $version_regexp ]]; then process_release "$input" else process_hotfix "$input" fi - - shift 1 - - while getopts 'v' option; do - case "${option}" in - v) - mute= - ;; - *) - print_usage_and_exit "💥 Error: Unknown option '${option}'" - ;; - esac - done } process_release() { # expected input e.g. "1.72.0" @@ -104,9 +109,17 @@ process_release() { # expected input e.g. "1.72.0" echo "Processing version number: $version" - if release_branch_exists; then - is_subsequent_release=1 + if [[ "$is_subsequent_release" -eq 1 ]]; then + # check if the release branch exists (it must exist for a subsequent release) + if ! release_branch_exists; then + die "💥 Error: Release branch does not exist for a subsequent release!" + fi base_branch="$release_branch" + else + # check if the release branch does NOT exist (it must NOT exist for an initial release) + if release_branch_exists; then + die "💥 Error: Release branch already exists for an initial release!" + fi fi } @@ -242,7 +255,7 @@ main() { read_command_line_arguments "$@" checkout_base_branch - if [[ $is_subsequent_release ]]; then + if [[ "$is_subsequent_release" -eq 1 ]]; then create_build_branch elif [[ $is_hotfix ]]; then create_build_branch