Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Privacy Pro survey support #2816

Merged
merged 28 commits into from
May 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9f11a01
Replace old survey classes.
samsymons May 22, 2024
26992a6
Add new parameters.
samsymons May 23, 2024
3137f96
Merge branch 'main' into sam/privacy-pro-surveys
samsymons May 23, 2024
b37b60e
Clean up pixels and tests.
samsymons May 23, 2024
a5beece
Pass the subscription manager through to the remote message fetcher.
samsymons May 23, 2024
205b0b5
Update class names.
samsymons May 23, 2024
8349e8b
Convert to Swift Concurrency.
samsymons May 23, 2024
67a083b
Fix test suite compilation.
samsymons May 23, 2024
f7564af
Fix up unit tests.
samsymons May 23, 2024
c47e4ab
Hide other surveys when the Privacy Pro survey is visible.
samsymons May 23, 2024
47ba861
Remove duplicate file entry.
samsymons May 23, 2024
2fdcf49
Debug menu for survey remote message.
samsymons May 23, 2024
e598a9f
Really fix duplicate file issue.
samsymons May 23, 2024
d8dab5c
Merge branch 'main' into sam/privacy-pro-surveys
samsymons May 23, 2024
30a0819
Fix subtle VPN presentation survey bug.
samsymons May 24, 2024
ca90535
Clean up pixels.
samsymons May 24, 2024
03a8c13
Check all attributes when matching.
samsymons May 24, 2024
efa97cc
Add new survey parameters.
samsymons May 24, 2024
ca701e8
Merge branch 'main' into sam/privacy-pro-surveys
samsymons May 24, 2024
7798f7b
Fix SwiftLint.
samsymons May 24, 2024
e723c05
Update labels for clarity and add PIR checks.
samsymons May 24, 2024
fe6ec89
Restore DBP `windowDidBecomeMain` call.
samsymons May 24, 2024
6973701
Clean up duplicate logic.
samsymons May 24, 2024
43dbeb4
Remove null file references
samsymons May 24, 2024
a49ea72
Add Privacy Pro parameters.
samsymons May 24, 2024
398e111
Merge branch 'main' into sam/privacy-pro-surveys
samsymons May 25, 2024
3d9aefe
Never use a negative amount of days for survey params.
samsymons May 25, 2024
76b2738
SwiftLint fixes.
samsymons May 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 35 additions & 63 deletions DuckDuckGo.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Privacy-Pro-128.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.
64 changes: 23 additions & 41 deletions DuckDuckGo/Common/Surveys/HomePageRemoteMessagingRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,32 +21,20 @@ import Networking

protocol HomePageRemoteMessagingRequest {

func fetchHomePageRemoteMessages<T: Decodable>(completion: @escaping (Result<[T], Error>) -> Void)
func fetchHomePageRemoteMessages() async -> Result<[SurveyRemoteMessage], Error>

}

final class DefaultHomePageRemoteMessagingRequest: HomePageRemoteMessagingRequest {

enum NetworkProtectionEndpoint {
enum SurveysEndpoint {
case debug
case production

var url: URL {
switch self {
case .debug: return URL(string: "https://staticcdn.duckduckgo.com/macos-desktop-browser/network-protection/messages-v2-debug.json")!
case .production: return URL(string: "https://staticcdn.duckduckgo.com/macos-desktop-browser/network-protection/messages-v2.json")!
}
}
}

enum DataBrokerProtectionEndpoint {
case debug
case production

var url: URL {
switch self {
case .debug: return URL(string: "https://staticcdn.duckduckgo.com/macos-desktop-browser/dbp/messages-debug.json")!
case .production: return URL(string: "https://staticcdn.duckduckgo.com/macos-desktop-browser/dbp/messages.json")!
case .debug: return URL(string: "https://staticcdn.duckduckgo.com/macos-desktop-browser/surveys/surveys-debug.json")!
case .production: return URL(string: "https://staticcdn.duckduckgo.com/macos-desktop-browser/surveys/surveys.json")!
}
}
}
Expand All @@ -56,19 +44,11 @@ final class DefaultHomePageRemoteMessagingRequest: HomePageRemoteMessagingReques
case requestCompletedWithoutErrorOrResponse
}

static func networkProtectionMessagesRequest() -> HomePageRemoteMessagingRequest {
static func surveysRequest() -> HomePageRemoteMessagingRequest {
#if DEBUG || REVIEW
return DefaultHomePageRemoteMessagingRequest(endpointURL: NetworkProtectionEndpoint.debug.url)
return DefaultHomePageRemoteMessagingRequest(endpointURL: SurveysEndpoint.debug.url)
#else
return DefaultHomePageRemoteMessagingRequest(endpointURL: NetworkProtectionEndpoint.production.url)
#endif
}

static func dataBrokerProtectionMessagesRequest() -> HomePageRemoteMessagingRequest {
#if DEBUG || REVIEW
return DefaultHomePageRemoteMessagingRequest(endpointURL: DataBrokerProtectionEndpoint.debug.url)
#else
return DefaultHomePageRemoteMessagingRequest(endpointURL: DataBrokerProtectionEndpoint.production.url)
return DefaultHomePageRemoteMessagingRequest(endpointURL: SurveysEndpoint.production.url)
#endif
}

Expand All @@ -78,25 +58,27 @@ final class DefaultHomePageRemoteMessagingRequest: HomePageRemoteMessagingReques
self.endpointURL = endpointURL
}

func fetchHomePageRemoteMessages<T: Decodable>(completion: @escaping (Result<[T], Error>) -> Void) {
func fetchHomePageRemoteMessages() async -> Result<[SurveyRemoteMessage], Error> {
let httpMethod = APIRequest.HTTPMethod.get
let configuration = APIRequest.Configuration(url: endpointURL, method: httpMethod, body: nil)
let request = APIRequest(configuration: configuration)

request.fetch { response, error in
if let error {
completion(Result.failure(error))
} else if let responseData = response?.data {
do {
let decoder = JSONDecoder()
let decoded = try decoder.decode([T].self, from: responseData)
completion(Result.success(decoded))
} catch {
completion(.failure(HomePageRemoteMessagingRequestError.failedToDecodeMessages))
}
} else {
completion(.failure(HomePageRemoteMessagingRequestError.requestCompletedWithoutErrorOrResponse))
do {
let response = try await request.fetch()

guard let data = response.data else {
return .failure(HomePageRemoteMessagingRequestError.requestCompletedWithoutErrorOrResponse)
}

do {
let decoder = JSONDecoder()
let decoded = try decoder.decode([SurveyRemoteMessage].self, from: data)
return .success(decoded)
} catch {
return .failure(HomePageRemoteMessagingRequestError.failedToDecodeMessages)
}
} catch {
return .failure(error)
}
}

Expand Down
42 changes: 15 additions & 27 deletions DuckDuckGo/Common/Surveys/HomePageRemoteMessagingStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,21 @@

import Foundation

protocol HomePageRemoteMessagingStorage {
protocol SurveyRemoteMessagingStorage {

func store<Message: Codable>(messages: [Message]) throws
func storedMessages<Message: Codable>() -> [Message]
func store(messages: [SurveyRemoteMessage]) throws
func storedMessages() -> [SurveyRemoteMessage]

func dismissRemoteMessage(with id: String)
func dismissedMessageIDs() -> [String]

}

final class DefaultHomePageRemoteMessagingStorage: HomePageRemoteMessagingStorage {
final class DefaultSurveyRemoteMessagingStorage: SurveyRemoteMessagingStorage {

enum NetworkProtectionConstants {
static let dismissedMessageIdentifiersKey = "home.page.network-protection.dismissed-message-identifiers"
static let networkProtectionMessagesFileName = "network-protection-messages.json"
}

enum DataBrokerProtectionConstants {
static let dismissedMessageIdentifiersKey = "home.page.dbp.dismissed-message-identifiers"
static let networkProtectionMessagesFileName = "dbp-messages.json"
enum SurveyConstants {
static let dismissedMessageIdentifiersKey = "home.page.survey.dismissed-message-identifiers"
static let surveyMessagesFileName = "survey-messages.json"
}

private let userDefaults: UserDefaults
Expand All @@ -48,23 +43,16 @@ final class DefaultHomePageRemoteMessagingStorage: HomePageRemoteMessagingStorag
URL.sandboxApplicationSupportURL
}

static func networkProtection() -> DefaultHomePageRemoteMessagingStorage {
return DefaultHomePageRemoteMessagingStorage(
messagesFileName: NetworkProtectionConstants.networkProtectionMessagesFileName,
dismissedMessageIdentifiersKey: NetworkProtectionConstants.dismissedMessageIdentifiersKey
)
}

static func dataBrokerProtection() -> DefaultHomePageRemoteMessagingStorage {
return DefaultHomePageRemoteMessagingStorage(
messagesFileName: DataBrokerProtectionConstants.networkProtectionMessagesFileName,
dismissedMessageIdentifiersKey: DataBrokerProtectionConstants.dismissedMessageIdentifiersKey
static func surveys() -> DefaultSurveyRemoteMessagingStorage {
return DefaultSurveyRemoteMessagingStorage(
messagesFileName: SurveyConstants.surveyMessagesFileName,
dismissedMessageIdentifiersKey: SurveyConstants.dismissedMessageIdentifiersKey
)
}

init(
userDefaults: UserDefaults = .standard,
messagesDirectoryURL: URL = DefaultHomePageRemoteMessagingStorage.applicationSupportURL,
messagesDirectoryURL: URL = DefaultSurveyRemoteMessagingStorage.applicationSupportURL,
messagesFileName: String,
dismissedMessageIdentifiersKey: String
) {
Expand All @@ -73,15 +61,15 @@ final class DefaultHomePageRemoteMessagingStorage: HomePageRemoteMessagingStorag
self.dismissedMessageIdentifiersKey = dismissedMessageIdentifiersKey
}

func store<Message: Codable>(messages: [Message]) throws {
func store(messages: [SurveyRemoteMessage]) throws {
let encoded = try JSONEncoder().encode(messages)
try encoded.write(to: messagesURL)
}

func storedMessages<Message: Codable>() -> [Message] {
func storedMessages() -> [SurveyRemoteMessage] {
do {
let messagesData = try Data(contentsOf: messagesURL)
let messages = try JSONDecoder().decode([Message].self, from: messagesData)
let messages = try JSONDecoder().decode([SurveyRemoteMessage].self, from: messagesData)

return messages
} catch {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// DataBrokerProtectionRemoteMessage.swift
// SurveyRemoteMessage.swift
//
// Copyright © 2023 DuckDuckGo. All rights reserved.
//
Expand All @@ -18,10 +18,10 @@

import Foundation
import Common
import Subscription

struct DataBrokerRemoteMessageAction: Codable, Equatable, Hashable {
struct SurveyRemoteMessageAction: Codable, Equatable, Hashable {
enum Action: String, Codable {
case openDataBrokerProtection
case openSurveyURL
case openURL
}
Expand All @@ -31,23 +31,31 @@ struct DataBrokerRemoteMessageAction: Codable, Equatable, Hashable {
let actionURL: String?
}

struct DataBrokerProtectionRemoteMessage: Codable, Equatable, Hashable {
struct SurveyRemoteMessage: Codable, Equatable, Identifiable, Hashable {

struct Attributes: Codable, Equatable, Hashable {
let subscriptionStatus: String?
let subscriptionBillingPeriod: String?
let minimumDaysSinceSubscriptionStarted: Int?
let maximumDaysUntilSubscriptionExpirationOrRenewal: Int?
let daysSinceVPNEnabled: Int?
let daysSincePIREnabled: Int?
}

let id: String
let cardTitle: String
let cardDescription: String
/// If this is set, the message won't be displayed if DBP hasn't been used, even if the usage and access booleans are false
let daysSinceDataBrokerProtectionEnabled: Int?
let requiresDataBrokerProtectionUsage: Bool
let requiresDataBrokerProtectionAccess: Bool
let action: DataBrokerRemoteMessageAction
let attributes: Attributes
let action: SurveyRemoteMessageAction

func presentableSurveyURL(
statisticsStore: StatisticsStore = LocalStatisticsStore(),
activationDateStore: WaitlistActivationDateStore = DefaultWaitlistActivationDateStore(source: .dbp),
vpnActivationDateStore: WaitlistActivationDateStore = DefaultWaitlistActivationDateStore(source: .netP),
pirActivationDateStore: WaitlistActivationDateStore = DefaultWaitlistActivationDateStore(source: .dbp),
operatingSystemVersion: String = ProcessInfo.processInfo.operatingSystemVersion.description,
appVersion: String = AppVersion.shared.versionNumber,
hardwareModel: String? = HardwareModel.model
hardwareModel: String? = HardwareModel.model,
subscription: Subscription?
) -> URL? {
if let actionType = action.actionType, actionType == .openURL, let urlString = action.actionURL, let url = URL(string: urlString) {
return url
Expand All @@ -62,8 +70,11 @@ struct DataBrokerProtectionRemoteMessage: Codable, Equatable, Hashable {
operatingSystemVersion: operatingSystemVersion,
appVersion: appVersion,
hardwareModel: hardwareModel,
daysSinceActivation: activationDateStore.daysSinceActivation(),
daysSinceLastActive: activationDateStore.daysSinceLastActive()
subscription: subscription,
daysSinceVPNActivated: vpnActivationDateStore.daysSinceActivation(),
daysSinceVPNLastActive: vpnActivationDateStore.daysSinceLastActive(),
daysSincePIRActivated: pirActivationDateStore.daysSinceActivation(),
daysSincePIRLastActive: pirActivationDateStore.daysSinceLastActive()
)

return surveyURLBuilder.buildSurveyURL(from: surveyURL)
Expand Down
Loading
Loading