Skip to content

Commit

Permalink
Check entitlement periodically and while rekeying NetP (#2461)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/0/1206409081785856/f

Description:

This PR adds entitlement check for NetP
  • Loading branch information
quanganhdo authored Mar 1, 2024
1 parent 5f06e4c commit 11ec9e4
Show file tree
Hide file tree
Showing 14 changed files with 117 additions and 38 deletions.
1 change: 1 addition & 0 deletions Core/NetworkProtectionNotificationIdentifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
8 changes: 4 additions & 4 deletions DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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)";
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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" */ = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
},
{
Expand Down
13 changes: 13 additions & 0 deletions DuckDuckGo/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
}
Expand Down
16 changes: 16 additions & 0 deletions DuckDuckGo/CriticalAlerts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

}
2 changes: 2 additions & 0 deletions DuckDuckGo/EventMapping+NetworkProtectionError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ extension EventMapping where Event == NetworkProtectionError {
params[PixelParameters.keychainErrorCode] = String(status)
case .noAuthTokenFound:
pixelEvent = .networkProtectionNoAuthTokenFoundError
case .vpnAccessRevoked:
return
case
.noServerRegistrationInfo,
.couldNotSelectClosestServer,
Expand Down
30 changes: 9 additions & 21 deletions DuckDuckGo/MainViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ class MainViewController: UIViewController {
subscribeToEmailProtectionStatusNotifications()

#if NETWORK_PROTECTION && SUBSCRIPTION
subscribeToNetworkProtectionSubscriptionEvents()
subscribeToNetworkProtectionEvents()
#endif

findInPageView.delegate = self
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
9 changes: 6 additions & 3 deletions DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}

Expand All @@ -69,7 +70,8 @@ extension NetworkProtectionCodeRedemptionCoordinator {
environment: settings.selectedEnvironment,
tokenStore: NetworkProtectionKeychainTokenStore(),
isManualCodeRedemptionFlow: isManualCodeRedemptionFlow,
errorEvents: .networkProtectionAppDebugEvents
errorEvents: .networkProtectionAppDebugEvents,
isSubscriptionEnabled: AppDependencyProvider.shared.featureFlagger.isFeatureOn(.subscription)
)
}
}
Expand All @@ -95,7 +97,8 @@ extension NetworkProtectionLocationListCompositeRepository {
self.init(
environment: settings.selectedEnvironment,
tokenStore: NetworkProtectionKeychainTokenStore(),
errorEvents: .networkProtectionAppDebugEvents
errorEvents: .networkProtectionAppDebugEvents,
isSubscriptionEnabled: AppDependencyProvider.shared.featureFlagger.isFeatureOn(.subscription)
)
}
}
Expand Down
9 changes: 7 additions & 2 deletions DuckDuckGo/UserText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand Down
14 changes: 13 additions & 1 deletion DuckDuckGo/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -265,6 +276,15 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider {
activationDateStore.updateLastActiveDate()
}

private static func entitlementCheck() async -> Result<Bool, Error> {
let result = await AccountManager().hasEntitlement(for: .networkProtection)
switch result {
case .success(let hasEntitlement):
return .success(hasEntitlement)
case .failure(let error):
return .failure(error)
}
}
}

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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)

Expand Down
2 changes: 2 additions & 0 deletions PacketTunnelProvider/UserText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions PacketTunnelProvider/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
@@ -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.";

Expand Down

0 comments on commit 11ec9e4

Please sign in to comment.