diff --git a/.maestro/release_tests/bookmarks.yaml b/.maestro/release_tests/bookmarks.yaml index 4e0822e12d..d77e81e46f 100644 --- a/.maestro/release_tests/bookmarks.yaml +++ b/.maestro/release_tests/bookmarks.yaml @@ -20,7 +20,7 @@ tags: id: "searchEntry" - tapOn: id: "searchEntry" -- inputText: "https://privacy-test-pages.glitch.me" +- inputText: "https://privacy-test-pages.site" - pressKey: Enter # Manage onboarding diff --git a/.maestro/release_tests/browsing.yaml b/.maestro/release_tests/browsing.yaml index 3be02ab923..f191138307 100644 --- a/.maestro/release_tests/browsing.yaml +++ b/.maestro/release_tests/browsing.yaml @@ -20,7 +20,7 @@ tags: id: "searchEntry" - tapOn: id: "searchEntry" -- inputText: "https://privacy-test-pages.glitch.me" +- inputText: "https://privacy-test-pages.site" - pressKey: Enter - tapOn: optional: true diff --git a/.maestro/release_tests/favorites.yaml b/.maestro/release_tests/favorites.yaml index c3d8ae1b28..cdaa68fd46 100644 --- a/.maestro/release_tests/favorites.yaml +++ b/.maestro/release_tests/favorites.yaml @@ -20,7 +20,7 @@ tags: id: "searchEntry" - tapOn: id: "searchEntry" -- inputText: "https://privacy-test-pages.glitch.me" +- inputText: "https://privacy-test-pages.site" - pressKey: Enter # Manage onboarding diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index cd6af05c70..1505c5ea25 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 7.112.0 +MARKETING_VERSION = 7.113.0 diff --git a/Core/AppPrivacyConfigurationDataProvider.swift b/Core/AppPrivacyConfigurationDataProvider.swift index 8e97b6a832..2e95e5c71c 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 = "\"c595f46fe54bfa96bbff4f30fc3940d8\"" - public static let embeddedDataSHA = "911e6616b6869c0940c492240d43c0cf60274755dd45a50cc635c8b7c792cb87" + public static let embeddedDataETag = "\"4cac4f65624262686a265d3b95a8374b\"" + public static let embeddedDataSHA = "6da9ab104a4b2adca51862ad942821e629a24929a95016b045f7bdd0028e1f71" } public var embeddedDataEtag: String { diff --git a/Core/DataStoreWarmup.swift b/Core/DataStoreWarmup.swift index 79087b9fc2..0ef67ac0b6 100644 --- a/Core/DataStoreWarmup.swift +++ b/Core/DataStoreWarmup.swift @@ -34,26 +34,36 @@ public class DataStoreWarmup { private class BlockingNavigationDelegate: NSObject, WKNavigationDelegate { - let finished = PassthroughSubject() + var finished: PassthroughSubject? = PassthroughSubject() func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy { return .allow } func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - finished.send() + if let finished { + finished.send() + self.finished = nil + } else { + Pixel.fire(pixel: .webKitWarmupUnexpectedDidFinish, includedParameters: [.appVersion]) + } } func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { Pixel.fire(pixel: .webKitDidTerminateDuringWarmup) - // We won't get a `didFinish` if the webview crashes - finished.send() + + if let finished { + finished.send() + self.finished = nil + } else { + Pixel.fire(pixel: .webKitWarmupUnexpectedDidTerminate, includedParameters: [.appVersion]) + } } var cancellable: AnyCancellable? func waitForLoad() async { await withCheckedContinuation { continuation in - cancellable = finished.sink { _ in + cancellable = finished?.sink { _ in continuation.resume() } } diff --git a/Core/Pixel.swift b/Core/Pixel.swift index 06786bb9ee..ef0ebe0850 100644 --- a/Core/Pixel.swift +++ b/Core/Pixel.swift @@ -271,16 +271,6 @@ private extension Pixel.Event { } -/// NSError supports this through `NSUnderlyingError`, but there's no support for this for Swift's `Error`. This protocol does that. -/// -/// The reason why this protocol returns a code and a domain instead of just an `Error` or `NSError` is so that the error implementing -/// this protocol has full control over these values, and is able to override them as it best sees fit. -/// -protocol ErrorWithUnderlyingError: Error { - var underlyingErrorCode: Int { get } - var underlyingErrorDomain: String { get } -} - extension Dictionary where Key == String, Value == String { mutating func appendErrorPixelParams(error: Error) { let nsError = error as NSError @@ -288,10 +278,7 @@ extension Dictionary where Key == String, Value == String { self[PixelParameters.errorCode] = "\(nsError.code)" self[PixelParameters.errorDomain] = nsError.domain - if let underlyingError = error as? ErrorWithUnderlyingError { - self[PixelParameters.underlyingErrorCode] = "\(underlyingError.underlyingErrorCode)" - self[PixelParameters.underlyingErrorDomain] = underlyingError.underlyingErrorDomain - } else if let underlyingError = nsError.userInfo["NSUnderlyingError"] as? NSError { + if let underlyingError = nsError.userInfo["NSUnderlyingError"] as? NSError { self[PixelParameters.underlyingErrorCode] = "\(underlyingError.code)" self[PixelParameters.underlyingErrorDomain] = underlyingError.domain } else if let sqlErrorCode = nsError.userInfo["NSSQLiteErrorDomain"] as? NSNumber { diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index e03ecb6f1a..ba826f3a0a 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -319,11 +319,6 @@ extension Pixel { case networkProtectionClientFailedToParseRedeemResponse case networkProtectionClientInvalidAuthToken - case networkProtectionServerListStoreFailedToEncodeServerList - case networkProtectionServerListStoreFailedToDecodeServerList - case networkProtectionServerListStoreFailedToWriteServerList - case networkProtectionServerListStoreFailedToReadServerList - case networkProtectionKeychainErrorFailedToCastKeychainValueToData case networkProtectionKeychainReadError case networkProtectionKeychainWriteError @@ -413,7 +408,10 @@ extension Pixel { case webKitDidTerminate case webKitTerminationDidReloadCurrentTab case webKitDidTerminateDuringWarmup - + + case webKitWarmupUnexpectedDidFinish + case webKitWarmupUnexpectedDidTerminate + case backgroundTaskSubmissionFailed case blankOverlayNotDismissed @@ -580,6 +578,7 @@ extension Pixel { case privacyProPurchaseFailureAccountNotCreated case privacyProPurchaseSuccess case privacyProRestorePurchaseOfferPageEntry + case privacyProRestorePurchaseClick case privacyProRestorePurchaseEmailStart case privacyProRestorePurchaseStoreStart case privacyProRestorePurchaseEmailSuccess @@ -601,6 +600,10 @@ extension Pixel { case privacyProSubscriptionManagementEmail case privacyProSubscriptionManagementPlanBilling case privacyProSubscriptionManagementRemoval + case privacyProFeatureEnabled + case privacyProPromotionDialogShownVPN + case privacyProVPNAccessRevokedDialogShown + case privacyProVPNBetaStoppedWhenPrivacyProEnabled // Full site address setting case settingsShowFullSiteAddressEnabled @@ -906,10 +909,6 @@ extension Pixel.Event { case .networkProtectionClientFailedToRedeemInviteCode: return "m_netp_backend_api_error_failed_to_redeem_invite_code" case .networkProtectionClientFailedToParseRedeemResponse: return "m_netp_backend_api_error_parsing_redeem_response_failed" case .networkProtectionClientInvalidAuthToken: return "m_netp_backend_api_error_invalid_auth_token" - case .networkProtectionServerListStoreFailedToEncodeServerList: return "m_netp_storage_error_failed_to_encode_server_list" - case .networkProtectionServerListStoreFailedToDecodeServerList: return "m_netp_storage_error_failed_to_decode_server_list" - case .networkProtectionServerListStoreFailedToWriteServerList: return "m_netp_storage_error_server_list_file_system_write_failed" - case .networkProtectionServerListStoreFailedToReadServerList: return "m_netp_storage_error_server_list_file_system_read_failed" case .networkProtectionKeychainErrorFailedToCastKeychainValueToData: return "m_netp_keychain_error_failed_to_cast_keychain_value_to_data" case .networkProtectionKeychainReadError: return "m_netp_keychain_error_read_failed" case .networkProtectionKeychainWriteError: return "m_netp_keychain_error_write_failed" @@ -995,7 +994,10 @@ extension Pixel.Event { case .webKitDidTerminate: return "m_d_wkt" case .webKitDidTerminateDuringWarmup: return "m_d_webkit-terminated-during-warmup" case .webKitTerminationDidReloadCurrentTab: return "m_d_wktct" - + + case .webKitWarmupUnexpectedDidFinish: return "m_d_webkit-warmup-unexpected-did-finish" + case .webKitWarmupUnexpectedDidTerminate: return "m_d_webkit-warmup-unexpected-did-terminate" + case .backgroundTaskSubmissionFailed: return "m_bt_rf" case .blankOverlayNotDismissed: return "m_d_ovs" @@ -1163,6 +1165,7 @@ extension Pixel.Event { case .privacyProPurchaseFailureBackendError: return "m_privacy-pro_app_subscription-purchase_failure_account-creation" case .privacyProPurchaseSuccess: return "m_privacy-pro_app_subscription-purchase_success" case .privacyProRestorePurchaseOfferPageEntry: return "m_privacy-pro_offer_restore-purchase_click" + case .privacyProRestorePurchaseClick: return "m_privacy-pro_app-settings_restore-purchase_click" case .privacyProRestorePurchaseEmailStart: return "m_privacy-pro_activate-subscription_enter-email_click" case .privacyProRestorePurchaseStoreStart: return "m_privacy-pro_activate-subscription_restore-purchase_click" case .privacyProRestorePurchaseEmailSuccess: return "m_privacy-pro_app_subscription-restore-using-email_success" @@ -1186,6 +1189,11 @@ extension Pixel.Event { case .privacyProSubscriptionManagementRemoval: return "m_privacy-pro_settings_remove-from-device_click" case .settingsShowFullSiteAddressEnabled: return "m_settings_show_full_url_on" case .settingsShowFullSiteAddressDisabled: return "m_settings_show_full_url_off" + // Launch + case .privacyProFeatureEnabled: return "m_privacy-pro_feature_enabled" + case .privacyProPromotionDialogShownVPN: return "m_privacy-pro_promotion-dialog_shown_vpn" + case .privacyProVPNAccessRevokedDialogShown: return "m_privacy-pro_vpn-access-revoked-dialog_shown" + case .privacyProVPNBetaStoppedWhenPrivacyProEnabled: return "m_privacy-pro_vpn-beta-stopped-when-privacy-pro-enabled" // Web case .privacyProOfferMonthlyPriceClick: return "m_privacy-pro_offer_monthly-price_click" case .privacyProOfferYearlyPriceClick: return "m_privacy-pro_offer_yearly-price_click" diff --git a/Core/StringExtension.swift b/Core/StringExtension.swift index b79146a7ea..7910fa553a 100644 --- a/Core/StringExtension.swift +++ b/Core/StringExtension.swift @@ -22,6 +22,10 @@ import BrowserServicesKit extension String { + public func truncated(length: Int, trailing: String = "…") -> String { + return (self.count > length) ? self.prefix(length) + trailing : self + } + /// Useful if loaded from UserText, for example public func format(arguments: CVarArg...) -> String { return String(format: self, arguments: arguments) diff --git a/Core/SyncBookmarksAdapter.swift b/Core/SyncBookmarksAdapter.swift index 0d3a4acf2f..9494349344 100644 --- a/Core/SyncBookmarksAdapter.swift +++ b/Core/SyncBookmarksAdapter.swift @@ -153,7 +153,7 @@ public final class SyncBookmarksAdapter { ) if !didMigrateToImprovedListsHandling { didMigrateToImprovedListsHandling = true - provider.lastSyncTimestamp = nil + provider.updateSyncTimestamps(server: nil, local: nil) } bindSyncErrorPublisher(provider) diff --git a/Core/ios-config.json b/Core/ios-config.json index d2593f8526..dd4ef388f4 100644 --- a/Core/ios-config.json +++ b/Core/ios-config.json @@ -1,6 +1,6 @@ { "readme": "https://github.com/duckduckgo/privacy-configuration", - "version": 1710501855617, + "version": 1711567148287, "features": { "adClickAttribution": { "readme": "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#3rd-party-tracker-loading-protection", @@ -77,9 +77,6 @@ { "domain": "earth.google.com" }, - { - "domain": "instructure.com" - }, { "domain": "iscorp.com" }, @@ -114,7 +111,7 @@ ] }, "state": "enabled", - "hash": "a92fae2cdccf479cc1bbe840cd32627b" + "hash": "51b76aa7b92d78ad52106b04ac809843" }, "androidBrowserConfig": { "exceptions": [], @@ -255,18 +252,12 @@ { "domain": "metro.co.uk" }, - { - "domain": "youtube.com" - }, { "domain": "newsmax.com" }, { "domain": "meneame.net" }, - { - "domain": "espn.com" - }, { "domain": "usaa.com" }, @@ -274,13 +265,13 @@ "domain": "publico.es" }, { - "domain": "cnbc.com" + "domain": "leboncoin.fr" }, { - "domain": "earth.google.com" + "domain": "www.ffbb.com" }, { - "domain": "instructure.com" + "domain": "earth.google.com" }, { "domain": "iscorp.com" @@ -298,13 +289,15 @@ "settings": { "disabledCMPs": [ "generic-cosmetic", - "termsfeed3" + "termsfeed3", + "strato.de" ] }, "state": "enabled", "features": { "onByDefault": { - "state": "disabled", + "state": "enabled", + "minSupportedVersion": "7.113.0", "rollout": { "steps": [ { @@ -320,7 +313,7 @@ } } }, - "hash": "9aaa080c235ddd8df4295c4d73c87a94" + "hash": "3149ef7db2b6835e6d0dc7c2a843cfff" }, "autofill": { "exceptions": [ @@ -978,9 +971,6 @@ { "domain": "earth.google.com" }, - { - "domain": "instructure.com" - }, { "domain": "iscorp.com" }, @@ -1006,16 +996,13 @@ } }, "state": "disabled", - "hash": "9c70121360bcdfeb63770d8d9aeee770" + "hash": "36e8971fa9bb204b78a5929a14a108dd" }, "clickToPlay": { "exceptions": [ { "domain": "earth.google.com" }, - { - "domain": "instructure.com" - }, { "domain": "iscorp.com" }, @@ -1036,7 +1023,7 @@ } }, "state": "disabled", - "hash": "ba97e20bd75a4dcd4ef376ec9b7fccc1" + "hash": "4390af06f967ef97a827aeab0ac0d1ca" }, "clientBrandHint": { "exceptions": [], @@ -1070,9 +1057,6 @@ { "domain": "earth.google.com" }, - { - "domain": "instructure.com" - }, { "domain": "iscorp.com" }, @@ -1083,7 +1067,7 @@ "domain": "sundancecatalog.com" } ], - "hash": "910e25ffe4d683b3c708a1578d097a16" + "hash": "e37447d42ee8194f185e35e40f577f41" }, "cookie": { "settings": { @@ -1128,9 +1112,6 @@ { "domain": "earth.google.com" }, - { - "domain": "instructure.com" - }, { "domain": "iscorp.com" }, @@ -1142,7 +1123,7 @@ } ], "state": "disabled", - "hash": "7c7ceca9eeb664059750ea96938669b0" + "hash": "37a27966915571085613911b47e6e2eb" }, "customUserAgent": { "settings": { @@ -1266,9 +1247,6 @@ { "domain": "earth.google.com" }, - { - "domain": "instructure.com" - }, { "domain": "iscorp.com" }, @@ -1713,6 +1691,14 @@ { "selector": "#ez-content-blocker-container", "type": "hide" + }, + { + "selector": ".m-balloon-header--ad", + "type": "hide-empty" + }, + { + "selector": ".m-in-content-ad-row", + "type": "hide-empty" } ], "styleTagExceptions": [ @@ -1755,6 +1741,7 @@ "advertisementcontinue reading the main story", "advertisement\ncontinue reading the main story", "advertisement\n\ncontinue reading the main story", + "advertisement - continue reading below", "advertisement\n\nhide ad", "advertisementhide ad", "advertisement - scroll to continue", @@ -1768,6 +1755,7 @@ "anzeige", "close ad", "close this ad", + "content continues below", "x", "_", "sponsored", @@ -1847,6 +1835,33 @@ } ] }, + { + "domain": "accuweather.com", + "rules": [ + { + "selector": ".glacier-ad", + "type": "hide-empty" + }, + { + "selector": "#connatix", + "type": "hide-empty" + }, + { + "selector": ".adhesion-header", + "type": "hide-empty" + }, + { + "selector": ".header-placeholder.has-alerts.has-adhesion", + "type": "modify-style", + "values": [ + { + "property": "height", + "value": "76px" + } + ] + } + ] + }, { "domain": "acidadeon.com", "rules": [ @@ -2084,6 +2099,31 @@ } ] }, + { + "domain": "clevelandclinic.org", + "rules": [ + { + "selector": "[data-identity='adhesive-ad']", + "type": "closest-empty" + }, + { + "selector": "[data-identity='billboard-ad']", + "type": "hide-empty" + }, + { + "selector": "[data-identity='leaderboard-ad']", + "type": "hide" + }, + { + "selector": "[data-identity='sticky-leaderboard-ad']", + "type": "hide" + }, + { + "selector": "[data-identity='leaderboard-ad-page-header-placeholder']", + "type": "hide" + } + ] + }, { "domain": "cnbc.com", "rules": [ @@ -2102,6 +2142,31 @@ } ] }, + { + "domain": "comicbook.com", + "rules": [ + { + "selector": "body:not(.skybox-loaded)>header", + "type": "modify-style", + "values": [ + { + "property": "top", + "value": "0px" + } + ] + }, + { + "selector": "body.pcm-public:not(.skybox-loaded)", + "type": "modify-style", + "values": [ + { + "property": "margin-top", + "value": "90px" + } + ] + } + ] + }, { "domain": "corriere.it", "rules": [ @@ -2968,6 +3033,23 @@ } ] }, + { + "domain": "n4g.com", + "rules": [ + { + "selector": ".top-ads-container-outer", + "type": "closest-empty" + }, + { + "selector": ".f-item-ad", + "type": "closest-empty" + }, + { + "selector": ".f-item-ad-inhouse", + "type": "closest-empty" + } + ] + }, { "domain": "nasdaq.com", "rules": [ @@ -3277,6 +3359,14 @@ } ] }, + { + "domain": "prajwaldesai.com", + "rules": [ + { + "type": "disable-default" + } + ] + }, { "domain": "primagames.com", "rules": [ @@ -3318,6 +3408,26 @@ { "selector": "#marquee-ad", "type": "closest-empty" + }, + { + "selector": ".js_sticky-top-ad", + "type": "hide-empty" + }, + { + "selector": ".js_sticky-footer", + "type": "hide-empty" + }, + { + "selector": "#leftrail_dynamic_ad_wrapper", + "type": "hide-empty" + }, + { + "selector": "#splashy-ad-container-top", + "type": "hide-empty" + }, + { + "selector": ".ad-mobile", + "type": "closest-empty" } ] }, @@ -3394,6 +3504,15 @@ } ] }, + { + "domain": "runnersworld.com", + "rules": [ + { + "selector": ".ad-disclaimer", + "type": "closest-empty" + } + ] + }, { "domain": "scmp.com", "rules": [ @@ -3619,6 +3738,18 @@ } ] }, + { + "domain": "thetvdb.com", + "rules": [ + { + "type": "disable-default" + }, + { + "selector": "[data-aa-adunit]", + "type": "hide" + } + ] + }, { "domain": "thewindowsclub.com", "rules": [ @@ -3740,6 +3871,14 @@ } ] }, + { + "domain": "tumblr.com", + "rules": [ + { + "type": "disable-default" + } + ] + }, { "domain": "tvtropes.org", "rules": [ @@ -3894,6 +4033,22 @@ { "selector": "#YDC-Lead-Stack", "type": "hide-empty" + }, + { + "selector": "#topAd", + "type": "hide-empty" + }, + { + "selector": "#neoLeadAdMobile", + "type": "hide-empty" + }, + { + "selector": ".caas-da", + "type": "hide-empty" + }, + { + "selector": ".gam-placeholder", + "type": "closest-empty" } ] }, @@ -4037,16 +4192,13 @@ ] }, "state": "enabled", - "hash": "2f1178300a22f85803bc42c676ea2cab" + "hash": "313fb06b3f83c0fbe6ac7e7c358ee38e" }, "exceptionHandler": { "exceptions": [ { "domain": "earth.google.com" }, - { - "domain": "instructure.com" - }, { "domain": "iscorp.com" }, @@ -4058,7 +4210,7 @@ } ], "state": "disabled", - "hash": "2b0b6ee567814d75aa2646d494a45a78" + "hash": "5e792dd491428702bc0104240fbce0ce" }, "fingerprintingAudio": { "state": "disabled", @@ -4069,9 +4221,6 @@ { "domain": "earth.google.com" }, - { - "domain": "instructure.com" - }, { "domain": "iscorp.com" }, @@ -4082,7 +4231,7 @@ "domain": "sundancecatalog.com" } ], - "hash": "40b13d6ca36cd3de287345ab9e5839fb" + "hash": "f25a8f2709e865c2bd743828c7ee2f77" }, "fingerprintingBattery": { "exceptions": [ @@ -4092,9 +4241,6 @@ { "domain": "earth.google.com" }, - { - "domain": "instructure.com" - }, { "domain": "iscorp.com" }, @@ -4106,7 +4252,7 @@ } ], "state": "enabled", - "hash": "038608803499bebc30460a84ed27579f" + "hash": "440f8d663d59430c93d66208655d9238" }, "fingerprintingCanvas": { "settings": { @@ -4200,9 +4346,6 @@ { "domain": "earth.google.com" }, - { - "domain": "instructure.com" - }, { "domain": "iscorp.com" }, @@ -4214,7 +4357,7 @@ } ], "state": "disabled", - "hash": "98b5e91ff539dfb6c81699e32b76f70c" + "hash": "ea4c565bae27996f0d651300d757594c" }, "fingerprintingHardware": { "settings": { @@ -4260,9 +4403,6 @@ { "domain": "earth.google.com" }, - { - "domain": "instructure.com" - }, { "domain": "iscorp.com" }, @@ -4274,7 +4414,7 @@ } ], "state": "enabled", - "hash": "ed0d208ef9ffcba9851eddf68a005583" + "hash": "46fbcd4738329731c1b11e88e3afcb7b" }, "fingerprintingScreenSize": { "settings": { @@ -4317,9 +4457,6 @@ { "domain": "earth.google.com" }, - { - "domain": "instructure.com" - }, { "domain": "iscorp.com" }, @@ -4331,7 +4468,7 @@ } ], "state": "enabled", - "hash": "264749fcf7f5e7e03478bb6f0df4a48a" + "hash": "0fb22f84b750e0d29bad55bd95d9ce2b" }, "fingerprintingTemporaryStorage": { "exceptions": [ @@ -4347,9 +4484,6 @@ { "domain": "earth.google.com" }, - { - "domain": "instructure.com" - }, { "domain": "iscorp.com" }, @@ -4361,16 +4495,13 @@ } ], "state": "enabled", - "hash": "c8f4dcd850359636b47ebc31a26f1f1d" + "hash": "f1632b92379847c92c95bcffefbc1bd2" }, "googleRejected": { "exceptions": [ { "domain": "earth.google.com" }, - { - "domain": "instructure.com" - }, { "domain": "iscorp.com" }, @@ -4382,7 +4513,7 @@ } ], "state": "disabled", - "hash": "2b0b6ee567814d75aa2646d494a45a78" + "hash": "5e792dd491428702bc0104240fbce0ce" }, "gpc": { "state": "enabled", @@ -4420,9 +4551,6 @@ { "domain": "earth.google.com" }, - { - "domain": "instructure.com" - }, { "domain": "iscorp.com" }, @@ -4445,7 +4573,7 @@ "privacy-test-pages.site" ] }, - "hash": "d1dd05d2cbbb9425a925cc162aaa681f" + "hash": "549a6e76edaf16c1fffced31b97e9553" }, "harmfulApis": { "settings": { @@ -4550,9 +4678,6 @@ { "domain": "earth.google.com" }, - { - "domain": "instructure.com" - }, { "domain": "iscorp.com" }, @@ -4564,7 +4689,7 @@ } ], "state": "disabled", - "hash": "9d0f5f4f8c02e79246e2d809cada2fdb" + "hash": "44d3e707cba3ee0a3578f52dc2ce2aa4" }, "history": { "state": "enabled", @@ -4586,9 +4711,6 @@ { "domain": "earth.google.com" }, - { - "domain": "instructure.com" - }, { "domain": "iscorp.com" }, @@ -4599,7 +4721,7 @@ "domain": "sundancecatalog.com" } ], - "hash": "ea6d5ad048e35c75c451bff6fe58cb11" + "hash": "f772808ed34cc9ea8cbcbb7cdaf74429" }, "incontextSignup": { "exceptions": [], @@ -4637,9 +4759,6 @@ { "domain": "earth.google.com" }, - { - "domain": "instructure.com" - }, { "domain": "iscorp.com" }, @@ -4658,7 +4777,7 @@ ] }, "state": "enabled", - "hash": "f8dc40f1f5687f403f381452d66eb0d0" + "hash": "698de7b963d7d7942c5c5d1e986bb1b1" }, "networkProtection": { "state": "enabled", @@ -4687,7 +4806,23 @@ "domain": "earth.google.com" }, { - "domain": "instructure.com" + "domain": "iscorp.com" + }, + { + "domain": "marvel.com" + }, + { + "domain": "sundancecatalog.com" + } + ], + "state": "disabled", + "hash": "841fa92b9728c9754f050662678f82c7" + }, + "performanceMetrics": { + "state": "enabled", + "exceptions": [ + { + "domain": "earth.google.com" }, { "domain": "iscorp.com" @@ -4699,8 +4834,7 @@ "domain": "sundancecatalog.com" } ], - "state": "disabled", - "hash": "d07b5bf740e4d648c94e1ac65c4305d9" + "hash": "38558d5e7b231d4b27e7dd76814387a7" }, "privacyDashboard": { "exceptions": [], @@ -4712,7 +4846,7 @@ } }, "toggleReports": { - "state": "internal", + "state": "enabled", "rollout": { "steps": [ { @@ -4723,7 +4857,23 @@ } }, "state": "enabled", - "hash": "0d76cb4a367fc6738f7c4aa6a66f0a04" + "hash": "f7cce63c16c142db4ff5764b542a6c52" + }, + "privacyPro": { + "state": "enabled", + "exceptions": [], + "features": { + "isLaunched": { + "state": "disabled" + }, + "isLaunchedOverride": { + "state": "disabled" + }, + "allowPurchase": { + "state": "enabled" + } + }, + "hash": "5dc8a8fec03c9993bce2b6ec95779903" }, "privacyProtectionsPopup": { "state": "disabled", @@ -4753,9 +4903,6 @@ { "domain": "earth.google.com" }, - { - "domain": "instructure.com" - }, { "domain": "iscorp.com" }, @@ -4767,7 +4914,7 @@ } ], "state": "disabled", - "hash": "1679be76968fe50858b3cc664b8fcbad" + "hash": "0d3df0f7c24ebde89d2dced4e2d34322" }, "requestFilterer": { "state": "disabled", @@ -4775,9 +4922,6 @@ { "domain": "earth.google.com" }, - { - "domain": "instructure.com" - }, { "domain": "iscorp.com" }, @@ -4791,7 +4935,7 @@ "settings": { "windowInMs": 0 }, - "hash": "219a51a9aafbc9c1bae4bad55d7ce437" + "hash": "0fff8017d8ea4b5609b8f5c110be1401" }, "runtimeChecks": { "state": "disabled", @@ -4799,9 +4943,6 @@ { "domain": "earth.google.com" }, - { - "domain": "instructure.com" - }, { "domain": "iscorp.com" }, @@ -4813,16 +4954,13 @@ } ], "settings": {}, - "hash": "e2246d7c78df2167134e1428b04d51ca" + "hash": "800a19533c728bbec7e31e466f898268" }, "serviceworkerInitiatedRequests": { "exceptions": [ { "domain": "earth.google.com" }, - { - "domain": "instructure.com" - }, { "domain": "iscorp.com" }, @@ -4834,7 +4972,7 @@ } ], "state": "disabled", - "hash": "2b0b6ee567814d75aa2646d494a45a78" + "hash": "5e792dd491428702bc0104240fbce0ce" }, "sync": { "state": "enabled", @@ -5042,9 +5180,10 @@ "advertising.com": { "rules": [ { - "rule": "adserver.adtech.advertising.com/pubapi/3.0/1/54669.7/0/0/ADTECH;v=2;cmd=bid;cors=yes", + "rule": "adserver.adtech.advertising.com/pubapi/3.0/1/", "domains": [ - "collider.com" + "collider.com", + "si.com" ] } ] @@ -5106,6 +5245,7 @@ "rule": "c.amazon-adsystem.com/aax2/apstag.js", "domains": [ "applesfera.com", + "fattoincasadabenedetta.it", "inquirer.com", "thesurfersview.com", "wildrivers.lostcoastoutpost.com" @@ -5819,6 +5959,12 @@ "" ] }, + { + "rule": "go.ezodn.com", + "domains": [ + "airplaneacademy.com" + ] + }, { "rule": "ezodn.com", "domains": [ @@ -6160,6 +6306,7 @@ "hscprojects.com", "kits4beats.com", "magicgameworld.com", + "ncaa.com", "rocketnews24.com", "youmath.it", "zefoy.com" @@ -6909,6 +7056,12 @@ "domains": [ "" ] + }, + { + "rule": "cdn.optimizely.com/js/24096340716.js", + "domains": [ + "hgtv.com" + ] } ] }, @@ -7132,6 +7285,12 @@ "domains": [ "newser.com" ] + }, + { + "rule": "a.pub.network/core/prebid-universal-creative.js", + "domains": [ + "titantv.com" + ] } ] }, @@ -7284,6 +7443,22 @@ } ] }, + "scorecardresearch.com": { + "rules": [ + { + "rule": "sb.scorecardresearch.com/c2/plugins/streamingtag_plugin_jwplayer.js", + "domains": [ + "" + ] + }, + { + "rule": "sb.scorecardresearch.com/internal-c2/default/streamingtag_plugin_jwplayer.js", + "domains": [ + "" + ] + } + ] + }, "searchspring.io": { "rules": [ { @@ -7852,9 +8027,6 @@ { "domain": "earth.google.com" }, - { - "domain": "instructure.com" - }, { "domain": "iscorp.com" }, @@ -7865,7 +8037,7 @@ "domain": "sundancecatalog.com" } ], - "hash": "4d2da0fe5691d4283ebfb1021270c6ea" + "hash": "9d0207772c29e4b74f7dd0356c9f84d9" }, "trackingCookies1p": { "settings": { @@ -7878,9 +8050,6 @@ { "domain": "earth.google.com" }, - { - "domain": "instructure.com" - }, { "domain": "iscorp.com" }, @@ -7892,7 +8061,7 @@ } ], "state": "disabled", - "hash": "bfd8b32efe8d633fe670bf6ab1b00240" + "hash": "4dddf681372a2aea9788090b13db6e6f" }, "trackingCookies3p": { "settings": { @@ -7902,9 +8071,6 @@ { "domain": "earth.google.com" }, - { - "domain": "instructure.com" - }, { "domain": "iscorp.com" }, @@ -7916,7 +8082,7 @@ } ], "state": "disabled", - "hash": "d07b5bf740e4d648c94e1ac65c4305d9" + "hash": "841fa92b9728c9754f050662678f82c7" }, "trackingParameters": { "exceptions": [ @@ -7926,9 +8092,6 @@ { "domain": "earth.google.com" }, - { - "domain": "instructure.com" - }, { "domain": "iscorp.com" }, @@ -7972,7 +8135,7 @@ ] }, "state": "enabled", - "hash": "f64c29121e46b2c79c23e8e7efc58c59" + "hash": "1df4ca1a649e81401fb5e872212b4dd0" }, "userAgentRotation": { "settings": { @@ -7982,9 +8145,6 @@ { "domain": "earth.google.com" }, - { - "domain": "instructure.com" - }, { "domain": "iscorp.com" }, @@ -7996,7 +8156,7 @@ } ], "state": "disabled", - "hash": "4498ff835bed7ce27ff2a568db599155" + "hash": "f65d10dfdf6739feab99a08d42734747" }, "voiceSearch": { "exceptions": [], @@ -8008,9 +8168,6 @@ { "domain": "earth.google.com" }, - { - "domain": "instructure.com" - }, { "domain": "iscorp.com" }, @@ -8061,7 +8218,7 @@ } ] }, - "hash": "1b8acba9eed9ba83fdfe0da1e9d8db87" + "hash": "592a1fb6314f04875fc44a66ef7c2433" }, "windowsPermissionUsage": { "exceptions": [], @@ -8073,9 +8230,6 @@ { "domain": "earth.google.com" }, - { - "domain": "instructure.com" - }, { "domain": "iscorp.com" }, @@ -8087,7 +8241,7 @@ } ], "state": "disabled", - "hash": "2b0b6ee567814d75aa2646d494a45a78" + "hash": "5e792dd491428702bc0104240fbce0ce" }, "windowsWaitlist": { "exceptions": [], diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index b76bb7a78c..8bf3c09b35 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -262,8 +262,8 @@ 4B0295192537BC6700E00CEF /* ConfigurationDebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0295182537BC6700E00CEF /* ConfigurationDebugViewController.swift */; }; 4B0F3F502B9BFF2100392892 /* NetworkProtectionFAQView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0F3F4F2B9BFF2100392892 /* NetworkProtectionFAQView.swift */; }; 4B274F602AFEAECC003F0745 /* NetworkProtectionWidgetRefreshModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B274F5F2AFEAECC003F0745 /* NetworkProtectionWidgetRefreshModel.swift */; }; - 4B2754EC29E8C7DF00394032 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 4B2754EB29E8C7DF00394032 /* Lottie */; }; 4B37E0502B928CA6009E81CA /* vpn-light-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = 4B37E04F2B928CA6009E81CA /* vpn-light-mode.json */; }; + 4B412ACC2BBB3D0900A39F5E /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B412ACB2BBB3D0900A39F5E /* LazyView.swift */; }; 4B470ED6299C49800086EBDC /* AppTrackingProtectionDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B470ED5299C49800086EBDC /* AppTrackingProtectionDatabase.swift */; }; 4B470ED9299C4AED0086EBDC /* AppTrackingProtectionModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 4B470ED7299C4AED0086EBDC /* AppTrackingProtectionModel.xcdatamodeld */; }; 4B470EDB299C4FB20086EBDC /* AppTrackingProtectionListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B470EDA299C4FB20086EBDC /* AppTrackingProtectionListViewModel.swift */; }; @@ -636,6 +636,7 @@ 98F3A1DC217B373E0011A0D4 /* DarkTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F3A1DB217B373E0011A0D4 /* DarkTheme.swift */; }; 98F6EA472863124100720957 /* ContentBlockerRulesLists.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F6EA462863124100720957 /* ContentBlockerRulesLists.swift */; }; 98F78B8E22419093007CACF4 /* ThemableNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F78B8D22419093007CACF4 /* ThemableNavigationController.swift */; }; + 9F8FE9492BAE50E50071E372 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 9F8FE9482BAE50E50071E372 /* Lottie */; }; AA3D854523D9942200788410 /* AppIconSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3D854423D9942200788410 /* AppIconSettingsViewController.swift */; }; AA3D854723D9E88E00788410 /* AppIconSettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3D854623D9E88E00788410 /* AppIconSettingsCell.swift */; }; AA3D854923DA1DFB00788410 /* AppIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3D854823DA1DFB00788410 /* AppIcon.swift */; }; @@ -819,15 +820,16 @@ D668D9272B6937D2008E2FF2 /* SubscriptionITPViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D668D9262B6937D2008E2FF2 /* SubscriptionITPViewModel.swift */; }; D668D9292B69681C008E2FF2 /* IdentityTheftRestorationPagesUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = D668D9282B69681C008E2FF2 /* IdentityTheftRestorationPagesUserScript.swift */; }; D668D92B2B696840008E2FF2 /* IdentityTheftRestorationPagesFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = D668D92A2B696840008E2FF2 /* IdentityTheftRestorationPagesFeature.swift */; }; + D66F683D2BB333C100AE93E2 /* SubscriptionContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66F683C2BB333C100AE93E2 /* SubscriptionContainerView.swift */; }; + D670E5BB2BB6A75300941A42 /* SubscriptionNavigationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D670E5BA2BB6A75200941A42 /* SubscriptionNavigationCoordinator.swift */; }; + D670E5BD2BB6AA0000941A42 /* View+AppearModifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D670E5BC2BB6AA0000941A42 /* View+AppearModifiers.swift */; }; D68A21442B7EC08500BB372E /* SubscriptionExternalLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68A21432B7EC08500BB372E /* SubscriptionExternalLinkView.swift */; }; D68A21462B7EC16200BB372E /* SubscriptionExternalLinkViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68A21452B7EC16200BB372E /* SubscriptionExternalLinkViewModel.swift */; }; D68DF81C2B58302E0023DBEA /* SubscriptionRestoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68DF81B2B58302E0023DBEA /* SubscriptionRestoreView.swift */; }; D68DF81E2B5830380023DBEA /* SubscriptionRestoreViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68DF81D2B5830380023DBEA /* SubscriptionRestoreViewModel.swift */; }; - D69DBB502B72B1D300156310 /* View+TopMostController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69DBB4F2B72B1D200156310 /* View+TopMostController.swift */; }; D69FBF762B28BE3600B505F1 /* SettingsSubscriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69FBF752B28BE3600B505F1 /* SettingsSubscriptionView.swift */; }; D6BFCB5F2B7524AA0051FF81 /* SubscriptionPIRView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BFCB5E2B7524AA0051FF81 /* SubscriptionPIRView.swift */; }; D6BFCB612B7525160051FF81 /* SubscriptionPIRViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BFCB602B7525160051FF81 /* SubscriptionPIRViewModel.swift */; }; - D6D95CE12B6D52DA00960317 /* RootPresentationMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D95CE02B6D52DA00960317 /* RootPresentationMode.swift */; }; D6D95CE32B6D9F8800960317 /* AsyncHeadlessWebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D95CE22B6D9F8800960317 /* AsyncHeadlessWebViewModel.swift */; }; D6E0C1832B7A2B1E00D5E1E9 /* DesktopDownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E0C1822B7A2B1E00D5E1E9 /* DesktopDownloadView.swift */; }; D6E0C1852B7A2B9400D5E1E9 /* DesktopDownloadPlatformConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E0C1842B7A2B9400D5E1E9 /* DesktopDownloadPlatformConstants.swift */; }; @@ -1390,6 +1392,7 @@ 4B0F3F4F2B9BFF2100392892 /* NetworkProtectionFAQView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionFAQView.swift; sourceTree = ""; }; 4B274F5F2AFEAECC003F0745 /* NetworkProtectionWidgetRefreshModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionWidgetRefreshModel.swift; sourceTree = ""; }; 4B37E04F2B928CA6009E81CA /* vpn-light-mode.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "vpn-light-mode.json"; sourceTree = ""; }; + 4B412ACB2BBB3D0900A39F5E /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = ""; }; 4B470ED5299C49800086EBDC /* AppTrackingProtectionDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppTrackingProtectionDatabase.swift; sourceTree = ""; }; 4B470ED8299C4AED0086EBDC /* AppTrackingProtectionModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = AppTrackingProtectionModel.xcdatamodel; sourceTree = ""; }; 4B470EDA299C4FB20086EBDC /* AppTrackingProtectionListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppTrackingProtectionListViewModel.swift; sourceTree = ""; }; @@ -2491,15 +2494,16 @@ D668D9262B6937D2008E2FF2 /* SubscriptionITPViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionITPViewModel.swift; sourceTree = ""; }; D668D9282B69681C008E2FF2 /* IdentityTheftRestorationPagesUserScript.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentityTheftRestorationPagesUserScript.swift; sourceTree = ""; }; D668D92A2B696840008E2FF2 /* IdentityTheftRestorationPagesFeature.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentityTheftRestorationPagesFeature.swift; sourceTree = ""; }; + D66F683C2BB333C100AE93E2 /* SubscriptionContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionContainerView.swift; sourceTree = ""; }; + D670E5BA2BB6A75200941A42 /* SubscriptionNavigationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionNavigationCoordinator.swift; sourceTree = ""; }; + D670E5BC2BB6AA0000941A42 /* View+AppearModifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+AppearModifiers.swift"; sourceTree = ""; }; D68A21432B7EC08500BB372E /* SubscriptionExternalLinkView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionExternalLinkView.swift; sourceTree = ""; }; D68A21452B7EC16200BB372E /* SubscriptionExternalLinkViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionExternalLinkViewModel.swift; sourceTree = ""; }; D68DF81B2B58302E0023DBEA /* SubscriptionRestoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionRestoreView.swift; sourceTree = ""; }; D68DF81D2B5830380023DBEA /* SubscriptionRestoreViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionRestoreViewModel.swift; sourceTree = ""; }; - D69DBB4F2B72B1D200156310 /* View+TopMostController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+TopMostController.swift"; sourceTree = ""; }; D69FBF752B28BE3600B505F1 /* SettingsSubscriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSubscriptionView.swift; sourceTree = ""; }; D6BFCB5E2B7524AA0051FF81 /* SubscriptionPIRView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionPIRView.swift; sourceTree = ""; }; D6BFCB602B7525160051FF81 /* SubscriptionPIRViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionPIRViewModel.swift; sourceTree = ""; }; - D6D95CE02B6D52DA00960317 /* RootPresentationMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootPresentationMode.swift; sourceTree = ""; }; D6D95CE22B6D9F8800960317 /* AsyncHeadlessWebViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncHeadlessWebViewModel.swift; sourceTree = ""; }; D6E0C1822B7A2B1E00D5E1E9 /* DesktopDownloadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DesktopDownloadView.swift; sourceTree = ""; }; D6E0C1842B7A2B9400D5E1E9 /* DesktopDownloadPlatformConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DesktopDownloadPlatformConstants.swift; sourceTree = ""; }; @@ -2737,12 +2741,12 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 9F8FE9492BAE50E50071E372 /* Lottie in Frameworks */, 853273B624FFE0BB00E3C778 /* WidgetKit.framework in Frameworks */, 0238E44F29C0FAA100615E30 /* FindInPageIOSJSSupport in Frameworks */, 3760DFED299315EF0045A446 /* Waitlist in Frameworks */, F1D43AFA2B99C1D300BAB743 /* BareBonesBrowserKit in Frameworks */, F143C2EB1E4A4CD400CFDE3A /* Core.framework in Frameworks */, - 4B2754EC29E8C7DF00394032 /* Lottie in Frameworks */, 31E69A63280F4CB600478327 /* DuckUI in Frameworks */, CB941A6E2B96AB08000F9E7A /* PrivacyDashboard in Frameworks */, F42D541D29DCA40B004C4FF1 /* DesignResourcesKit in Frameworks */, @@ -4654,7 +4658,7 @@ isa = PBXGroup; children = ( D664C7982B289AA000CBFA76 /* WKUserContentController+Handler.swift */, - D69DBB4F2B72B1D200156310 /* View+TopMostController.swift */, + D670E5BC2BB6AA0000941A42 /* View+AppearModifiers.swift */, ); path = Extensions; sourceTree = ""; @@ -4663,6 +4667,7 @@ isa = PBXGroup; children = ( D664C7AD2B289AA000CBFA76 /* PurchaseInProgressView.swift */, + D66F683C2BB333C100AE93E2 /* SubscriptionContainerView.swift */, D664C7AE2B289AA000CBFA76 /* SubscriptionFlowView.swift */, D68DF81B2B58302E0023DBEA /* SubscriptionRestoreView.swift */, D64648AC2B59936B0033090B /* SubscriptionEmailView.swift */, @@ -4670,8 +4675,8 @@ D68A21432B7EC08500BB372E /* SubscriptionExternalLinkView.swift */, D6BFCB5E2B7524AA0051FF81 /* SubscriptionPIRView.swift */, D6F93E3D2B50A8A0004C268D /* SubscriptionSettingsView.swift */, - D6D95CE02B6D52DA00960317 /* RootPresentationMode.swift */, D60B1F262B9DDE5A00AE4760 /* SubscriptionGoogleView.swift */, + D670E5BA2BB6A75200941A42 /* SubscriptionNavigationCoordinator.swift */, ); path = Views; sourceTree = ""; @@ -5546,6 +5551,7 @@ 4BBBBA912B03291700D965DA /* VPNWaitlistUserText.swift */, 986DA94924884B18004A7E39 /* WebViewTransition.swift */, EE9D68D72AE15AD600B55EF4 /* UIApplicationExtension.swift */, + 4B412ACB2BBB3D0900A39F5E /* LazyView.swift */, ); name = UserInterface; sourceTree = ""; @@ -5770,9 +5776,9 @@ 3760DFEC299315EF0045A446 /* Waitlist */, F42D541C29DCA40B004C4FF1 /* DesignResourcesKit */, 0238E44E29C0FAA100615E30 /* FindInPageIOSJSSupport */, - 4B2754EB29E8C7DF00394032 /* Lottie */, CB941A6D2B96AB08000F9E7A /* PrivacyDashboard */, F1D43AF92B99C1D300BAB743 /* BareBonesBrowserKit */, + 9F8FE9482BAE50E50071E372 /* Lottie */, ); productName = DuckDuckGo; productReference = 84E341921E2F7EFB00BDBA6F /* DuckDuckGo.app */; @@ -6082,10 +6088,10 @@ F42D541B29DCA40B004C4FF1 /* XCRemoteSwiftPackageReference "DesignResourcesKit" */, 0202568C29881E4300E694E7 /* XCRemoteSwiftPackageReference "CocoaAsyncSocket" */, 0238E44D29C0FAA100615E30 /* XCRemoteSwiftPackageReference "ios-js-support" */, - 4B2754EA29E8C7DF00394032 /* XCRemoteSwiftPackageReference "lottie-ios" */, 854007E52B57FB020001BD98 /* XCRemoteSwiftPackageReference "ZIPFoundation" */, B6F997C22B8F374300476735 /* XCRemoteSwiftPackageReference "apple-toolbox" */, F1D43AF82B99C1D300BAB743 /* XCRemoteSwiftPackageReference "BareBonesBrowser" */, + 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */, ); productRefGroup = 84E341931E2F7EFB00BDBA6F /* Products */; projectDirPath = ""; @@ -6643,6 +6649,7 @@ 020108A929A7C1CD00644F9D /* AppTrackerImageCache.swift in Sources */, 4B78074E2B183A1F009DB2CF /* SurveyURLBuilder.swift in Sources */, 3132FA2A27A0788F00DD7A12 /* QuickLookPreviewHelper.swift in Sources */, + D670E5BB2BB6A75300941A42 /* SubscriptionNavigationCoordinator.swift in Sources */, C1D21E2D293A5965006E5A05 /* AutofillLoginSession.swift in Sources */, 4B53648A26718D0E001AA041 /* EmailWaitlist.swift in Sources */, 027F48762A4B5FBE001A1C6C /* AppTPLinkButton.swift in Sources */, @@ -6749,6 +6756,7 @@ B60DFF072872B64B0061E7C2 /* JSAlertController.swift in Sources */, 981FED6E22025151008488D7 /* BlankSnapshotViewController.swift in Sources */, 98F3A1DC217B373E0011A0D4 /* DarkTheme.swift in Sources */, + D66F683D2BB333C100AE93E2 /* SubscriptionContainerView.swift in Sources */, 851B128822200575004781BC /* Onboarding.swift in Sources */, 3151F0EE2735800800226F58 /* VoiceSearchFeedbackView.swift in Sources */, 857EEB752095FFAC008A005C /* HomeRowInstructionsViewController.swift in Sources */, @@ -6780,7 +6788,6 @@ 31C70B5528045E3500FB6AD1 /* SecureVaultErrorReporter.swift in Sources */, F4CE6D1B257EA33C00D0A6AA /* FireButtonAnimator.swift in Sources */, 85582E0029D7409700E9AE35 /* SyncSettingsViewController+PDFRendering.swift in Sources */, - D69DBB502B72B1D300156310 /* View+TopMostController.swift in Sources */, EE0153EF2A70021E002A8B26 /* NetworkProtectionInviteView.swift in Sources */, 9888F77B2224980500C46159 /* FeedbackViewController.swift in Sources */, D6E83C662B23936F006C8AFB /* SettingsDebugView.swift in Sources */, @@ -6794,6 +6801,7 @@ 1E4FAA6427D8DFB900ADC5B3 /* OngoingDownloadRowViewModel.swift in Sources */, 8C4724502217A14B004C9B2D /* TabViewControllerLongPressBookmarkExtension.swift in Sources */, 1EDE39D22705D4A200C99C72 /* FileSizeDebugViewController.swift in Sources */, + 4B412ACC2BBB3D0900A39F5E /* LazyView.swift in Sources */, 85047C772A0D5D3D00D2FF3F /* SyncSettingsViewController+SyncDelegate.swift in Sources */, 85DDE0402AC6FF65006ABCA2 /* MainView.swift in Sources */, 980891A72237D5D800313A70 /* FeedbackPresenter.swift in Sources */, @@ -6909,6 +6917,7 @@ 984D035C24AE15CD0066CFB8 /* TabSwitcherSettings.swift in Sources */, D6E83C562B21ECC1006C8AFB /* SettingsLegacyViewProvider.swift in Sources */, 98B31292218CCB8C00E54DE1 /* AppDependencyProvider.swift in Sources */, + D670E5BD2BB6AA0000941A42 /* View+AppearModifiers.swift in Sources */, C13F3F6A2B7F883A0083BE40 /* AuthConfirmationPromptViewController.swift in Sources */, 02C57C4B2514FEFB009E5129 /* DoNotSellSettingsViewController.swift in Sources */, 02A54A9C2A097C95000C8FED /* AppTPHomeViewSectionRenderer.swift in Sources */, @@ -6966,7 +6975,6 @@ D6E83C682B23B6A3006C8AFB /* FontSettings.swift in Sources */, 31EF52E1281B3BDC0034796E /* AutofillLoginListItemViewModel.swift in Sources */, 1E4FAA6627D8DFC800ADC5B3 /* CompleteDownloadRowViewModel.swift in Sources */, - D6D95CE12B6D52DA00960317 /* RootPresentationMode.swift in Sources */, 83004E862193E5ED00DA013C /* TabViewControllerBrowsingMenuExtension.swift in Sources */, EE01EB402AFBD0000096AAC9 /* NetworkProtectionVPNSettingsViewModel.swift in Sources */, EE72CA852A862D000043B5B3 /* NetworkProtectionDebugViewController.swift in Sources */, @@ -8262,7 +8270,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProvider.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -8299,7 +8307,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -8391,7 +8399,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -8419,7 +8427,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -8552,7 +8560,7 @@ MTL_ENABLE_DEBUG_INFO = NO; OTHER_LDFLAGS = "-ld_classic"; SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = NETWORK_PROTECTION; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "NETWORK_PROTECTION SUBSCRIPTION"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; @@ -8569,7 +8577,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -8595,7 +8603,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGo/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -8660,7 +8668,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -8695,7 +8703,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -8729,7 +8737,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -8760,7 +8768,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9047,7 +9055,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9078,7 +9086,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -9107,7 +9115,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -9141,7 +9149,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -9172,7 +9180,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProviderAlpha.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -9205,11 +9213,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 3; + DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -9443,7 +9451,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9470,7 +9478,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9503,7 +9511,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9541,7 +9549,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9577,7 +9585,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9612,11 +9620,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 3; + DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -9790,11 +9798,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 3; + DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -9823,10 +9831,10 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 3; + DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10012,14 +10020,6 @@ version = 2.0.0; }; }; - 4B2754EA29E8C7DF00394032 /* XCRemoteSwiftPackageReference "lottie-ios" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/duckduckgo/lottie-ios.git"; - requirement = { - kind = exactVersion; - version = 3.3.0; - }; - }; 854007E52B57FB020001BD98 /* XCRemoteSwiftPackageReference "ZIPFoundation" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/weichsel/ZIPFoundation.git"; @@ -10033,7 +10033,15 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 129.1.6; + version = 132.0.1; + }; + }; + 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/airbnb/lottie-spm.git"; + requirement = { + kind = exactVersion; + version = 4.4.1; }; }; B6F997C22B8F374300476735 /* XCRemoteSwiftPackageReference "apple-toolbox" */ = { @@ -10151,11 +10159,6 @@ package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = SyncDataProviders; }; - 4B2754EB29E8C7DF00394032 /* Lottie */ = { - isa = XCSwiftPackageProductDependency; - package = 4B2754EA29E8C7DF00394032 /* XCRemoteSwiftPackageReference "lottie-ios" */; - productName = Lottie; - }; 4B948E2529DCCDB9002531FA /* Persistence */ = { isa = XCSwiftPackageProductDependency; package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; @@ -10225,6 +10228,11 @@ package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = Bookmarks; }; + 9F8FE9482BAE50E50071E372 /* Lottie */ = { + isa = XCSwiftPackageProductDependency; + package = 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */; + productName = Lottie; + }; B6F997CD2B8F380D00476735 /* SwiftLintPlugin */ = { isa = XCSwiftPackageProductDependency; package = B6F997C22B8F374300476735 /* XCRemoteSwiftPackageReference "apple-toolbox" */; diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index e0a30aaaa8..58ea241b57 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "branch" : "anh/pp/add-metadata", - "revision" : "0cba47cf651175806bc151366605f8b19802d3ee" + "revision" : "73f68ee1c0dda3cd4a0b0cc3cc38a6cc7e605829", + "version" : "132.0.1" } }, { @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "2f44185cca2edefbae7557393a61a23c282abbf8", - "version" : "5.7.0" + "revision" : "62d5dc3d02f6a8347dc5f0b52162a0107d38b74c", + "version" : "5.8.0" } }, { @@ -100,12 +100,12 @@ } }, { - "identity" : "lottie-ios", + "identity" : "lottie-spm", "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/lottie-ios.git", + "location" : "https://github.com/airbnb/lottie-spm.git", "state" : { - "revision" : "abf5510e261c85ffddd29de0bca9b72592ea2bdd", - "version" : "3.3.0" + "revision" : "3bd43e12d6fb54654366a61f7cfaca787318b8ce", + "version" : "4.4.1" } }, { @@ -122,8 +122,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/privacy-dashboard", "state" : { - "revision" : "43a6e1c1864846679a254e60c91332c3fbd922ee", - "version" : "3.3.0" + "revision" : "620921fea14569eb00745cb5a44890d5890d99ec", + "version" : "3.4.0" } }, { @@ -183,7 +183,7 @@ { "identity" : "trackerradarkit", "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/TrackerRadarKit.git", + "location" : "https://github.com/duckduckgo/TrackerRadarKit", "state" : { "revision" : "a6b7ba151d9dc6684484f3785293875ec01cc1ff", "version" : "1.2.2" diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme index d3926becad..e564636928 100644 --- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme +++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme @@ -124,7 +124,7 @@ + isEnabled = "NO"> - + - + @@ -93,13 +93,13 @@ - + - + - + @@ -133,16 +133,16 @@ - + - + - + - + diff --git a/DuckDuckGo/DaxOnboardingViewController.swift b/DuckDuckGo/DaxOnboardingViewController.swift index 05939278ad..1e92a93f79 100644 --- a/DuckDuckGo/DaxOnboardingViewController.swift +++ b/DuckDuckGo/DaxOnboardingViewController.swift @@ -69,7 +69,8 @@ class DaxOnboardingViewController: UIViewController, Onboarding { guard !view.isHidden else { return } daxDialogContainerHeight.constant = daxDialog?.calculateHeight() ?? 0 - + self.daxDialog?.reset() + DispatchQueue.main.asyncAfter(deadline: .now() + Constants.animationDelay) { self.transitionFromOnboarding() } diff --git a/DuckDuckGo/DefaultNetworkProtectionVisibility.swift b/DuckDuckGo/DefaultNetworkProtectionVisibility.swift index 406a8100ce..e71f0ead81 100644 --- a/DuckDuckGo/DefaultNetworkProtectionVisibility.swift +++ b/DuckDuckGo/DefaultNetworkProtectionVisibility.swift @@ -74,7 +74,6 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { return hasLegacyAuthToken || hasBeenInvited } - // todo - https://app.asana.com/0/0/1206844038943626/f func isPrivacyProLaunched() -> Bool { if let subscriptionOverrideEnabled = userDefaults.subscriptionOverrideEnabled { #if ALPHA || DEBUG @@ -87,7 +86,6 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { return AppDependencyProvider.shared.subscriptionFeatureAvailability.isFeatureAvailable } - // todo - https://app.asana.com/0/0/1206844038943626/f func shouldMonitorEntitlement() -> Bool { isPrivacyProLaunched() } diff --git a/DuckDuckGo/EventMapping+NetworkProtectionError.swift b/DuckDuckGo/EventMapping+NetworkProtectionError.swift index 8aaa5c559a..8e8b428b47 100644 --- a/DuckDuckGo/EventMapping+NetworkProtectionError.swift +++ b/DuckDuckGo/EventMapping+NetworkProtectionError.swift @@ -72,24 +72,17 @@ extension EventMapping where Event == NetworkProtectionError { pixelEvent = .networkProtectionNoAuthTokenFoundError case .vpnAccessRevoked: return - case - .noServerRegistrationInfo, + case .noServerRegistrationInfo, .couldNotSelectClosestServer, .couldNotGetPeerPublicKey, .couldNotGetPeerHostName, .couldNotGetInterfaceAddressRange, .failedToEncodeRegisterKeyRequest, - .noServerListFound, .serverListInconsistency, .failedToFetchRegisteredServers, .failedToFetchServerList, .failedToParseServerListResponse, .failedToParseRegisteredServersResponse, - .failedToEncodeServerList, - .failedToDecodeServerList, - .failedToWriteServerList, - .couldNotCreateServerListDirectory, - .failedToReadServerList, .wireGuardCannotLocateTunnelFileDescriptor, .wireGuardInvalidState, .wireGuardDnsResolution, diff --git a/DuckDuckGo/Feedback/VPNMetadataCollector.swift b/DuckDuckGo/Feedback/VPNMetadataCollector.swift index b858cf4672..3fb97ffc52 100644 --- a/DuckDuckGo/Feedback/VPNMetadataCollector.swift +++ b/DuckDuckGo/Feedback/VPNMetadataCollector.swift @@ -291,7 +291,7 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { enableSource: .init(from: accessManager.networkProtectionAccessType()), betaParticipant: accessType == .waitlistJoined, hasToken: hasToken, - subscriptionActive: AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)).accessToken != nil + subscriptionActive: AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)).isUserAuthenticated ) } } diff --git a/DuckDuckGo/FireButtonAnimator.swift b/DuckDuckGo/FireButtonAnimator.swift index ef8c2af543..b5aca8dc41 100644 --- a/DuckDuckGo/FireButtonAnimator.swift +++ b/DuckDuckGo/FireButtonAnimator.swift @@ -46,9 +46,9 @@ enum FireButtonAnimationType: String, CaseIterable, Identifiable, CustomStringCo } } - var composition: Animation? { + var composition: LottieAnimation? { guard let fileName = fileName else { return nil } - return Animation.named(fileName, animationCache: LRUAnimationCache.sharedCache) + return LottieAnimation.named(fileName, animationCache: DefaultAnimationCache.sharedCache) } var transition: Double { @@ -98,8 +98,8 @@ enum FireButtonAnimationType: String, CaseIterable, Identifiable, CustomStringCo class FireButtonAnimator { private let appSettings: AppSettings - private var preLoadedComposition: Animation? - + private var preLoadedComposition: LottieAnimation? + init(appSettings: AppSettings) { self.appSettings = appSettings reloadPreLoadedComposition() @@ -133,7 +133,7 @@ class FireButtonAnimator { window.addSubview(snapshot) - let animationView = AnimationView(animation: composition) + let animationView = LottieAnimationView(animation: composition) let currentAnimation = appSettings.currentFireButtonAnimation let speed = currentAnimation.speed animationView.contentMode = .scaleAspectFill diff --git a/DuckDuckGo/LazyView.swift b/DuckDuckGo/LazyView.swift new file mode 100644 index 0000000000..7605f98e3d --- /dev/null +++ b/DuckDuckGo/LazyView.swift @@ -0,0 +1,32 @@ +// +// LazyView.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftUI + +// https://gist.github.com/chriseidhof/d2fcafb53843df343fe07f3c0dac41d5 +struct LazyView: View { + let build: () -> Content + init(_ build: @autoclosure @escaping () -> Content) { + self.build = build + } + var body: Content { + build() + } +} diff --git a/DuckDuckGo/LottieView.swift b/DuckDuckGo/LottieView.swift index dc44a51579..c6143a769f 100644 --- a/DuckDuckGo/LottieView.swift +++ b/DuckDuckGo/LottieView.swift @@ -40,8 +40,8 @@ struct LottieView: UIViewRepresentable { var isAnimating: Binding private let loopMode: LoopMode - let animationView = AnimationView() - + let animationView = LottieAnimationView() + init(lottieFile: String, delay: TimeInterval = 0, loopMode: LoopMode = .mode(.playOnce), isAnimating: Binding = .constant(true)) { self.lottieFile = lottieFile self.delay = delay @@ -49,8 +49,8 @@ struct LottieView: UIViewRepresentable { self.loopMode = loopMode } - func makeUIView(context: Context) -> some AnimationView { - animationView.animation = Animation.named(lottieFile) + func makeUIView(context: Context) -> some LottieAnimationView { + animationView.animation = LottieAnimation.named(lottieFile) animationView.contentMode = .scaleAspectFit animationView.clipsToBounds = false diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index bbd685c65a..b9606d9378 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -1404,6 +1404,7 @@ class MainViewController: UIViewController { } dismiss(animated: true) { self.present(alertController, animated: true, completion: nil) + DailyPixel.fireDailyAndCount(pixel: .privacyProVPNAccessRevokedDialogShown) self.tunnelDefaults.showEntitlementAlert = false } } @@ -1420,6 +1421,7 @@ class MainViewController: UIViewController { @objc private func onNetworkProtectionAccountSignIn(_ notification: Notification) { tunnelDefaults.resetEntitlementMessaging() + tunnelDefaults.vpnEarlyAccessOverAlertAlreadyShown = true os_log("[NetP Subscription] Reset expired entitlement messaging", log: .networkProtection, type: .info) } @@ -1428,10 +1430,20 @@ class MainViewController: UIViewController { Task { guard case .success(false) = await AccountManager().hasEntitlement(for: .networkProtection) else { return } - tunnelDefaults.enableEntitlementMessaging() - let controller = NetworkProtectionTunnelController() + + if await controller.isInstalled { + tunnelDefaults.enableEntitlementMessaging() + } + + if await controller.isConnected { + DailyPixel.fireDailyAndCount(pixel: .privacyProVPNBetaStoppedWhenPrivacyProEnabled, withAdditionalParameters: [ + "reason": "entitlement-change" + ]) + } + await controller.stop() + await controller.removeVPN() } } @@ -1439,6 +1451,13 @@ class MainViewController: UIViewController { private func onNetworkProtectionAccountSignOut(_ notification: Notification) { Task { let controller = NetworkProtectionTunnelController() + + if await controller.isConnected { + DailyPixel.fireDailyAndCount(pixel: .privacyProVPNBetaStoppedWhenPrivacyProEnabled, withAdditionalParameters: [ + "reason": "account-signed-out" + ]) + } + await controller.stop() await controller.removeVPN() } diff --git a/DuckDuckGo/MenuButton.swift b/DuckDuckGo/MenuButton.swift index 999618f248..1f29040c37 100644 --- a/DuckDuckGo/MenuButton.swift +++ b/DuckDuckGo/MenuButton.swift @@ -54,7 +54,7 @@ class MenuButton: UIView { private let bookmarksIconView = UIImageView() - let anim = AnimationView(name: "menu_light") + let anim = LottieAnimationView(name: "menu_light") let pointerView: UIView = UIView(frame: CGRect(x: 0, y: 0, width: Constants.pointerViewWidth, @@ -190,9 +190,9 @@ extension MenuButton: Themable { switch theme.currentImageSet { case .light: - anim.animation = Animation.named("menu_light") + anim.animation = LottieAnimation.named("menu_light") case .dark: - anim.animation = Animation.named("menu_dark") + anim.animation = LottieAnimation.named("menu_dark") } if currentState == State.closeImage { diff --git a/DuckDuckGo/NetworkProtectionAccessController.swift b/DuckDuckGo/NetworkProtectionAccessController.swift index 289addbfa4..a1acf99f11 100644 --- a/DuckDuckGo/NetworkProtectionAccessController.swift +++ b/DuckDuckGo/NetworkProtectionAccessController.swift @@ -143,7 +143,6 @@ struct NetworkProtectionAccessController: NetworkProtectionAccess { } func revokeNetworkProtectionAccess() { - networkProtectionWaitlistStorage.deleteWaitlistState() try? NetworkProtectionKeychainTokenStore().deleteToken() Task { diff --git a/DuckDuckGo/NetworkProtectionDebugViewController.swift b/DuckDuckGo/NetworkProtectionDebugViewController.swift index d48adbac5e..a6b2a301c2 100644 --- a/DuckDuckGo/NetworkProtectionDebugViewController.swift +++ b/DuckDuckGo/NetworkProtectionDebugViewController.swift @@ -681,6 +681,7 @@ shouldShowVPNShortcut: \(vpnVisibility.shouldShowVPNShortcut() ? "YES" : "NO") vpnSettings.selectedEnvironment = .production } vpnSettings.selectedServer = .automatic + NetworkProtectionLocationListCompositeRepository.clearCache() tableView.reloadData() case .updateSubscriptionOverride: let defaults = UserDefaults.networkProtectionGroupDefaults diff --git a/DuckDuckGo/NetworkProtectionFeatureVisibility.swift b/DuckDuckGo/NetworkProtectionFeatureVisibility.swift index a698deb730..45f47e0153 100644 --- a/DuckDuckGo/NetworkProtectionFeatureVisibility.swift +++ b/DuckDuckGo/NetworkProtectionFeatureVisibility.swift @@ -18,6 +18,7 @@ // import Foundation +import Subscription public protocol NetworkProtectionFeatureVisibility { func isWaitlistBetaActive() -> Bool @@ -49,8 +50,16 @@ public extension NetworkProtectionFeatureVisibility { !isPrivacyProLaunched() && isWaitlistBetaActive() && isWaitlistUser() } - // todo - https://app.asana.com/0/0/1206827703748771/f func shouldShowVPNShortcut() -> Bool { - isPrivacyProLaunched() || shouldKeepVPNAccessViaWaitlist() + if isPrivacyProLaunched() { +#if SUBSCRIPTION + let accountManager = AccountManager() + return accountManager.isUserAuthenticated +#else + return false +#endif + } else { + return shouldKeepVPNAccessViaWaitlist() + } } } diff --git a/DuckDuckGo/NetworkProtectionStatusView.swift b/DuckDuckGo/NetworkProtectionStatusView.swift index 2705b9fdc6..953881fd00 100644 --- a/DuckDuckGo/NetworkProtectionStatusView.swift +++ b/DuckDuckGo/NetworkProtectionStatusView.swift @@ -199,7 +199,7 @@ struct NetworkProtectionStatusView: View { private func about() -> some View { Section { if statusModel.shouldShowFAQ { - NavigationLink(UserText.netPVPNSettingsFAQ, destination: NetworkProtectionFAQView()) + NavigationLink(UserText.netPVPNSettingsFAQ, destination: LazyView(NetworkProtectionFAQView())) .daxBodyRegular() .foregroundColor(.init(designSystemColor: .textPrimary)) } diff --git a/DuckDuckGo/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtectionTunnelController.swift index 1d130bf0cc..aaba16d63b 100644 --- a/DuckDuckGo/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtectionTunnelController.swift @@ -85,6 +85,13 @@ final class NetworkProtectionTunnelController: TunnelController { // MARK: - Connection Status Querying + var isInstalled: Bool { + get async { + let tunnelManager = await loadTunnelManager() + return tunnelManager != nil + } + } + /// Queries Network Protection to know if its VPN is connected. /// /// - Returns: `true` if the VPN is connected, connecting or reasserting, and `false` otherwise. diff --git a/DuckDuckGo/NetworkProtectionVisibilityForTunnelProvider.swift b/DuckDuckGo/NetworkProtectionVisibilityForTunnelProvider.swift index 964641bf59..2b50eee28d 100644 --- a/DuckDuckGo/NetworkProtectionVisibilityForTunnelProvider.swift +++ b/DuckDuckGo/NetworkProtectionVisibilityForTunnelProvider.swift @@ -34,16 +34,14 @@ struct NetworkProtectionVisibilityForTunnelProvider: NetworkProtectionFeatureVis preconditionFailure("Does not apply to Tunnel Provider") } - // todo - https://app.asana.com/0/0/1206844038943626/f func isPrivacyProLaunched() -> Bool { #if SUBSCRIPTION - AccountManager().accessToken != nil + AccountManager().isUserAuthenticated #else false #endif } - // todo - https://app.asana.com/0/0/1206844038943626/f func shouldMonitorEntitlement() -> Bool { isPrivacyProLaunched() } diff --git a/DuckDuckGo/OmniBar.swift b/DuckDuckGo/OmniBar.swift index 7feb722c3e..aa7a17d573 100644 --- a/DuckDuckGo/OmniBar.swift +++ b/DuckDuckGo/OmniBar.swift @@ -375,7 +375,7 @@ class OmniBar: UIView { } func refreshText(forUrl url: URL?, forceFullURL: Bool = false) { - + guard !textField.isEditing else { return } guard let url = url else { textField.text = nil return diff --git a/DuckDuckGo/PrivacyIconView.swift b/DuckDuckGo/PrivacyIconView.swift index aa5e2267fb..cd287e5050 100644 --- a/DuckDuckGo/PrivacyIconView.swift +++ b/DuckDuckGo/PrivacyIconView.swift @@ -28,12 +28,12 @@ enum PrivacyIcon { class PrivacyIconView: UIView { @IBOutlet var daxLogoImageView: UIImageView! - @IBOutlet var staticShieldAnimationView: AnimationView! - @IBOutlet var staticShieldDotAnimationView: AnimationView! - - @IBOutlet var shieldAnimationView: AnimationView! - @IBOutlet var shieldDotAnimationView: AnimationView! - + @IBOutlet var staticShieldAnimationView: LottieAnimationView! + @IBOutlet var staticShieldDotAnimationView: LottieAnimationView! + + @IBOutlet var shieldAnimationView: LottieAnimationView! + @IBOutlet var shieldDotAnimationView: LottieAnimationView! + public required init?(coder aDecoder: NSCoder) { icon = .shield @@ -54,15 +54,15 @@ class PrivacyIconView: UIView { updateAccessibilityLabels(for: icon) } - func loadAnimations(for theme: Theme, animationCache cache: AnimationCacheProvider = LRUAnimationCache.sharedCache) { + func loadAnimations(for theme: Theme, animationCache cache: AnimationCacheProvider = DefaultAnimationCache.sharedCache) { let useLightStyle = theme.currentImageSet == .light - let shieldAnimation = Animation.named(useLightStyle ? "shield" : "dark-shield", animationCache: cache) + let shieldAnimation = LottieAnimation.named(useLightStyle ? "shield" : "dark-shield", animationCache: cache) shieldAnimationView.animation = shieldAnimation staticShieldAnimationView.animation = shieldAnimation staticShieldAnimationView.currentProgress = 0.0 - let shieldWithDotAnimation = Animation.named(useLightStyle ? "shield-dot" : "dark-shield-dot", animationCache: cache) + let shieldWithDotAnimation = LottieAnimation.named(useLightStyle ? "shield-dot" : "dark-shield-dot", animationCache: cache) shieldDotAnimationView.animation = shieldWithDotAnimation staticShieldDotAnimationView.animation = shieldWithDotAnimation staticShieldDotAnimationView.currentProgress = 1.0 @@ -128,7 +128,7 @@ class PrivacyIconView: UIView { daxLogoImageView.isHidden = true } - func shieldAnimationView(for icon: PrivacyIcon) -> AnimationView? { + func shieldAnimationView(for icon: PrivacyIcon) -> LottieAnimationView? { switch icon { case .shield: return shieldAnimationView diff --git a/DuckDuckGo/PrivacyInfoContainerView.swift b/DuckDuckGo/PrivacyInfoContainerView.swift index 713d760b3f..73b3ff8907 100644 --- a/DuckDuckGo/PrivacyInfoContainerView.swift +++ b/DuckDuckGo/PrivacyInfoContainerView.swift @@ -27,9 +27,9 @@ class PrivacyInfoContainerView: UIView { @IBOutlet var privacyIcon: PrivacyIconView! @IBOutlet var maskingView: UIView! - @IBOutlet var trackers1Animation: AnimationView! - @IBOutlet var trackers2Animation: AnimationView! - @IBOutlet var trackers3Animation: AnimationView! + @IBOutlet var trackers1Animation: LottieAnimationView! + @IBOutlet var trackers2Animation: LottieAnimationView! + @IBOutlet var trackers3Animation: LottieAnimationView! override func awakeFromNib() { super.awakeFromNib() @@ -41,24 +41,26 @@ class PrivacyInfoContainerView: UIView { [trackers1Animation, trackers2Animation, trackers3Animation].forEach { animationView in animationView.contentMode = .scaleAspectFill animationView.backgroundBehavior = .pauseAndRestore + // Trackers animation do not render properly using Lottie CoreAnimation. Running them on the CPU seems working fine. + animationView.configuration = LottieConfiguration(renderingEngine: .mainThread) } loadAnimations(for: ThemeManager.shared.currentTheme) } - private func loadAnimations(for theme: Theme, animationCache cache: AnimationCacheProvider = LRUAnimationCache.sharedCache) { + private func loadAnimations(for theme: Theme, animationCache cache: AnimationCacheProvider = DefaultAnimationCache.sharedCache) { let useLightStyle = theme.currentImageSet == .light - trackers1Animation.animation = Animation.named(useLightStyle ? "trackers-1" : "dark-trackers-1", animationCache: cache) - trackers2Animation.animation = Animation.named(useLightStyle ? "trackers-2" : "dark-trackers-2", animationCache: cache) - trackers3Animation.animation = Animation.named(useLightStyle ? "trackers-3" : "dark-trackers-3", animationCache: cache) + trackers1Animation.animation = LottieAnimation.named(useLightStyle ? "trackers-1" : "dark-trackers-1", animationCache: cache) + trackers2Animation.animation = LottieAnimation.named(useLightStyle ? "trackers-2" : "dark-trackers-2", animationCache: cache) + trackers3Animation.animation = LottieAnimation.named(useLightStyle ? "trackers-3" : "dark-trackers-3", animationCache: cache) privacyIcon.loadAnimations(for: theme, animationCache: cache) currentlyLoadedStyle = theme.currentImageSet } - func trackerAnimationView(for trackerCount: Int) -> AnimationView? { + func trackerAnimationView(for trackerCount: Int) -> LottieAnimationView? { switch trackerCount { case 0: return nil case 1: return trackers1Animation diff --git a/DuckDuckGo/Settings.bundle/Root.plist b/DuckDuckGo/Settings.bundle/Root.plist index beab9dd941..0a06622ca2 100644 --- a/DuckDuckGo/Settings.bundle/Root.plist +++ b/DuckDuckGo/Settings.bundle/Root.plist @@ -6,7 +6,7 @@ DefaultValue - 7.112.0 + 7.113.0 Key version Title diff --git a/DuckDuckGo/SettingsLegacyViewProvider.swift b/DuckDuckGo/SettingsLegacyViewProvider.swift index 7749d9bcb2..aa7fd200bf 100644 --- a/DuckDuckGo/SettingsLegacyViewProvider.swift +++ b/DuckDuckGo/SettingsLegacyViewProvider.swift @@ -91,6 +91,7 @@ class SettingsLegacyViewProvider: ObservableObject { var syncSettings: UIViewController { return SyncSettingsViewController(syncService: self.syncService, syncBookmarksAdapter: self.syncDataProviders.bookmarksAdapter, + syncCredentialsAdapter: self.syncDataProviders.credentialsAdapter, appSettings: self.appSettings) } diff --git a/DuckDuckGo/SettingsState.swift b/DuckDuckGo/SettingsState.swift index c80e7c22ac..78441a3e00 100644 --- a/DuckDuckGo/SettingsState.swift +++ b/DuckDuckGo/SettingsState.swift @@ -44,6 +44,7 @@ struct SettingsState { var enabled: Bool var canPurchase: Bool var hasActiveSubscription: Bool + var isSubscriptionPendingActivation: Bool } struct SyncSettings { @@ -113,8 +114,10 @@ struct SettingsState { speechRecognitionAvailable: false, loginsEnabled: false, networkProtection: NetworkProtection(enabled: false, status: ""), - subscription: Subscription(enabled: false, canPurchase: false, - hasActiveSubscription: false), + subscription: Subscription(enabled: false, + canPurchase: false, + hasActiveSubscription: false, + isSubscriptionPendingActivation: false), sync: SyncSettings(enabled: false, title: "") ) } diff --git a/DuckDuckGo/SettingsSubscriptionView.swift b/DuckDuckGo/SettingsSubscriptionView.swift index c0e19bf564..df5b603b87 100644 --- a/DuckDuckGo/SettingsSubscriptionView.swift +++ b/DuckDuckGo/SettingsSubscriptionView.swift @@ -22,16 +22,16 @@ import UIKit #if SUBSCRIPTION import Subscription +import Core @available(iOS 15.0, *) struct SettingsSubscriptionView: View { @EnvironmentObject var viewModel: SettingsViewModel - @StateObject var subscriptionFlowViewModel = SubscriptionFlowViewModel() - @StateObject var subscriptionRestoreViewModel = SubscriptionRestoreViewModel() - @State var isShowingSubscriptionFlow = false - @State var isShowingSubscriptionRestoreFlow = false + @EnvironmentObject var subscriptionNavigationCoordinator: SubscriptionNavigationCoordinator @State var isShowingDBP = false @State var isShowingITP = false + @State var isShowingRestoreFlow = false + @State var isShowingSubscribeFlow = false enum Constants { static let purchaseDescriptionPadding = 5.0 @@ -40,7 +40,7 @@ struct SettingsSubscriptionView: View { static let navigationDelay = 0.3 static let infoIcon = "info-16" } - + private var subscriptionDescriptionView: some View { VStack(alignment: .leading) { Text(UserText.settingsPProSubscribe).daxBodyRegular() @@ -51,18 +51,6 @@ struct SettingsSubscriptionView: View { } } - private var learnMoreView: some View { - Text(UserText.settingsPProLearnMore) - .daxBodyRegular() - .foregroundColor(Color.init(designSystemColor: .accent)) - } - - private var iHaveASubscriptionView: some View { - Text(UserText.settingsPProIHaveASubscription) - .daxBodyRegular() - .foregroundColor(Color.init(designSystemColor: .accent)) - } - @ViewBuilder private var restorePurchaseView: some View { let text = !viewModel.isRestoringSubscription ? UserText.subscriptionActivateAppleIDButton : UserText.subscriptionRestoringTitle @@ -90,35 +78,30 @@ struct SettingsSubscriptionView: View { @ViewBuilder private var purchaseSubscriptionView: some View { + Group { SettingsCustomCell(content: { subscriptionDescriptionView }) - SettingsCustomCell(content: { learnMoreView }, - action: { isShowingSubscriptionFlow = true }, - isButton: true ) - - // Subscription Purchase - .sheet(isPresented: $isShowingSubscriptionFlow, - onDismiss: { Task { viewModel.onAppear() } }, - content: { - SubscriptionFlowView(viewModel: subscriptionFlowViewModel).interactiveDismissDisabled() - }) - - SettingsCustomCell(content: { iHaveASubscriptionView }, - action: { - isShowingSubscriptionRestoreFlow = true - }, - isButton: true ) - // Subscription Restore - .sheet(isPresented: $isShowingSubscriptionRestoreFlow, - onDismiss: { Task { viewModel.onAppear() } }, - content: { - SubscriptionRestoreView(viewModel: subscriptionRestoreViewModel).interactiveDismissDisabled() - }) + let subscribeView = SubscriptionContainerView(currentView: .subscribe) + .navigationViewStyle(.stack) + .environmentObject(subscriptionNavigationCoordinator) + let restoreView = SubscriptionContainerView(currentView: .restore) + .navigationViewStyle(.stack) + .environmentObject(subscriptionNavigationCoordinator) + .onFirstAppear { + Pixel.fire(pixel: .privacyProRestorePurchaseClick) + } + + NavigationLink(destination: subscribeView, + isActive: $isShowingSubscribeFlow, + label: { SettingsCellView(label: UserText.settingsPProLearnMore ) }) + NavigationLink(destination: restoreView, + isActive: $isShowingRestoreFlow, + label: { SettingsCellView(label: UserText.settingsPProIHaveASubscription ) }) } } - + @ViewBuilder private var noEntitlementsAvailableView: some View { Group { @@ -140,7 +123,7 @@ struct SettingsSubscriptionView: View { @ViewBuilder private var subscriptionDetailsView: some View { - Group { + if viewModel.shouldShowNetP { SettingsCellView(label: UserText.settingsPProVPNTitle, subtitle: viewModel.state.networkProtection.status != "" ? viewModel.state.networkProtection.status : nil, @@ -150,84 +133,59 @@ struct SettingsSubscriptionView: View { } if viewModel.shouldShowDBP { - SettingsCellView(label: UserText.settingsPProDBPTitle, - subtitle: UserText.settingsPProDBPSubTitle, - action: { isShowingDBP.toggle() }, isButton: true) - - .sheet(isPresented: $isShowingDBP) { - SubscriptionPIRView() - } + NavigationLink(destination: SubscriptionPIRView(), + isActive: $isShowingDBP, + label: { + SettingsCellView(label: UserText.settingsPProDBPTitle, + subtitle: UserText.settingsPProDBPSubTitle) + }) } - + if viewModel.shouldShowITP { - SettingsCellView(label: UserText.settingsPProITRTitle, - subtitle: UserText.settingsPProITRSubTitle, - action: { isShowingITP.toggle() }, isButton: true) - - .sheet(isPresented: $isShowingITP) { - SubscriptionITPView() - } + NavigationLink(destination: SubscriptionITPView(), + isActive: $isShowingITP, + label: { + SettingsCellView(label: UserText.settingsPProITRTitle, + subtitle: UserText.settingsPProITRSubTitle) + }) } - NavigationLink(destination: SubscriptionSettingsView()) { + NavigationLink(destination: SubscriptionSettingsView().environmentObject(subscriptionNavigationCoordinator)) { SettingsCustomCell(content: { manageSubscriptionView }) - } - } } var body: some View { - if viewModel.state.subscription.enabled { + if viewModel.state.subscription.enabled && viewModel.state.subscription.canPurchase { Section(header: Text(UserText.settingsPProSection)) { - if viewModel.state.subscription.hasActiveSubscription { - - if !viewModel.isLoadingSubscriptionState { - // Allow managing the subscription if we have some entitlements - if viewModel.shouldShowDBP || viewModel.shouldShowITP || viewModel.shouldShowNetP { - subscriptionDetailsView - - // If no entitlements it should mean the backend is still out of sync - } else { - noEntitlementsAvailableView - } + // Allow managing the subscription if we have some entitlements + if viewModel.shouldShowDBP || viewModel.shouldShowITP || viewModel.shouldShowNetP { + subscriptionDetailsView + + // If no entitlements it should mean the backend is still out of sync + } else { + noEntitlementsAvailableView } + + } else if viewModel.state.subscription.isSubscriptionPendingActivation { + noEntitlementsAvailableView } else { purchaseSubscriptionView - } - } - // Selected Feature handler for Subscription Flow - .onChange(of: subscriptionFlowViewModel.selectedFeature) { value in - guard let value else { return } - viewModel.triggerDeepLinkNavigation(to: value) - } - - // Selected Feature handler for Subscription Restore - .onChange(of: subscriptionRestoreViewModel.emailViewModel.selectedFeature) { value in - guard let value else { return } - viewModel.triggerDeepLinkNavigation(to: value) - } - - // Selected Feature handler for SubscriptionActivation - .onChange(of: subscriptionFlowViewModel.state.shouldActivateSubscription) { value in - if value { - viewModel.triggerDeepLinkNavigation(to: .subscriptionRestoreFlow) + .onReceive(subscriptionNavigationCoordinator.$shouldPopToAppSettings) { shouldDismiss in + if shouldDismiss { + isShowingRestoreFlow = false + isShowingSubscribeFlow = false } } - - // Selected Feature handler for Show Plans - .onChange(of: subscriptionRestoreViewModel.state.shouldShowPlans) { value in - if value { - viewModel.triggerDeepLinkNavigation(to: .subscriptionFlow) - } - } + } } } diff --git a/DuckDuckGo/SettingsView.swift b/DuckDuckGo/SettingsView.swift index c15d879d28..5d4d6d11db 100644 --- a/DuckDuckGo/SettingsView.swift +++ b/DuckDuckGo/SettingsView.swift @@ -21,11 +21,12 @@ import SwiftUI import UIKit import DesignResourcesKit + struct SettingsView: View { @StateObject var viewModel: SettingsViewModel @Environment(\.presentationMode) var presentationMode - + @State private var subscriptionNavigationCoordinator = SubscriptionNavigationCoordinator() @State private var shouldDisplayDeepLinkSheet: Bool = false @State private var shouldDisplayDeepLinkPush: Bool = false #if SUBSCRIPTION @@ -56,7 +57,7 @@ struct SettingsView: View { SettingsPrivacyView() #if SUBSCRIPTION if #available(iOS 15, *) { - SettingsSubscriptionView() + SettingsSubscriptionView().environmentObject(subscriptionNavigationCoordinator) } #endif SettingsCustomizeView() @@ -97,16 +98,19 @@ struct SettingsView: View { } }) - .onReceive(viewModel.$deepLinkTarget, perform: { link in - guard let link else { return } + .onReceive(viewModel.$deepLinkTarget.removeDuplicates(), perform: { link in + guard let link, link != self.deepLinkTarget else { + return + } + self.deepLinkTarget = link - + switch link.type { case .sheet: DispatchQueue.main.async { self.shouldDisplayDeepLinkSheet = true } - case .navigation: + case .navigationLink: DispatchQueue.main.async { self.shouldDisplayDeepLinkPush = true } @@ -130,9 +134,9 @@ struct SettingsView: View { case .itr: SubscriptionITPView() case .subscriptionFlow: - SubscriptionFlowView() + SubscriptionContainerView(currentView: .subscribe).environmentObject(subscriptionNavigationCoordinator) case .subscriptionRestoreFlow: - SubscriptionRestoreView() + SubscriptionContainerView(currentView: .restore).environmentObject(subscriptionNavigationCoordinator) default: EmptyView() } diff --git a/DuckDuckGo/SettingsViewModel.swift b/DuckDuckGo/SettingsViewModel.swift index 9ce8c3e193..2ef9137929 100644 --- a/DuckDuckGo/SettingsViewModel.swift +++ b/DuckDuckGo/SettingsViewModel.swift @@ -55,11 +55,15 @@ final class SettingsViewModel: ObservableObject { private var isPrivacyProEnabled: Bool { AppDependencyProvider.shared.subscriptionFeatureAvailability.isFeatureAvailable } + // Cache subscription state in memory to prevent UI glitches + private var cacheSubscriptionState: SettingsState.Subscription = SettingsState.Subscription(enabled: false, + canPurchase: false, + hasActiveSubscription: false, + isSubscriptionPendingActivation: false) // Sheet Presentation & Navigation @Published var isRestoringSubscription: Bool = false @Published var shouldDisplayRestoreSubscriptionError: Bool = false - @Published var isLoadingSubscriptionState: Bool = false @Published var shouldShowNetP = false @Published var shouldShowDBP = false @Published var shouldShowITP = false @@ -266,8 +270,8 @@ extension SettingsViewModel { // other dependencies are observable (Such as AppIcon and netP) // and we can use subscribers (Currently called from the view onAppear) @MainActor - private func initState() async { - self.state = await SettingsState( + private func initState() { + self.state = SettingsState( appTheme: appSettings.currentThemeName, appIcon: AppIconManager.shared.appIcon, fireButtonAnimation: appSettings.currentFireButtonAnimation, @@ -288,11 +292,12 @@ extension SettingsViewModel { speechRecognitionAvailable: AppDependencyProvider.shared.voiceSearchHelper.isSpeechRecognizerAvailable, loginsEnabled: featureFlagger.isFeatureOn(.autofillAccessCredentialManagement), networkProtection: getNetworkProtectionState(), - subscription: getSubscriptionState(), + subscription: cacheSubscriptionState, sync: getSyncState() ) setupSubscribers() + Task { await refreshSubscriptionState() } } @@ -305,12 +310,20 @@ extension SettingsViewModel { #endif return SettingsState.NetworkProtection(enabled: enabled, status: "") } + + private func refreshSubscriptionState() async { + let state = await self.getSubscriptionState() + DispatchQueue.main.async { + self.state.subscription = state + } + } private func getSubscriptionState() async -> SettingsState.Subscription { var enabled = false var canPurchase = false var hasActiveSubscription = false - + var isSubscriptionPendingActivation = false + #if SUBSCRIPTION if #available(iOS 15, *) { enabled = isPrivacyProEnabled @@ -318,15 +331,27 @@ extension SettingsViewModel { await setupSubscriptionEnvironment() if let token = AccountManager().accessToken { let subscriptionResult = await SubscriptionService.getSubscription(accessToken: token) - if case .success(let subscription) = subscriptionResult { + switch subscriptionResult { + case .success(let subscription): hasActiveSubscription = subscription.isActive + + cacheSubscriptionState = SettingsState.Subscription(enabled: enabled, + canPurchase: canPurchase, + hasActiveSubscription: hasActiveSubscription, + isSubscriptionPendingActivation: isSubscriptionPendingActivation) + + case .failure: + if await PurchaseManager.hasActiveSubscription() { + isSubscriptionPendingActivation = true + } } } } #endif return SettingsState.Subscription(enabled: enabled, canPurchase: canPurchase, - hasActiveSubscription: hasActiveSubscription) + hasActiveSubscription: hasActiveSubscription, + isSubscriptionPendingActivation: isSubscriptionPendingActivation) } private func getSyncState() -> SettingsState.SyncSettings { @@ -369,36 +394,48 @@ extension SettingsViewModel { setupSubscriptionPurchaseOptions() return } - - isLoadingSubscriptionState = true + // Fetch available subscriptions from the backend (or sign out) switch await SubscriptionService.getSubscription(accessToken: token) { - case .success(let subscription) where subscription.isActive: - - // Check entitlements and update UI accordingly - let entitlements: [Entitlement.ProductName] = [.networkProtection, .dataBrokerProtection, .identityTheftRestoration] - for entitlement in entitlements { - if case let .success(result) = await AccountManager().hasEntitlement(for: entitlement) { - switch entitlement { - case .identityTheftRestoration: - self.shouldShowITP = result - case .dataBrokerProtection: - self.shouldShowDBP = result - case .networkProtection: - self.shouldShowNetP = result - case .unknown: - return + case .success(let subscription): + if subscription.isActive { + state.subscription.hasActiveSubscription = true + state.subscription.isSubscriptionPendingActivation = false + + // Check entitlements and update UI accordingly + let entitlements: [Entitlement.ProductName] = [.networkProtection, .dataBrokerProtection, .identityTheftRestoration] + for entitlement in entitlements { + if case let .success(result) = await AccountManager().hasEntitlement(for: entitlement) { + switch entitlement { + case .identityTheftRestoration: + self.shouldShowITP = result + case .dataBrokerProtection: + self.shouldShowDBP = result + case .networkProtection: + self.shouldShowNetP = result + case .unknown: + return + } } } + } else { + // Sign out in case subscription is no longer active, reset the state + state.subscription.hasActiveSubscription = false + state.subscription.isSubscriptionPendingActivation = false + signOutUser() } - isLoadingSubscriptionState = false - - default: + + case .failure: // Account is active but there's not a valid subscription / entitlements - isLoadingSubscriptionState = false - signOutUser() + if await PurchaseManager.hasActiveSubscription() { + state.subscription.isSubscriptionPendingActivation = true + } else { + // Sign out in case access token is present but no subscription and there is no active transaction on Apple ID + signOutUser() + } } + } @available(iOS 15.0, *) @@ -420,7 +457,7 @@ extension SettingsViewModel { signOutObserver = NotificationCenter.default.addObserver(forName: .accountDidSignOut, object: nil, queue: .main) { [weak self] _ in if #available(iOS 15.0, *) { guard let strongSelf = self else { return } - Task { await strongSelf.setupSubscriptionEnvironment() } + Task { await strongSelf.refreshSubscriptionState() } } } } @@ -637,11 +674,11 @@ extension SettingsViewModel { // Specify cases that require .push presentation // Example: // case .dbp: - // return .push + // return .sheet case .netP: return .UIKitView default: - return .sheet + return .navigationLink } } } @@ -649,7 +686,7 @@ extension SettingsViewModel { // Define DeepLinkType outside the enum if not already defined enum DeepLinkType { case sheet - case navigation + case navigationLink case UIKitView } diff --git a/DuckDuckGo/Subscription/AsyncHeadlessWebview/HeadlessWebViewCoordinator.swift b/DuckDuckGo/Subscription/AsyncHeadlessWebview/HeadlessWebViewCoordinator.swift index d8fc66a461..5acd4df139 100644 --- a/DuckDuckGo/Subscription/AsyncHeadlessWebview/HeadlessWebViewCoordinator.swift +++ b/DuckDuckGo/Subscription/AsyncHeadlessWebview/HeadlessWebViewCoordinator.swift @@ -188,11 +188,22 @@ extension HeadlessWebViewCoordinator: WKNavigationDelegate { } func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: any Error) { - onNavigationError?(error) + handleWebViewError(error) + } func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { - onNavigationError?(error) + handleWebViewError(error) + } + + private func handleWebViewError(_ error: Error) { + let NSError = error as NSError + // Check for the specific NSURLErrorDomain and the cancelled error code -999 and ignore + if NSError.domain == NSURLErrorDomain && NSError.code == -999 { + return + } else { + onNavigationError?(error) + } } // Javascript Confirm dialogs Delegate diff --git a/DuckDuckGo/Subscription/Extensions/View+AppearModifiers.swift b/DuckDuckGo/Subscription/Extensions/View+AppearModifiers.swift new file mode 100644 index 0000000000..df8ce7bda2 --- /dev/null +++ b/DuckDuckGo/Subscription/Extensions/View+AppearModifiers.swift @@ -0,0 +1,70 @@ +// +// View+AppearModifiers.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 + +public struct OnFirstAppearModifier: ViewModifier { + + private let onFirstAppearAction: () -> Void + @State private var hasAppeared = false + + public init(_ onFirstAppearAction: @escaping () -> Void) { + self.onFirstAppearAction = onFirstAppearAction + } + + public func body(content: Content) -> some View { + content + .onAppear { + guard !hasAppeared else { return } + hasAppeared = true + onFirstAppearAction() + } + } +} + +public struct OnFirstDisappearModifier: ViewModifier { + + private let onFirstDisappearAction: () -> Void + @State private var hasDisappeared = false + + public init(_ onFirstDisappearAction: @escaping () -> Void) { + self.onFirstDisappearAction = onFirstDisappearAction + } + + public func body(content: Content) -> some View { + content + .onDisappear { + guard !hasDisappeared else { return } + hasDisappeared = true + onFirstDisappearAction() + } + } +} + +extension View { + + func onFirstAppear(_ onFirstAppearAction: @escaping () -> Void ) -> some View { + return modifier(OnFirstAppearModifier(onFirstAppearAction)) + } + + func onFirstDisappear(_ onFirstDisappearAction: @escaping () -> Void ) -> some View { + return modifier(OnFirstDisappearModifier(onFirstDisappearAction)) + } + +} diff --git a/DuckDuckGo/Subscription/Extensions/View+TopMostController.swift b/DuckDuckGo/Subscription/Extensions/View+TopMostController.swift deleted file mode 100644 index 2d26949767..0000000000 --- a/DuckDuckGo/Subscription/Extensions/View+TopMostController.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// View+TopMostController.swift -// DuckDuckGo -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import SwiftUI - -extension View { - - // Grabs the topMost controller so we can properly present sheets anywhere - func topMostViewController() -> UIViewController? { - guard let keyWindow = UIApplication.shared.connectedScenes - .filter({ $0.activationState == .foregroundActive }) - .compactMap({ $0 as? UIWindowScene }) - .first?.windows - .filter({ $0.isKeyWindow }).first else { - return nil - } - - var topController = keyWindow.rootViewController - while let presentedController = topController?.presentedViewController { - topController = presentedController - } - return topController - } -} diff --git a/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift b/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift index c22131fd2d..7845e5ee46 100644 --- a/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift +++ b/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift @@ -225,6 +225,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec // Check for active subscriptions if await PurchaseManager.hasActiveSubscription() { setTransactionError(.hasActiveSubscription) + Pixel.fire(pixel: .privacyProRestoreAfterPurchaseAttempt) return nil } @@ -248,7 +249,6 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec setTransactionError(.accountCreationFailed) case .activeSubscriptionAlreadyPresent: setTransactionError(.hasActiveSubscription) - Pixel.fire(pixel: .privacyProRestoreAfterPurchaseAttempt) default: setTransactionError(.purchaseFailed) } @@ -413,6 +413,10 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec setTransactionStatus(.idle) setTransactionError(nil) broker = nil + onFeatureSelected = nil + onSetSubscription = nil + onActivateSubscription = nil + onBackToSettings = nil } } diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift index 638c1d313d..dc46e7e153 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift @@ -37,24 +37,28 @@ final class SubscriptionEmailViewModel: ObservableObject { var viewTitle = UserText.subscriptionActivateEmailTitle var webViewModel: AsyncHeadlessWebViewViewModel + enum SelectedFeature { + case netP, dbp, itr, none + } + struct State { var subscriptionEmail: String? var managingSubscriptionEmail = false var transactionError: SubscriptionRestoreError? var shouldDisplaynavigationError: Bool = false - var shouldDisplayInactiveError: Bool = false + var isPresentingInactiveError: Bool = false var canNavigateBack: Bool = false var shouldDismissView: Bool = false - var shouldDismissStack: Bool = false var subscriptionActive: Bool = false + var backButtonTitle: String = UserText.backButtonTitle + var selectedFeature: SelectedFeature = .none + var shouldPopToSubscriptionSettings: Bool = false + var shouldPopToAppSettings: Bool = false } // Read only View State - Should only be modified from the VM @Published private(set) var state = State() - // Publish the currently selected feature - @Published var selectedFeature: SettingsViewModel.SettingsDeepLinkSection? - private static let allowedDomains = [ "duckduckgo.com" ] enum SubscriptionRestoreError: Error { @@ -65,8 +69,8 @@ final class SubscriptionEmailViewModel: ObservableObject { private var cancellables = Set() - init(userScript: SubscriptionPagesUserScript = SubscriptionPagesUserScript(), - subFeature: SubscriptionPagesUseSubscriptionFeature = SubscriptionPagesUseSubscriptionFeature(), + init(userScript: SubscriptionPagesUserScript, + subFeature: SubscriptionPagesUseSubscriptionFeature, accountManager: AccountManager = AccountManager()) { self.userScript = userScript self.subFeature = subFeature @@ -76,12 +80,6 @@ final class SubscriptionEmailViewModel: ObservableObject { settings: AsyncHeadlessWebViewSettings(bounces: false, allowedDomains: Self.allowedDomains, contentBlocking: false)) - - Task { - await initializeView() - await setupSubscribers() - } - setupObservers() } @MainActor @@ -89,7 +87,13 @@ final class SubscriptionEmailViewModel: ObservableObject { if state.canNavigateBack { await webViewModel.navigationCoordinator.goBack() } else { - state.shouldDismissView = true + // If not in the Welcome page, dismiss the view, otherwise, assume we + // came from Activation, so dismiss the entire stack + if webViewModel.url?.forComparison() != URL.subscriptionPurchase.forComparison() { + state.shouldDismissView = true + } else { + state.shouldPopToAppSettings = true + } } } @@ -97,13 +101,9 @@ final class SubscriptionEmailViewModel: ObservableObject { state.shouldDismissView = false } - func onAppear() { - Task { await initializeView() } - webViewModel.navigationCoordinator.navigateTo(url: emailURL ) - } - @MainActor - private func initializeView() { + func onFirstAppear() { + setupObservers() if accountManager.isUserAuthenticated { // If user is authenticated, we want to "Add or manage email" instead of activating emailURL = accountManager.email == nil ? URL.addEmailToSubscription : URL.manageSubscriptionEmail @@ -112,30 +112,37 @@ final class SubscriptionEmailViewModel: ObservableObject { // Also we assume subscription requires managing, and not activation state.managingSubscriptionEmail = true } + if webViewModel.url?.forComparison() != URL.subscriptionActivateSuccess { + self.webViewModel.navigationCoordinator.navigateTo(url: self.emailURL) + } } - private func setupSubscribers() async { + func onFirstDisappear() { + cancellables.removeAll() + canGoBackCancellable = nil + } + + private func setupObservers() { + + // Webview navigation canGoBackCancellable = webViewModel.$canGoBack .receive(on: DispatchQueue.main) .sink { [weak self] value in - self?.state.canNavigateBack = false - if self?.webViewModel.url != URL.activateSubscriptionViaEmail.forComparison() { - self?.state.canNavigateBack = value - } + self?.updateBackButton(canNavigateBack: value) } - } - - private func setupObservers() { + // Feature Callback subFeature.onSetSubscription = { + DailyPixel.fireDailyAndCount(pixel: .privacyProRestorePurchaseEmailSuccess) UniquePixel.fire(pixel: .privacyProSubscriptionActivated) DispatchQueue.main.async { self.state.subscriptionActive = true } + self.dismissStack() } subFeature.onBackToSettings = { - self.dismissView() + self.dismissStack() } subFeature.onFeatureSelected = { feature in @@ -143,15 +150,14 @@ final class SubscriptionEmailViewModel: ObservableObject { switch feature { case .netP: UniquePixel.fire(pixel: .privacyProWelcomeVPN) - self.selectedFeature = .netP + self.state.selectedFeature = .netP case .itr: UniquePixel.fire(pixel: .privacyProWelcomePersonalInformationRemoval) - self.selectedFeature = .itr + self.state.selectedFeature = .itr case .dbp: UniquePixel.fire(pixel: .privacyProWelcomeIdentityRestoration) - self.selectedFeature = .dbp + self.state.selectedFeature = .dbp } - self.state.shouldDismissStack = true } } @@ -166,7 +172,7 @@ final class SubscriptionEmailViewModel: ObservableObject { } } .store(in: &cancellables) - + webViewModel.$navigationError .receive(on: DispatchQueue.main) .sink { [weak self] error in @@ -179,14 +185,21 @@ final class SubscriptionEmailViewModel: ObservableObject { .store(in: &cancellables) } - func shouldDisplayBackButton() -> Bool { - // Hide the back button after activation - if state.subscriptionActive && - (webViewModel.url == URL.subscriptionActivateSuccess.forComparison() || - webViewModel.url == URL.subscriptionPurchase.forComparison()) { - return false + func updateBackButton(canNavigateBack: Bool) { + + // Disable Browser navigation by default + self.state.canNavigateBack = false + + // If the view is not Activation Success, or Welcome page, allow WebView Back Navigation + if self.webViewModel.url?.forComparison() != URL.subscriptionActivateSuccess.forComparison() && + self.webViewModel.url?.forComparison() != URL.subscriptionPurchase.forComparison() { + self.state.canNavigateBack = canNavigateBack + self.state.backButtonTitle = UserText.backButtonTitle + } else { + self.state.backButtonTitle = UserText.settingsTitle } - return true + + } // MARK: - @@ -199,11 +212,7 @@ final class SubscriptionEmailViewModel: ObservableObject { default: state.transactionError = .generalError } - state.shouldDisplayInactiveError = true - } - - private func completeActivation() { - DailyPixel.fireDailyAndCount(pixel: .privacyProRestorePurchaseEmailSuccess) + state.isPresentingInactiveError = true } func dismissView() { @@ -212,8 +221,15 @@ final class SubscriptionEmailViewModel: ObservableObject { } } + func dismissStack() { + DispatchQueue.main.async { + self.state.shouldPopToSubscriptionSettings = true + } + } + deinit { cancellables.removeAll() + canGoBackCancellable = nil } diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionExternalLinkViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionExternalLinkViewModel.swift index fd1c2557a7..f2f5feb45d 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionExternalLinkViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionExternalLinkViewModel.swift @@ -53,7 +53,7 @@ final class SubscriptionExternalLinkViewModel: ObservableObject { } } - func initializeView() { + func onFirstAppear() { Task { await setupSubscribers() } webViewModel.navigationCoordinator.navigateTo(url: url) diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift index caac67b827..518a9cd0e9 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift @@ -43,20 +43,20 @@ final class SubscriptionFlowViewModel: ObservableObject { static let navigationBarHideThreshold = 80.0 } - + enum SelectedFeature { + case netP, dbp, itr, none + } + struct State { var hasActiveSubscription = false var transactionStatus: SubscriptionTransactionStatus = .idle var userTappedRestoreButton = false - var shouldDismissView = false var shouldActivateSubscription = false - var shouldShowNavigationBar: Bool = false var canNavigateBack: Bool = false var transactionError: SubscriptionPurchaseError? + var shouldHideBackButton = false + var selectedFeature: SelectedFeature = .none } - - // Publish the currently selected feature - @Published var selectedFeature: SettingsViewModel.SettingsDeepLinkSection? // Read only View State - Should only be modified from the VM @Published private(set) var state = State() @@ -67,14 +67,13 @@ final class SubscriptionFlowViewModel: ObservableObject { allowedDomains: allowedDomains, contentBlocking: false) - init(userScript: SubscriptionPagesUserScript = SubscriptionPagesUserScript(), - subFeature: SubscriptionPagesUseSubscriptionFeature = SubscriptionPagesUseSubscriptionFeature(), + init(userScript: SubscriptionPagesUserScript, + subFeature: SubscriptionPagesUseSubscriptionFeature, purchaseManager: PurchaseManager = PurchaseManager.shared, selectedFeature: SettingsViewModel.SettingsDeepLinkSection? = nil) { self.userScript = userScript self.subFeature = subFeature self.purchaseManager = purchaseManager - self.selectedFeature = selectedFeature self.webViewModel = AsyncHeadlessWebViewViewModel(userScript: userScript, subFeature: subFeature, settings: webViewSettings) @@ -100,9 +99,7 @@ final class SubscriptionFlowViewModel: ObservableObject { subFeature.onActivateSubscription = { DispatchQueue.main.async { - self.state.shouldDismissView = true self.state.shouldActivateSubscription = true - } } @@ -111,15 +108,14 @@ final class SubscriptionFlowViewModel: ObservableObject { switch feature { case .netP: UniquePixel.fire(pixel: .privacyProWelcomeVPN) - self.selectedFeature = .netP - case .itr: - UniquePixel.fire(pixel: .privacyProWelcomePersonalInformationRemoval) - self.selectedFeature = .itr + self.state.selectedFeature = .netP case .dbp: + UniquePixel.fire(pixel: .privacyProWelcomePersonalInformationRemoval) + self.state.selectedFeature = .dbp + case .itr: UniquePixel.fire(pixel: .privacyProWelcomeIdentityRestoration) - self.selectedFeature = .dbp + self.state.selectedFeature = .itr } - self.state.shouldDismissView = true } } @@ -198,16 +194,6 @@ final class SubscriptionFlowViewModel: ObservableObject { // swiftlint:enable cyclomatic_complexity private func setupWebViewObservers() async { - webViewModel.$scrollPosition - .receive(on: DispatchQueue.main) - .sink { [weak self] value in - guard let strongSelf = self else { return } - DispatchQueue.main.async { - strongSelf.state.shouldShowNavigationBar = value.y > Constants.navigationBarHideThreshold - } - } - .store(in: &cancellables) - webViewModel.$navigationError .receive(on: DispatchQueue.main) .sink { [weak self] error in @@ -234,9 +220,25 @@ final class SubscriptionFlowViewModel: ObservableObject { } private func backButtonForURL(currentURL: URL) -> Bool { - return currentURL != URL.subscriptionBaseURL.forComparison() && - currentURL != URL.subscriptionActivateSuccess.forComparison() && - currentURL != URL.subscriptionPurchase.forComparison() + print(currentURL) + return currentURL.forComparison() != URL.subscriptionBaseURL.forComparison() && + currentURL.forComparison() != URL.subscriptionActivateSuccess.forComparison() && + currentURL.forComparison() != URL.subscriptionPurchase.forComparison() + } + + private func cleanUp() { + canGoBackCancellable?.cancel() + subFeature.cleanup() + cancellables.removeAll() + } + + @MainActor + func resetState() { + self.state = State() + } + + deinit { + cleanUp() } @MainActor @@ -248,41 +250,23 @@ final class SubscriptionFlowViewModel: ObservableObject { private func backButtonEnabled(_ enabled: Bool) { state.canNavigateBack = enabled } + + // MARK: - - func initializeViewData() async { - Pixel.fire(pixel: .privacyProOfferScreenImpression, debounce: 2) + func onFirstAppear() async { + DispatchQueue.main.async { + self.resetState() + } await self.setupTransactionObserver() await self .setupWebViewObservers() + if webViewModel.url == nil { + self.webViewModel.navigationCoordinator.navigateTo(url: self.purchaseURL) + } + Pixel.fire(pixel: .privacyProOfferScreenImpression) } - - @MainActor - func onAppear() { - resetState() - } - - @MainActor - func onDisappear() { - resetState() - } - - @MainActor - private func resetState() { - self.webViewModel.navigationCoordinator.navigateTo(url: self.purchaseURL ) - self.selectedFeature = nil - self.state.shouldDismissView = false - self.state.shouldActivateSubscription = false - } - - @MainActor - func finalizeSubscriptionFlow() { - self.state.shouldDismissView = true - } - - deinit { - canGoBackCancellable?.cancel() - selectedFeature = nil - subFeature.cleanup() - cancellables.removeAll() + + func onFirstDisappear() async { + cleanUp() } @MainActor @@ -311,6 +295,6 @@ final class SubscriptionFlowViewModel: ObservableObject { func clearTransactionError() { state.transactionError = nil } - + } #endif diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionITPViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionITPViewModel.swift index 99694a5468..6efa74f56d 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionITPViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionITPViewModel.swift @@ -30,10 +30,9 @@ final class SubscriptionITPViewModel: ObservableObject { var userScript: IdentityTheftRestorationPagesUserScript? var subFeature: IdentityTheftRestorationPagesFeature? var manageITPURL = URL.identityTheftRestoration - var viewTitle = UserText.subscriptionTitle + var viewTitle = UserText.settingsPProITRTitle enum Constants { - static let navigationBarHideThreshold = 15.0 static let downloadableContent = ["application/pdf"] static let blankURL = "about:blank" static let externalSchemes = ["tel", "sms", "facetime"] @@ -41,7 +40,6 @@ final class SubscriptionITPViewModel: ObservableObject { // State variables var itpURL = URL.identityTheftRestoration - @Published var shouldShowNavigationBar: Bool = false @Published var canNavigateBack: Bool = false @Published var isDownloadableContent: Bool = false @Published var activityItems: [Any] = [] @@ -55,11 +53,7 @@ final class SubscriptionITPViewModel: ObservableObject { } private var currentURL: URL? - private static let allowedDomains = [ - "duckduckgo.com", - "microsoftonline.com", - "duosecurity.com", - ] + private static let allowedDomains = [ "duckduckgo.com" ] private var externalLinksViewModel: SubscriptionExternalLinkViewModel? // Limit navigation to these external domains @@ -81,8 +75,7 @@ final class SubscriptionITPViewModel: ObservableObject { subFeature: subFeature, settings: webViewSettings) } - - // swiftlint:disable function_body_length + private func setupSubscribers() async { webViewModel.$navigationError @@ -96,14 +89,6 @@ final class SubscriptionITPViewModel: ObservableObject { } .store(in: &cancellables) - webViewModel.$scrollPosition - .receive(on: DispatchQueue.main) - .throttle(for: .milliseconds(100), scheduler: DispatchQueue.main, latest: true) - .sink { [weak self] value in - self?.shouldShowNavigationBar = (value.y > Constants.navigationBarHideThreshold) - } - .store(in: &cancellables) - webViewModel.$contentType .receive(on: DispatchQueue.main) .sink { [weak self] value in @@ -146,9 +131,9 @@ final class SubscriptionITPViewModel: ObservableObject { self?.canNavigateBack = value } } - // swiftlint:enable function_body_length + - func initializeView() { + func onFirstAppear() { webViewModel.navigationCoordinator.navigateTo(url: manageITPURL ) Task { await setupSubscribers() } Pixel.fire(pixel: .privacyProIdentityRestorationSettings) diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionPIRViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionPIRViewModel.swift index 75f6cf72ea..658dc2fff4 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionPIRViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionPIRViewModel.swift @@ -24,9 +24,9 @@ import Core @available(iOS 15.0, *) final class SubscriptionPIRViewModel: ObservableObject { - var viewTitle = UserText.subscriptionTitle + var viewTitle = UserText.settingsPProDBPTitle - func onAppear() { + func onFirstAppear() { Pixel.fire(pixel: .privacyProPersonalInformationRemovalSettings) } } diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift index 4ed77dc2a4..3a56f4cc29 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift @@ -43,14 +43,12 @@ final class SubscriptionRestoreViewModel: ObservableObject { var transactionStatus: SubscriptionTransactionStatus = .idle var activationResult: SubscriptionActivationResult = .unknown var subscriptionEmail: String? - var shouldShowWelcomePage = false - var shouldNavigateToActivationFlow = false + var isShowingWelcomePage = false + var isShowingActivationFlow = false var shouldShowPlans = false var shouldDismissView = false - - var viewTitle: String { - isAddingDevice ? UserText.subscriptionAddDeviceTitle : UserText.subscriptionActivate - } + var isLoading = false + var viewTitle: String = "" } // Publish the currently selected feature @@ -58,12 +56,9 @@ final class SubscriptionRestoreViewModel: ObservableObject { // Read only View State - Should only be modified from the VM @Published private(set) var state = State() - - // Email View Model - var emailViewModel = SubscriptionEmailViewModel() - init(userScript: SubscriptionPagesUserScript = SubscriptionPagesUserScript(), - subFeature: SubscriptionPagesUseSubscriptionFeature = SubscriptionPagesUseSubscriptionFeature(), + init(userScript: SubscriptionPagesUserScript, + subFeature: SubscriptionPagesUseSubscriptionFeature, purchaseManager: PurchaseManager = PurchaseManager.shared, accountManager: AccountManager = AccountManager(), isAddingDevice: Bool = false) { @@ -74,28 +69,56 @@ final class SubscriptionRestoreViewModel: ObservableObject { self.state.isAddingDevice = false } - func initializeView() { - Pixel.fire(pixel: .privacyProSettingsAddDevice) - Task { await setupTransactionObserver() } + func onFirstAppear() async { + DispatchQueue.main.async { + self.resetState() + } + await setupContent() + await setupTransactionObserver() + } + + func onFirstDisappear() async { + cleanUp() } - @MainActor - func onAppear() { - resetState() + private func cleanUp() { + cancellables.removeAll() + } + + private func setupContent() async { + if state.isAddingDevice { + DispatchQueue.main.async { + self.state.isLoading = true + } + Pixel.fire(pixel: .privacyProSettingsAddDevice) + guard let token = accountManager.accessToken else { return } + switch await accountManager.fetchAccountDetails(with: token) { + case .success(let details): + DispatchQueue.main.async { + self.state.subscriptionEmail = details.email + self.state.isLoading = false + self.state.viewTitle = UserText.subscriptionAddDeviceTitle + } + default: + state.isLoading = false + } + } else { + DispatchQueue.main.async { + self.state.viewTitle = UserText.subscriptionActivate + } + } } @MainActor private func resetState() { - state.subscriptionEmail = accountManager.email - state.isAddingDevice = false if accountManager.isUserAuthenticated { state.isAddingDevice = true } - state.shouldNavigateToActivationFlow = false + state.isShowingActivationFlow = false state.shouldShowPlans = false - state.shouldShowWelcomePage = false + state.isShowingWelcomePage = false state.shouldDismissView = false } @@ -161,14 +184,13 @@ final class SubscriptionRestoreViewModel: ObservableObject { @MainActor func showActivationFlow(_ visible: Bool) { if visible != state.shouldDismissView { - self.state.shouldNavigateToActivationFlow = visible + self.state.isShowingActivationFlow = visible } } @MainActor func showPlans() { state.shouldShowPlans = true - state.shouldDismissView = true } @MainActor diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionSettingsViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionSettingsViewModel.swift index 9b306c7bf3..d7d3134d2c 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionSettingsViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionSettingsViewModel.swift @@ -37,14 +37,14 @@ final class SubscriptionSettingsViewModel: ObservableObject { struct State { var subscriptionDetails: String = "" var subscriptionType: String = "" - var shouldDisplayRemovalNotice: Bool = false + var isShowingRemovalNotice: Bool = false var shouldDismissView: Bool = false - var shouldDisplayGoogleView: Bool = false - var shouldDisplayFAQView: Bool = false + var isShowingGoogleView: Bool = false + var isShowingFAQView: Bool = false // Used to display stripe WebUI var stripeViewModel: SubscriptionExternalLinkViewModel? - var shouldDisplayStripeView: Bool = false + var isShowingStripeView: Bool = false // Used to display the FAQ WebUI var FAQViewModel: SubscriptionExternalLinkViewModel = SubscriptionExternalLinkViewModel(url: URL.subscriptionFAQ) @@ -59,7 +59,6 @@ final class SubscriptionSettingsViewModel: ObservableObject { init(accountManager: AccountManager = AccountManager()) { self.accountManager = accountManager - Task { await fetchAndUpdateSubscriptionDetails() } setupSubscriptionUpdater() setupNotificationObservers() } @@ -70,21 +69,26 @@ final class SubscriptionSettingsViewModel: ObservableObject { return formatter }() - @MainActor - func fetchAndUpdateSubscriptionDetails(cachePolicy: SubscriptionService.CachePolicy = .returnCacheDataElseLoad) { + func onFirstAppear() { + fetchAndUpdateSubscriptionDetails() + } + + private func fetchAndUpdateSubscriptionDetails(cachePolicy: SubscriptionService.CachePolicy = .returnCacheDataElseLoad) { Task { guard let token = self.accountManager.accessToken else { return } let subscriptionResult = await SubscriptionService.getSubscription(accessToken: token, cachePolicy: cachePolicy) switch subscriptionResult { case .success(let subscription): subscriptionInfo = subscription - updateSubscriptionsStatusMessage(status: subscription.status, + await updateSubscriptionsStatusMessage(status: subscription.status, date: subscription.expiresOrRenewsAt, product: subscription.productId, billingPeriod: subscription.billingPeriod) case .failure: AccountManager().signOut() - state.shouldDismissView = true + DispatchQueue.main.async { + self.state.shouldDismissView = true + } } } } @@ -117,12 +121,11 @@ final class SubscriptionSettingsViewModel: ObservableObject { private func setupSubscriptionUpdater() { subscriptionUpdateTimer = Timer.scheduledTimer(withTimeInterval: 10.0, repeats: true) { [weak self] _ in guard let strongSelf = self else { return } - Task { - await strongSelf.fetchAndUpdateSubscriptionDetails(cachePolicy: .reloadIgnoringLocalCacheData) - } + strongSelf.fetchAndUpdateSubscriptionDetails(cachePolicy: .reloadIgnoringLocalCacheData) } } + @MainActor private func updateSubscriptionsStatusMessage(status: Subscription.Status, date: Date, product: String, billingPeriod: Subscription.BillingPeriod) { let statusString = (status == .autoRenewable) ? UserText.subscriptionRenews : UserText.subscriptionExpires state.subscriptionDetails = UserText.subscriptionInfo(status: statusString, expiration: dateFormatter.string(from: date)) @@ -137,26 +140,26 @@ final class SubscriptionSettingsViewModel: ObservableObject { } func displayGoogleView(_ value: Bool) { - if value != state.shouldDisplayGoogleView { - state.shouldDisplayGoogleView = value + if value != state.isShowingGoogleView { + state.isShowingGoogleView = value } } func displayStripeView(_ value: Bool) { - if value != state.shouldDisplayStripeView { - state.shouldDisplayStripeView = value + if value != state.isShowingStripeView { + state.isShowingStripeView = value } } func displayRemovalNotice(_ value: Bool) { - if value != state.shouldDisplayRemovalNotice { - state.shouldDisplayRemovalNotice = value + if value != state.isShowingRemovalNotice { + state.isShowingRemovalNotice = value } } func displayFAQView(_ value: Bool) { - if value != state.shouldDisplayFAQView { - state.shouldDisplayFAQView = value + if value != state.isShowingFAQView { + state.isShowingFAQView = value } } diff --git a/DuckDuckGo/Subscription/Views/RootPresentationMode.swift b/DuckDuckGo/Subscription/Views/RootPresentationMode.swift deleted file mode 100644 index e43fc4bc08..0000000000 --- a/DuckDuckGo/Subscription/Views/RootPresentationMode.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// RootPresentationMode.swift -// DuckDuckGo -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import SwiftUI - -/* - iOS15 does not support NavigationStack navigation so this creates a 'RootPresentationMode' - environment that views can use to create a binding for dismissal of the whole stack of views - See: https://stackoverflow.com/questions/57334455/how-can-i-pop-to-the-root-view-using-swiftui - */ -struct RootPresentationModeKey: EnvironmentKey { - static let defaultValue: Binding = .constant(RootPresentationMode()) -} - -extension EnvironmentValues { - var rootPresentationMode: Binding { - get { return self[RootPresentationModeKey.self] } - set { self[RootPresentationModeKey.self] = newValue } - } -} - -typealias RootPresentationMode = Bool - -extension RootPresentationMode { - - public mutating func dismiss() { - self.toggle() - } -} diff --git a/DuckDuckGo/Subscription/Views/SubscriptionContainerView.swift b/DuckDuckGo/Subscription/Views/SubscriptionContainerView.swift new file mode 100644 index 0000000000..037e36740f --- /dev/null +++ b/DuckDuckGo/Subscription/Views/SubscriptionContainerView.swift @@ -0,0 +1,62 @@ +// +// SubscriptionContainerView.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftUI + +#if SUBSCRIPTION +@available(iOS 15.0, *) +struct SubscriptionContainerView: View { + + enum CurrentView { + case subscribe, restore + } + + @Environment(\.dismiss) var dismiss + @EnvironmentObject var subscriptionNavigationCoordinator: SubscriptionNavigationCoordinator + @State private var currentViewState: CurrentView + private let flowViewModel: SubscriptionFlowViewModel + private let restoreViewModel: SubscriptionRestoreViewModel + private let emailViewModel: SubscriptionEmailViewModel + + init(currentView: CurrentView) { + _currentViewState = State(initialValue: currentView) + + let userScript = SubscriptionPagesUserScript() + let subFeature = SubscriptionPagesUseSubscriptionFeature() + flowViewModel = SubscriptionFlowViewModel(userScript: userScript, subFeature: subFeature) + restoreViewModel = SubscriptionRestoreViewModel(userScript: userScript, subFeature: subFeature) + emailViewModel = SubscriptionEmailViewModel(userScript: userScript, subFeature: subFeature) + } + + var body: some View { + VStack { + switch currentViewState { + case .subscribe: + SubscriptionFlowView(viewModel: flowViewModel, + currentView: $currentViewState).environmentObject(subscriptionNavigationCoordinator) + case .restore: + SubscriptionRestoreView(viewModel: restoreViewModel, + emailViewModel: emailViewModel, + currentView: $currentViewState).environmentObject(subscriptionNavigationCoordinator) + } + } + } +} +#endif diff --git a/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift b/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift index 99fe2c826a..6fc62f7643 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift @@ -21,18 +21,21 @@ import SwiftUI import Foundation import Core +import Combine @available(iOS 15.0, *) struct SubscriptionEmailView: View { - @StateObject var viewModel = SubscriptionEmailViewModel() + @StateObject var viewModel: SubscriptionEmailViewModel + @EnvironmentObject var subscriptionNavigationCoordinator: SubscriptionNavigationCoordinator @Environment(\.dismiss) var dismiss - - @State var shouldDisplayInactiveError = false - @State var shouldDisplayNavigationError = false - @State var isModal = true - - var onDismissStack: (() -> Void)? + + @State var isPresentingInactiveError = false + @State var isPresentingNavigationError = false + @State var backButtonText = UserText.backButtonTitle + @State private var isShowingITR = false + @State private var isShowingDBP = false + @State private var isShowingNetP = false enum Constants { static let navButtonPadding: CGFloat = 20.0 @@ -40,14 +43,23 @@ struct SubscriptionEmailView: View { } var body: some View { + // Hidden Navigation Links for Onboarding sections + NavigationLink(destination: NetworkProtectionRootView(inviteCompletion: {}).navigationViewStyle(.stack), + isActive: $isShowingNetP, + label: { EmptyView() }) + NavigationLink(destination: SubscriptionITPView().navigationViewStyle(.stack), + isActive: $isShowingITR, + label: { EmptyView() }) + NavigationLink(destination: SubscriptionPIRView().navigationViewStyle(.stack), + isActive: $isShowingDBP, + label: { EmptyView() }) + baseView + .toolbar { ToolbarItemGroup(placement: .navigationBarLeading) { browserBackButton } - ToolbarItem(placement: .navigationBarTrailing) { - closeButton - } } .navigationBarTitleDisplayMode(.inline) .navigationViewStyle(.stack) @@ -55,7 +67,7 @@ struct SubscriptionEmailView: View { .tint(Color.init(designSystemColor: .textPrimary)) .accentColor(Color.init(designSystemColor: .textPrimary)) - .alert(isPresented: $shouldDisplayInactiveError) { + .alert(isPresented: $isPresentingInactiveError) { Alert( title: Text(UserText.subscriptionRestoreEmailInactiveTitle), message: Text(UserText.subscriptionRestoreEmailInactiveMessage), @@ -65,7 +77,7 @@ struct SubscriptionEmailView: View { ) } - .alert(isPresented: $shouldDisplayNavigationError) { + .alert(isPresented: $isPresentingNavigationError) { Alert( title: Text(UserText.subscriptionBackendErrorTitle), message: Text(UserText.subscriptionBackendErrorMessage), @@ -73,49 +85,58 @@ struct SubscriptionEmailView: View { viewModel.dismissView() }) } - - .onAppear { - viewModel.onAppear() - } - .onChange(of: viewModel.state.shouldDisplayInactiveError) { value in - shouldDisplayInactiveError = value + .onChange(of: viewModel.state.isPresentingInactiveError) { value in + isPresentingInactiveError = value } .onChange(of: viewModel.state.shouldDisplaynavigationError) { value in - shouldDisplayNavigationError = value + isPresentingNavigationError = value } // Observe changes to shouldDismissView .onChange(of: viewModel.state.shouldDismissView) { shouldDismiss in if shouldDismiss { dismiss() - - // Reset shouldDismissView after dismissal to ensure it can trigger again - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - viewModel.resetDismissalState() - } + } + } + + .onChange(of: viewModel.state.shouldPopToSubscriptionSettings) { shouldDismiss in + if shouldDismiss { + subscriptionNavigationCoordinator.shouldPopToSubscriptionSettings = true + } + } + + .onChange(of: viewModel.state.shouldPopToAppSettings) { shouldDismiss in + if shouldDismiss { + subscriptionNavigationCoordinator.shouldPopToAppSettings = true + } + } + + .onChange(of: viewModel.state.selectedFeature) { feature in + switch feature { + case .dbp: + self.isShowingDBP = true + case .itr: + self.isShowingITR = true + case .netP: + self.isShowingNetP = true + default: + break } } .navigationTitle(viewModel.viewTitle) - .onAppear(perform: { + .onFirstAppear { setUpAppearances() - viewModel.onAppear() - }) + viewModel.onFirstAppear() + } } // MARK: - - @ViewBuilder - private var closeButton: some View { - if isModal { - Button(UserText.subscriptionCloseButton) { onDismissStack?() } - } - } - private var baseView: some View { ZStack { VStack { @@ -127,16 +148,14 @@ struct SubscriptionEmailView: View { @ViewBuilder private var browserBackButton: some View { - if viewModel.shouldDisplayBackButton() { - Button(action: { - Task { await viewModel.navigateBack() } - }, label: { - HStack(spacing: 0) { - Image(systemName: Constants.backButtonImage) - Text(UserText.backButtonTitle).foregroundColor(Color(designSystemColor: .textPrimary)) - } - }) - } + Button(action: { + Task { await viewModel.navigateBack() } + }, label: { + HStack(spacing: 0) { + Image(systemName: Constants.backButtonImage) + Text(viewModel.state.backButtonTitle).foregroundColor(Color(designSystemColor: .textPrimary)) + } + }) } private func setUpAppearances() { diff --git a/DuckDuckGo/Subscription/Views/SubscriptionExternalLinkView.swift b/DuckDuckGo/Subscription/Views/SubscriptionExternalLinkView.swift index 5edfcc0155..4b5af9a3d5 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionExternalLinkView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionExternalLinkView.swift @@ -50,10 +50,10 @@ struct SubscriptionExternalLinkView: View { .navigationViewStyle(.stack) .navigationTitle(title ?? "") - .onAppear(perform: { + .onFirstAppear { setUpAppearances() - viewModel.initializeView() - }) + viewModel.onFirstAppear() + } }.tint(Color(designSystemColor: .textPrimary)) } diff --git a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift index 99ec18a506..3f8d3fe006 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift @@ -25,13 +25,20 @@ import Core @available(iOS 15.0, *) struct SubscriptionFlowView: View { - + @Environment(\.dismiss) var dismiss - @StateObject var viewModel = SubscriptionFlowViewModel() - + @EnvironmentObject var subscriptionNavigationCoordinator: SubscriptionNavigationCoordinator + @StateObject var viewModel: SubscriptionFlowViewModel + + @State private var isPurchaseInProgress = false + @State private var isShowingITR = false + @State private var isShowingDBP = false + @State private var isShowingNetP = false + @Binding var currentView: SubscriptionContainerView.CurrentView + // Local View State @State private var errorMessage: SubscriptionErrorMessage = .general - @State private var shouldPresentError: Bool = false + @State private var isPresentingError: Bool = false enum Constants { static let daxLogo = "Home" @@ -49,38 +56,38 @@ struct SubscriptionFlowView: View { } var body: some View { - NavigationView { - baseView - .toolbar { - ToolbarItemGroup(placement: .navigationBarLeading) { - backButton - } - ToolbarItem(placement: .principal) { - HStack { - Image(Constants.daxLogo) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: Constants.daxLogoSize, height: Constants.daxLogoSize) - Text(viewModel.viewTitle).daxBodyRegular() - } + + // Hidden Navigation Links for Onboarding sections + NavigationLink(destination: NetworkProtectionRootView(inviteCompletion: {}).navigationViewStyle(.stack), + isActive: $isShowingNetP, + label: { EmptyView() }) + NavigationLink(destination: SubscriptionITPView().navigationViewStyle(.stack), + isActive: $isShowingITR, + label: { EmptyView() }) + NavigationLink(destination: SubscriptionPIRView().navigationViewStyle(.stack), + isActive: $isShowingDBP, + label: { EmptyView() }) + + baseView + .toolbar { + ToolbarItemGroup(placement: .navigationBarLeading) { + backButton + } + ToolbarItem(placement: .principal) { + HStack { + Image(Constants.daxLogo) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: Constants.daxLogoSize, height: Constants.daxLogoSize) + Text(viewModel.viewTitle).daxBodyRegular() } } - .edgesIgnoringSafeArea(.top) - .navigationBarTitleDisplayMode(.inline) - .navigationBarHidden(!viewModel.state.shouldShowNavigationBar).animation(.easeOut) - } - .applyInsetGroupedListStyle() - .tint(Color(designSystemColor: .textPrimary)) - } - - @ViewBuilder - private var dismissButton: some View { - Button(action: { - viewModel.finalizeSubscriptionFlow() - }, label: { Text(UserText.subscriptionCloseButton) }) - .padding(Constants.navButtonPadding) - .contentShape(Rectangle()) - .tint(Color(designSystemColor: .textPrimary)) + } + .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(viewModel.state.canNavigateBack || viewModel.subFeature.transactionStatus != .idle) + .interactiveDismissDisabled(viewModel.subFeature.transactionStatus != .idle) + .edgesIgnoringSafeArea(.bottom) + .tint(Color(designSystemColor: .textPrimary)) } @ViewBuilder @@ -116,27 +123,32 @@ struct SubscriptionFlowView: View { private var baseView: some View { ZStack(alignment: .top) { webView - - // Show a dismiss button while the bar is not visible - // But it should be hidden while performing a transaction - if !viewModel.state.shouldShowNavigationBar && viewModel.state.transactionStatus == .idle { - HStack { - backButton.padding(.leading, Constants.navButtonPadding) - Spacer() - dismissButton - } + } + + .onChange(of: viewModel.state.selectedFeature) { feature in + switch feature { + case .dbp: + self.isShowingDBP = true + case .itr: + self.isShowingITR = true + case .netP: + self.isShowingNetP = true + default: + break } } - .onChange(of: viewModel.state.shouldDismissView) { result in + .onChange(of: viewModel.state.shouldActivateSubscription) { result in if result { - dismiss() + withAnimation { + currentView = .restore + } } } .onChange(of: viewModel.state.transactionError) { value in - if !shouldPresentError { + if !isPresentingError { let displayError: Bool = { switch value { case .hasActiveSubscription: @@ -154,30 +166,29 @@ struct SubscriptionFlowView: View { }() if displayError { - shouldPresentError = true + isPresentingError = true } } } - .onAppear(perform: { + .onFirstAppear { setUpAppearances() - Task { await viewModel.initializeViewData() } - viewModel.onAppear() - }) + Task { await viewModel.onFirstAppear() } + } - .onDisappear(perform: { - viewModel.onDisappear() - }) + .onFirstDisappear { + Task { await viewModel.onFirstDisappear() } + } + + .onAppear { + Task { await viewModel.onFirstAppear() } + } - .alert(isPresented: $shouldPresentError) { + .alert(isPresented: $isPresentingError) { getAlert(error: self.errorMessage) } - // The trailing close button should be hidden when a transaction is in progress - .navigationBarItems(trailing: viewModel.state.transactionStatus == .idle - ? Button(UserText.subscriptionCloseButton) { viewModel.finalizeSubscriptionFlow() } - : nil) } private func getAlert(error: SubscriptionErrorMessage) -> Alert { @@ -189,7 +200,7 @@ struct SubscriptionFlowView: View { message: Text(UserText.subscriptionFoundText), primaryButton: .cancel(Text(UserText.subscriptionFoundCancel)) { viewModel.clearTransactionError() - viewModel.finalizeSubscriptionFlow() + dismiss() }, secondaryButton: .default(Text(UserText.subscriptionFoundRestore)) { viewModel.restoreAppstoreTransaction() @@ -200,7 +211,8 @@ struct SubscriptionFlowView: View { title: Text(UserText.subscriptionAppStoreErrorTitle), message: Text(UserText.subscriptionAppStoreErrorMessage), dismissButton: .cancel(Text(UserText.actionOK)) { - Task { await viewModel.initializeViewData() } + viewModel.clearTransactionError() + dismiss() } ) case .backend, .general: @@ -208,7 +220,8 @@ struct SubscriptionFlowView: View { title: Text(UserText.subscriptionBackendErrorTitle), message: Text(UserText.subscriptionBackendErrorMessage), dismissButton: .cancel(Text(UserText.subscriptionBackendErrorButton)) { - viewModel.finalizeSubscriptionFlow() + viewModel.clearTransactionError() + dismiss() } ) } diff --git a/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift b/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift index e411c109fd..5904854dc2 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift @@ -51,40 +51,35 @@ struct SubscriptionITPView: View { } var body: some View { - NavigationView { - baseView - .toolbar { - ToolbarItemGroup(placement: .navigationBarLeading) { - backButton - } - ToolbarItem(placement: .principal) { - HStack { - Image(Constants.daxLogo) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: Constants.daxLogoSize, height: Constants.daxLogoSize) - Text(viewModel.viewTitle).daxBodyRegular() - } - } - ToolbarItem(placement: .navigationBarTrailing) { - shareButton - } - ToolbarItem(placement: .navigationBarTrailing) { - Button(UserText.subscriptionCloseButton) { dismiss() } + + baseView + + .toolbar { + ToolbarItemGroup(placement: .navigationBarLeading) { + backButton + } + ToolbarItem(placement: .principal) { + HStack { + Image(Constants.daxLogo) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: Constants.daxLogoSize, height: Constants.daxLogoSize) + Text(viewModel.viewTitle).daxBodyRegular() } } - .edgesIgnoringSafeArea(.all) - .navigationBarTitleDisplayMode(.inline) - .navigationBarHidden(!viewModel.shouldShowNavigationBar && !viewModel.isDownloadableContent).animation(.snappy) - - .onAppear(perform: { - setUpAppearances() - viewModel.initializeView() - }) - + ToolbarItem(placement: .navigationBarTrailing) { + shareButton + } } + .edgesIgnoringSafeArea(.bottom) + .navigationBarBackButtonHidden(viewModel.canNavigateBack) + .navigationBarTitleDisplayMode(.inline) .tint(Color(designSystemColor: .textPrimary)) + .onFirstAppear { + viewModel.onFirstAppear() + setUpAppearances() + } .alert(isPresented: $viewModel.navigationError) { Alert( @@ -109,17 +104,6 @@ struct SubscriptionITPView: View { private var baseView: some View { ZStack(alignment: .top) { webView - - // Show a dismiss button while the bar is not visible - // But it should be hidden while performing a transaction - if !shouldShowNavigationBar { - HStack { - backButton.padding(.leading, Constants.navButtonPadding) - Spacer() - dismissButton - } - } - } } @@ -157,15 +141,6 @@ struct SubscriptionITPView: View { } } - @ViewBuilder - private var dismissButton: some View { - Button(action: { dismiss() }, label: { Text(UserText.subscriptionCloseButton) }) - .padding(Constants.navButtonPadding) - .contentShape(Rectangle()) - .tint(Color(designSystemColor: .textPrimary)) - } - - private func setUpAppearances() { let navAppearance = UINavigationBar.appearance() navAppearance.backgroundColor = UIColor(designSystemColor: .surface) diff --git a/DuckDuckGo/Subscription/Views/SubscriptionNavigationCoordinator.swift b/DuckDuckGo/Subscription/Views/SubscriptionNavigationCoordinator.swift new file mode 100644 index 0000000000..b8fe1f7867 --- /dev/null +++ b/DuckDuckGo/Subscription/Views/SubscriptionNavigationCoordinator.swift @@ -0,0 +1,25 @@ +// +// SubscriptionNavigationCoordinator.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 + +final class SubscriptionNavigationCoordinator: ObservableObject { + @Published var shouldPopToSubscriptionSettings: Bool = false + @Published var shouldPopToAppSettings: Bool = false +} diff --git a/DuckDuckGo/Subscription/Views/SubscriptionPIRView.swift b/DuckDuckGo/Subscription/Views/SubscriptionPIRView.swift index 6911051167..9756c040ca 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionPIRView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionPIRView.swift @@ -50,42 +50,32 @@ struct SubscriptionPIRView: View { } var body: some View { - NavigationView { - ZStack { - gradientBackground - ScrollView { - VStack { - header - .padding(.top, Constants.headerPadding) - baseView - .frame(maxWidth: 600) - } + ZStack { + gradientBackground + ScrollView { + VStack { + baseView + .frame(maxWidth: 600) } - } - .edgesIgnoringSafeArea(.all) - }.onAppear(perform: { - viewModel.onAppear() - }) - } - - private var header: some View { - GeometryReader { geometry in - HStack { - Spacer().frame(width: geometry.size.width / 3) - HStack(alignment: .center) { + + } + .toolbar { + ToolbarItem(placement: .principal) { + HStack { Image(Constants.daxLogo) .resizable() .aspectRatio(contentMode: .fit) .frame(width: Constants.daxLogoSize, height: Constants.daxLogoSize) Text(viewModel.viewTitle).daxBodyRegular() } - .frame(width: geometry.size.width / 3, alignment: .center) - dismissButton - .frame(width: geometry.size.width / 3, alignment: .trailing) } } + .onFirstAppear { + viewModel.onFirstAppear() + } } + private var gradientBackground: some View { ZStack { diff --git a/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift b/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift index af5c7941d8..f010b8aef0 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionRestoreView.swift @@ -27,14 +27,17 @@ import Core @available(iOS 15.0, *) // swiftlint:disable type_body_length struct SubscriptionRestoreView: View { - + @Environment(\.dismiss) var dismiss - @StateObject var viewModel = SubscriptionRestoreViewModel() + + @EnvironmentObject var subscriptionNavigationCoordinator: SubscriptionNavigationCoordinator + @StateObject var viewModel: SubscriptionRestoreViewModel + @StateObject var emailViewModel: SubscriptionEmailViewModel @State private var isAlertVisible = false - @State private var shouldShowWelcomePage = false - @State private var shouldNavigateToActivationFlow = false - @State var isModal = true + @State private var isShowingWelcomePage = false + @State private var isShowingActivationFlow = false + @Binding var currentView: SubscriptionContainerView.CurrentView private enum Constants { static let heroImage = "ManageSubscriptionHero" @@ -61,7 +64,6 @@ struct SubscriptionRestoreView: View { static let buttonCornerRadius = 8.0 static let buttonInsets = EdgeInsets(top: 10.0, leading: 16.0, bottom: 10.0, trailing: 16.0) static let buttonTopPadding: CGFloat = 20 - } var body: some View { @@ -71,10 +73,8 @@ struct SubscriptionRestoreView: View { } } else { ZStack { + baseView - NavigationView { - baseView - } if viewModel.state.transactionStatus != .idle { PurchaseInProgressView(status: getTransactionStatus()) } @@ -82,10 +82,8 @@ struct SubscriptionRestoreView: View { } } } - - @ViewBuilder - private var baseView: some View { - + + private var contentView: some View { Group { ScrollView { VStack(spacing: Constants.sectionSpacing) { @@ -95,10 +93,8 @@ struct SubscriptionRestoreView: View { Spacer() // Hidden link to display Email Activation View - NavigationLink(destination: SubscriptionEmailView(viewModel: viewModel.emailViewModel, - isModal: isModal, - onDismissStack: { viewModel.dismissView() }), - isActive: $shouldNavigateToActivationFlow) { + NavigationLink(destination: SubscriptionEmailView(viewModel: emailViewModel).environmentObject(subscriptionNavigationCoordinator), + isActive: $isShowingActivationFlow) { EmptyView() }.isDetailLink(false) @@ -113,57 +109,57 @@ struct SubscriptionRestoreView: View { .navigationBarBackButtonHidden(viewModel.state.transactionStatus != .idle) .navigationBarTitleDisplayMode(.inline) .applyInsetGroupedListStyle() - .navigationBarItems(trailing: closeButton) + .interactiveDismissDisabled(viewModel.subFeature.transactionStatus != .idle) .tint(Color.init(designSystemColor: .textPrimary)) .accentColor(Color.init(designSystemColor: .textPrimary)) } - - .alert(isPresented: $isAlertVisible) { getAlert() } - - .onChange(of: viewModel.state.activationResult) { result in - if result != .unknown { - isAlertVisible = true + } + + @ViewBuilder + private var baseView: some View { + + contentView + .alert(isPresented: $isAlertVisible) { getAlert() } + + .onChange(of: viewModel.state.activationResult) { result in + if result != .unknown { + isAlertVisible = true + } } - } - - // Navigation Flow Binding - .onChange(of: viewModel.state.shouldNavigateToActivationFlow) { result in - shouldNavigateToActivationFlow = result - } - .onChange(of: shouldNavigateToActivationFlow) { result in - viewModel.showActivationFlow(result) - } - - .onChange(of: viewModel.state.shouldDismissView) { result in - if result { - dismiss() + + // Navigation Flow Binding + .onChange(of: viewModel.state.isShowingActivationFlow) { result in + isShowingActivationFlow = result } - } - - .onChange(of: viewModel.state.shouldShowPlans) { result in - if result { - dismiss() + .onChange(of: isShowingActivationFlow) { result in + viewModel.showActivationFlow(result) + } + + .onChange(of: viewModel.state.shouldDismissView) { result in + if result { + dismiss() + } + } + + .onChange(of: viewModel.state.shouldShowPlans) { result in + if result { + currentView = .subscribe + } } - } - .onAppear { - viewModel.initializeView() - viewModel.onAppear() - setUpAppearances() - } + .onFirstAppear { + Task { await viewModel.onFirstAppear() } + setUpAppearances() + } + + .onFirstDisappear { + Task { await viewModel.onFirstDisappear() } + } } - - + // MARK: - - @ViewBuilder - private var closeButton: some View { - if isModal { - Button(UserText.subscriptionCloseButton) { viewModel.dismissView() } - } - } - private var emailView: some View { emailCellContent .padding(Constants.boxPadding) @@ -185,37 +181,39 @@ struct SubscriptionRestoreView: View { .foregroundColor(Color(designSystemColor: .textPrimary)) } - VStack(alignment: .leading) { - if !viewModel.state.isAddingDevice { - Text(UserText.subscriptionActivateEmailDescription) - .daxSubheadRegular() - .foregroundColor(Color(designSystemColor: .textSecondary)) - getCellButton(buttonText: UserText.subscriptionActivateEmailButton, - action: { - DailyPixel.fireDailyAndCount(pixel: .privacyProRestorePurchaseEmailStart) - DailyPixel.fire(pixel: .privacyProWelcomeAddDevice) - viewModel.showActivationFlow(true) - }) - } else if viewModel.state.subscriptionEmail == nil { - Text(UserText.subscriptionAddDeviceEmailDescription) - .daxSubheadRegular() - .foregroundColor(Color(designSystemColor: .textSecondary)) - getCellButton(buttonText: UserText.subscriptionRestoreAddEmailButton, - action: { - Pixel.fire(pixel: .privacyProAddDeviceEnterEmail, debounce: 1) - viewModel.showActivationFlow(true) - }) - } else { - Text(viewModel.state.subscriptionEmail ?? "").daxSubheadSemibold() - Text(UserText.subscriptionManageEmailDescription) - .daxSubheadRegular() - .foregroundColor(Color(designSystemColor: .textSecondary)) - HStack { - getCellButton(buttonText: UserText.subscriptionManageEmailButton, + if !viewModel.state.isLoading { + VStack(alignment: .leading) { + if !viewModel.state.isAddingDevice { + Text(UserText.subscriptionActivateEmailDescription) + .daxSubheadRegular() + .foregroundColor(Color(designSystemColor: .textSecondary)) + getCellButton(buttonText: UserText.subscriptionActivateEmailButton, + action: { + DailyPixel.fireDailyAndCount(pixel: .privacyProRestorePurchaseEmailStart) + DailyPixel.fire(pixel: .privacyProWelcomeAddDevice) + viewModel.showActivationFlow(true) + }) + } else if viewModel.state.subscriptionEmail == nil { + Text(UserText.subscriptionAddDeviceEmailDescription) + .daxSubheadRegular() + .foregroundColor(Color(designSystemColor: .textSecondary)) + getCellButton(buttonText: UserText.subscriptionRestoreAddEmailButton, action: { - Pixel.fire(pixel: .privacyProSubscriptionManagementEmail, debounce: 1) + Pixel.fire(pixel: .privacyProAddDeviceEnterEmail, debounce: 1) viewModel.showActivationFlow(true) }) + } else { + Text(viewModel.state.subscriptionEmail ?? "").daxSubheadSemibold() + Text(UserText.subscriptionManageEmailDescription) + .daxSubheadRegular() + .foregroundColor(Color(designSystemColor: .textSecondary)) + HStack { + getCellButton(buttonText: UserText.subscriptionManageEmailButton, + action: { + Pixel.fire(pixel: .privacyProSubscriptionManagementEmail, debounce: 1) + viewModel.showActivationFlow(true) + }) + } } } } diff --git a/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift b/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift index 098ca6aa6b..e93ca2e55f 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift @@ -22,23 +22,19 @@ import SwiftUI import DesignResourcesKit import Core -class SceneEnvironment: ObservableObject { - weak var windowScene: UIWindowScene? -} - #if SUBSCRIPTION @available(iOS 15.0, *) struct SubscriptionSettingsView: View { - - @Environment(\.presentationMode) var presentationMode + @Environment(\.dismiss) var dismiss @StateObject var viewModel = SubscriptionSettingsViewModel() - @StateObject var sceneEnvironment = SceneEnvironment() + @EnvironmentObject var subscriptionNavigationCoordinator: SubscriptionNavigationCoordinator - @State var shouldDisplayStripeView = false - @State var shouldDisplayGoogleView = false - @State var shouldDisplayRemovalNotice = false - @State var shouldDisplayFAQView = false + @State var isShowingStripeView = false + @State var isShowingGoogleView = false + @State var isShowingRemovalNotice = false + @State var isShowingFAQView = false + @State var isShowingRestoreView = false var body: some View { optionsView @@ -78,7 +74,7 @@ struct SubscriptionSettingsView: View { Task { viewModel.manageSubscription() } }, isButton: true) - .sheet(isPresented: $shouldDisplayStripeView) { + .sheet(isPresented: $isShowingStripeView) { if let stripeViewModel = viewModel.state.stripeViewModel { SubscriptionExternalLinkView(viewModel: stripeViewModel, title: UserText.subscriptionManagePlan) } @@ -89,12 +85,14 @@ struct SubscriptionSettingsView: View { private var devicesSection: some View { Section(header: Text(UserText.subscriptionManageDevices)) { - NavigationLink(destination: SubscriptionRestoreView(isModal: false)) { + NavigationLink(destination: SubscriptionContainerView(currentView: .restore) + .environmentObject(subscriptionNavigationCoordinator), + isActive: $isShowingRestoreView) { SettingsCustomCell(content: { Text(UserText.subscriptionAddDeviceButton) .daxBodyRegular() }) - } + }.isDetailLink(false) SettingsCustomCell(content: { Text(UserText.subscriptionRemoveFromDevice) @@ -126,7 +124,7 @@ struct SubscriptionSettingsView: View { @ViewBuilder private var optionsView: some View { NavigationLink(destination: SubscriptionGoogleView(), - isActive: $shouldDisplayGoogleView) { + isActive: $isShowingGoogleView) { EmptyView() } @@ -147,40 +145,46 @@ struct SubscriptionSettingsView: View { } // Google Binding - .onChange(of: viewModel.state.shouldDisplayGoogleView) { value in - shouldDisplayGoogleView = value + .onChange(of: viewModel.state.isShowingGoogleView) { value in + isShowingGoogleView = value } - .onChange(of: shouldDisplayGoogleView) { value in + .onChange(of: isShowingGoogleView) { value in viewModel.displayGoogleView(value) } // Stripe Binding - .onChange(of: viewModel.state.shouldDisplayStripeView) { value in - shouldDisplayStripeView = value + .onChange(of: viewModel.state.isShowingStripeView) { value in + isShowingStripeView = value } - .onChange(of: shouldDisplayStripeView) { value in + .onChange(of: isShowingStripeView) { value in viewModel.displayStripeView(value) } // Removal Notice - .onChange(of: viewModel.state.shouldDisplayRemovalNotice) { value in - shouldDisplayRemovalNotice = value + .onChange(of: viewModel.state.isShowingRemovalNotice) { value in + isShowingRemovalNotice = value } - .onChange(of: shouldDisplayRemovalNotice) { value in + .onChange(of: isShowingRemovalNotice) { value in viewModel.displayRemovalNotice(value) } // Removal Notice - .onChange(of: viewModel.state.shouldDisplayFAQView) { value in - shouldDisplayFAQView = value + .onChange(of: viewModel.state.isShowingFAQView) { value in + isShowingFAQView = value } - .onChange(of: shouldDisplayFAQView) { value in + .onChange(of: isShowingFAQView) { value in viewModel.displayFAQView(value) } + + .onReceive(subscriptionNavigationCoordinator.$shouldPopToSubscriptionSettings) { shouldDismiss in + if shouldDismiss { + isShowingRestoreView = false + } + } // Remove subscription - .alert(isPresented: $shouldDisplayRemovalNotice) { + .alert(isPresented: $isShowingRemovalNotice) { Alert( title: Text(UserText.subscriptionRemoveFromDeviceConfirmTitle), message: Text(UserText.subscriptionRemoveFromDeviceConfirmText), @@ -189,17 +193,17 @@ struct SubscriptionSettingsView: View { secondaryButton: .destructive(Text(UserText.subscriptionRemove)) { Pixel.fire(pixel: .privacyProSubscriptionManagementRemoval) viewModel.removeSubscription() - presentationMode.wrappedValue.dismiss() + dismiss() } ) } - .sheet(isPresented: $shouldDisplayFAQView, content: { + .sheet(isPresented: $isShowingFAQView, content: { SubscriptionExternalLinkView(viewModel: viewModel.state.FAQViewModel, title: UserText.subscriptionFAQ) }) - .onAppear { - viewModel.fetchAndUpdateSubscriptionDetails() + .onFirstAppear { + viewModel.onFirstAppear() } } @@ -214,18 +218,6 @@ struct SubscriptionSettingsView: View { } #endif - -#if SUBSCRIPTION && DEBUG -@available(iOS 15.0, *) - -struct SubscriptionSettingsView_Previews: PreviewProvider { - static var previews: some View { - NavigationView { - SubscriptionSettingsView().navigationBarTitleDisplayMode(.inline) - } - } -} - // Commented out because CI fails if a SwiftUI preview is enabled https://app.asana.com/0/414709148257752/1206774081310425/f // @available(iOS 15.0, *) // struct SubscriptionSettingsView_Previews: PreviewProvider { @@ -233,5 +225,3 @@ struct SubscriptionSettingsView_Previews: PreviewProvider { // SubscriptionSettingsView() // } // } - -#endif diff --git a/DuckDuckGo/SyncSettingsViewController.swift b/DuckDuckGo/SyncSettingsViewController.swift index 7db8cd9667..c3ed122ced 100644 --- a/DuckDuckGo/SyncSettingsViewController.swift +++ b/DuckDuckGo/SyncSettingsViewController.swift @@ -31,6 +31,7 @@ class SyncSettingsViewController: UIHostingController { let syncService: DDGSyncing let syncBookmarksAdapter: SyncBookmarksAdapter + let syncCredentialsAdapter: SyncCredentialsAdapter var connector: RemoteConnecting? let userAuthenticator = UserAuthenticator(reason: UserText.syncUserUserAuthenticationReason) @@ -55,9 +56,15 @@ class SyncSettingsViewController: UIHostingController { var cancellables = Set() // For some reason, on iOS 14, the viewDidLoad wasn't getting called so do some setup here - init(syncService: DDGSyncing, syncBookmarksAdapter: SyncBookmarksAdapter, appSettings: AppSettings = AppDependencyProvider.shared.appSettings) { + init( + syncService: DDGSyncing, + syncBookmarksAdapter: SyncBookmarksAdapter, + syncCredentialsAdapter: SyncCredentialsAdapter, + appSettings: AppSettings = AppDependencyProvider.shared.appSettings + ) { self.syncService = syncService self.syncBookmarksAdapter = syncBookmarksAdapter + self.syncCredentialsAdapter = syncCredentialsAdapter let viewModel = SyncSettingsViewModel( isOnDevEnvironment: { syncService.serverEnvironment == .development }, @@ -72,6 +79,9 @@ class SyncSettingsViewController: UIHostingController { setUpFaviconsFetcherSwitch(viewModel) setUpFavoritesDisplayModeSwitch(viewModel, appSettings) setUpSyncPaused(viewModel, appSettings) + if DDGSync.isFieldValidationEnabled { + setUpSyncInvalidObjectsInfo(viewModel) + } setUpSyncFeatureFlags(viewModel) refreshForState(syncService.authState) @@ -186,6 +196,27 @@ class SyncSettingsViewController: UIHostingController { .store(in: &cancellables) } + private func setUpSyncInvalidObjectsInfo(_ viewModel: SyncSettingsViewModel) { + syncService.isSyncInProgressPublisher + .removeDuplicates() + .filter { !$0 } + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.updateInvalidObjects(viewModel) + } + .store(in: &cancellables) + } + + private func updateInvalidObjects(_ viewModel: SyncSettingsViewModel) { + viewModel.invalidBookmarksTitles = syncBookmarksAdapter.provider? + .fetchDescriptionsForObjectsThatFailedValidation() + .map { $0.truncated(length: 15) } ?? [] + + let invalidCredentialsObjects: [String] = (try? syncCredentialsAdapter.provider?.fetchDescriptionsForObjectsThatFailedValidation()) ?? [] + viewModel.invalidCredentialsTitles = invalidCredentialsObjects.map({ $0.truncated(length: 15) }) + } + + override func viewDidLoad() { super.viewDidLoad() applyTheme(ThemeManager.shared.currentTheme) diff --git a/DuckDuckGo/TabSwitcherButton.swift b/DuckDuckGo/TabSwitcherButton.swift index c3e888ba86..27a4f6f2d5 100644 --- a/DuckDuckGo/TabSwitcherButton.swift +++ b/DuckDuckGo/TabSwitcherButton.swift @@ -48,7 +48,7 @@ class TabSwitcherButton: UIView { var workItem: DispatchWorkItem? - let anim = AnimationView(name: "new_tab") + let anim = LottieAnimationView(name: "new_tab") let label = UILabel() let pointerView: UIView = UIView(frame: CGRect(x: 0, y: 0, @@ -201,9 +201,9 @@ extension TabSwitcherButton: Themable { switch theme.currentImageSet { case .light: - anim.animation = Animation.named("new_tab_dark") + anim.animation = LottieAnimation.named("new_tab_dark") case .dark: - anim.animation = Animation.named("new_tab") + anim.animation = LottieAnimation.named("new_tab") } addSubview(anim) diff --git a/DuckDuckGo/ViewHighlighter.swift b/DuckDuckGo/ViewHighlighter.swift index 8a479d4a4d..f208154305 100644 --- a/DuckDuckGo/ViewHighlighter.swift +++ b/DuckDuckGo/ViewHighlighter.swift @@ -33,7 +33,7 @@ class ViewHighlighter { guard let center = view.superview?.convert(view.center, to: nil) else { return } let size = max(view.frame.width, view.frame.height) * 5.5 - let highlightView = AnimationView(name: "view_highlight") + let highlightView = LottieAnimationView(name: "view_highlight") highlightView.frame = CGRect(x: 0, y: 0, width: size, height: size) highlightView.center = center highlightView.isUserInteractionEnabled = false diff --git a/DuckDuckGo/en.lproj/Localizable.stringsdict b/DuckDuckGo/en.lproj/Localizable.stringsdict index df96547b47..92f5db2104 100644 --- a/DuckDuckGo/en.lproj/Localizable.stringsdict +++ b/DuckDuckGo/en.lproj/Localizable.stringsdict @@ -226,33 +226,33 @@ I blocked them! Are you sure you want to delete this password? - autofill.delete.all.passwords.confirmation.body - - NSStringLocalizedFormatKey - %1$#@passwords@ - passwords - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - d - other - Your passwords will be deleted from this device. Make sure you still have a way to access your %2$#@accounts@. - one - Your password will be deleted from this device. Make sure you still have a way to access your %2$#@accounts@. - - accounts - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - d - other - accounts - one - account - - + autofill.delete.all.passwords.confirmation.body + + NSStringLocalizedFormatKey + %1$#@passwords@ + passwords + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + other + Your passwords will be deleted from this device. Make sure you still have a way to access your %2$#@accounts@. + one + Your password will be deleted from this device. Make sure you still have a way to access your %2$#@accounts@. + + accounts + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + other + accounts + one + account + + autofill.delete.all.passwords.sync.confirmation.body NSStringLocalizedFormatKey diff --git a/DuckDuckGoTests/MockSecureVault.swift b/DuckDuckGoTests/MockSecureVault.swift index ad0cac1d33..3368501fed 100644 --- a/DuckDuckGoTests/MockSecureVault.swift +++ b/DuckDuckGoTests/MockSecureVault.swift @@ -210,6 +210,10 @@ final class MockSecureVault: AutofillSecureVault { [] } + func accountTitlesForSyncableCredentials(modifiedBefore date: Date) throws -> [String] { + [] + } + func deleteSyncableCredentials(_ syncableCredentials: SecureVaultModels.SyncableCredentials, in database: Database) throws { } @@ -403,6 +407,10 @@ class MockDatabaseProvider: AutofillDatabaseProvider { [] } + func modifiedSyncableCredentials(before date: Date) throws -> [SecureVaultModels.SyncableCredentials] { + [] + } + func syncableCredentialsForSyncIds(_ syncIds: any Sequence, in database: Database) throws -> [SecureVaultModels.SyncableCredentials] { [] } diff --git a/DuckDuckGoTests/NetworkProtectionFeatureVisibilityTests.swift b/DuckDuckGoTests/NetworkProtectionFeatureVisibilityTests.swift index 24ce4c9297..579ed042c3 100644 --- a/DuckDuckGoTests/NetworkProtectionFeatureVisibilityTests.swift +++ b/DuckDuckGoTests/NetworkProtectionFeatureVisibilityTests.swift @@ -60,28 +60,28 @@ final class NetworkProtectionFeatureVisibilityTests: XCTestCase { XCTAssertTrue(mockWithVPNAccess.shouldMonitorEntitlement()) XCTAssertFalse(mockWithVPNAccess.shouldKeepVPNAccessViaWaitlist()) XCTAssertTrue(mockWithVPNAccess.shouldShowThankYouMessaging()) - XCTAssertTrue(mockWithVPNAccess.shouldShowVPNShortcut()) + XCTAssertFalse(mockWithVPNAccess.shouldShowVPNShortcut()) // Not current waitlist user -> Enforce entitlement check, no more VPN use, no thank-you let mockWithBetaActive = baseMock.adding([.isWaitlistBetaActive]) XCTAssertTrue(mockWithBetaActive.shouldMonitorEntitlement()) XCTAssertFalse(mockWithBetaActive.shouldKeepVPNAccessViaWaitlist()) XCTAssertFalse(mockWithBetaActive.shouldShowThankYouMessaging()) - XCTAssertTrue(mockWithBetaActive.shouldShowVPNShortcut()) + XCTAssertFalse(mockWithBetaActive.shouldShowVPNShortcut()) // Waitlist beta OFF, current waitlist user -> Show thank-you, enforce entitlement check, no more VPN use let mockWithBetaInactive = baseMock.adding([.isWaitlistUser]) XCTAssertTrue(mockWithBetaInactive.shouldMonitorEntitlement()) XCTAssertFalse(mockWithBetaInactive.shouldKeepVPNAccessViaWaitlist()) XCTAssertTrue(mockWithBetaInactive.shouldShowThankYouMessaging()) - XCTAssertTrue(mockWithBetaInactive.shouldShowVPNShortcut()) + XCTAssertFalse(mockWithBetaInactive.shouldShowVPNShortcut()) - // Waitlist beta OFF, not current wailist user -> Enforce entitlement check, nothing else + // Waitlist beta OFF, not current waitlist user -> Enforce entitlement check, nothing else let mockWithNothingElse = baseMock XCTAssertTrue(mockWithNothingElse.shouldMonitorEntitlement()) XCTAssertFalse(mockWithNothingElse.shouldKeepVPNAccessViaWaitlist()) XCTAssertFalse(mockWithNothingElse.shouldShowThankYouMessaging()) - XCTAssertTrue(mockWithNothingElse.shouldShowVPNShortcut()) + XCTAssertFalse(mockWithNothingElse.shouldShowVPNShortcut()) } } diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Resources/en.lproj/Localizable.strings b/LocalPackages/SyncUI/Sources/SyncUI/Resources/en.lproj/Localizable.strings index 68f7f7409b..d43eb7ff8e 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Resources/en.lproj/Localizable.strings +++ b/LocalPackages/SyncUI/Sources/SyncUI/Resources/en.lproj/Localizable.strings @@ -1,6 +1,12 @@ /* Standard Buttons - Back Button */ "back.button" = "Back"; +/* Do not translate - stringsdict entry */ +"bookmarks.invalid.objects.present.description" = "bookmarks.invalid.objects.present.description"; + +/* Alert title for invalid bookmarks being filtered out of synced data */ +"bookmarks.invalid.objects.present.title" = "Some bookmarks are not syncing due to excessively long content in certain fields."; + /* Sync Paused Errors - Bookmarks Limit Exceeded Action */ "bookmarks.limit.exceeded.action" = "Manage Bookmarks"; @@ -40,6 +46,12 @@ /* Connect With Server Sheet - Title */ "connect.with.server.sheet.title" = "Sync and Back Up This Device"; +/* Do not translate - stringsdict entry */ +"credentials.invalid.objects.present.description" = "credentials.invalid.objects.present.description"; + +/* Alert title for invalid logins being filtered out of synced data */ +"credentials.invalid.objects.present.title" = "Some logins are not syncing due to excessively long content in certain fields."; + /* Sync Paused Errors - Credentials Limit Exceeded Action */ "credentials.limit.exceeded.action" = "Manage Logins"; diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Resources/en.lproj/Localizable.stringsdict b/LocalPackages/SyncUI/Sources/SyncUI/Resources/en.lproj/Localizable.stringsdict new file mode 100644 index 0000000000..3b53f170f7 --- /dev/null +++ b/LocalPackages/SyncUI/Sources/SyncUI/Resources/en.lproj/Localizable.stringsdict @@ -0,0 +1,42 @@ + + + + + bookmarks.invalid.objects.present.description + + NSStringLocalizedFormatKey + %#@sites@ + sites + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + zero + Your bookmark for %2$@ can't sync because one of its fields exceeds the character limit. + one + Your bookmarks for %2$@ and 1 other site can't sync because some of their fields exceed the character limit. + other + Your bookmarks for %2$@ and %1$d other sites can't sync because some of their fields exceed the character limit. + + + credentials.invalid.objects.present.description + + NSStringLocalizedFormatKey + %#@sites@ + sites + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + zero + Your password for %2$@ can't sync because one of its fields exceeds the character limit. + one + Your passwords for %2$@ and 1 other site can't sync because some of their fields exceed the character limit. + other + Your passwords for %2$@ and %1$d other sites can't sync because some of their fields exceed the character limit. + + + + diff --git a/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/SyncSettingsViewModel.swift b/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/SyncSettingsViewModel.swift index 93dd2b1df3..55ea76d867 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/SyncSettingsViewModel.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/SyncSettingsViewModel.swift @@ -81,6 +81,8 @@ public class SyncSettingsViewModel: ObservableObject { @Published public var isSyncingDevices = false @Published public var isSyncBookmarksPaused = false @Published public var isSyncCredentialsPaused = false + @Published public var invalidBookmarksTitles: [String] = [] + @Published public var invalidCredentialsTitles: [String] = [] @Published var isBusy = false @Published var recoveryCode = "" diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/Internal/UserText.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/Internal/UserText.swift index b36ab14850..9e92787079 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Views/Internal/UserText.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/Internal/UserText.swift @@ -48,6 +48,20 @@ public struct UserText { static let credentialsLimitExceededDescription = NSLocalizedString("credentials.limit.exceeded.description", bundle: Bundle.module, value: "Logins limit exceeded. Delete some to resume syncing.", comment: "Sync Paused Errors - Credentials Limit Exceeded Description") static let bookmarksLimitExceededAction = NSLocalizedString("bookmarks.limit.exceeded.action", bundle: Bundle.module, value: "Manage Bookmarks", comment: "Sync Paused Errors - Bookmarks Limit Exceeded Action") static let credentialsLimitExceededAction = NSLocalizedString("credentials.limit.exceeded.action", bundle: Bundle.module, value: "Manage Logins", comment: "Sync Paused Errors - Credentials Limit Exceeded Action") + // Sync Filtered Items Errors + static let invalidBookmarksPresentTitle = NSLocalizedString("bookmarks.invalid.objects.present.title", bundle: Bundle.module, value: "Some bookmarks are not syncing due to excessively long content in certain fields.", comment: "Alert title for invalid bookmarks being filtered out of synced data") + static let invalidCredentialsPresentTitle = NSLocalizedString("credentials.invalid.objects.present.title", bundle: Bundle.module, value: "Some logins are not syncing due to excessively long content in certain fields.", comment: "Alert title for invalid logins being filtered out of synced data") + + static func invalidBookmarksPresentDescription(_ invalidItemTitle: String, numberOfOtherInvalidItems: Int) -> String { + let message = NSLocalizedString("bookmarks.invalid.objects.present.description", bundle: Bundle.module, comment: "Do not translate - stringsdict entry") + return String(format: message, numberOfOtherInvalidItems, invalidItemTitle) + } + + static func invalidCredentialsPresentDescription(_ invalidItemTitle: String, numberOfOtherInvalidItems: Int) -> String { + let message = NSLocalizedString("credentials.invalid.objects.present.description", bundle: Bundle.module, comment: "Do not translate - stringsdict entry") + return String(format: message, numberOfOtherInvalidItems, invalidItemTitle) + } + // Synced Devices static let syncedDevicesSectionHeader = NSLocalizedString("synced.devices.section.header", bundle: Bundle.module, value: "Synced Devices", comment: "Synced Devices - Section Header") static let syncedDevicesThisDeviceLabel = NSLocalizedString("synced.devices.this.device.label", bundle: Bundle.module, value: "This Device", comment: "Synced Devices - This Device Label") diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/SyncSettingsView.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/SyncSettingsView.swift index fc442ce77c..085c048387 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Views/SyncSettingsView.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/SyncSettingsView.swift @@ -59,6 +59,14 @@ public struct SyncSettingsView: View { syncPaused(for: .credentials) } + if !model.invalidBookmarksTitles.isEmpty { + syncHasInvalidItems(for: .bookmarks) + } + + if !model.invalidCredentialsTitles.isEmpty { + syncHasInvalidItems(for: .credentials) + } + devices() options() diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/SyncSettingsViewExtension.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/SyncSettingsViewExtension.swift index bfec4047d5..6010da91b3 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Views/SyncSettingsViewExtension.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/SyncSettingsViewExtension.swift @@ -286,6 +286,53 @@ extension SyncSettingsView { } } + @ViewBuilder + func syncHasInvalidItems(for itemType: LimitedItemType) -> some View { + var title: String { + switch itemType { + case .bookmarks: + return UserText.invalidBookmarksPresentTitle + case .credentials: + return UserText.invalidCredentialsPresentTitle + } + } + var description: String { + switch itemType { + case .bookmarks: + assert(!model.invalidBookmarksTitles.isEmpty) + let firstInvalidBookmarkTitle = model.invalidBookmarksTitles.first ?? "" + return UserText.invalidBookmarksPresentDescription( + firstInvalidBookmarkTitle, + numberOfOtherInvalidItems: model.invalidBookmarksTitles.count - 1 + ) + + case .credentials: + assert(!model.invalidCredentialsTitles.isEmpty) + let firstInvalidCredentialTitle = model.invalidCredentialsTitles.first ?? "" + return UserText.invalidCredentialsPresentDescription( + firstInvalidCredentialTitle, + numberOfOtherInvalidItems: model.invalidCredentialsTitles.count - 1 + ) + } + } + var actionTitle: String { + switch itemType { + case .bookmarks: + return UserText.bookmarksLimitExceededAction + case .credentials: + return UserText.credentialsLimitExceededAction + } + } + SyncWarningMessageView(title: title, message: description, buttonTitle: actionTitle) { + switch itemType { + case .bookmarks: + model.manageBookmarks() + case .credentials: + model.manageLogins() + } + } + } + @ViewBuilder func devEnvironmentIndicator() -> some View { if model.isOnDevEnvironment { diff --git a/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift b/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift index a2d7eb5989..9b86b6768f 100644 --- a/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift +++ b/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift @@ -149,20 +149,6 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { pixelEvent = .networkProtectionClientInvalidAuthToken case .serverListInconsistency: return - case .failedToEncodeServerList: - pixelEvent = .networkProtectionServerListStoreFailedToEncodeServerList - case .failedToDecodeServerList: - pixelEvent = .networkProtectionServerListStoreFailedToDecodeServerList - case .failedToWriteServerList(let eventError): - pixelEvent = .networkProtectionServerListStoreFailedToWriteServerList - pixelError = eventError - case .noServerListFound: - return - case .couldNotCreateServerListDirectory: - return - case .failedToReadServerList(let eventError): - pixelEvent = .networkProtectionServerListStoreFailedToReadServerList - pixelError = eventError case .failedToCastKeychainValueToData(let field): pixelEvent = .networkProtectionKeychainErrorFailedToCastKeychainValueToData params[PixelParameters.keychainFieldName] = field diff --git a/fastlane/metadata/en-US/description.txt b/fastlane/metadata/en-US/description.txt index e16d809a31..4720b87e7a 100644 --- a/fastlane/metadata/en-US/description.txt +++ b/fastlane/metadata/en-US/description.txt @@ -1,27 +1,35 @@ DuckDuckGo is a free browser that provides the most comprehensive online privacy protection in one app. Unlike most popular browsers, it has powerful privacy protections by default, including our search engine that doesn’t track your history and over a dozen other built-in protections. Millions of people use DuckDuckGo as their go-to browser to protect their everyday online activities, from searching to browsing, emailing, and more. -  + FEATURE HIGHLIGHTS -• Search Privately by Default - DuckDuckGo Private Search comes built-in, so you can easily search the web without being tracked. - -• Block Tracking Cookies While Browsing - Prevent most 3rd-party Internet cookies from tracking you as you browse online. -  -• Escape Website Trackers Before They Load - Automatically stop most hidden trackers (3rd-party scripts) from loading, which prevents companies from collecting and using any personal data from these trackers. Stay secure with our cutting-edge tracker-blocking technology – 3rd-Party Tracker Loading Protection, which goes above and beyond what you get in most popular web browsers by default. -  -• Automatically Enforce Encryption - Force many sites you visit to automatically use an encrypted (HTTPS) connection, which helps shield your data from Wi-Fi snoopers and network onlookers like your Internet provider. -  -• Block Email Trackers (Beta) - Over 85% of emails sent to Duck Addresses (@duck.com) contain trackers that can detect when you’ve opened a message, where you were when you opened it, and what device you were using. Email Protection makes it easy to block most email trackers and hide your existing address when signing up for things online, all without switching email providers. - -• Escape Fingerprinting - Help stop companies from creating a unique identifier for you by blocking their attempts to combine specific information about your web browser and device settings. -  -We feature many protections not available on most Internet browsers (even incognito browsers), including protection from link tracking, Google AMP tracking, and more. -  +• Search Privately by Default: DuckDuckGo Private Search comes built-in, so you can easily search online without being tracked. + +• Block Most Trackers Before They Load: Our 3rd-Party Tracker Loading Protection exceeds what most popular browsers offer by default, stopping hidden tracking scripts from loading and collecting your personal data. + +• Enable Built-in Email Protection: Block most email trackers and hide your existing email address with @duck.com addresses. + +• Automatically Enforce Encryption: Shield your data from network and Wi-Fi snoopers by forcing many sites to use an HTTPS connection. + +• Escape Fingerprinting: Make it harder for companies to create a unique identifier for you by blocking attempts to combine info about your browser and device. + +• Sync and Backup: Securely sync bookmarks and passwords across your devices. + +We feature many protections not available on most browsers, even in private browsing mode, including protection from link tracking, AMP tracking, and more. + + EVERYDAY PRIVACY CONTROLS -• Tap Fire Button, Burn Data - Clear your tabs and browsing data fast with our Fire Button. -  -• Signal Your Privacy Preference with Global Privacy Control (GPC) - Built in to our app, GPC intends to help you express your opt-out rights automatically by telling websites not to sell or share your personal information. Whether it can be used to enforce your legal rights (for example, current or future CCPA, GDPR requirements) depends on the laws in your jurisdiction. +• Clear your tabs and browsing data in a flash with the Fire Button. -Read more about our free Tracking Protections at https://help.duckduckgo.com/privacy/web-tracking-protections +• Banish cookie pop-ups and automatically set your preferences to minimize cookies and maximize privacy.   + +• Signal Your Privacy Preference with Global Privacy Control (GPC) built into our app. GPC intends to help you express your opt-out rights automatically by telling websites not to sell or share your personal info. Whether it can be used to enforce your legal rights depends on the laws in your jurisdiction. + +Privacy Pro Pricing & Terms  -You don't need to wait to take back your privacy. Join the millions of people using DuckDuckGo and protect many of your everyday online activities with one app. It's privacy, simplified. +Payment will be charged automatically to your iTunes account until you cancel, which you can do in app settings. You have the option to provide an email address to activate your subscription on other devices, and we will only use that email address to verify your subscription. For terms of service and privacy policy, visit https://duckduckgo.com/pro/privacy-terms + +You don't need to wait to take back your privacy. Join the millions of people using DuckDuckGo and protect many of your everyday online activities with one app. It's privacy, simplified. + +Read more about our free Tracking Protections at https://help.duckduckgo.com/privacy/web-tracking-protections Privacy Policy: https://duckduckgo.com/privacy/ +Terms of Service: https://duckduckgo.com/terms \ No newline at end of file diff --git a/fastlane/metadata/en-US/keywords.txt b/fastlane/metadata/en-US/keywords.txt index 3b6f2b7f4b..f9cae6671c 100644 --- a/fastlane/metadata/en-US/keywords.txt +++ b/fastlane/metadata/en-US/keywords.txt @@ -1 +1 @@ -web,privacy,search,email,secure,online,tracker,duc,duck,go,data,ddg,incognito,ad,block,vpn,internet \ No newline at end of file +web,encrypt,search,email,secure,online,tracker,duc,duck,go,data,ddg,incognito,ad,block,vpn,internet \ No newline at end of file