| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/autofill/core/browser/credit_card_save_manager.h" |
| |
| #include <stddef.h> |
| #include <stdint.h> |
| |
| #include <algorithm> |
| #include <limits> |
| #include <map> |
| #include <set> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/feature_list.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/strings/string16.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_piece.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "build/build_config.h" |
| #include "components/autofill/core/browser/autofill_client.h" |
| #include "components/autofill/core/browser/autofill_experiments.h" |
| #include "components/autofill/core/browser/autofill_metrics.h" |
| #include "components/autofill/core/browser/autofill_profile.h" |
| #include "components/autofill/core/browser/autofill_type.h" |
| #include "components/autofill/core/browser/credit_card.h" |
| #include "components/autofill/core/browser/form_structure.h" |
| #include "components/autofill/core/browser/legacy_strike_database.h" |
| #include "components/autofill/core/browser/payments/payments_client.h" |
| #include "components/autofill/core/browser/payments/payments_util.h" |
| #include "components/autofill/core/browser/personal_data_manager.h" |
| #include "components/autofill/core/browser/strike_database.h" |
| #include "components/autofill/core/browser/validation.h" |
| #include "components/autofill/core/common/autofill_clock.h" |
| #include "components/autofill/core/common/autofill_constants.h" |
| #include "components/autofill/core/common/autofill_features.h" |
| #include "components/autofill/core/common/autofill_util.h" |
| #include "services/identity/public/cpp/identity_manager.h" |
| #include "url/gurl.h" |
| |
| namespace autofill { |
| |
| namespace { |
| |
| // If |name| consists of three whitespace-separated parts and the second of the |
| // three parts is a single character or a single character followed by a period, |
| // returns the result of joining the first and third parts with a space. |
| // Otherwise, returns |name|. |
| // |
| // Note that a better way to do this would be to use SplitName from |
| // src/components/autofill/core/browser/contact_info.cc. However, for now we |
| // want the logic of which variations of names are considered to be the same to |
| // exactly match the logic applied on the Payments server. |
| base::string16 RemoveMiddleInitial(const base::string16& name) { |
| std::vector<base::StringPiece16> parts = |
| base::SplitStringPiece(name, base::kWhitespaceUTF16, |
| base::KEEP_WHITESPACE, base::SPLIT_WANT_NONEMPTY); |
| if (parts.size() == 3 && (parts[1].length() == 1 || |
| (parts[1].length() == 2 && |
| base::EndsWith(parts[1], base::ASCIIToUTF16("."), |
| base::CompareCase::SENSITIVE)))) { |
| parts.erase(parts.begin() + 1); |
| return base::JoinString(parts, base::ASCIIToUTF16(" ")); |
| } |
| return name; |
| } |
| |
| } // namespace |
| |
| CreditCardSaveManager::CreditCardSaveManager( |
| AutofillClient* client, |
| payments::PaymentsClient* payments_client, |
| const std::string& app_locale, |
| PersonalDataManager* personal_data_manager) |
| : client_(client), |
| payments_client_(payments_client), |
| app_locale_(app_locale), |
| personal_data_manager_(personal_data_manager), |
| weak_ptr_factory_(this) { |
| // This is to initialize StrikeDatabase is if it hasn't been already, so that |
| // its cache would be loaded and ready to use when the first CCSM is created. |
| if (base::FeatureList::IsEnabled( |
| features::kAutofillSaveCreditCardUsesStrikeSystemV2)) { |
| // Only init when |kAutofillSaveCreditCardUsesStrikeSystemV2| is enabled. If |
| // flag is off and LegacyStrikeDatabase instead of StrikeDatabase is used, |
| // this init will cause failure on GetStrikes(). |
| client_->GetStrikeDatabase(); |
| } |
| } |
| |
| CreditCardSaveManager::~CreditCardSaveManager() {} |
| |
| void CreditCardSaveManager::AttemptToOfferCardLocalSave( |
| const CreditCard& card) { |
| local_card_save_candidate_ = card; |
| show_save_prompt_ = base::nullopt; |
| |
| // Query the Autofill StrikeDatabase on if we should pop up the |
| // offer-to-save prompt for this card. |
| if (base::FeatureList::IsEnabled( |
| features::kAutofillSaveCreditCardUsesStrikeSystemV2)) { |
| OnDidGetStrikesForLocalSave(GetCreditCardSaveStrikeDatabase()->GetStrikes( |
| base::UTF16ToUTF8(local_card_save_candidate_.LastFourDigits()))); |
| } else if (base::FeatureList::IsEnabled( |
| features::kAutofillSaveCreditCardUsesStrikeSystem)) { |
| LegacyStrikeDatabase* strike_database = client_->GetLegacyStrikeDatabase(); |
| strike_database->GetStrikes( |
| strike_database->GetKeyForCreditCardSave( |
| base::UTF16ToUTF8(local_card_save_candidate_.LastFourDigits())), |
| base::BindRepeating(&CreditCardSaveManager::OnDidGetStrikesForLocalSave, |
| weak_ptr_factory_.GetWeakPtr())); |
| } else { |
| // Skip retrieving data from the Autofill LegacyStrikeDatabase; assume 0 |
| // strikes. |
| OnDidGetStrikesForLocalSave(0); |
| } |
| } |
| |
| void CreditCardSaveManager::AttemptToOfferCardUploadSave( |
| const FormStructure& submitted_form, |
| const CreditCard& card, |
| const bool uploading_local_card) { |
| // Abort the uploading if |payments_client_| is nullptr. |
| if (!payments_client_) |
| return; |
| upload_request_ = payments::PaymentsClient::UploadRequestDetails(); |
| upload_request_.card = card; |
| uploading_local_card_ = uploading_local_card; |
| show_save_prompt_ = base::nullopt; |
| |
| // In an ideal scenario, when uploading a card, we would have: |
| // 1) Card number and expiration |
| // 2) CVC |
| // 3) 1+ recently-used or modified addresses that meet validation rules (or |
| // only the address countries if the relevant feature is enabled). |
| // 4) Cardholder name or names on the address profiles |
| // At a minimum, only #1 (card number and expiration) is absolutely required |
| // in order to save a card to Google Payments. We perform all checks before |
| // returning or logging in order to know where we stand with regards to card |
| // upload information. Then, we ping Google Payments and ask if upload save |
| // should be offered with the given amount of information, letting Payments |
| // make the final offer-to-save decision. |
| found_cvc_field_ = false; |
| found_value_in_cvc_field_ = false; |
| found_cvc_value_in_non_cvc_field_ = false; |
| |
| for (const auto& field : submitted_form) { |
| const bool is_valid_cvc = IsValidCreditCardSecurityCode( |
| field->value, upload_request_.card.network()); |
| if (field->Type().GetStorableType() == CREDIT_CARD_VERIFICATION_CODE) { |
| found_cvc_field_ = true; |
| if (!field->value.empty()) |
| found_value_in_cvc_field_ = true; |
| if (is_valid_cvc) { |
| upload_request_.cvc = field->value; |
| break; |
| } |
| } else if (is_valid_cvc && |
| field->Type().GetStorableType() == UNKNOWN_TYPE) { |
| found_cvc_value_in_non_cvc_field_ = true; |
| } |
| } |
| |
| // Upload requires that recently used or modified addresses meet the |
| // client-side validation rules. This call also begins setting the value of |
| // |upload_decision_metrics_|. |
| SetProfilesForCreditCardUpload(card, &upload_request_); |
| |
| pending_upload_request_origin_ = submitted_form.main_frame_origin(); |
| |
| if (upload_request_.cvc.empty()) { |
| // Apply the CVC decision to |upload_decision_metrics_| to denote a problem |
| // was found. |
| upload_decision_metrics_ |= GetCVCCardUploadDecisionMetric(); |
| } |
| |
| // Add active Chrome experiments to the request payload here (currently none). |
| |
| // We store the detected values in the upload request, because the addresses |
| // are being possibly modified in the next code block, and we want the |
| // detected values to reflect addresses *before* they are modified. |
| upload_request_.detected_values = GetDetectedValues(); |
| // If the user must provide cardholder name, log it and set |
| // |should_request_name_from_user_| so the offer-to-save dialog know to ask |
| // for it. |
| should_request_name_from_user_ = false; |
| if (upload_request_.detected_values & DetectedValue::USER_PROVIDED_NAME) { |
| upload_decision_metrics_ |= |
| AutofillMetrics::USER_REQUESTED_TO_PROVIDE_CARDHOLDER_NAME; |
| should_request_name_from_user_ = true; |
| } |
| |
| // If the user must provide expiration month or expration year, log it and set |
| // |should_request_expiration_date_from_user_| so the offer-to-save dialog |
| // knows to ask for it. |
| should_request_expiration_date_from_user_ = false; |
| if (upload_request_.detected_values & |
| DetectedValue::USER_PROVIDED_EXPIRATION_DATE) { |
| upload_decision_metrics_ |= |
| AutofillMetrics::USER_REQUESTED_TO_PROVIDE_EXPIRATION_DATE; |
| LogSaveCardRequestExpirationDateReasonMetric(); |
| should_request_expiration_date_from_user_ = true; |
| } |
| |
| // The cardholder name and expiration date fix flows cannot both be |
| // active at the same time. If they are, abort offering upload. |
| if (should_request_name_from_user_ && |
| should_request_expiration_date_from_user_) { |
| DCHECK(base::FeatureList::IsEnabled( |
| features::kAutofillUpstreamEditableExpirationDate)); |
| LogCardUploadDecisions(upload_decision_metrics_); |
| pending_upload_request_origin_ = url::Origin(); |
| return; |
| } |
| // If the relevant feature is enabled, only send the country of the |
| // recently-used addresses. We make a copy here to avoid modifying |
| // |upload_request_.profiles|, which should always have full addresses even |
| // after this function goes out of scope. |
| bool send_only_country_in_addresses = base::FeatureList::IsEnabled( |
| features::kAutofillSendOnlyCountryInGetUploadDetails); |
| std::vector<AutofillProfile> country_only_profiles; |
| if (send_only_country_in_addresses) { |
| for (const AutofillProfile& address : upload_request_.profiles) { |
| AutofillProfile country_only; |
| country_only.SetInfo(ADDRESS_HOME_COUNTRY, |
| address.GetInfo(ADDRESS_HOME_COUNTRY, app_locale_), |
| app_locale_); |
| country_only_profiles.emplace_back(std::move(country_only)); |
| } |
| } |
| |
| // All required data is available, start the upload process. |
| if (observer_for_testing_) |
| observer_for_testing_->OnDecideToRequestUploadSave(); |
| payments_client_->GetUploadDetails( |
| send_only_country_in_addresses ? country_only_profiles |
| : upload_request_.profiles, |
| upload_request_.detected_values, upload_request_.active_experiments, |
| app_locale_, |
| base::BindOnce(&CreditCardSaveManager::OnDidGetUploadDetails, |
| weak_ptr_factory_.GetWeakPtr()), |
| payments::kUploadCardBillableServiceNumber, |
| payments::PaymentsClient::UploadCardSource::UPSTREAM_CHECKOUT_FLOW); |
| // Query the Autofill StrikeDatabase on if we should pop up the |
| // offer-to-save prompt for this card. |
| if (base::FeatureList::IsEnabled( |
| features::kAutofillSaveCreditCardUsesStrikeSystemV2)) { |
| OnDidGetStrikesForUploadSave(GetCreditCardSaveStrikeDatabase()->GetStrikes( |
| base::UTF16ToUTF8(upload_request_.card.LastFourDigits()))); |
| } else if (base::FeatureList::IsEnabled( |
| features::kAutofillSaveCreditCardUsesStrikeSystem)) { |
| LegacyStrikeDatabase* strike_database = client_->GetLegacyStrikeDatabase(); |
| strike_database->GetStrikes( |
| strike_database->GetKeyForCreditCardSave( |
| base::UTF16ToUTF8(upload_request_.card.LastFourDigits())), |
| base::BindRepeating( |
| &CreditCardSaveManager::OnDidGetStrikesForUploadSave, |
| weak_ptr_factory_.GetWeakPtr())); |
| } else { |
| // Skip retrieving data from the Autofill LegacyStrikeDatabase; assume 0 |
| // strikes. |
| OnDidGetStrikesForUploadSave(0); |
| } |
| } |
| |
| bool CreditCardSaveManager::IsCreditCardUploadEnabled() { |
| #if defined(OS_IOS) |
| // If observer_for_testing_ is set, assume we are in a browsertest and |
| // credit card upload should be enabled by default. |
| // TODO(crbug.com/859761): Remove dependency from iOS tests on this behavior. |
| if (observer_for_testing_) |
| return true; |
| #endif // defined(OS_IOS) |
| return ::autofill::IsCreditCardUploadEnabled( |
| client_->GetPrefs(), client_->GetSyncService(), |
| personal_data_manager_->GetAccountInfoForPaymentsServer().email); |
| } |
| |
| bool CreditCardSaveManager::IsUploadEnabledForNetwork( |
| const std::string& network) { |
| if (network == kEloCard && |
| base::FeatureList::IsEnabled(features::kAutofillUpstreamDisallowElo)) { |
| return false; |
| } else if (network == kJCBCard && |
| base::FeatureList::IsEnabled( |
| features::kAutofillUpstreamDisallowJcb)) { |
| return false; |
| } |
| return true; |
| } |
| |
| void CreditCardSaveManager::OnDidUploadCard( |
| AutofillClient::PaymentsRpcResult result, |
| const std::string& server_id) { |
| if (observer_for_testing_) |
| observer_for_testing_->OnReceivedUploadCardResponse(); |
| |
| if (result == AutofillClient::SUCCESS && |
| upload_request_.card.HasFirstAndLastName()) { |
| AutofillMetrics::LogSaveCardWithFirstAndLastNameComplete( |
| /*is_local=*/false); |
| } |
| |
| if (result == AutofillClient::SUCCESS) { |
| // If the upload succeeds and we can store unmasked cards on this OS, we |
| // will keep a copy of the card as a full server card on the device. |
| if (!server_id.empty() && |
| OfferStoreUnmaskedCards(payments_client_->is_off_the_record()) && |
| !IsAutofillNoLocalSaveOnUploadSuccessExperimentEnabled()) { |
| upload_request_.card.set_record_type(CreditCard::FULL_SERVER_CARD); |
| upload_request_.card.SetServerStatus(CreditCard::OK); |
| upload_request_.card.set_server_id(server_id); |
| DCHECK(personal_data_manager_); |
| if (personal_data_manager_) |
| personal_data_manager_->AddFullServerCreditCard(upload_request_.card); |
| } |
| if (base::FeatureList::IsEnabled( |
| features::kAutofillSaveCreditCardUsesStrikeSystemV2)) { |
| // Log how many strikes the card had when it was saved. |
| LogStrikesPresentWhenCardSaved( |
| /*is_local=*/false, |
| GetCreditCardSaveStrikeDatabase()->GetStrikes( |
| base::UTF16ToUTF8(upload_request_.card.LastFourDigits()))); |
| // Clear all strikes for this card, in case it is later removed. |
| GetCreditCardSaveStrikeDatabase()->ClearStrikes( |
| base::UTF16ToUTF8(upload_request_.card.LastFourDigits())); |
| } else if (base::FeatureList::IsEnabled( |
| features::kAutofillSaveCreditCardUsesStrikeSystem)) { |
| LegacyStrikeDatabase* strike_database = |
| client_->GetLegacyStrikeDatabase(); |
| // Log how many strikes the card had when it was saved. |
| strike_database->GetStrikes( |
| strike_database->GetKeyForCreditCardSave( |
| base::UTF16ToUTF8(upload_request_.card.LastFourDigits())), |
| base::BindRepeating( |
| &CreditCardSaveManager::LogStrikesPresentWhenCardSaved, |
| weak_ptr_factory_.GetWeakPtr(), |
| /*is_local=*/false)); |
| // Clear all strikes for this card, in case it is later removed. |
| strike_database->ClearAllStrikesForKey( |
| strike_database->GetKeyForCreditCardSave( |
| base::UTF16ToUTF8(upload_request_.card.LastFourDigits())), |
| base::DoNothing()); |
| } |
| } else { |
| if (show_save_prompt_.value()) { |
| if (base::FeatureList::IsEnabled( |
| features::kAutofillSaveCreditCardUsesStrikeSystemV2)) { |
| // If the upload failed and the bubble was actually shown (NOT just the |
| // icon), count that as a strike against offering upload in the future. |
| int nth_strike_added = GetCreditCardSaveStrikeDatabase()->AddStrike( |
| base::UTF16ToUTF8(upload_request_.card.LastFourDigits())); |
| // Notify the browsertests that a strike was added. |
| OnStrikeChangeComplete(nth_strike_added); |
| } else if (base::FeatureList::IsEnabled( |
| features::kAutofillSaveCreditCardUsesStrikeSystem)) { |
| // If the upload failed and the bubble was actually shown (NOT just the |
| // icon), count that as a strike against offering upload in the future. |
| LegacyStrikeDatabase* strike_database = |
| client_->GetLegacyStrikeDatabase(); |
| strike_database->AddStrike( |
| strike_database->GetKeyForCreditCardSave( |
| base::UTF16ToUTF8(upload_request_.card.LastFourDigits())), |
| base::BindRepeating(&CreditCardSaveManager::OnStrikeChangeComplete, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| } |
| } |
| } |
| |
| CreditCardSaveStrikeDatabase* |
| CreditCardSaveManager::GetCreditCardSaveStrikeDatabase() { |
| if (strike_database_.get() == nullptr) { |
| strike_database_ = std::make_unique<CreditCardSaveStrikeDatabase>( |
| CreditCardSaveStrikeDatabase(client_->GetStrikeDatabase())); |
| } |
| return strike_database_.get(); |
| } |
| |
| void CreditCardSaveManager::OnDidGetStrikesForLocalSave(const int num_strikes) { |
| show_save_prompt_ = |
| num_strikes < kMaxStrikesToPreventPoppingUpOfferToSavePrompt; |
| |
| OfferCardLocalSave(); |
| } |
| |
| void CreditCardSaveManager::OnDidGetStrikesForUploadSave( |
| const int num_strikes) { |
| show_save_prompt_ = |
| num_strikes < kMaxStrikesToPreventPoppingUpOfferToSavePrompt; |
| |
| // Only offer upload once both Payments and the Autofill LegacyStrikeDatabase |
| // have returned their decisions. Use population of |
| // |upload_request_.context_token| as an indicator of the Payments call |
| // returning successfully. |
| if (!upload_request_.context_token.empty()) |
| OfferCardUploadSave(); |
| } |
| |
| void CreditCardSaveManager::OnDidGetUploadDetails( |
| AutofillClient::PaymentsRpcResult result, |
| const base::string16& context_token, |
| std::unique_ptr<base::Value> legal_message) { |
| if (observer_for_testing_) |
| observer_for_testing_->OnReceivedGetUploadDetailsResponse(); |
| if (result == AutofillClient::SUCCESS) { |
| // Do *not* call payments_client_->Prepare() here. We shouldn't send |
| // credentials until the user has explicitly accepted a prompt to upload. |
| upload_request_.context_token = context_token; |
| legal_message_ = base::DictionaryValue::From(std::move(legal_message)); |
| |
| // Only offer upload once both Payments and the Autofill |
| // LegacyStrikeDatabase have returned their decisions. Use presence of |
| // |show_save_prompt_| as an indicator of LegacyStrikeDatabase retrieving |
| // its data. |
| if (show_save_prompt_ != base::nullopt) |
| OfferCardUploadSave(); |
| } else { |
| // If the upload details request failed and we *know* we have all possible |
| // information (card number, expiration, cvc, name, and address), fall back |
| // to a local save (for new cards only). It indicates that "Payments doesn't |
| // want this card" or "Payments doesn't currently support this country", in |
| // which case the upload details request will consistently fail and if we |
| // don't fall back to a local save, the user will never be offered *any* |
| // kind of credit card save. (Note that this could intermittently backfire |
| // if there's a network breakdown or Payments outage, resulting in sometimes |
| // showing upload and sometimes offering local save, but such cases should |
| // be rare.) |
| // |
| // Note that calling AttemptToOfferCardLocalSave(~) pings the Autofill |
| // LegacyStrikeDatabase again, but A) the result should be cached so this |
| // shouldn't hit the disk, and B) the alternative would require hooking into |
| // the LegacyStrikeDatabase's GetStrikes() call already in progress, which |
| // would be hacky at worst and require additional class state variables at |
| // best. |
| bool found_name_and_postal_code_and_cvc = |
| (upload_request_.detected_values & DetectedValue::CARDHOLDER_NAME || |
| upload_request_.detected_values & DetectedValue::ADDRESS_NAME) && |
| upload_request_.detected_values & DetectedValue::POSTAL_CODE && |
| upload_request_.detected_values & DetectedValue::CVC; |
| if (found_name_and_postal_code_and_cvc && !uploading_local_card_) |
| AttemptToOfferCardLocalSave(upload_request_.card); |
| upload_decision_metrics_ |= |
| AutofillMetrics::UPLOAD_NOT_OFFERED_GET_UPLOAD_DETAILS_FAILED; |
| LogCardUploadDecisions(upload_decision_metrics_); |
| } |
| } |
| |
| void CreditCardSaveManager::OfferCardLocalSave() { |
| #if defined(OS_ANDROID) || defined(OS_IOS) |
| bool is_mobile_build = true; |
| #else |
| bool is_mobile_build = false; |
| #endif // #if defined(OS_ANDROID) || defined(OS_IOS) |
| |
| // If |show_save_prompt_.value()| is false, desktop builds will still offer |
| // save in the omnibox without popping-up the bubble. Mobile builds, however, |
| // should not display the offer-to-save infobar at all. |
| if (!is_mobile_build || show_save_prompt_.value()) { |
| if (observer_for_testing_) |
| observer_for_testing_->OnOfferLocalSave(); |
| client_->ConfirmSaveCreditCardLocally( |
| local_card_save_candidate_, show_save_prompt_.value(), |
| base::BindOnce(&CreditCardSaveManager::OnUserDidDecideOnLocalSave, |
| weak_ptr_factory_.GetWeakPtr())); |
| |
| // Log metrics. |
| if (local_card_save_candidate_.HasFirstAndLastName()) |
| AutofillMetrics::LogSaveCardWithFirstAndLastNameOffered( |
| /*is_local=*/true); |
| } |
| if (!show_save_prompt_.value()) { |
| AutofillMetrics::LogCreditCardSaveNotOfferedDueToMaxStrikesMetric( |
| AutofillMetrics::SaveTypeMetric::LOCAL); |
| } |
| } |
| |
| void CreditCardSaveManager::OfferCardUploadSave() { |
| #if defined(OS_ANDROID) || defined(OS_IOS) |
| bool is_mobile_build = true; |
| #else |
| bool is_mobile_build = false; |
| #endif // #if defined(OS_ANDROID) || defined(OS_IOS) |
| |
| // If |show_save_prompt_.value()| is false, desktop builds will still offer |
| // save in the omnibox without popping-up the bubble. Mobile builds, however, |
| // should not display the offer-to-save infobar at all. |
| if (!is_mobile_build || show_save_prompt_.value()) { |
| user_did_accept_upload_prompt_ = false; |
| |
| client_->ConfirmSaveCreditCardToCloud( |
| upload_request_.card, std::move(legal_message_), |
| should_request_name_from_user_, |
| should_request_expiration_date_from_user_, show_save_prompt_.value(), |
| base::BindOnce(&CreditCardSaveManager::OnUserDidDecideOnUploadSave, |
| weak_ptr_factory_.GetWeakPtr())); |
| client_->LoadRiskData( |
| base::BindOnce(&CreditCardSaveManager::OnDidGetUploadRiskData, |
| weak_ptr_factory_.GetWeakPtr())); |
| |
| // Log metrics. |
| AutofillMetrics::LogUploadOfferedCardOriginMetric( |
| uploading_local_card_ ? AutofillMetrics::OFFERING_UPLOAD_OF_LOCAL_CARD |
| : AutofillMetrics::OFFERING_UPLOAD_OF_NEW_CARD); |
| if (upload_request_.card.HasFirstAndLastName()) { |
| AutofillMetrics::LogSaveCardWithFirstAndLastNameOffered( |
| /*is_local=*/false); |
| } |
| // Set that upload was offered. |
| upload_decision_metrics_ |= AutofillMetrics::UPLOAD_OFFERED; |
| } else { |
| // Set that upload was abandoned due to the Autofill LegacyStrikeDatabase |
| // returning too many strikes for a mobile infobar to be displayed. |
| upload_decision_metrics_ |= |
| AutofillMetrics::UPLOAD_NOT_OFFERED_MAX_STRIKES_ON_MOBILE; |
| } |
| LogCardUploadDecisions(upload_decision_metrics_); |
| if (!show_save_prompt_.value()) { |
| AutofillMetrics::LogCreditCardSaveNotOfferedDueToMaxStrikesMetric( |
| AutofillMetrics::SaveTypeMetric::SERVER); |
| } |
| } |
| |
| void CreditCardSaveManager::OnUserDidDecideOnLocalSave( |
| AutofillClient::SaveCardOfferUserDecision user_decision) { |
| switch (user_decision) { |
| case AutofillClient::ACCEPTED: |
| if (local_card_save_candidate_.HasFirstAndLastName()) |
| AutofillMetrics::LogSaveCardWithFirstAndLastNameComplete( |
| /*is_local=*/true); |
| if (base::FeatureList::IsEnabled( |
| features::kAutofillSaveCreditCardUsesStrikeSystemV2)) { |
| // Log how many strikes the card had when it was saved. |
| LogStrikesPresentWhenCardSaved( |
| /*is_local=*/true, |
| GetCreditCardSaveStrikeDatabase()->GetStrikes(base::UTF16ToUTF8( |
| local_card_save_candidate_.LastFourDigits()))); |
| // Clear all strikes for this card, in case it is later removed. |
| GetCreditCardSaveStrikeDatabase()->ClearStrikes( |
| base::UTF16ToUTF8(local_card_save_candidate_.LastFourDigits())); |
| } else if (base::FeatureList::IsEnabled( |
| features::kAutofillSaveCreditCardUsesStrikeSystem)) { |
| LegacyStrikeDatabase* strike_database = |
| client_->GetLegacyStrikeDatabase(); |
| // Log how many strikes the card had when it was saved. |
| strike_database->GetStrikes( |
| strike_database->GetKeyForCreditCardSave( |
| base::UTF16ToUTF8(local_card_save_candidate_.LastFourDigits())), |
| base::BindRepeating( |
| &CreditCardSaveManager::LogStrikesPresentWhenCardSaved, |
| weak_ptr_factory_.GetWeakPtr(), |
| /*is_local=*/true)); |
| // Clear all strikes for this card, in case it is later removed. |
| strike_database->ClearAllStrikesForKey( |
| strike_database->GetKeyForCreditCardSave( |
| base::UTF16ToUTF8(local_card_save_candidate_.LastFourDigits())), |
| base::DoNothing()); |
| } |
| |
| personal_data_manager_->OnAcceptedLocalCreditCardSave( |
| local_card_save_candidate_); |
| break; |
| |
| case AutofillClient::DECLINED: |
| case AutofillClient::IGNORED: |
| OnUserDidIgnoreOrDeclineSave(local_card_save_candidate_.LastFourDigits()); |
| break; |
| } |
| } |
| |
| void CreditCardSaveManager::LogStrikesPresentWhenCardSaved( |
| bool is_local, |
| const int num_strikes) { |
| std::string suffix = is_local ? "StrikesPresentWhenLocalCardSaved" |
| : "StrikesPresentWhenServerCardSaved"; |
| base::UmaHistogramCounts1000("Autofill.StrikeDatabase." + suffix, |
| num_strikes); |
| } |
| |
| void CreditCardSaveManager::SetProfilesForCreditCardUpload( |
| const CreditCard& card, |
| payments::PaymentsClient::UploadRequestDetails* upload_request) { |
| std::vector<AutofillProfile> candidate_profiles; |
| const base::Time now = AutofillClock::Now(); |
| const base::TimeDelta fifteen_minutes = base::TimeDelta::FromMinutes(15); |
| // Reset |upload_decision_metrics_| to begin logging detected problems. |
| upload_decision_metrics_ = 0; |
| bool has_profile = false; |
| bool has_modified_profile = false; |
| |
| // First, collect all of the addresses used or modified recently. |
| for (AutofillProfile* profile : personal_data_manager_->GetProfiles()) { |
| has_profile = true; |
| if ((now - profile->modification_date()) < fifteen_minutes) { |
| has_modified_profile = true; |
| candidate_profiles.push_back(*profile); |
| } else if ((now - profile->use_date()) < fifteen_minutes) { |
| candidate_profiles.push_back(*profile); |
| } |
| } |
| |
| AutofillMetrics::LogHasModifiedProfileOnCreditCardFormSubmission( |
| has_modified_profile); |
| |
| if (candidate_profiles.empty()) { |
| upload_decision_metrics_ |= |
| has_profile |
| ? AutofillMetrics::UPLOAD_NOT_OFFERED_NO_RECENTLY_USED_ADDRESS |
| : AutofillMetrics::UPLOAD_NOT_OFFERED_NO_ADDRESS_PROFILE; |
| } |
| |
| // If any of the names on the card or the addresses don't match the |
| // candidate set is invalid. This matches the rules for name matching applied |
| // server-side by Google Payments and ensures that we don't send upload |
| // requests that are guaranteed to fail. |
| const base::string16 card_name = |
| card.GetInfo(AutofillType(CREDIT_CARD_NAME_FULL), app_locale_); |
| base::string16 verified_name; |
| if (candidate_profiles.empty()) { |
| verified_name = card_name; |
| } else { |
| bool found_conflicting_names = false; |
| verified_name = RemoveMiddleInitial(card_name); |
| for (const AutofillProfile& profile : candidate_profiles) { |
| const base::string16 address_name = |
| RemoveMiddleInitial(profile.GetInfo(NAME_FULL, app_locale_)); |
| if (address_name.empty()) |
| continue; |
| if (verified_name.empty()) { |
| verified_name = address_name; |
| } else if (!base::EqualsCaseInsensitiveASCII(verified_name, |
| address_name)) { |
| found_conflicting_names = true; |
| break; |
| } |
| } |
| if (found_conflicting_names) { |
| upload_decision_metrics_ |= |
| AutofillMetrics::UPLOAD_NOT_OFFERED_CONFLICTING_NAMES; |
| } |
| } |
| |
| // If neither the card nor any of the addresses have a name associated with |
| // them, the candidate set is invalid. |
| if (verified_name.empty()) { |
| upload_decision_metrics_ |= AutofillMetrics::UPLOAD_NOT_OFFERED_NO_NAME; |
| } |
| |
| // If any of the candidate addresses have a non-empty zip that doesn't match |
| // any other non-empty zip, then the candidate set is invalid. |
| base::string16 verified_zip; |
| const AutofillType kZipCode(ADDRESS_HOME_ZIP); |
| for (const AutofillProfile& profile : candidate_profiles) { |
| const base::string16 zip = profile.GetRawInfo(ADDRESS_HOME_ZIP); |
| if (!zip.empty()) { |
| if (verified_zip.empty()) { |
| verified_zip = zip; |
| } else { |
| // To compare two zips, we check to see if either is a prefix of the |
| // other. This allows us to consider a 5-digit zip and a zip+4 to be a |
| // match if the first 5 digits are the same without hardcoding any |
| // specifics of how postal codes are represented. (They can be numeric |
| // or alphanumeric and vary from 3 to 10 digits long by country. See |
| // https://en.wikipedia.org/wiki/Postal_code#Presentation.) The Payments |
| // backend will apply a more sophisticated address-matching procedure. |
| // This check is simply meant to avoid offering upload in cases that are |
| // likely to fail. |
| if (!(StartsWith(verified_zip, zip, base::CompareCase::SENSITIVE) || |
| StartsWith(zip, verified_zip, base::CompareCase::SENSITIVE))) { |
| upload_decision_metrics_ |= |
| AutofillMetrics::UPLOAD_NOT_OFFERED_CONFLICTING_ZIPS; |
| break; |
| } |
| } |
| } |
| } |
| |
| // If none of the candidate addresses have a zip, the candidate set is |
| // invalid. |
| if (verified_zip.empty() && !candidate_profiles.empty()) |
| upload_decision_metrics_ |= AutofillMetrics::UPLOAD_NOT_OFFERED_NO_ZIP_CODE; |
| |
| // Set up |upload_request->profiles|. |
| upload_request->profiles.assign(candidate_profiles.begin(), |
| candidate_profiles.end()); |
| if (!has_modified_profile) { |
| for (const AutofillProfile& profile : candidate_profiles) { |
| UMA_HISTOGRAM_COUNTS_1000( |
| "Autofill.DaysSincePreviousUseAtSubmission.Profile", |
| (profile.use_date() - profile.previous_use_date()).InDays()); |
| } |
| } |
| } |
| |
| int CreditCardSaveManager::GetDetectedValues() const { |
| int detected_values = 0; |
| |
| // Report detecting CVC if it was found. |
| if (!upload_request_.cvc.empty()) { |
| detected_values |= DetectedValue::CVC; |
| } |
| |
| // If cardholder name exists, set it as detected as long as |
| // UPLOAD_NOT_OFFERED_CONFLICTING_NAMES was not set. |
| if (!upload_request_.card |
| .GetInfo(AutofillType(CREDIT_CARD_NAME_FULL), app_locale_) |
| .empty() && |
| !(upload_decision_metrics_ & |
| AutofillMetrics::UPLOAD_NOT_OFFERED_CONFLICTING_NAMES)) { |
| detected_values |= DetectedValue::CARDHOLDER_NAME; |
| } |
| |
| // Go through the upload request's profiles and set the following as detected: |
| // - ADDRESS_NAME, as long as UPLOAD_NOT_OFFERED_CONFLICTING_NAMES was not |
| // set |
| // - POSTAL_CODE, as long as UPLOAD_NOT_OFFERED_CONFLICTING_ZIPS was not set |
| // - Any other address fields found on any addresses, regardless of conflicts |
| for (const AutofillProfile& profile : upload_request_.profiles) { |
| if (!profile.GetInfo(NAME_FULL, app_locale_).empty() && |
| !(upload_decision_metrics_ & |
| AutofillMetrics::UPLOAD_NOT_OFFERED_CONFLICTING_NAMES)) { |
| detected_values |= DetectedValue::ADDRESS_NAME; |
| } |
| if (!profile.GetInfo(ADDRESS_HOME_ZIP, app_locale_).empty() && |
| !(upload_decision_metrics_ & |
| AutofillMetrics::UPLOAD_NOT_OFFERED_CONFLICTING_ZIPS)) { |
| detected_values |= DetectedValue::POSTAL_CODE; |
| } |
| if (!profile.GetInfo(ADDRESS_HOME_LINE1, app_locale_).empty()) { |
| detected_values |= DetectedValue::ADDRESS_LINE; |
| } |
| if (!profile.GetInfo(ADDRESS_HOME_CITY, app_locale_).empty()) { |
| detected_values |= DetectedValue::LOCALITY; |
| } |
| if (!profile.GetInfo(ADDRESS_HOME_STATE, app_locale_).empty()) { |
| detected_values |= DetectedValue::ADMINISTRATIVE_AREA; |
| } |
| if (!profile.GetRawInfo(ADDRESS_HOME_COUNTRY).empty()) { |
| detected_values |= DetectedValue::COUNTRY_CODE; |
| } |
| } |
| |
| // If the billing_customer_number is non-zero, it means the user has a Google |
| // Payments account. Include a bit for existence of this account (NOT the id |
| // itself), as it will help determine if a new Payments customer might need to |
| // be created when save is accepted. |
| if (payments::GetBillingCustomerId(personal_data_manager_, |
| payments_client_->GetPrefService()) != 0) { |
| detected_values |= DetectedValue::HAS_GOOGLE_PAYMENTS_ACCOUNT; |
| } |
| |
| if (base::FeatureList::IsEnabled( |
| features::kAutofillUpstreamEditableExpirationDate)) { |
| // If expiration date month or expiration year are missing, signal that |
| // expiration date will be explicitly requested in the offer-to-save bubble. |
| if (!upload_request_.card |
| .GetInfo(AutofillType(CREDIT_CARD_EXP_MONTH), app_locale_) |
| .empty()) { |
| detected_values |= DetectedValue::CARD_EXPIRATION_MONTH; |
| } |
| if (!(upload_request_.card |
| .GetInfo(AutofillType(CREDIT_CARD_EXP_4_DIGIT_YEAR), app_locale_) |
| .empty())) { |
| detected_values |= DetectedValue::CARD_EXPIRATION_YEAR; |
| } |
| |
| // Set |USER_PROVIDED_EXPIRATION_DATE| if expiration date is detected as |
| // expired or missing. |
| if (detected_values & DetectedValue::CARD_EXPIRATION_MONTH && |
| detected_values & DetectedValue::CARD_EXPIRATION_YEAR) { |
| int month_value = 0, year_value = 0; |
| bool parsable = |
| base::StringToInt( |
| upload_request_.card.GetInfo(AutofillType(CREDIT_CARD_EXP_MONTH), |
| app_locale_), |
| &month_value) && |
| base::StringToInt( |
| upload_request_.card.GetInfo( |
| AutofillType(CREDIT_CARD_EXP_4_DIGIT_YEAR), app_locale_), |
| &year_value); |
| DCHECK(parsable); |
| if (!IsValidCreditCardExpirationDate(year_value, month_value, |
| AutofillClock::Now())) { |
| detected_values |= DetectedValue::USER_PROVIDED_EXPIRATION_DATE; |
| } |
| } else { |
| detected_values |= DetectedValue::USER_PROVIDED_EXPIRATION_DATE; |
| } |
| } |
| |
| // If one of the following is true, signal that cardholder name will be |
| // explicitly requested in the offer-to-save bubble: |
| // 1) Name is conflicting/missing, and the user does NOT have a Google |
| // Payments account |
| // 2) The AutofillUpstreamAlwaysRequestCardholderName experiment is enabled |
| // (should only ever be used by testers, never launched) |
| if ((!(detected_values & DetectedValue::CARDHOLDER_NAME) && |
| !(detected_values & DetectedValue::ADDRESS_NAME) && |
| !(detected_values & DetectedValue::HAS_GOOGLE_PAYMENTS_ACCOUNT) && |
| features::IsAutofillUpstreamEditableCardholderNameExperimentEnabled()) || |
| features:: |
| IsAutofillUpstreamAlwaysRequestCardholderNameExperimentEnabled()) { |
| detected_values |= DetectedValue::USER_PROVIDED_NAME; |
| } |
| |
| return detected_values; |
| } |
| |
| void CreditCardSaveManager::OnUserDidDecideOnUploadSave( |
| AutofillClient::SaveCardOfferUserDecision user_decision, |
| const AutofillClient::UserProvidedCardDetails& user_provided_card_details) { |
| switch (user_decision) { |
| case AutofillClient::ACCEPTED: |
| // On Android, requesting cardholder name is a two step flow. |
| #if defined(OS_ANDROID) |
| if (should_request_name_from_user_) { |
| client_->ConfirmAccountNameFixFlow(base::BindOnce( |
| &CreditCardSaveManager::OnUserDidAcceptAccountNameFixFlow, |
| weak_ptr_factory_.GetWeakPtr())); |
| } else { |
| OnUserDidAcceptUploadHelper(user_provided_card_details); |
| } |
| #else |
| OnUserDidAcceptUploadHelper(user_provided_card_details); |
| #endif |
| break; |
| |
| case AutofillClient::DECLINED: |
| case AutofillClient::IGNORED: |
| OnUserDidIgnoreOrDeclineSave(upload_request_.card.LastFourDigits()); |
| break; |
| } |
| |
| personal_data_manager_->OnUserAcceptedUpstreamOffer(); |
| } |
| |
| #if defined(OS_ANDROID) |
| void CreditCardSaveManager::OnUserDidAcceptAccountNameFixFlow( |
| const base::string16& cardholder_name) { |
| DCHECK(should_request_name_from_user_); |
| |
| OnUserDidAcceptUploadHelper({cardholder_name, |
| /*expiration_date_month=*/base::string16(), |
| /*expiration_date_year=*/base::string16()}); |
| } |
| #endif |
| |
| void CreditCardSaveManager::OnUserDidAcceptUploadHelper( |
| const AutofillClient::UserProvidedCardDetails& user_provided_card_details) { |
| // If cardholder name was explicitly requested for the user to enter/confirm, |
| // replace the name on |upload_request_.card| with the entered name. (Note |
| // that it is possible a name already existed on the card if conflicting names |
| // were found, which this intentionally overwrites.) |
| if (!user_provided_card_details.cardholder_name.empty()) { |
| DCHECK(should_request_name_from_user_); |
| upload_request_.card.SetInfo(CREDIT_CARD_NAME_FULL, |
| user_provided_card_details.cardholder_name, |
| app_locale_); |
| } |
| |
| user_did_accept_upload_prompt_ = true; |
| // If expiration date was explicitly requested for the user to select, replace |
| // the expiration date on |upload_request_.card| with the selected date. |
| if (!user_provided_card_details.expiration_date_month.empty() && |
| !user_provided_card_details.expiration_date_year.empty()) { |
| DCHECK(should_request_expiration_date_from_user_); |
| upload_request_.card.SetInfo( |
| CREDIT_CARD_EXP_MONTH, user_provided_card_details.expiration_date_month, |
| app_locale_); |
| upload_request_.card.SetInfo( |
| CREDIT_CARD_EXP_4_DIGIT_YEAR, |
| user_provided_card_details.expiration_date_year, app_locale_); |
| } |
| // Populating risk data and offering upload occur asynchronously. |
| // If |risk_data| has already been loaded, send the upload card request. |
| // Otherwise, continue to wait and let OnDidGetUploadRiskData handle it. |
| if (!upload_request_.risk_data.empty()) |
| SendUploadCardRequest(); |
| } |
| |
| void CreditCardSaveManager::OnDidGetUploadRiskData( |
| const std::string& risk_data) { |
| upload_request_.risk_data = risk_data; |
| // Populating risk data and offering upload occur asynchronously. |
| // If the dialog has already been accepted, send the upload card request. |
| // Otherwise, continue to wait for the user to accept the save dialog. |
| if (user_did_accept_upload_prompt_) |
| SendUploadCardRequest(); |
| } |
| |
| void CreditCardSaveManager::SendUploadCardRequest() { |
| if (observer_for_testing_) |
| observer_for_testing_->OnSentUploadCardRequest(); |
| upload_request_.app_locale = app_locale_; |
| upload_request_.billing_customer_number = payments::GetBillingCustomerId( |
| personal_data_manager_, payments_client_->GetPrefService()); |
| |
| AutofillMetrics::LogUploadAcceptedCardOriginMetric( |
| uploading_local_card_ |
| ? AutofillMetrics::USER_ACCEPTED_UPLOAD_OF_LOCAL_CARD |
| : AutofillMetrics::USER_ACCEPTED_UPLOAD_OF_NEW_CARD); |
| payments_client_->UploadCard( |
| upload_request_, base::BindOnce(&CreditCardSaveManager::OnDidUploadCard, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void CreditCardSaveManager::OnUserDidIgnoreOrDeclineSave( |
| const base::string16& card_last_four_digits) { |
| if (show_save_prompt_.value()) { |
| if (base::FeatureList::IsEnabled( |
| features::kAutofillSaveCreditCardUsesStrikeSystemV2)) { |
| // If the user rejected or ignored save and the offer-to-save bubble or |
| // infobar was actually shown (NOT just the icon if on desktop), count |
| // that as a strike against offering upload in the future. |
| int nth_strike_added = GetCreditCardSaveStrikeDatabase()->AddStrike( |
| base::UTF16ToUTF8(card_last_four_digits)); |
| OnStrikeChangeComplete(nth_strike_added); |
| } else if (base::FeatureList::IsEnabled( |
| features::kAutofillSaveCreditCardUsesStrikeSystem)) { |
| // If the user rejected or ignored save and the offer-to-save bubble or |
| // infobar was actually shown (NOT just the icon if on desktop), count |
| // that as a strike against offering upload in the future. |
| LegacyStrikeDatabase* strike_database = |
| client_->GetLegacyStrikeDatabase(); |
| strike_database->AddStrike( |
| strike_database->GetKeyForCreditCardSave( |
| base::UTF16ToUTF8(card_last_four_digits)), |
| base::BindRepeating(&CreditCardSaveManager::OnStrikeChangeComplete, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| } |
| } |
| |
| void CreditCardSaveManager::OnStrikeChangeComplete(const int num_strikes) { |
| if (observer_for_testing_) |
| observer_for_testing_->OnStrikeChangeComplete(); |
| } |
| |
| AutofillMetrics::CardUploadDecisionMetric |
| CreditCardSaveManager::GetCVCCardUploadDecisionMetric() const { |
| // This function assumes a valid CVC was not found. |
| if (found_cvc_field_) { |
| return found_value_in_cvc_field_ ? AutofillMetrics::INVALID_CVC_VALUE |
| : AutofillMetrics::CVC_VALUE_NOT_FOUND; |
| } |
| return found_cvc_value_in_non_cvc_field_ |
| ? AutofillMetrics::FOUND_POSSIBLE_CVC_VALUE_IN_NON_CVC_FIELD |
| : AutofillMetrics::CVC_FIELD_NOT_FOUND; |
| } |
| |
| void CreditCardSaveManager::LogCardUploadDecisions( |
| int upload_decision_metrics) { |
| AutofillMetrics::LogCardUploadDecisionMetrics(upload_decision_metrics); |
| AutofillMetrics::LogCardUploadDecisionsUkm( |
| client_->GetUkmRecorder(), client_->GetUkmSourceId(), |
| pending_upload_request_origin_.GetURL(), upload_decision_metrics); |
| pending_upload_request_origin_ = url::Origin(); |
| } |
| |
| void CreditCardSaveManager::LogSaveCardRequestExpirationDateReasonMetric() { |
| bool is_month_empty = |
| upload_request_.card |
| .GetInfo(AutofillType(CREDIT_CARD_EXP_MONTH), app_locale_) |
| .empty(); |
| bool is_year_empty = |
| upload_request_.card |
| .GetInfo(AutofillType(CREDIT_CARD_EXP_4_DIGIT_YEAR), app_locale_) |
| .empty(); |
| |
| if (is_month_empty && is_year_empty) { |
| AutofillMetrics::LogSaveCardRequestExpirationDateReasonMetric( |
| AutofillMetrics::SaveCardRequestExpirationDateReasonMetric:: |
| kMonthAndYearMissing); |
| } else if (is_month_empty) { |
| AutofillMetrics::LogSaveCardRequestExpirationDateReasonMetric( |
| AutofillMetrics::SaveCardRequestExpirationDateReasonMetric:: |
| kMonthMissingOnly); |
| } else if (is_year_empty) { |
| AutofillMetrics::LogSaveCardRequestExpirationDateReasonMetric( |
| AutofillMetrics::SaveCardRequestExpirationDateReasonMetric:: |
| kYearMissingOnly); |
| } else { |
| int month = 0, year = 0; |
| bool parsable = |
| base::StringToInt( |
| upload_request_.card.GetInfo( |
| AutofillType(CREDIT_CARD_EXP_4_DIGIT_YEAR), app_locale_), |
| &year) && |
| base::StringToInt(upload_request_.card.GetInfo( |
| AutofillType(CREDIT_CARD_EXP_MONTH), app_locale_), |
| &month); |
| DCHECK(parsable); |
| // Month and year are not empty, so they must be expired. |
| DCHECK(!IsValidCreditCardExpirationDate(year, month, AutofillClock::Now())); |
| AutofillMetrics::LogSaveCardRequestExpirationDateReasonMetric( |
| AutofillMetrics::SaveCardRequestExpirationDateReasonMetric:: |
| kExpirationDatePresentButExpired); |
| } |
| } |
| |
| } // namespace autofill |