Skip to content

Commit

Permalink
Privacy Pro survey support (#2816)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/72649045549333/1206971614154218/f
Tech Design URL:
CC:

Description:

This PR adds support for showing surveys to Privacy Pro users.
  • Loading branch information
samsymons authored May 26, 2024
1 parent 4d1917b commit 86fd337
Show file tree
Hide file tree
Showing 28 changed files with 923 additions and 1,342 deletions.
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

0 comments on commit 86fd337

Please sign in to comment.