Skip to content

Commit

Permalink
Prompt to store cards and identities (#76)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/0/1201938691568505/f
iOS PR: duckduckgo/iOS#1086
macOS PR: duckduckgo/macos-browser#461

Optional:

CC: @brindy

Description:

This PR updates BrowserServicesKit to handle the new pmHandlerStoreData call. As a part of this, it runs some logic to look up whether the provided payment method or address already exist in the database.

Steps to test this PR:

See macOS PR for more information
  • Loading branch information
samsymons authored Mar 29, 2022
1 parent 7d2c892 commit 52171e6
Show file tree
Hide file tree
Showing 13 changed files with 696 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public protocol AutofillSecureVaultDelegate: AnyObject {
) -> Void)

func autofillUserScript(_: AutofillUserScript, didRequestPasswordManagerForDomain domain: String)
func autofillUserScript(_: AutofillUserScript, didRequestStoreCredentialsForDomain domain: String, username: String, password: String)
func autofillUserScript(_: AutofillUserScript, didRequestStoreDataForDomain domain: String, data: AutofillUserScript.DetectedAutofillData)
func autofillUserScript(_: AutofillUserScript, didRequestAccountsForDomain domain: String,
completionHandler: @escaping ([SecureVaultModels.WebsiteAccount]) -> Void)
func autofillUserScript(_: AutofillUserScript, didRequestCredentialsForAccount accountId: Int64,
Expand Down Expand Up @@ -84,7 +84,7 @@ extension AutofillUserScript {
addressPostalCode: identity.addressPostalCode,
addressCountryCode: identity.addressCountryCode,
phone: identity.homePhone, // Replace with single "phone number" column
emailAddress: identity.emailAddress)
emailAddress: identity.emailAddress ?? "")
}
}

Expand Down Expand Up @@ -131,6 +131,60 @@ extension AutofillUserScript {
let id: Int64
let username: String
}

// MARK: - Requests

public struct IncomingCredentials {

private enum Constants {
static let credentialsKey = "credentials"
static let usernameKey = "username"
static let passwordKey = "password"
}

let username: String?
let password: String

init(username: String?, password: String) {
self.username = username
self.password = password
}

init?(autofillDictionary: [String: Any]) {
guard let credentialsDictionary = autofillDictionary[Constants.credentialsKey] as? [String: String],
let password = credentialsDictionary[Constants.passwordKey] else {
return nil
}

// Usernames are optional, as the Autofill script can pass a generated password through without a corresponding username.
self.init(username: credentialsDictionary[Constants.usernameKey], password: password)
}

}

/// Represents the incoming Autofill data provided by the user script.
///
/// Identities and Credit Cards can be converted to their final model objects directly, but credentials cannot as they have to looked up in the Secure Vault first, hence the existence of a standalone
/// `IncomingCredentials` type.
public struct DetectedAutofillData {

public let identity: SecureVaultModels.Identity?
public let credentials: IncomingCredentials?
public let creditCard: SecureVaultModels.CreditCard?

init(dictionary: [String: Any]) {
self.identity = .init(autofillDictionary: dictionary)
self.creditCard = .init(autofillDictionary: dictionary)
self.credentials = IncomingCredentials(autofillDictionary: dictionary)
}

init(identity: SecureVaultModels.Identity?, credentials: AutofillUserScript.IncomingCredentials?, creditCard: SecureVaultModels.CreditCard?) {
self.identity = identity
self.credentials = credentials
self.creditCard = creditCard
}

}

// MARK: - Responses

Expand Down Expand Up @@ -210,20 +264,20 @@ extension AutofillUserScript {
}

}

func pmStoreCredentials(_ message: AutofillMessage, _ replyHandler: @escaping MessageReplyHandler) {
func pmStoreData(_ message: AutofillMessage, _ replyHandler: @escaping MessageReplyHandler) {
defer {
replyHandler(nil)
}

guard let body = message.messageBody as? [String: Any],
let username = body["username"] as? String,
let password = body["password"] as? String else {

guard let body = message.messageBody as? [String: Any] else {
return
}


let incomingData = DetectedAutofillData(dictionary: body)
let domain = hostProvider.hostForMessage(message)
vaultDelegate?.autofillUserScript(self, didRequestStoreCredentialsForDomain: domain, username: username, password: password)

vaultDelegate?.autofillUserScript(self, didRequestStoreDataForDomain: domain, data: incomingData)
}

func pmGetAccounts(_ message: AutofillMessage, _ replyHandler: @escaping MessageReplyHandler) {
Expand Down
9 changes: 6 additions & 3 deletions Sources/BrowserServicesKit/Autofill/AutofillUserScript.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
//

import WebKit
import os.log

public class AutofillUserScript: NSObject, UserScript {

Expand All @@ -35,7 +36,7 @@ public class AutofillUserScript: NSObject, UserScript {

case pmHandlerGetAutofillInitData

case pmHandlerStoreCredentials
case pmHandlerStoreData
case pmHandlerGetAccounts
case pmHandlerGetAutofillCredentials
case pmHandlerGetIdentity
Expand Down Expand Up @@ -80,8 +81,11 @@ public class AutofillUserScript: NSObject, UserScript {

internal func messageHandlerFor(_ messageName: String) -> MessageHandler? {
guard let message = MessageName(rawValue: messageName) else {
os_log("Failed to parse Autofill User Script message: '%{public}s'", log: .userScripts, type: .debug, messageName)
return nil
}

os_log("AutofillUserScript: received '%{public}s'", log: .userScripts, type: .debug, messageName)

switch message {
case .emailHandlerStoreToken: return emailStoreToken
Expand All @@ -93,10 +97,9 @@ public class AutofillUserScript: NSObject, UserScript {

case .pmHandlerGetAutofillInitData: return pmGetAutoFillInitData

case .pmHandlerStoreCredentials: return pmStoreCredentials
case .pmHandlerStoreData: return pmStoreData
case .pmHandlerGetAccounts: return pmGetAccounts
case .pmHandlerGetAutofillCredentials: return pmGetAutofillCredentials

case .pmHandlerGetIdentity: return pmGetIdentity
case .pmHandlerGetCreditCard: return pmGetCreditCard

Expand Down
18 changes: 18 additions & 0 deletions Sources/BrowserServicesKit/Common/Extensions/StringExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,23 @@ extension String {
func encodingPlusesAsSpaces() -> String {
return replacingOccurrences(of: "+", with: "%20")
}

func removingCharacters(in set: CharacterSet) -> String {
let filtered = unicodeScalars.filter { !set.contains($0) }
return String(String.UnicodeScalarView(filtered))
}

func autofillNormalized() -> String {
let autofillCharacterSet = CharacterSet.whitespacesAndNewlines.union(.punctuationCharacters).union(.symbols)

var normalizedString = self

normalizedString = normalizedString.removingCharacters(in: autofillCharacterSet)
normalizedString = normalizedString.folding(options: .diacriticInsensitive, locale: .current)
normalizedString = normalizedString.localizedLowercase

return normalizedString
}

}

43 changes: 43 additions & 0 deletions Sources/BrowserServicesKit/Common/Logging.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//
// Logging.swift
// DuckDuckGo
//
// Copyright © 2022 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 os

extension OSLog {

static var userScripts: OSLog {
Logging.userScriptsEnabled ? Logging.userScriptsLog : .disabled
}

static var passwordManager: OSLog {
Logging.passwordManagerEnabled ? Logging.passwordManagerLog : .disabled
}

}

struct Logging {

fileprivate static let userScriptsEnabled = false
fileprivate static let userScriptsLog: OSLog = OSLog(subsystem: Bundle.main.bundleIdentifier ?? "DuckDuckGo", category: "User Scripts")

fileprivate static let passwordManagerEnabled = false
fileprivate static let passwordManagerLog: OSLog = OSLog(subsystem: Bundle.main.bundleIdentifier ?? "DuckDuckGo", category: "Password Manager")

}
2 changes: 1 addition & 1 deletion Sources/BrowserServicesKit/Resources/duckduckgo-autofill
Submodule duckduckgo-autofill updated 65 files
+0 −3 .babelrc
+1 −1 .eslintrc
+7 −0 babel.config.js
+1 −1 dist/autofill.css
+1,397 −634 dist/autofill.js
+6 −1 jest.config.js
+105 −104 package-lock.json
+1 −0 package.json
+37 −0 packages/password/index.html
+10 −8 packages/password/index.js
+4 −4 packages/password/lib/apple.password.js
+16 −7 packages/password/lib/constants.js
+10 −8 packages/password/lib/rules-parser.js
+6 −3 packages/password/rules.json
+15 −15 packages/password/tests/apple.password.test.js
+27 −7 packages/password/tests/generate.test.js
+2 −1 scripts/release.js
+5 −7 src/DeviceInterface.js
+2 −2 src/DeviceInterface/AndroidInterface.js
+54 −37 src/DeviceInterface/AppleDeviceInterface.js
+4 −10 src/DeviceInterface/ExtensionInterface.js
+54 −27 src/DeviceInterface/InterfacePrototype.js
+13 −0 src/DeviceInterface/tests/AndroidInterface.test.js
+114 −47 src/Form/Form.js
+293 −14 src/Form/Form.test.js
+4 −6 src/Form/FormAnalyzer.js
+560 −253 src/Form/countryNames.js
+166 −10 src/Form/formatters.js
+53 −0 src/Form/formatters.test.js
+72 −4 src/Form/input-classifiers.test.js
+20 −12 src/Form/inputTypeConfig.js
+8 −1 src/Form/listenForFormSubmission.js
+16 −11 src/Form/matching-configuration.js
+3 −3 src/Form/matching-types.d.ts
+22 −22 src/Form/matching.js
+3 −3 src/Form/matching.test.js
+32 −25 src/Form/selectors-css.js
+3 −3 src/Form/test-cases/apple_signup.html
+45 −32 src/Form/test-cases/chewy_login.html
+173 −0 src/Form/test-cases/financialtimes_login.html
+2 −2 src/Form/test-cases/fox_signup.html
+7 −0 src/Form/test-cases/hackernews_login_signup.html
+17 −9 src/Form/test-cases/index.js
+2 −2 src/Form/test-cases/mapquest_login.html
+68 −0 src/Form/test-cases/mbank_login.html
+40 −0 src/Form/test-cases/mkelectricalcontracting_contact.html
+621 −0 src/Form/test-cases/samash_checkout.html
+88 −0 src/Form/test-cases/samash_checkout_international.html
+59 −0 src/Form/test-cases/samash_login.html
+97 −0 src/Form/test-cases/samash_signup.html
+10 −10 src/Form/test-cases/steam_checkout.html
+1 −1 src/Form/test-cases/wayfair_checkout.html
+1 −1 src/InputTypes/Identity.js
+1 −2 src/UI/DataAutofill.js
+7 −2 src/UI/EmailAutofill.js
+11 −5 src/UI/Tooltip.js
+1 −1 src/UI/styles/autofill-tooltip-styles.js
+29 −17 src/appleDeviceUtils/appleDeviceUtils.js
+5 −2 src/appleDeviceUtils/appleDeviceUtils.test.js
+57 −49 src/autofill-utils.js
+78 −2 src/autofill-utils.test.js
+59 −0 src/config.js
+46 −2 src/device-interface.d.ts
+8 −6 src/scanForInputs.js
+1 −0 tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public struct CreditCardValidation {

case unknown

var displayName: String {
public var displayName: String {
switch self {
case .amex:
return "American Express"
Expand Down
18 changes: 18 additions & 0 deletions Sources/BrowserServicesKit/SecureVault/SecureVault.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,14 @@ public protocol SecureVault {

func identities() throws -> [SecureVaultModels.Identity]
func identityFor(id: Int64) throws -> SecureVaultModels.Identity?
func existingIdentityForAutofill(matching proposedIdentity: SecureVaultModels.Identity) throws -> SecureVaultModels.Identity?
@discardableResult
func storeIdentity(_ identity: SecureVaultModels.Identity) throws -> Int64
func deleteIdentityFor(identityId: Int64) throws

func creditCards() throws -> [SecureVaultModels.CreditCard]
func creditCardFor(id: Int64) throws -> SecureVaultModels.CreditCard?
func existingCardForAutofill(matching proposedCard: SecureVaultModels.CreditCard) throws -> SecureVaultModels.CreditCard?
@discardableResult
func storeCreditCard(_ card: SecureVaultModels.CreditCard) throws -> Int64
func deleteCreditCardFor(cardId: Int64) throws
Expand Down Expand Up @@ -275,6 +277,14 @@ class DefaultSecureVault: SecureVault {
try self.providers.database.deleteIdentityForIdentityId(identityId)
}
}

func existingIdentityForAutofill(matching proposedIdentity: SecureVaultModels.Identity) throws -> SecureVaultModels.Identity? {
let identities = try self.identities()

return identities.first { existingIdentity in
existingIdentity.hasAutofillEquality(comparedTo: proposedIdentity)
}
}

// MARK: - Credit Cards

Expand Down Expand Up @@ -304,6 +314,14 @@ class DefaultSecureVault: SecureVault {
return card
}
}

func existingCardForAutofill(matching proposedCard: SecureVaultModels.CreditCard) throws -> SecureVaultModels.CreditCard? {
let cards = try self.creditCards()

return cards.first { existingCard in
existingCard.hasAutofillEquality(comparedTo: proposedCard)
}
}

@discardableResult
func storeCreditCard(_ card: SecureVaultModels.CreditCard) throws -> Int64 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -878,6 +878,9 @@ extension SecureVaultModels.Identity: PersistableRecord, FetchableRecord {
homePhone = row[Columns.homePhone]
mobilePhone = row[Columns.mobilePhone]
emailAddress = row[Columns.emailAddress]

autofillEqualityName = normalizedAutofillName()
autofillEqualityAddressStreet = addressStreet?.autofillNormalized()
}

public func encode(to container: inout PersistenceContainer) {
Expand Down
Loading

0 comments on commit 52171e6

Please sign in to comment.