From 11ec9e4e48ed359cba50127c4bee33a4dd6196aa Mon Sep 17 00:00:00 2001 From: Anh Do <18567+quanganhdo@users.noreply.github.com> Date: Fri, 1 Mar 2024 00:54:02 -0500 Subject: [PATCH] Check entitlement periodically and while rekeying NetP (#2461) Task/Issue URL: https://app.asana.com/0/0/1206409081785856/f Description: This PR adds entitlement check for NetP --- ...workProtectionNotificationIdentifier.swift | 1 + DuckDuckGo.xcodeproj/project.pbxproj | 8 ++--- .../xcshareddata/swiftpm/Package.resolved | 4 +-- DuckDuckGo/AppDelegate.swift | 13 ++++++++ DuckDuckGo/CriticalAlerts.swift | 16 ++++++++++ .../EventMapping+NetworkProtectionError.swift | 2 ++ DuckDuckGo/MainViewController.swift | 30 ++++++------------- ...orkProtectionConvenienceInitialisers.swift | 9 ++++-- DuckDuckGo/UserText.swift | 9 ++++-- DuckDuckGo/en.lproj/Localizable.strings | 14 ++++++++- ...etworkProtectionPacketTunnelProvider.swift | 28 ++++++++++++++--- ...orkProtectionUNNotificationPresenter.swift | 16 +++++++++- PacketTunnelProvider/UserText.swift | 2 ++ .../en.lproj/Localizable.strings | 3 ++ 14 files changed, 117 insertions(+), 38 deletions(-) diff --git a/Core/NetworkProtectionNotificationIdentifier.swift b/Core/NetworkProtectionNotificationIdentifier.swift index 20e1a7631a..5ac235621c 100644 --- a/Core/NetworkProtectionNotificationIdentifier.swift +++ b/Core/NetworkProtectionNotificationIdentifier.swift @@ -23,4 +23,5 @@ public enum NetworkProtectionNotificationIdentifier: String { case connection = "network-protection.notification.connection" case superseded = "network-protection.notification.superseded" case test = "network-protection.notification.test" + case entitlement = "network-protection.notification.entitlement" } diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 205c844c33..3de5ac902f 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -8944,7 +8944,7 @@ MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG APP_TRACKING_PROTECTION NETWORK_PROTECTION SUBSCRIPTION"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG APP_TRACKING_PROTECTION NETWORK_PROTECTION SUBSCRIPTION ALPHA"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = "1,2"; VALID_ARCHS = "$(ARCHS_STANDARD_64_BIT)"; @@ -8956,7 +8956,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = "DDG-AppIcon-Alpha"; - CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; + CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CURRENT_PROJECT_VERSION = 3; @@ -9081,7 +9081,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProvider.entitlements; + CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProviderAlpha.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 3; @@ -9949,7 +9949,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 114.0.0; + version = 114.1.0; }; }; 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 cf556941ff..018b6e092f 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "revision" : "c9eae1b4a4ce5b6854d6934e57f900f62d2f3917", - "version" : "114.0.0" + "revision" : "045a8782c3dbbf79fc088b38120dea1efadc13e1", + "version" : "114.1.0" } }, { diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index 2745fc9dc2..1f712dd2c6 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -301,6 +301,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { VPNWaitlist.shared.registerBackgroundRefreshTaskHandler() #endif +#if NETWORK_PROTECTION && SUBSCRIPTION + if VPNSettings(defaults: .networkProtectionGroupDefaults).showEntitlementAlert { + presentExpiredEntitlementAlert() + } +#endif + RemoteMessaging.registerBackgroundRefreshTaskHandler( bookmarksDatabase: bookmarksDatabase, favoritesDisplayMode: AppDependencyProvider.shared.appSettings.favoritesDisplayMode @@ -346,6 +352,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate { window?.rootViewController?.present(alertController, animated: true, completion: nil) } + private func presentExpiredEntitlementAlert() { + let alertController = CriticalAlerts.makeExpiredEntitlementAlert() + window?.rootViewController?.present(alertController, animated: true) { + VPNSettings(defaults: .networkProtectionGroupDefaults).apply(change: .setShowEntitlementAlert(false)) + } + } + private func cleanUpMacPromoExperiment2() { UserDefaults.standard.removeObject(forKey: "com.duckduckgo.ios.macPromoMay23.exp2.cohort") } diff --git a/DuckDuckGo/CriticalAlerts.swift b/DuckDuckGo/CriticalAlerts.swift index c2e46242b3..c62183fe0c 100644 --- a/DuckDuckGo/CriticalAlerts.swift +++ b/DuckDuckGo/CriticalAlerts.swift @@ -70,4 +70,20 @@ struct CriticalAlerts { return alertController } + static func makeExpiredEntitlementAlert() -> UIAlertController { + let alertController = UIAlertController(title: UserText.vpnAccessRevokedAlertTitle, + message: UserText.vpnAccessRevokedAlertMessage, + preferredStyle: .alert) + alertController.overrideUserInterfaceStyle() + + let closeButton = UIAlertAction(title: UserText.vpnAccessRevokedAlertActionCancel, style: .cancel) + let signInButton = UIAlertAction(title: UserText.vpnAccessRevokedAlertActionSubscribe, style: .default) { _ in + UIApplication.shared.open(URL.emailProtectionQuickLink, options: [:], completionHandler: nil) + } + + alertController.addAction(closeButton) + alertController.addAction(signInButton) + return alertController + } + } diff --git a/DuckDuckGo/EventMapping+NetworkProtectionError.swift b/DuckDuckGo/EventMapping+NetworkProtectionError.swift index 2f465c4113..8aaa5c559a 100644 --- a/DuckDuckGo/EventMapping+NetworkProtectionError.swift +++ b/DuckDuckGo/EventMapping+NetworkProtectionError.swift @@ -70,6 +70,8 @@ extension EventMapping where Event == NetworkProtectionError { params[PixelParameters.keychainErrorCode] = String(status) case .noAuthTokenFound: pixelEvent = .networkProtectionNoAuthTokenFoundError + case .vpnAccessRevoked: + return case .noServerRegistrationInfo, .couldNotSelectClosestServer, diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index c2f67cd89d..d125eaccaf 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -263,7 +263,7 @@ class MainViewController: UIViewController { subscribeToEmailProtectionStatusNotifications() #if NETWORK_PROTECTION && SUBSCRIPTION - subscribeToNetworkProtectionSubscriptionEvents() + subscribeToNetworkProtectionEvents() #endif findInPageView.delegate = self @@ -1327,19 +1327,13 @@ class MainViewController: UIViewController { } #if NETWORK_PROTECTION && SUBSCRIPTION - private func subscribeToNetworkProtectionSubscriptionEvents() { + private func subscribeToNetworkProtectionEvents() { NotificationCenter.default.publisher(for: .accountDidSignIn) .receive(on: DispatchQueue.main) .sink { [weak self] notification in self?.onNetworkProtectionAccountSignIn(notification) } .store(in: &netpCancellables) - NotificationCenter.default.publisher(for: .accountDidSignOut) - .receive(on: DispatchQueue.main) - .sink { [weak self] notification in - self?.onNetworkProtectionAccountSignOut(notification) - } - .store(in: &netpCancellables) } @objc @@ -1349,25 +1343,19 @@ class MainViewController: UIViewController { return } + VPNSettings(defaults: .networkProtectionGroupDefaults).resetEntitlementMessaging() + print("[NetP Subscription] Reset expired entitlement messaging") + Task { do { - try await NetworkProtectionCodeRedemptionCoordinator().exchange(accessToken: token) - print("[NetP Subscription] Exchanged access token for auth token successfully") + // todo - https://app.asana.com/0/0/1206541966681608/f + try NetworkProtectionKeychainTokenStore().store(NetworkProtectionKeychainTokenStore.makeToken(from: token)) + print("[NetP Subscription] Stored derived NetP auth token") } catch { - print("[NetP Subscription] Failed to exchange access token for auth token: \(error)") + print("[NetP Subscription] Failed to store derived NetP auth token: \(error)") } } } - - @objc - private func onNetworkProtectionAccountSignOut(_ notification: Notification) { - do { - try NetworkProtectionKeychainTokenStore().deleteToken() - print("[NetP Subscription] Deleted NetP auth token after signing out from Privacy Pro") - } catch { - print("[NetP Subscription] Failed to delete NetP auth token after signing out from Privacy Pro: \(error)") - } - } #endif @objc diff --git a/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift b/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift index 01d9287bd7..922220f2a8 100644 --- a/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift +++ b/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift @@ -58,7 +58,8 @@ extension NetworkProtectionKeychainTokenStore { convenience init() { self.init(keychainType: .dataProtection(.unspecified), serviceName: "\(Bundle.main.bundleIdentifier!).authToken", - errorEvents: .networkProtectionAppDebugEvents) + errorEvents: .networkProtectionAppDebugEvents, + isSubscriptionEnabled: AppDependencyProvider.shared.featureFlagger.isFeatureOn(.subscription)) } } @@ -69,7 +70,8 @@ extension NetworkProtectionCodeRedemptionCoordinator { environment: settings.selectedEnvironment, tokenStore: NetworkProtectionKeychainTokenStore(), isManualCodeRedemptionFlow: isManualCodeRedemptionFlow, - errorEvents: .networkProtectionAppDebugEvents + errorEvents: .networkProtectionAppDebugEvents, + isSubscriptionEnabled: AppDependencyProvider.shared.featureFlagger.isFeatureOn(.subscription) ) } } @@ -95,7 +97,8 @@ extension NetworkProtectionLocationListCompositeRepository { self.init( environment: settings.selectedEnvironment, tokenStore: NetworkProtectionKeychainTokenStore(), - errorEvents: .networkProtectionAppDebugEvents + errorEvents: .networkProtectionAppDebugEvents, + isSubscriptionEnabled: AppDependencyProvider.shared.featureFlagger.isFeatureOn(.subscription) ) } } diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index 2a304470cc..a9e746b13a 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -658,6 +658,11 @@ In addition to the details entered into this form, your app issue report will co static let vpnFeedbackFormSubmittedMessage = NSLocalizedString("vpn.feedback-form.submitted.message", value: "Thank You! Feedback submitted.", comment: "Toast message when the VPN feedback form is submitted successfully") + static let vpnAccessRevokedAlertTitle = NSLocalizedString("vpn.access-revoked.alert.title", value: "VPN disconnected due to expired subscription", comment: "Alert title for the alert when the Privacy Pro subscription expires") + static let vpnAccessRevokedAlertMessage = NSLocalizedString("vpn.access-revoked.alert.message", value: "Subscribe to Privacy Pro to reconnect DuckDuckGo VPN.", comment: "Alert message for the alert when the Privacy Pro subscription expiress") + static let vpnAccessRevokedAlertActionSubscribe = NSLocalizedString("vpn.access-revoked.alert.action.subscribe", value: "Subscribe", comment: "Primary action for the alert when the subscription expires") + static let vpnAccessRevokedAlertActionCancel = NSLocalizedString("vpn.access-revoked.alert.action.cancel", value: "Dismiss", comment: "Cancel action for the alert when the subscription expires") + // MARK: Notifications public static let macWaitlistAvailableNotificationTitle = NSLocalizedString("mac-waitlist.available.notification.title", value: "DuckDuckGo for Mac is ready!", comment: "Title for the macOS waitlist notification") @@ -919,10 +924,10 @@ But if you *do* want a peek under the hood, you can find more information about static let networkProtectionNotificationPromptTitle = NSLocalizedString("network-protection.waitlist.notification-prompt-title", value: "Know the instant you're invited", comment: "Title for the alert to confirm enabling notifications") static let networkProtectionNotificationPromptDescription = NSLocalizedString("network-protection.waitlist.notification-prompt-description", value: "Get a notification when your copy of Network Protection early access is ready.", comment: "Subtitle for the alert to confirm enabling notifications") - + // MARK: Settings Screeen public static let settingsTitle = NSLocalizedString("settings.title", value: "Settings", comment: "Title for the Settings View") - + // General Section public static let settingsSetDefault = NSLocalizedString("settings.default.browser", value: "Set as Default Browser", comment: "Settings screen cell text for setting the app as default browser") public static let settingsAddToDock = NSLocalizedString("settings.add.to.dock", value: "Add App to Your Dock", comment: "Settings screen cell text for adding the app to the dock") diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index 28af223ca9..9229afac4d 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -2035,7 +2035,7 @@ But if you *do* want a peek under the hood, you can find more information about "subscription.manage.devices" = "Manage Devices"; /* Description for Email Management options */ -"subscription.manage.email.description" = "You can use this email to activate your subscription on your other devices."; +"subscription.manage.email.description" = "You can use this email to activate your subscription from browser settings in the DuckDuckGo app on your other devices."; /* Manage Plan header */ "subscription.manage.plan" = "Manage Plan"; @@ -2238,6 +2238,18 @@ But if you *do* want a peek under the hood, you can find more information about /* Voice-search footer note with on-device privacy warning */ "voiceSearch.footer.note" = "Audio is processed on-device. It's not stored or shared with anyone, including DuckDuckGo."; +/* Cancel action for the alert when the subscription expires */ +"vpn.access-revoked.alert.action.cancel" = "Dismiss"; + +/* Primary action for the alert when the subscription expires */ +"vpn.access-revoked.alert.action.subscribe" = "Subscribe"; + +/* Alert message for the alert when the Privacy Pro subscription expiress */ +"vpn.access-revoked.alert.message" = "Subscribe to Privacy Pro to reconnect DuckDuckGo VPN."; + +/* Alert title for the alert when the Privacy Pro subscription expires */ +"vpn.access-revoked.alert.title" = "VPN disconnected due to expired subscription"; + /* Title for the Cancel button of the VPN feedback form */ "vpn.feedback-form.button.cancel" = "Cancel"; diff --git a/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift b/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift index dc5ddd2d44..f9e7fa92f7 100644 --- a/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift +++ b/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift @@ -26,6 +26,7 @@ import Core import Networking import NetworkExtension import NetworkProtection +import Subscription // Initial implementation for initial Network Protection tests. Will be fleshed out with https://app.asana.com/0/1203137811378537/1204630829332227/f final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { @@ -162,6 +163,8 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { params[PixelParameters.wireguardErrorCode] = String(code) case .noAuthTokenFound: pixelEvent = .networkProtectionNoAuthTokenFoundError + case .vpnAccessRevoked: + return case .unhandledError(function: let function, line: let line, error: let error): pixelEvent = .networkProtectionUnhandledError params[PixelParameters.function] = function @@ -201,24 +204,32 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { } @objc init() { +#if ALPHA + let isSubscriptionEnabled = true +#else + let isSubscriptionEnabled = false +#endif let tokenStore = NetworkProtectionKeychainTokenStore(keychainType: .dataProtection(.unspecified), - errorEvents: nil) + errorEvents: nil, + isSubscriptionEnabled: isSubscriptionEnabled) let errorStore = NetworkProtectionTunnelErrorStore() let notificationsPresenter = NetworkProtectionUNNotificationPresenter() let settings = VPNSettings(defaults: .networkProtectionGroupDefaults) - let nofificationsPresenterDecorator = NetworkProtectionNotificationsPresenterTogglableDecorator( + let notificationsPresenterDecorator = NetworkProtectionNotificationsPresenterTogglableDecorator( settings: settings, wrappee: notificationsPresenter ) notificationsPresenter.requestAuthorization() - super.init(notificationsPresenter: nofificationsPresenterDecorator, + super.init(notificationsPresenter: notificationsPresenterDecorator, tunnelHealthStore: NetworkProtectionTunnelHealthStore(), controllerErrorStore: errorStore, keychainType: .dataProtection(.unspecified), tokenStore: tokenStore, debugEvents: Self.networkProtectionDebugEvents(controllerErrorStore: errorStore), providerEvents: Self.packetTunnelProviderEvents, - settings: settings) + settings: settings, + isSubscriptionEnabled: isSubscriptionEnabled, + entitlementCheck: Self.entitlementCheck) startMonitoringMemoryPressureEvents() observeServerChanges() APIRequest.Headers.setUserAgent(DefaultUserAgentManager.duckDuckGoUserAgent) @@ -265,6 +276,15 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { activationDateStore.updateLastActiveDate() } + private static func entitlementCheck() async -> Result { + let result = await AccountManager().hasEntitlement(for: .networkProtection) + switch result { + case .success(let hasEntitlement): + return .success(hasEntitlement) + case .failure(let error): + return .failure(error) + } + } } #endif diff --git a/PacketTunnelProvider/NetworkProtection/NetworkProtectionUNNotificationPresenter.swift b/PacketTunnelProvider/NetworkProtection/NetworkProtectionUNNotificationPresenter.swift index 28358d72c8..0afc858191 100644 --- a/PacketTunnelProvider/NetworkProtection/NetworkProtectionUNNotificationPresenter.swift +++ b/PacketTunnelProvider/NetworkProtection/NetworkProtectionUNNotificationPresenter.swift @@ -62,7 +62,7 @@ final class NetworkProtectionUNNotificationPresenter: NSObject, NetworkProtectio content.title = UserText.networkProtectionNotificationsTitle content.body = body - if #available(iOSApplicationExtension 15.0, *) { + if #available(iOS 15.0, *) { content.interruptionLevel = .timeSensitive content.relevanceScore = 0 } @@ -105,6 +105,20 @@ final class NetworkProtectionUNNotificationPresenter: NSObject, NetworkProtectio func showSupersededNotification() { } + func showEntitlementNotification(completion: @escaping (Error?) -> Void) { + let identifier = NetworkProtectionNotificationIdentifier.entitlement.rawValue + let content = notificationContent(body: UserText.networkProtectionEntitlementExpiredNotificationBody) + let request = UNNotificationRequest(identifier: identifier, content: content, trigger: .none) + + requestAlertAuthorization { authorized in + guard authorized else { return } + self.userNotificationCenter.removeDeliveredNotifications(withIdentifiers: [identifier]) + self.userNotificationCenter.add(request) { error in + completion(error) + } + } + } + private func showNotification(_ identifier: NetworkProtectionNotificationIdentifier, _ content: UNNotificationContent) { let request = UNNotificationRequest(identifier: identifier.rawValue, content: content, trigger: .none) diff --git a/PacketTunnelProvider/UserText.swift b/PacketTunnelProvider/UserText.swift index 820a4a6ebf..7243db5e2b 100644 --- a/PacketTunnelProvider/UserText.swift +++ b/PacketTunnelProvider/UserText.swift @@ -40,5 +40,7 @@ final class UserText { static let networkProtectionConnectionInterruptedNotificationBody = NSLocalizedString("network.protection.interrupted.notification.body", value: "Network Protection was interrupted. Attempting to reconnect now...", comment: "The body of the notification shown when Network Protection's connection is interrupted") static let networkProtectionConnectionFailureNotificationBody = NSLocalizedString("network.protection.failure.notification.body", value: "Network Protection failed to connect. Please try again later.", comment: "The body of the notification shown when Network Protection fails to reconnect") + + static let networkProtectionEntitlementExpiredNotificationBody = NSLocalizedString("network.protection.entitlement.expired.notification.body", value: "VPN disconnected due to expired subscription. Subscribe to Privacy Pro to reconnect DuckDuckGo VPN.", comment: "The body of the notification when Privacy Pro subscription expired") } // swiftlint:enable line_length diff --git a/PacketTunnelProvider/en.lproj/Localizable.strings b/PacketTunnelProvider/en.lproj/Localizable.strings index 1a6d5fe4de..3ceec0f887 100644 --- a/PacketTunnelProvider/en.lproj/Localizable.strings +++ b/PacketTunnelProvider/en.lproj/Localizable.strings @@ -1,3 +1,6 @@ +/* The body of the notification when Privacy Pro subscription expired */ +"network.protection.entitlement.expired.notification.body" = "VPN disconnected due to expired subscription. Subscribe to Privacy Pro to reconnect DuckDuckGo VPN."; + /* The body of the notification shown when Network Protection fails to reconnect */ "network.protection.failure.notification.body" = "Network Protection failed to connect. Please try again later.";