Skip to content

Commit

Permalink
NetP waitlist final touches (#2209)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/0/1206027185389966/f
Tech Design URL:
CC:

Description:

This PR makes some final touches for the NetP waitlist, including:

Adding RMF support
Adding pixels
Updates NetP to only be visible to US users (unless they're internal)
Recording the date at which the user activated NetP, in order to show messages remotely if necessary
  • Loading branch information
samsymons authored Dec 4, 2023
1 parent ffa0cad commit 08b34f8
Show file tree
Hide file tree
Showing 17 changed files with 316 additions and 19 deletions.
16 changes: 16 additions & 0 deletions Core/PixelEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,14 @@ extension Pixel {

case networkProtectionUnhandledError

case networkProtectionWaitlistUserActive
case networkProtectionSettingsRowDisplayed
case networkProtectionWaitlistIntroScreenDisplayed
case networkProtectionWaitlistTermsDisplayed
case networkProtectionWaitlistTermsAccepted
case networkProtectionWaitlistNotificationShown
case networkProtectionWaitlistNotificationLaunched

// MARK: remote messaging pixels

case remoteMessageShown
Expand Down Expand Up @@ -876,6 +884,14 @@ extension Pixel.Event {
case .networkProtectionMemoryCritical: return "m_netp_vpn_memory_critical"
case .networkProtectionUnhandledError: return "m_netp_unhandled_error"

case .networkProtectionWaitlistUserActive: return "m_netp_waitlist_user_active"
case .networkProtectionSettingsRowDisplayed: return "m_netp_waitlist_settings_entry_viewed"
case .networkProtectionWaitlistIntroScreenDisplayed: return "m_netp_waitlist_intro_screen_viewed"
case .networkProtectionWaitlistTermsDisplayed: return "m_netp_waitlist_terms_viewed"
case .networkProtectionWaitlistTermsAccepted: return "m_netp_waitlist_terms_accepted"
case .networkProtectionWaitlistNotificationShown: return "m_netp_waitlist_notification_shown"
case .networkProtectionWaitlistNotificationLaunched: return "m_netp_waitlist_notification_launched"

// MARK: remote messaging pixels

case .remoteMessageShown: return "m_remote_message_shown"
Expand Down
12 changes: 11 additions & 1 deletion DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -289,12 +289,15 @@
4B6484F327FD1E350050A7A1 /* MenuControllerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B6484E927FD1E340050A7A1 /* MenuControllerView.swift */; };
4B6484FC27FFD14F0050A7A1 /* WindowsBrowserWaitlistTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B6484FB27FFD14F0050A7A1 /* WindowsBrowserWaitlistTests.swift */; };
4B75EA9226A266CB00018634 /* PrintingUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B75EA9126A266CB00018634 /* PrintingUserScript.swift */; };
4B78074E2B183A1F009DB2CF /* SurveyURLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B78074D2B183A1F009DB2CF /* SurveyURLBuilder.swift */; };
4B83396C29AC0701003F7EA9 /* AppTrackingProtectionStoringModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B470EE2299C6DD10086EBDC /* AppTrackingProtectionStoringModel.swift */; };
4B83396F29AC1437003F7EA9 /* AppTrackingProtectionListModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B83396E29AC1437003F7EA9 /* AppTrackingProtectionListModelTests.swift */; };
4B83397129AC18C9003F7EA9 /* AppTrackingProtectionStoringModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B83397029AC18C9003F7EA9 /* AppTrackingProtectionStoringModelTests.swift */; };
4B83397329AFB8D2003F7EA9 /* AppTrackingProtectionFeedbackModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B83397229AFB8D2003F7EA9 /* AppTrackingProtectionFeedbackModel.swift */; };
4B83397529AFBCE6003F7EA9 /* AppTrackingProtectionFeedbackModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B83397429AFBCE6003F7EA9 /* AppTrackingProtectionFeedbackModelTests.swift */; };
4B948E2629DCCDB9002531FA /* Persistence in Frameworks */ = {isa = PBXBuildFile; productRef = 4B948E2529DCCDB9002531FA /* Persistence */; };
4BB697A42B1D99C4003699B5 /* VPNWaitlistActivationDateStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B78074B2B1823C5009DB2CF /* VPNWaitlistActivationDateStore.swift */; };
4BB697A52B1D99C5003699B5 /* VPNWaitlistActivationDateStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B78074B2B1823C5009DB2CF /* VPNWaitlistActivationDateStore.swift */; };
4BB7CBB02AF59C310014A35F /* VPNWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB7CBAF2AF59C310014A35F /* VPNWidget.swift */; };
4BBBBA872B02E85400D965DA /* DesignResourcesKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4BBBBA862B02E85400D965DA /* DesignResourcesKit */; };
4BBBBA8D2B031B4200D965DA /* VPNWaitlistDebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBBBA892B031B4200D965DA /* VPNWaitlistDebugViewController.swift */; };
Expand Down Expand Up @@ -1326,6 +1329,8 @@
4B6484E927FD1E340050A7A1 /* MenuControllerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MenuControllerView.swift; sourceTree = "<group>"; };
4B6484FB27FFD14F0050A7A1 /* WindowsBrowserWaitlistTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowsBrowserWaitlistTests.swift; sourceTree = "<group>"; };
4B75EA9126A266CB00018634 /* PrintingUserScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrintingUserScript.swift; sourceTree = "<group>"; };
4B78074B2B1823C5009DB2CF /* VPNWaitlistActivationDateStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNWaitlistActivationDateStore.swift; sourceTree = "<group>"; };
4B78074D2B183A1F009DB2CF /* SurveyURLBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurveyURLBuilder.swift; sourceTree = "<group>"; };
4B83396E29AC1437003F7EA9 /* AppTrackingProtectionListModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppTrackingProtectionListModelTests.swift; sourceTree = "<group>"; };
4B83397029AC18C9003F7EA9 /* AppTrackingProtectionStoringModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppTrackingProtectionStoringModelTests.swift; sourceTree = "<group>"; };
4B83397229AFB8D2003F7EA9 /* AppTrackingProtectionFeedbackModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppTrackingProtectionFeedbackModel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3499,6 +3504,8 @@
4BBBBA8B2B031B4200D965DA /* VPNWaitlistView.swift */,
4BBBBA8A2B031B4200D965DA /* VPNWaitlistViewController.swift */,
4BCD146A2B05C4B5000B1E4C /* VPNWaitlistTermsAndConditionsViewController.swift */,
4B78074B2B1823C5009DB2CF /* VPNWaitlistActivationDateStore.swift */,
4B78074D2B183A1F009DB2CF /* SurveyURLBuilder.swift */,
);
name = VPN;
sourceTree = "<group>";
Expand Down Expand Up @@ -6214,6 +6221,7 @@
02025AEB2988229800E694E7 /* Utils.swift in Sources */,
02025AEC2988229800E694E7 /* AppTrackingProtectionPacketTunnelProvider.swift in Sources */,
02025B1029884DC500E694E7 /* AppTrackerDataParser.swift in Sources */,
4BB697A52B1D99C5003699B5 /* VPNWaitlistActivationDateStore.swift in Sources */,
EEFC6A602AC0F2F80065027D /* UserText.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -6243,6 +6251,7 @@
8528AE81212F15D600D0BD74 /* AppRatingPrompt.xcdatamodeld in Sources */,
1E24295E293F57FA00584836 /* LottieView.swift in Sources */,
8577A1C5255D2C0D00D43FCD /* HitTestingToolbar.swift in Sources */,
4BB697A42B1D99C4003699B5 /* VPNWaitlistActivationDateStore.swift in Sources */,
853C5F5B21BFF0AE001F7A05 /* HomeCollectionView.swift in Sources */,
3132FA2627A0784600DD7A12 /* FilePreviewHelper.swift in Sources */,
9820FF502244FECC008D4782 /* UIScrollViewExtension.swift in Sources */,
Expand Down Expand Up @@ -6317,6 +6326,7 @@
F1386BA41E6846C40062FC3C /* TabDelegate.swift in Sources */,
C1B924B72ACD6E6800EE7B06 /* AutofillNeverSavedTableViewCell.swift in Sources */,
020108A929A7C1CD00644F9D /* AppTrackerImageCache.swift in Sources */,
4B78074E2B183A1F009DB2CF /* SurveyURLBuilder.swift in Sources */,
3132FA2A27A0788F00DD7A12 /* QuickLookPreviewHelper.swift in Sources */,
C1D21E2D293A5965006E5A05 /* AutofillLoginSession.swift in Sources */,
4B53648A26718D0E001AA041 /* EmailWaitlist.swift in Sources */,
Expand Down Expand Up @@ -9207,7 +9217,7 @@
repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit";
requirement = {
kind = exactVersion;
version = 87.3.0;
version = 88.0.0;
};
};
C14882EB27F211A000D59F0C /* XCRemoteSwiftPackageReference "SwiftSoup" */ = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
"repositoryURL": "https://github.com/DuckDuckGo/BrowserServicesKit",
"state": {
"branch": null,
"revision": "9228f64c71e1f2ed76792482f1cfaca6ff9eb5e2",
"version": "87.3.0"
"revision": "7b35b4c604f1cf17b48aabf72526b15a2fdcc6ce",
"version": "88.0.0"
}
},
{
Expand Down
7 changes: 7 additions & 0 deletions DuckDuckGo/AppDelegate+Waitlists.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
//

import Foundation
import Core
import BackgroundTasks
import NetworkProtection

Expand All @@ -42,6 +43,10 @@ extension AppDelegate {

#if NETWORK_PROTECTION
private func checkNetworkProtectionWaitlist() {
if AppDependencyProvider.shared.featureFlagger.isFeatureOn(.networkProtectionWaitlistAccess) {
DailyPixel.fire(pixel: .networkProtectionWaitlistUserActive)
}

VPNWaitlist.shared.fetchInviteCodeIfAvailable { [weak self] error in
guard error == nil else {
#if !DEBUG
Expand Down Expand Up @@ -89,6 +94,8 @@ extension AppDelegate {
do {
try await NetworkProtectionCodeRedemptionCoordinator().redeem(inviteCode)
VPNWaitlist.shared.sendInviteCodeAvailableNotification()

DailyPixel.fire(pixel: .networkProtectionWaitlistNotificationShown)
} catch {}
}
}
Expand Down
1 change: 1 addition & 0 deletions DuckDuckGo/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate {

if identifier == VPNWaitlist.notificationIdentifier {
presentNetworkProtectionWaitlistModal()
DailyPixel.fire(pixel: .networkProtectionWaitlistNotificationLaunched)
}
#endif
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "RemoteMessageVPNAnnounce.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.
16 changes: 16 additions & 0 deletions DuckDuckGo/HomeMessageViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,22 @@ struct HomeMessageViewModel {
case .url(let value):
return {
LaunchTabNotification.postLaunchTabNotification(urlString: value)
onDidClose(buttonAction)
}
case .surveyURL(let value):
return {
#if NETWORK_PROTECTION
if let surveyURL = URL(string: value) {
let surveyURLBuilder = DefaultSurveyURLBuilder()
let surveyURLWithParameters = surveyURLBuilder.addSurveyParameters(to: surveyURL)
LaunchTabNotification.postLaunchTabNotification(urlString: surveyURLWithParameters.absoluteString)
} else {
LaunchTabNotification.postLaunchTabNotification(urlString: value)
}
#else
LaunchTabNotification.postLaunchTabNotification(urlString: value)
#endif

onDidClose(buttonAction)
}
case .appStore:
Expand Down
31 changes: 26 additions & 5 deletions DuckDuckGo/NetworkProtectionAccessController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,32 +56,53 @@ struct NetworkProtectionAccessController: NetworkProtectionAccess {
private let networkProtectionWaitlistStorage: WaitlistStorage
private let networkProtectionTermsAndConditionsStore: NetworkProtectionTermsAndConditionsStore
private let featureFlagger: FeatureFlagger
private let internalUserDecider: InternalUserDecider

private var isUserLocaleAllowed: Bool {
var regionCode: String?

if #available(iOS 16.0, *) {
regionCode = Locale.current.region?.identifier
} else {
regionCode = Locale.current.regionCode
}

return (regionCode ?? "US") == "US"
}


init(
networkProtectionActivation: NetworkProtectionFeatureActivation = NetworkProtectionKeychainTokenStore(),
networkProtectionWaitlistStorage: WaitlistStorage = WaitlistKeychainStore(waitlistIdentifier: VPNWaitlist.identifier),
networkProtectionTermsAndConditionsStore: NetworkProtectionTermsAndConditionsStore = NetworkProtectionTermsAndConditionsUserDefaultsStore(),
featureFlagger: FeatureFlagger = AppDependencyProvider.shared.featureFlagger
featureFlagger: FeatureFlagger = AppDependencyProvider.shared.featureFlagger,
internalUserDecider: InternalUserDecider = AppDependencyProvider.shared.internalUserDecider
) {
self.networkProtectionActivation = networkProtectionActivation
self.networkProtectionWaitlistStorage = networkProtectionWaitlistStorage
self.networkProtectionTermsAndConditionsStore = networkProtectionTermsAndConditionsStore
self.featureFlagger = featureFlagger
self.internalUserDecider = internalUserDecider
}

func networkProtectionAccessType() -> NetworkProtectionAccessType {
// First, check for users who have activated the VPN via an invite code:
// Only show NetP to US or internal users:
guard isUserLocaleAllowed || internalUserDecider.isInternalUser else {
return .none
}

// Check for users who have activated the VPN via an invite code:
if networkProtectionActivation.isFeatureActivated && !networkProtectionWaitlistStorage.isInvited {
return .inviteCodeInvited
}

// Next, check if the waitlist is still active; if not, the user has no access.
// Check if the waitlist is still active; if not, the user has no access.
let isWaitlistActive = featureFlagger.isFeatureOn(.networkProtectionWaitlistActive)
if !isWaitlistActive {
return .none
}

// Next, check if a waitlist user has NetP access and whether they need to accept T&C.
// Check if a waitlist user has NetP access and whether they need to accept T&C.
if networkProtectionActivation.isFeatureActivated && networkProtectionWaitlistStorage.isInvited {
if networkProtectionTermsAndConditionsStore.networkProtectionWaitlistTermsAndConditionsAccepted {
return .waitlistInvited
Expand All @@ -90,7 +111,7 @@ struct NetworkProtectionAccessController: NetworkProtectionAccess {
}
}

// Next, check if the user has waitlist access at all and whether they've already joined.
// Check if the user has waitlist access at all and whether they've already joined.
let hasWaitlistAccess = featureFlagger.isFeatureOn(.networkProtectionWaitlistAccess)
if hasWaitlistAccess {
if networkProtectionWaitlistStorage.isOnWaitlist {
Expand Down
39 changes: 29 additions & 10 deletions DuckDuckGo/RemoteMessaging.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import BrowserServicesKit
import Persistence
import Bookmarks
import RemoteMessaging
import NetworkProtection

struct RemoteMessaging {

Expand Down Expand Up @@ -149,17 +150,35 @@ struct RemoteMessaging {
case .success(let statusResponse):
os_log("Successfully fetched remote messages", log: .remoteMessaging, type: .debug)

let isNetworkProtectionWaitlistUser: Bool
let daysSinceNetworkProtectionEnabled: Int

#if NETWORK_PROTECTION
let vpnAccess = NetworkProtectionAccessController()
let accessType = vpnAccess.networkProtectionAccessType()
let isVPNActivated = NetworkProtectionKeychainTokenStore().isFeatureActivated
let activationDateStore = DefaultVPNWaitlistActivationDateStore()

isNetworkProtectionWaitlistUser = (accessType == .waitlistInvited) && isVPNActivated
daysSinceNetworkProtectionEnabled = activationDateStore.daysSinceActivation() ?? -1
#else
isNetworkProtectionWaitlistUser = false
daysSinceNetworkProtectionEnabled = -1
#endif

let remoteMessagingConfigMatcher = RemoteMessagingConfigMatcher(
appAttributeMatcher: AppAttributeMatcher(statisticsStore: statisticsStore,
variantManager: variantManager,
isInternalUser: AppDependencyProvider.shared.internalUserDecider.isInternalUser),
userAttributeMatcher: UserAttributeMatcher(statisticsStore: statisticsStore,
variantManager: variantManager,
bookmarksCount: bookmarksCount,
favoritesCount: favoritesCount,
appTheme: AppUserDefaults().currentThemeName.rawValue,
isWidgetInstalled: isWidgetInstalled),
dismissedMessageIds: remoteMessagingStore.fetchDismissedRemoteMessageIds()
appAttributeMatcher: AppAttributeMatcher(statisticsStore: statisticsStore,
variantManager: variantManager,
isInternalUser: AppDependencyProvider.shared.internalUserDecider.isInternalUser),
userAttributeMatcher: UserAttributeMatcher(statisticsStore: statisticsStore,
variantManager: variantManager,
bookmarksCount: bookmarksCount,
favoritesCount: favoritesCount,
appTheme: AppUserDefaults().currentThemeName.rawValue,
isWidgetInstalled: isWidgetInstalled,
isNetPWaitlistUser: isNetworkProtectionWaitlistUser,
daysSinceNetPEnabled: daysSinceNetworkProtectionEnabled),
dismissedMessageIds: remoteMessagingStore.fetchDismissedRemoteMessageIds()
)

let processor = RemoteMessagingConfigProcessor(remoteMessagingConfigMatcher: remoteMessagingConfigMatcher)
Expand Down
4 changes: 4 additions & 0 deletions DuckDuckGo/SettingsViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,10 @@ class SettingsViewController: UITableViewController {
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let theme = ThemeManager.shared.currentTheme
cell.backgroundColor = theme.tableCellBackgroundColor

if cell == netPCell {
DailyPixel.fire(pixel: .networkProtectionSettingsRowDisplayed)
}
}

override func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection: Int) {
Expand Down
Loading

0 comments on commit 08b34f8

Please sign in to comment.