Skip to content

Commit

Permalink
Merge branch 'develop' into sam/improved-netp-waitlist-visibility-and…
Browse files Browse the repository at this point in the history
…-pixels

# Via GitHub
* develop:
  Basic NetP remote messaging (#1665)

# Conflicts:
#	DuckDuckGo/Main/View/MainViewController.swift
#	DuckDuckGo/Statistics/PixelParameters.swift
  • Loading branch information
samsymons committed Oct 2, 2023
2 parents fd4f7b5 + 075c821 commit 86fdd53
Show file tree
Hide file tree
Showing 27 changed files with 1,352 additions and 107 deletions.
82 changes: 82 additions & 0 deletions DuckDuckGo.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,6 @@ extension UserText {
static let networkProtectionWaitlistButtonJoinWaitlist = NSLocalizedString("network-protection.waitlist.button.join-waitlist", value: "Join the Waitlist", comment: "Join Waitlist button for Network Protection join waitlist screen")
static let networkProtectionWaitlistButtonAgreeAndContinue = NSLocalizedString("network-protection.waitlist.button.agree-and-continue", value: "Agree and Continue", comment: "Agree and Continue button for Network Protection join waitlist screen")

static let networkProtectionBetaEndedCardTitle = NSLocalizedString("network-protection.waitlist.beta-ended-card.title", value: "VPN Beta Closed", comment: "Title for the Network Protection beta ended card")
static let networkProtectionBetaEndedCardText = NSLocalizedString("network-protection.waitlist.beta-ended-card.text", value: "Thank you for participating! We look forward to sharing more with you in future product announcements.", comment: "Text for the Network Protection beta ended card")
static let networkProtectionBetaEndedCardAction = NSLocalizedString("network-protection.waitlist.beta-ended-card.action", value: "Dismiss", comment: "Action text for the Network Protection beta ended card")

}

// MARK: - Network Protection Terms of Service
Expand Down
44 changes: 44 additions & 0 deletions DuckDuckGo/Common/Utilities/HardwareModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//
// HardwareModel.swift
//
// Copyright © 2023 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 IOKit

struct HardwareModel {

static var model: String? {
let service = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOPlatformExpertDevice"))
var modelIdentifier: String?

if let modelData = IORegistryEntryCreateCFProperty(
service,
"model" as CFString,
kCFAllocatorDefault,
0
).takeRetainedValue() as? Data {
if let modelIdentifierCString = String(data: modelData, encoding: .utf8)?.cString(using: .utf8) {
modelIdentifier = String(cString: modelIdentifierCString)
}
}

IOObjectRelease(service)

return modelIdentifier
}

}
2 changes: 1 addition & 1 deletion DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@ public struct UserDefaultsWrapper<T> {
case homePageIsContinueSetupVisible = "home.page.is.continue.setup.visible"
case homePageIsRecentActivityVisible = "home.page.is.recent.activity.visible"
case homePageIsFirstSession = "home.page.is.first.session"
case homePageShowNetworkProtectionBetaEndedNotice = "home.page.network-protection.show-beta-ended-notice"

case appIsRelaunchingAutomatically = "app-relaunching-automatically"

Expand Down Expand Up @@ -176,6 +175,7 @@ public struct UserDefaultsWrapper<T> {
enum RemovedKeys: String, CaseIterable {
case passwordManagerDoNotPromptDomains = "com.duckduckgo.passwordmanager.do-not-prompt-domains"
case incrementalFeatureFlagTestHasSentPixel = "network-protection.incremental-feature-flag-test.has-sent-pixel"
case homePageShowNetworkProtectionBetaEndedNotice = "home.page.network-protection.show-beta-ended-notice"
}

private let key: Key
Expand Down
115 changes: 48 additions & 67 deletions DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ extension HomePage.Models {
let itemsRowCountWhenCollapsed = HomePage.featureRowCountWhenCollapsed
let gridWidth = FeaturesGridDimensions.width
let deleteActionTitle = UserText.newTabSetUpRemoveItemAction
let networkProtectionRemoteMessaging: NetworkProtectionRemoteMessaging
let privacyConfigurationManager: PrivacyConfigurationManaging

var isDay0SurveyEnabled: Bool {
Expand Down Expand Up @@ -103,9 +104,6 @@ extension HomePage.Models {
@UserDefaultsWrapper(key: .homePageShowSurveyDay7, defaultValue: true)
private var shouldShowSurveyDay7: Bool

@UserDefaultsWrapper(key: .homePageShowNetworkProtectionBetaEndedNotice, defaultValue: true)
private var shouldShowNetworkProtectionBetaEndedNotice: Bool

@UserDefaultsWrapper(key: .homePageIsFirstSession, defaultValue: true)
private var isFirstSession: Bool

Expand Down Expand Up @@ -139,6 +137,7 @@ extension HomePage.Models {
privacyPreferences: PrivacySecurityPreferences = PrivacySecurityPreferences.shared,
cookieConsentPopoverManager: CookieConsentPopoverManager = CookieConsentPopoverManager(),
duckPlayerPreferences: DuckPlayerPreferencesPersistor,
networkProtectionRemoteMessaging: NetworkProtectionRemoteMessaging,
privacyConfigurationManager: PrivacyConfigurationManaging = AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager) {
self.defaultBrowserProvider = defaultBrowserProvider
self.dataImportProvider = dataImportProvider
Expand All @@ -147,6 +146,7 @@ extension HomePage.Models {
self.privacyPreferences = privacyPreferences
self.cookieConsentPopoverManager = cookieConsentPopoverManager
self.duckPlayerPreferences = duckPlayerPreferences
self.networkProtectionRemoteMessaging = networkProtectionRemoteMessaging
self.privacyConfigurationManager = privacyConfigurationManager
refreshFeaturesMatrix()
NotificationCenter.default.addObserver(self, selector: #selector(newTabOpenNotification(_:)), name: HomePage.Models.newHomePageTabOpen, object: nil)
Expand Down Expand Up @@ -188,8 +188,8 @@ extension HomePage.Models {
visitSurvey(day: .day0)
case .surveyDay7:
visitSurvey(day: .day7)
case .networkProtectionBetaEndedNotice:
removeItem(for: .networkProtectionBetaEndedNotice)
case .networkProtectionRemoteMessage(let message):
handle(remoteMessage: message)
}
}
// swiftlint:enable cyclomatic_complexity
Expand All @@ -210,8 +210,9 @@ extension HomePage.Models {
shouldShowSurveyDay0 = false
case .surveyDay7:
shouldShowSurveyDay7 = false
case .networkProtectionBetaEndedNotice:
shouldShowNetworkProtectionBetaEndedNotice = false
case .networkProtectionRemoteMessage(let message):
networkProtectionRemoteMessaging.dismiss(message: message)
Pixel.fire(.networkProtectionRemoteMessageDismissed(messageID: message.id))
}
refreshFeaturesMatrix()
}
Expand All @@ -220,8 +221,13 @@ extension HomePage.Models {
func refreshFeaturesMatrix() {
var features: [FeatureType] = []

if shouldNetworkProtectionBetaEndedNoticeBeVisible {
features.append(.networkProtectionBetaEndedNotice)
for message in networkProtectionRemoteMessaging.presentableRemoteMessages() {
features.append(.networkProtectionRemoteMessage(message))
DailyPixel.fire(
pixel: .networkProtectionRemoteMessageDisplayed(messageID: message.id),
frequency: .dailyOnly,
includeAppVersionParameter: true
)
}

for feature in listOfFeatures {
Expand Down Expand Up @@ -254,8 +260,8 @@ extension HomePage.Models {
if shouldSurveyDay7BeVisible {
features.append(feature)
}
case .networkProtectionBetaEndedNotice:
break // Do nothing, as the NetP beta ended notice will always be added to the start of the list
case .networkProtectionRemoteMessage:
break // Do nothing, NetP remote messages get appended first
}
}
featuresMatrix = features.chunked(into: itemsPerRow)
Expand Down Expand Up @@ -353,53 +359,6 @@ extension HomePage.Models {
firstLaunchDate <= oneWeekAgo
}

/// The Network Protection beta ended card should only be displayed under the following conditions:
///
/// 1. The user has gone through the waitlist AND used Network Protection at least once
/// 2. The `waitlistBetaActive` flag has been set to disabled
/// 3. The user has not already dismissed the card
private var shouldNetworkProtectionBetaEndedNoticeBeVisible: Bool {
#if NETWORK_PROTECTION
// 1. The user has signed up for the waitlist AND used Network Protection at least once:

let waitlistStorage = NetworkProtectionWaitlist().waitlistStorage
let isWaitlistUser = waitlistStorage.isWaitlistUser && waitlistStorage.isInvited

guard isWaitlistUser else {
return false
}

let activationStore = WaitlistActivationDateStore()
guard activationStore.daysSinceActivation() != nil else {
return false
}

// 2. The `waitlistBetaActive` flag has been set to disabled

let featureOverrides = DefaultWaitlistBetaOverrides()
let waitlistFlagEnabled: Bool

switch featureOverrides.waitlistActive {
case .useRemoteValue:
waitlistFlagEnabled = privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(NetworkProtectionSubfeature.waitlistBetaActive)
case .on:
waitlistFlagEnabled = true
case .off:
waitlistFlagEnabled = false
}

guard !waitlistFlagEnabled else {
return false
}

// 3. The user has not already dismissed the card

return shouldShowNetworkProtectionBetaEndedNotice
#else
return false
#endif
}

private enum SurveyDay {
case day0
case day7
Expand Down Expand Up @@ -428,18 +387,40 @@ extension HomePage.Models {
}
}
}

@MainActor private func handle(remoteMessage: NetworkProtectionRemoteMessage) {
if let surveyURL = remoteMessage.presentableSurveyURL() {
let tab = Tab(content: .url(surveyURL), shouldLoadInBackground: true)
tabCollectionViewModel.append(tab: tab)
Pixel.fire(.networkProtectionRemoteMessageOpened(messageID: remoteMessage.id))
} else {
Pixel.fire(.networkProtectionRemoteMessageDismissed(messageID: remoteMessage.id))
}

// Dismiss the message after the user opens the survey, even if they just close the tab immediately afterwards.
networkProtectionRemoteMessaging.dismiss(message: remoteMessage)
refreshFeaturesMatrix()
}
}

// MARK: Feature Type
enum FeatureType: CaseIterable {
enum FeatureType: CaseIterable, Equatable, Hashable {

// CaseIterable doesn't work with enums that have associated values, so we have to implement it manually.
// We ignore the `networkProtectionRemoteMessage` case here to avoid it getting accidentally included - it has special handling and will get
// included elsewhere.
static var allCases: [HomePage.Models.FeatureType] {
[.duckplayer, .cookiePopUp, .emailProtection, .defaultBrowser, .importBookmarksAndPasswords, .surveyDay0, .surveyDay7]
}

case duckplayer
case cookiePopUp
case emailProtection
case defaultBrowser
case importBookmarksAndPasswords
case surveyDay0
case surveyDay7
case networkProtectionBetaEndedNotice
case networkProtectionRemoteMessage(NetworkProtectionRemoteMessage)

var title: String {
switch self {
Expand All @@ -457,8 +438,8 @@ extension HomePage.Models {
return UserText.newTabSetUpSurveyDay0CardTitle
case .surveyDay7:
return UserText.newTabSetUpSurveyDay7CardTitle
case .networkProtectionBetaEndedNotice:
return UserText.networkProtectionBetaEndedCardTitle
case .networkProtectionRemoteMessage(let message):
return message.cardTitle
}
}

Expand All @@ -478,8 +459,8 @@ extension HomePage.Models {
return UserText.newTabSetUpSurveyDay0Summary
case .surveyDay7:
return UserText.newTabSetUpSurveyDay7Summary
case .networkProtectionBetaEndedNotice:
return UserText.networkProtectionBetaEndedCardText
case .networkProtectionRemoteMessage(let message):
return message.cardDescription
}
}

Expand All @@ -499,8 +480,8 @@ extension HomePage.Models {
return UserText.newTabSetUpSurveyDay0Action
case .surveyDay7:
return UserText.newTabSetUpSurveyDay7Action
case .networkProtectionBetaEndedNotice:
return UserText.networkProtectionBetaEndedCardAction
case .networkProtectionRemoteMessage(let message):
return message.cardAction
}
}

Expand All @@ -522,7 +503,7 @@ extension HomePage.Models {
return NSImage(named: "Survey-128")!.resized(to: iconSize)!
case .surveyDay7:
return NSImage(named: "Survey-128")!.resized(to: iconSize)!
case .networkProtectionBetaEndedNotice:
case .networkProtectionRemoteMessage:
return NSImage(named: "VPN-Ended")!.resized(to: iconSize)!
}
}
Expand Down
2 changes: 1 addition & 1 deletion DuckDuckGo/HomePage/View/HomePageViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ final class HomePageViewController: NSViewController {
}

func createFeatureModel() -> HomePage.Models.ContinueSetUpModel {
let vm = HomePage.Models.ContinueSetUpModel(defaultBrowserProvider: SystemDefaultBrowserProvider(), dataImportProvider: BookmarksAndPasswordsImportStatusProvider(), tabCollectionViewModel: tabCollectionViewModel, duckPlayerPreferences: DuckPlayerPreferencesUserDefaultsPersistor())
let vm = HomePage.Models.ContinueSetUpModel(defaultBrowserProvider: SystemDefaultBrowserProvider(), dataImportProvider: BookmarksAndPasswordsImportStatusProvider(), tabCollectionViewModel: tabCollectionViewModel, duckPlayerPreferences: DuckPlayerPreferencesUserDefaultsPersistor(), networkProtectionRemoteMessaging: DefaultNetworkProtectionRemoteMessaging())
vm.delegate = self
return vm
}
Expand Down
7 changes: 7 additions & 0 deletions DuckDuckGo/Main/View/MainViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ final class MainViewController: NSViewController {

#if NETWORK_PROTECTION
sendActiveNetworkProtectionWaitlistUserPixel()
refreshNetworkProtectionMessages()
#endif
}

Expand All @@ -159,6 +160,12 @@ final class MainViewController: NSViewController {
}
}

private let networkProtectionMessaging = DefaultNetworkProtectionRemoteMessaging()

func refreshNetworkProtectionMessages() {
networkProtectionMessaging.fetchRemoteMessages()
}

override func encodeRestorableState(with coder: NSCoder) {
fatalError("Default AppKit State Restoration should not be used")
}
Expand Down
Loading

0 comments on commit 86fdd53

Please sign in to comment.