Skip to content

Commit

Permalink
macOS in-context signup updates (#359)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/0/1204636039042602/f
iOS PR: duckduckgo/iOS#1756
macOS PR: duckduckgo/macos-browser#1209
What kind of version bump will this require?: Major -- a new parameter is added to ContentScopeFeatureToggles

Optional:

Tech Design URL:
CC: @amddg44 @afterxleep

Description:
Adds support for getting and setting in-context signup dismissal, as well as opening and closing the Email Protection web app tab for users signing-in/signiing-up
  • Loading branch information
alistairjcbrown authored Jun 9, 2023
1 parent a1c93d2 commit 2e90e6c
Show file tree
Hide file tree
Showing 13 changed files with 152 additions and 13 deletions.
4 changes: 2 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/duckduckgo/duckduckgo-autofill.git",
"state" : {
"revision" : "d1558f7757b26f64364dd23d02d235c113d558ae",
"version" : "7.1.0"
"revision" : "44cd844b6bb5d8ccfefbd6e025817d800c26ad76",
"version" : "7.2.0"
}
},
{
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ let package = Package(
.library(name: "SyncDataProviders", targets: ["SyncDataProviders"]),
],
dependencies: [
.package(url: "https://github.com/duckduckgo/duckduckgo-autofill.git", exact: "7.1.0"),
.package(url: "https://github.com/duckduckgo/duckduckgo-autofill.git", exact: "7.2.0"),
.package(url: "https://github.com/duckduckgo/GRDB.swift.git", exact: "2.1.1"),
.package(url: "https://github.com/duckduckgo/TrackerRadarKit", exact: "1.2.1"),
.package(url: "https://github.com/duckduckgo/sync_crypto", exact: "0.2.0"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ public protocol AutofillEmailDelegate: AnyObject {
func autofillUserScriptDidRequestUserData(_ : AutofillUserScript, completionHandler: @escaping UserDataCompletion)
func autofillUserScriptDidRequestSignOut(_ : AutofillUserScript)
func autofillUserScriptDidRequestSignedInStatus(_: AutofillUserScript) -> Bool

func autofillUserScript(_ : AutofillUserScript, didRequestSetInContextPromptValue value: Double)
func autofillUserScriptDidRequestInContextPromptValue(_ : AutofillUserScript) -> Double?
func autofillUserScriptDidRequestInContextSignup(_ : AutofillUserScript)
func autofillUserScriptDidCompleteInContextSignup(_ : AutofillUserScript)
}

extension AutofillUserScript {
Expand Down Expand Up @@ -142,4 +145,37 @@ extension AutofillUserScript {
}
}

// MARK: In Context Email Protection

func setIncontextSignupPermanentlyDismissedAt(_ message: UserScriptMessage, replyHandler: @escaping MessageReplyHandler) {
guard let body = message.messageBody as? [String: Any],
let value = body["value"] as? Double else {
return
}
emailDelegate?.autofillUserScript(self, didRequestSetInContextPromptValue: value)
replyHandler(nil)
}

func getIncontextSignupDismissedAt(_ message: UserScriptMessage, replyHandler: @escaping MessageReplyHandler) {
let inContextEmailSignupPromptDismissedPermanentlyAt: Double? = emailDelegate?.autofillUserScriptDidRequestInContextPromptValue(self)
let inContextSignupDismissedAt = IncontextSignupDismissedAt(
permanentlyDismissedAt: inContextEmailSignupPromptDismissedPermanentlyAt
)
let response = GetIncontextSignupDismissedAtResponse(success: inContextSignupDismissedAt)

if let json = try? JSONEncoder().encode(response), let jsonString = String(data: json, encoding: .utf8) {
replyHandler(jsonString)
}
}

func startEmailProtectionSignup(_ message: UserScriptMessage, replyHandler: @escaping MessageReplyHandler) {
emailDelegate?.autofillUserScriptDidRequestInContextSignup(self)
replyHandler(nil)
}

func closeEmailProtectionTab(_ message: UserScriptMessage, replyHandler: @escaping MessageReplyHandler) {
emailDelegate?.autofillUserScriptDidCompleteInContextSignup(self)
replyHandler(nil)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,15 @@ extension AutofillUserScript {
let success: CredentialObject
let error: String?
}

struct IncontextSignupDismissedAt: Codable {
let permanentlyDismissedAt: Double?
}

struct GetIncontextSignupDismissedAtResponse: Codable {
let success: IncontextSignupDismissedAt
}

// swiftlint:enable nesting

struct RequestAutoFillCreditCardResponse: Codable {
Expand Down Expand Up @@ -752,7 +761,6 @@ extension AutofillUserScript {

vaultDelegate?.autofillUserScript(self, didSendPixel: JSPixel(pixelName: pixelName, pixelParameters: pixelParameters))
}

}

extension AutofillUserScript.RequestAvailableInputTypesResponse {
Expand Down
14 changes: 13 additions & 1 deletion Sources/BrowserServicesKit/Autofill/AutofillUserScript.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ import Common
import WebKit
import UserScript

public class AutofillUserScript: NSObject, UserScript, UserScriptMessageEncryption {
var previousIncontextSignupPermanentlyDismissedAt: Double? = nil
var previousEmailSignedIn: Bool? = nil

public class AutofillUserScript: NSObject, UserScript, UserScriptMessageEncryption {
internal enum MessageName: String, CaseIterable {
case emailHandlerStoreToken
case emailHandlerRemoveToken
Expand Down Expand Up @@ -53,6 +55,11 @@ public class AutofillUserScript: NSObject, UserScript, UserScriptMessageEncrypti
case checkCredentialsProviderStatus

case sendJSPixel

case setIncontextSignupPermanentlyDismissedAt
case getIncontextSignupDismissedAt
case startEmailProtectionSignup
case closeEmailProtectionTab
}

/// Represents if the autofill is loaded into the top autofill context.
Expand Down Expand Up @@ -142,6 +149,11 @@ public class AutofillUserScript: NSObject, UserScript, UserScriptMessageEncrypti
case .checkCredentialsProviderStatus: return checkCredentialsProviderStatus

case .sendJSPixel: return sendJSPixel

case .setIncontextSignupPermanentlyDismissedAt: return setIncontextSignupPermanentlyDismissedAt
case .getIncontextSignupDismissedAt: return getIncontextSignupDismissedAt
case .startEmailProtectionSignup: return startEmailProtectionSignup
case .closeEmailProtectionTab: return closeEmailProtectionTab
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,15 @@ public class OverlayAutofillUserScript: AutofillUserScript {
}

func closeAutofillParent(_ message: UserScriptMessage, _ replyHandler: MessageReplyHandler) {
guard let websiteAutofillInstance = websiteAutofillInstance else { return }
closeAutofillParent()
replyHandler(nil)
}

public func closeAutofillParent() {
guard let websiteAutofillInstance = websiteAutofillInstance else { return }
self.contentOverlay?.overlayAutofillUserScript(self, requestResizeToSize: CGSize(width: 0, height: 0))
websiteAutofillInstance.overlayAutofillUserScriptClose(self)
replyHandler(nil)
}

/// Used to create a top autofill context script for injecting into a ContentOverlay
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ public class WebsiteAutofillUserScript: AutofillUserScript {
/// Last user selected details in the top autofill overlay stored in the child.
var selectedDetailsData: SelectedDetailsData?

private enum CredentialsResponse {
static let none = "none"
static let state = "state"
static let stop = "stop"
static let ok = "ok"
}

public override var messageNames: [String] {
return WebsiteAutofillMessageName.allCases.map(\.rawValue) + super.messageNames
}
Expand Down Expand Up @@ -104,13 +111,29 @@ public class WebsiteAutofillUserScript: AutofillUserScript {

/// Called from the child autofill to return referenced credentials
func getSelectedCredentials(_ message: UserScriptMessage, _ replyHandler: MessageReplyHandler) {
var response = GetSelectedCredentialsResponse(type: "none")
if lastOpenHost == nil || message.messageHost != lastOpenHost {
response = GetSelectedCredentialsResponse(type: "stop")
var response = GetSelectedCredentialsResponse(type: CredentialsResponse.none)

let emailSignedIn = emailDelegate?.autofillUserScriptDidRequestSignedInStatus(self) ?? false
if (previousEmailSignedIn == nil) {
previousEmailSignedIn = emailSignedIn
}
let hasEmailSignedInStateChanged = previousEmailSignedIn != emailSignedIn
let inContextEmailSignupPromptDismissedPermanentlyAt: Double? = emailDelegate?.autofillUserScriptDidRequestInContextPromptValue(self)
let hasIncontextSignupStateChanged = previousIncontextSignupPermanentlyDismissedAt != inContextEmailSignupPromptDismissedPermanentlyAt

if (hasEmailSignedInStateChanged || hasIncontextSignupStateChanged) {
previousIncontextSignupPermanentlyDismissedAt = inContextEmailSignupPromptDismissedPermanentlyAt
previousEmailSignedIn = emailSignedIn
response = GetSelectedCredentialsResponse(type: CredentialsResponse.state)

} else if lastOpenHost == nil || message.messageHost != lastOpenHost {
response = GetSelectedCredentialsResponse(type: CredentialsResponse.stop)

} else if let selectedDetailsData = selectedDetailsData {
response = GetSelectedCredentialsResponse(type: "ok", data: selectedDetailsData.data, configType: selectedDetailsData.configType)
self.selectedDetailsData = nil
response = GetSelectedCredentialsResponse(type: CredentialsResponse.ok, data: selectedDetailsData.data, configType: selectedDetailsData.configType)
}

if let json = try? JSONEncoder().encode(response),
let jsonString = String(data: json, encoding: .utf8) {
replyHandler(jsonString)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public struct ContentScopeFeature: Encodable {
public struct ContentScopeFeatureToggles: Encodable {

public let emailProtection: Bool
public let emailProtectionIncontextSignup: Bool

public let credentialsAutofill: Bool
public let identitiesAutofill: Bool
Expand All @@ -66,6 +67,7 @@ public struct ContentScopeFeatureToggles: Encodable {

// Explicitly defined memberwise init only so it can be public
public init(emailProtection: Bool,
emailProtectionIncontextSignup: Bool,
credentialsAutofill: Bool,
identitiesAutofill: Bool,
creditCardsAutofill: Bool,
Expand All @@ -75,6 +77,7 @@ public struct ContentScopeFeatureToggles: Encodable {
thirdPartyCredentialsProvider: Bool) {

self.emailProtection = emailProtection
self.emailProtectionIncontextSignup = emailProtectionIncontextSignup
self.credentialsAutofill = credentialsAutofill
self.identitiesAutofill = identitiesAutofill
self.creditCardsAutofill = creditCardsAutofill
Expand All @@ -86,6 +89,7 @@ public struct ContentScopeFeatureToggles: Encodable {

enum CodingKeys: String, CodingKey {
case emailProtection = "emailProtection"
case emailProtectionIncontextSignup = "emailProtection_incontext_signup"

case credentialsAutofill = "inputType_credentials"
case identitiesAutofill = "inputType_identities"
Expand Down
37 changes: 35 additions & 2 deletions Sources/BrowserServicesKit/Email/EmailManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ public extension Notification.Name {
static let emailDidSignIn = Notification.Name("com.duckduckgo.browserServicesKit.EmailDidSignIn")
static let emailDidSignOut = Notification.Name("com.duckduckgo.browserServicesKit.EmailDidSignOut")
static let emailDidGenerateAlias = Notification.Name("com.duckduckgo.browserServicesKit.EmailDidGenerateAlias")
static let emailDidIncontextSignup = Notification.Name("com.duckduckgo.browserServicesKit.EmailDidIncontextSignup")
static let emailDidCloseEmailProtection = Notification.Name("com.duckduckgo.browserServicesKit.EmailDidCloseEmailProtection")
}

public enum AliasRequestError: Error {
Expand Down Expand Up @@ -132,7 +134,8 @@ public typealias UserDataCompletion = (_ username: String?, _ alias: String?, _
public class EmailManager {

private static let emailDomain = "duck.com"

private static let inContextEmailSignupPromptDismissedPermanentlyAtKey = "Autofill.InContextEmailSignup.dismissed.permanently.at"

private let storage: EmailManagerStorage
public weak var aliasPermissionDelegate: EmailManagerAliasPermissionDelegate?
public weak var requestDelegate: EmailManagerRequestDelegate?
Expand Down Expand Up @@ -238,6 +241,17 @@ public class EmailManager {
guard let username = username else { return nil }
return username + "@" + EmailManager.emailDomain
}

private var inContextEmailSignupPromptDismissedPermanentlyAt: Double? {
get {
UserDefaults().object(forKey: Self.inContextEmailSignupPromptDismissedPermanentlyAtKey) as? Double ?? nil
}

set {
UserDefaults().set(newValue, forKey: Self.inContextEmailSignupPromptDismissedPermanentlyAtKey)
}
}


public init(storage: EmailManagerStorage = EmailKeychainManager()) {
self.storage = storage
Expand Down Expand Up @@ -282,10 +296,12 @@ public class EmailManager {
}
}

public func resetEmailProtectionInContextPrompt() {
UserDefaults().setValue(nil, forKey: Self.inContextEmailSignupPromptDismissedPermanentlyAtKey)
}
}

extension EmailManager: AutofillEmailDelegate {

public func autofillUserScriptDidRequestSignedInStatus(_: AutofillUserScript) -> Bool {
return isSignedIn
}
Expand Down Expand Up @@ -375,6 +391,23 @@ extension EmailManager: AutofillEmailDelegate {

NotificationCenter.default.post(name: .emailDidSignIn, object: self, userInfo: notificationParameters)
}

public func autofillUserScript(_ : AutofillUserScript, didRequestSetInContextPromptValue value: Double) {
inContextEmailSignupPromptDismissedPermanentlyAt = value
}

public func autofillUserScriptDidRequestInContextPromptValue(_ : AutofillUserScript) -> Double? {
inContextEmailSignupPromptDismissedPermanentlyAt
}

public func autofillUserScriptDidRequestInContextSignup(_: AutofillUserScript) {
NotificationCenter.default.post(name: .emailDidIncontextSignup, object: self)
}

public func autofillUserScriptDidCompleteInContextSignup(_: AutofillUserScript) {
NotificationCenter.default.post(name: .emailDidCloseEmailProtection, object: self)
}

}

// MARK: - Token Management
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public enum PrivacyFeature: String {
case adClickAttribution
case windowsWaitlist
case windowsDownloadLink
case incontextSignup
}

/// An abstraction to be implemented by any "subfeature" of a given `PrivacyConfiguration` feature.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,21 @@ class MockUserScriptMessage: UserScriptMessage {
}

class MockAutofillEmailDelegate: AutofillEmailDelegate {
func autofillUserScript(_: BrowserServicesKit.AutofillUserScript, didRequestSetInContextPromptValue value: Double) {

}

func autofillUserScriptDidRequestInContextPromptValue(_: BrowserServicesKit.AutofillUserScript) -> Double? {
return nil
}

func autofillUserScriptDidRequestInContextSignup(_: BrowserServicesKit.AutofillUserScript) -> Void {

}

func autofillUserScriptDidCompleteInContextSignup(_: BrowserServicesKit.AutofillUserScript) -> Void {

}

var signedInCallback: (() -> Void)?
var signedOutCallback: (() -> Void)?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Foundation

extension ContentScopeFeatureToggles {
static let allTogglesOn = ContentScopeFeatureToggles(emailProtection: true,
emailProtectionIncontextSignup: true,
credentialsAutofill: true,
identitiesAutofill: true,
creditCardsAutofill: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ final class FingerprintingReferenceTests: XCTestCase {


let configFeatureToggle = ContentScopeFeatureToggles(emailProtection: false,
emailProtectionIncontextSignup: false,
credentialsAutofill: false,
identitiesAutofill: false,
creditCardsAutofill: false,
Expand Down

0 comments on commit 2e90e6c

Please sign in to comment.