blob: a027c658091878347da409c41b799f8e96f93f30 [file] [log] [blame]
// 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 "chrome/browser/ui/views/payments/shipping_address_editor_view_controller.h"
#include "base/bind.h"
#include "base/callback.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "chrome/browser/ui/views/payments/payment_request_dialog_view.h"
#include "chrome/browser/ui/views/payments/payment_request_dialog_view_ids.h"
#include "chrome/browser/ui/views/payments/validating_combobox.h"
#include "chrome/browser/ui/views/payments/validating_textfield.h"
#include "chrome/grit/generated_resources.h"
#include "components/autofill/core/browser/autofill_address_util.h"
#include "components/autofill/core/browser/autofill_type.h"
#include "components/autofill/core/browser/field_types.h"
#include "components/autofill/core/browser/geo/address_i18n.h"
#include "components/autofill/core/browser/geo/autofill_country.h"
#include "components/autofill/core/browser/geo/phone_number_i18n.h"
#include "components/autofill/core/browser/personal_data_manager.h"
#include "components/autofill/core/browser/ui/country_combobox_model.h"
#include "components/autofill/core/browser/validation.h"
#include "components/autofill/core/common/autofill_constants.h"
#include "components/autofill/core/common/autofill_l10n_util.h"
#include "components/payments/content/payment_request_state.h"
#include "components/payments/core/payment_request_data_util.h"
#include "components/payments/core/payments_profile_comparator.h"
#include "components/strings/grit/components_strings.h"
#include "third_party/libaddressinput/messages.h"
#include "third_party/libaddressinput/src/cpp/include/libaddressinput/address_data.h"
#include "third_party/libaddressinput/src/cpp/include/libaddressinput/address_formatter.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/views/controls/textfield/textfield.h"
namespace payments {
namespace {
// size_t doesn't have a defined maximum value, so this is a trick to create one
// as is done for std::string::npos.
// http://www.cplusplus.com/reference/string/string/npos
const size_t kInvalidCountryIndex = static_cast<size_t>(-1);
} // namespace
ShippingAddressEditorViewController::ShippingAddressEditorViewController(
PaymentRequestSpec* spec,
PaymentRequestState* state,
PaymentRequestDialogView* dialog,
BackNavigationType back_navigation_type,
base::OnceClosure on_edited,
base::OnceCallback<void(const autofill::AutofillProfile&)> on_added,
autofill::AutofillProfile* profile,
bool is_incognito)
: EditorViewController(spec,
state,
dialog,
back_navigation_type,
is_incognito),
on_edited_(std::move(on_edited)),
on_added_(std::move(on_added)),
profile_to_edit_(profile),
chosen_country_index_(kInvalidCountryIndex),
failed_to_load_region_data_(false) {
if (profile_to_edit_)
temporary_profile_ = *profile_to_edit_;
UpdateCountries(/*model=*/nullptr);
UpdateEditorFields();
}
ShippingAddressEditorViewController::~ShippingAddressEditorViewController() {}
bool ShippingAddressEditorViewController::IsEditingExistingItem() {
return !!profile_to_edit_;
}
std::vector<EditorField>
ShippingAddressEditorViewController::GetFieldDefinitions() {
return editor_fields_;
}
base::string16 ShippingAddressEditorViewController::GetInitialValueForType(
autofill::ServerFieldType type) {
return GetValueForType(temporary_profile_, type);
}
bool ShippingAddressEditorViewController::ValidateModelAndSave() {
// To validate the profile first, we use a temporary object.
autofill::AutofillProfile profile;
if (!SaveFieldsToProfile(&profile, /*ignore_errors=*/false))
return false;
if (!profile_to_edit_) {
// Add the profile (will not add a duplicate).
profile.set_origin(autofill::kSettingsOrigin);
if (!is_incognito())
state()->GetPersonalDataManager()->AddProfile(profile);
std::move(on_added_).Run(profile);
on_edited_.Reset();
} else {
autofill::ServerFieldTypeSet all_fields;
profile_to_edit_->GetSupportedTypes(&all_fields);
// Clear all the address data in |profile_to_edit_|, in anticipation of
// adding only the fields present in the editor. Prefer this method to
// copying |profile| into |profile_to_edit_|, because the latter object
// needs to retain other properties (use count, use date, guid,
// etc.).
for (autofill::ServerFieldType type : all_fields)
profile_to_edit_->SetRawInfo(type, base::string16());
bool success = SaveFieldsToProfile(profile_to_edit_,
/*ignore_errors=*/false);
DCHECK(success);
profile_to_edit_->set_origin(autofill::kSettingsOrigin);
if (!is_incognito())
state()->GetPersonalDataManager()->UpdateProfile(*profile_to_edit_);
state()->profile_comparator()->Invalidate(*profile_to_edit_);
std::move(on_edited_).Run();
on_added_.Reset();
}
return true;
}
std::unique_ptr<ValidationDelegate>
ShippingAddressEditorViewController::CreateValidationDelegate(
const EditorField& field) {
return std::make_unique<
ShippingAddressEditorViewController::ShippingAddressValidationDelegate>(
this, field);
}
std::unique_ptr<ui::ComboboxModel>
ShippingAddressEditorViewController::GetComboboxModelForType(
const autofill::ServerFieldType& type) {
switch (type) {
case autofill::ADDRESS_HOME_COUNTRY: {
auto model = std::make_unique<autofill::CountryComboboxModel>();
model->SetCountries(*state()->GetPersonalDataManager(),
base::Callback<bool(const std::string&)>(),
state()->GetApplicationLocale());
if (model->countries().size() != countries_.size())
UpdateCountries(model.get());
return model;
}
case autofill::ADDRESS_HOME_STATE: {
auto model = std::make_unique<autofill::RegionComboboxModel>();
region_model_ = model.get();
if (chosen_country_index_ < countries_.size()) {
model->LoadRegionData(countries_[chosen_country_index_].first,
state()->GetRegionDataLoader(),
/*timeout_ms=*/5000);
if (!model->IsPendingRegionDataLoad()) {
// If the data was already pre-loaded, the observer won't get notified
// so we have to check for failure here.
failed_to_load_region_data_ = model->failed_to_load_data();
}
} else {
failed_to_load_region_data_ = true;
}
if (failed_to_load_region_data_) {
// We can't update the view synchronously while building the view.
OnDataChanged(/*synchronous=*/false);
}
return model;
}
default:
NOTREACHED();
break;
}
return std::unique_ptr<ui::ComboboxModel>();
}
void ShippingAddressEditorViewController::OnPerformAction(
views::Combobox* sender) {
EditorViewController::OnPerformAction(sender);
if (sender->GetID() != GetInputFieldViewId(autofill::ADDRESS_HOME_COUNTRY))
return;
DCHECK_GE(sender->GetSelectedIndex(), 0);
if (chosen_country_index_ !=
static_cast<size_t>(sender->GetSelectedIndex())) {
chosen_country_index_ = sender->GetSelectedIndex();
failed_to_load_region_data_ = false;
// View update must be asynchronous to let the combobox finish performing
// the action.
OnDataChanged(/*synchronous=*/false);
}
}
void ShippingAddressEditorViewController::UpdateEditorView() {
region_model_ = nullptr;
EditorViewController::UpdateEditorView();
if (chosen_country_index_ > 0UL &&
chosen_country_index_ < countries_.size()) {
views::Combobox* country_combo_box =
static_cast<views::Combobox*>(dialog()->GetViewByID(
GetInputFieldViewId(autofill::ADDRESS_HOME_COUNTRY)));
DCHECK(country_combo_box);
DCHECK_EQ(countries_.size(),
static_cast<size_t>(country_combo_box->GetRowCount()));
country_combo_box->SetSelectedIndex(chosen_country_index_);
} else if (countries_.size() > 0UL) {
chosen_country_index_ = 0UL;
} else {
chosen_country_index_ = kInvalidCountryIndex;
}
}
base::string16 ShippingAddressEditorViewController::GetSheetTitle() {
// TODO(crbug.com/712074): Editor title should reflect the missing information
// in the case that one or more fields are missing.
return profile_to_edit_ ? l10n_util::GetStringUTF16(IDS_PAYMENTS_EDIT_ADDRESS)
: l10n_util::GetStringUTF16(IDS_PAYMENTS_ADD_ADDRESS);
}
std::unique_ptr<views::Button>
ShippingAddressEditorViewController::CreatePrimaryButton() {
std::unique_ptr<views::Button> button(
EditorViewController::CreatePrimaryButton());
button->SetID(static_cast<int>(DialogViewID::SAVE_ADDRESS_BUTTON));
return button;
}
ShippingAddressEditorViewController::ShippingAddressValidationDelegate::
ShippingAddressValidationDelegate(
ShippingAddressEditorViewController* controller,
const EditorField& field)
: field_(field), controller_(controller) {}
ShippingAddressEditorViewController::ShippingAddressValidationDelegate::
~ShippingAddressValidationDelegate() {}
bool ShippingAddressEditorViewController::ShippingAddressValidationDelegate::
ShouldFormat() {
return field_.type == autofill::PHONE_HOME_WHOLE_NUMBER;
}
base::string16
ShippingAddressEditorViewController::ShippingAddressValidationDelegate::Format(
const base::string16& text) {
if (controller_->chosen_country_index_ < controller_->countries_.size()) {
return base::UTF8ToUTF16(autofill::i18n::FormatPhoneForDisplay(
base::UTF16ToUTF8(text),
controller_->countries_[controller_->chosen_country_index_].first));
} else {
return text;
}
}
bool ShippingAddressEditorViewController::ShippingAddressValidationDelegate::
IsValidTextfield(views::Textfield* textfield,
base::string16* error_message) {
return ValidateValue(textfield->text(), error_message);
}
bool ShippingAddressEditorViewController::ShippingAddressValidationDelegate::
IsValidCombobox(views::Combobox* combobox, base::string16* error_message) {
return ValidateValue(combobox->GetTextForRow(combobox->GetSelectedIndex()),
error_message);
}
bool ShippingAddressEditorViewController::ShippingAddressValidationDelegate::
TextfieldValueChanged(views::Textfield* textfield, bool was_blurred) {
if (!was_blurred)
return true;
base::string16 error_message;
bool is_valid = ValidateValue(textfield->text(), &error_message);
controller_->DisplayErrorMessageForField(field_.type, error_message);
return is_valid;
}
bool ShippingAddressEditorViewController::ShippingAddressValidationDelegate::
ComboboxValueChanged(views::Combobox* combobox) {
base::string16 error_message;
bool is_valid = ValidateValue(
combobox->GetTextForRow(combobox->GetSelectedIndex()), &error_message);
controller_->DisplayErrorMessageForField(field_.type, error_message);
return is_valid;
}
void ShippingAddressEditorViewController::ShippingAddressValidationDelegate::
ComboboxModelChanged(views::Combobox* combobox) {
controller_->OnComboboxModelChanged(combobox);
}
bool ShippingAddressEditorViewController::ShippingAddressValidationDelegate::
ValidateValue(const base::string16& value, base::string16* error_message) {
// Show errors from merchant's retry() call. Note that changing the selected
// shipping address will clear the validation errors from retry().
autofill::AutofillProfile* invalid_shipping_profile =
controller_->state()->invalid_shipping_profile();
if (invalid_shipping_profile && error_message &&
value == controller_->GetValueForType(*invalid_shipping_profile,
field_.type)) {
*error_message = controller_->spec()->GetShippingAddressError(field_.type);
if (!error_message->empty())
return false;
}
if (!value.empty()) {
if (field_.type == autofill::PHONE_HOME_WHOLE_NUMBER &&
controller_->chosen_country_index_ < controller_->countries_.size() &&
!autofill::IsPossiblePhoneNumber(
value, controller_->countries_[controller_->chosen_country_index_]
.first)) {
if (error_message) {
*error_message = l10n_util::GetStringUTF16(
IDS_PAYMENTS_PHONE_INVALID_VALIDATION_MESSAGE);
}
return false;
}
if (field_.type == autofill::ADDRESS_HOME_STATE &&
value == l10n_util::GetStringUTF16(IDS_AUTOFILL_LOADING_REGIONS)) {
// Wait for the regions to be loaded or timeout before assessing validity.
return false;
}
// As long as other field types are non-empty, they are valid.
return true;
}
if (error_message && field_.required) {
*error_message = l10n_util::GetStringUTF16(
IDS_PREF_EDIT_DIALOG_FIELD_REQUIRED_VALIDATION_MESSAGE);
}
return !field_.required;
}
base::string16 ShippingAddressEditorViewController::GetValueForType(
const autofill::AutofillProfile& profile,
autofill::ServerFieldType type) {
if (type == autofill::PHONE_HOME_WHOLE_NUMBER) {
return autofill::i18n::GetFormattedPhoneNumberForDisplay(
profile, state()->GetApplicationLocale());
}
if (type == autofill::ADDRESS_HOME_STATE && region_model_) {
// For the state, check if the initial value matches either a region code or
// a region name.
base::string16 initial_region =
profile.GetInfo(type, state()->GetApplicationLocale());
autofill::l10n::CaseInsensitiveCompare compare;
for (const auto& region : region_model_->GetRegions()) {
if (compare.StringsEqual(initial_region,
base::UTF8ToUTF16(region.first)) ||
compare.StringsEqual(initial_region,
base::UTF8ToUTF16(region.second))) {
return base::UTF8ToUTF16(region.second);
}
}
return initial_region;
}
if (type == autofill::ADDRESS_HOME_STREET_ADDRESS) {
std::string street_address_line;
i18n::addressinput::GetStreetAddressLinesAsSingleLine(
*autofill::i18n::CreateAddressDataFromAutofillProfile(
profile, state()->GetApplicationLocale()),
&street_address_line);
return base::UTF8ToUTF16(street_address_line);
}
return profile.GetInfo(type, state()->GetApplicationLocale());
}
bool ShippingAddressEditorViewController::GetSheetId(DialogViewID* sheet_id) {
*sheet_id = DialogViewID::SHIPPING_ADDRESS_EDITOR_SHEET;
return true;
}
void ShippingAddressEditorViewController::UpdateCountries(
autofill::CountryComboboxModel* model) {
autofill::CountryComboboxModel local_model;
if (!model) {
local_model.SetCountries(*state()->GetPersonalDataManager(),
base::Callback<bool(const std::string&)>(),
state()->GetApplicationLocale());
model = &local_model;
}
for (size_t i = 0; i < model->countries().size(); ++i) {
autofill::AutofillCountry* country(model->countries()[i].get());
if (country) {
countries_.push_back(
std::make_pair(country->country_code(), country->name()));
} else {
// Separator, kept to make sure the size of the vector stays the same.
countries_.push_back(std::make_pair("", base::UTF8ToUTF16("")));
}
}
// If there is a profile to edit, make sure to use its country for the initial
// |chosen_country_index_|.
if (IsEditingExistingItem()) {
base::string16 chosen_country(temporary_profile_.GetInfo(
autofill::ADDRESS_HOME_COUNTRY, state()->GetApplicationLocale()));
for (chosen_country_index_ = 0; chosen_country_index_ < countries_.size();
++chosen_country_index_) {
if (chosen_country == countries_[chosen_country_index_].second)
break;
}
// Make sure the the country was actually found in |countries_| and was not
// empty, otherwise set |chosen_country_index_| to index 0, which is the
// default country based on the locale.
if (chosen_country_index_ >= countries_.size() || chosen_country.empty()) {
// But only if there is at least one country.
if (!countries_.empty()) {
LOG(ERROR) << "Unexpected country: " << chosen_country;
chosen_country_index_ = 0;
temporary_profile_.SetInfo(autofill::ADDRESS_HOME_COUNTRY,
countries_[chosen_country_index_].second,
state()->GetApplicationLocale());
} else {
LOG(ERROR) << "Unexpected empty country list!";
chosen_country_index_ = kInvalidCountryIndex;
}
}
} else if (!countries_.empty()) {
chosen_country_index_ = 0;
}
}
void ShippingAddressEditorViewController::UpdateEditorFields() {
editor_fields_.clear();
std::string chosen_country_code;
if (chosen_country_index_ < countries_.size())
chosen_country_code = countries_[chosen_country_index_].first;
std::unique_ptr<base::ListValue> components(new base::ListValue);
autofill::GetAddressComponents(chosen_country_code,
state()->GetApplicationLocale(),
components.get(), &language_code_);
for (size_t line_index = 0; line_index < components->GetSize();
++line_index) {
const base::ListValue* line = nullptr;
if (!components->GetList(line_index, &line)) {
NOTREACHED();
return;
}
DCHECK_NE(nullptr, line);
for (size_t component_index = 0; component_index < line->GetSize();
++component_index) {
const base::DictionaryValue* component = nullptr;
if (!line->GetDictionary(component_index, &component)) {
NOTREACHED();
return;
}
std::string field_type;
if (!component->GetString(autofill::kFieldTypeKey, &field_type)) {
NOTREACHED();
return;
}
std::string field_name;
if (!component->GetString(autofill::kFieldNameKey, &field_name)) {
NOTREACHED();
return;
}
bool field_length;
if (!component->GetBoolean(autofill::kFieldLengthKey, &field_length)) {
NOTREACHED();
return;
}
EditorField::LengthHint length_hint = EditorField::LengthHint::HINT_SHORT;
if (field_length == autofill::kLongField)
length_hint = EditorField::LengthHint::HINT_LONG;
else
DCHECK_EQ(autofill::kShortField, field_length);
autofill::ServerFieldType server_field_type =
autofill::GetFieldTypeFromString(field_type);
EditorField::ControlType control_type =
EditorField::ControlType::TEXTFIELD;
if (server_field_type == autofill::ADDRESS_HOME_COUNTRY ||
(server_field_type == autofill::ADDRESS_HOME_STATE &&
!failed_to_load_region_data_)) {
control_type = EditorField::ControlType::COMBOBOX;
}
editor_fields_.emplace_back(server_field_type,
base::UTF8ToUTF16(field_name), length_hint,
autofill::i18n::IsFieldRequired(
server_field_type, chosen_country_code),
control_type);
// Insert the Country combobox right after NAME_FULL.
if (server_field_type == autofill::NAME_FULL) {
editor_fields_.emplace_back(
autofill::ADDRESS_HOME_COUNTRY,
l10n_util::GetStringUTF16(
IDS_LIBADDRESSINPUT_COUNTRY_OR_REGION_LABEL),
EditorField::LengthHint::HINT_SHORT, /*required=*/true,
EditorField::ControlType::COMBOBOX);
}
}
}
// Always add phone number at the end.
editor_fields_.emplace_back(
autofill::PHONE_HOME_WHOLE_NUMBER,
l10n_util::GetStringUTF16(IDS_AUTOFILL_FIELD_LABEL_PHONE),
EditorField::LengthHint::HINT_SHORT, /*required=*/true,
EditorField::ControlType::TEXTFIELD_NUMBER);
}
void ShippingAddressEditorViewController::OnDataChanged(bool synchronous) {
SaveFieldsToProfile(&temporary_profile_, /*ignore_errors*/ true);
// Normalization is guaranteed to be synchronous and rules should have been
// loaded already.
state()->GetAddressNormalizer()->NormalizeAddressSync(&temporary_profile_);
UpdateEditorFields();
if (synchronous) {
UpdateEditorView();
} else {
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE,
base::BindOnce(&ShippingAddressEditorViewController::UpdateEditorView,
base::Unretained(this)));
}
}
bool ShippingAddressEditorViewController::SaveFieldsToProfile(
autofill::AutofillProfile* profile,
bool ignore_errors) {
const std::string& locale = state()->GetApplicationLocale();
// The country must be set first, because the profile uses the country to
// interpret some of the data (e.g., phone numbers) passed to SetInfo.
views::Combobox* combobox =
static_cast<views::Combobox*>(dialog()->GetViewByID(
GetInputFieldViewId(autofill::ADDRESS_HOME_COUNTRY)));
// The combobox can be null when saving to temporary profile while updating
// the view.
if (combobox) {
base::string16 country(
combobox->GetTextForRow(combobox->GetSelectedIndex()));
bool success =
profile->SetInfo(autofill::ADDRESS_HOME_COUNTRY, country, locale);
LOG_IF(ERROR, !success && !ignore_errors)
<< "Can't set profile country to: " << country;
if (!success && !ignore_errors)
return false;
}
bool success = true;
for (const auto& field : text_fields()) {
// ValidatingTextfield* is the key, EditorField is the value.
if (field.first->IsValid()) {
success =
profile->SetInfo(field.second.type, field.first->text(), locale);
} else {
success = false;
}
LOG_IF(ERROR, !success && !ignore_errors)
<< "Can't setinfo(" << field.second.type << ", " << field.first->text();
if (!success && !ignore_errors)
return false;
}
for (const auto& field : comboboxes()) {
// ValidatingCombobox* is the key, EditorField is the value.
ValidatingCombobox* combobox = field.first;
// The country has already been dealt with.
if (combobox->GetID() ==
GetInputFieldViewId(autofill::ADDRESS_HOME_COUNTRY))
continue;
if (combobox->IsValid()) {
success = profile->SetInfo(
field.second.type,
combobox->GetTextForRow(combobox->GetSelectedIndex()), locale);
} else {
success = false;
}
LOG_IF(ERROR, !success && !ignore_errors)
<< "Can't setinfo(" << field.second.type << ", "
<< combobox->GetTextForRow(combobox->GetSelectedIndex());
if (!success && !ignore_errors)
return false;
}
profile->set_language_code(language_code_);
return success;
}
void ShippingAddressEditorViewController::OnComboboxModelChanged(
views::Combobox* combobox) {
if (combobox->GetID() != GetInputFieldViewId(autofill::ADDRESS_HOME_STATE))
return;
autofill::RegionComboboxModel* model =
static_cast<autofill::RegionComboboxModel*>(combobox->model());
if (model->IsPendingRegionDataLoad())
return;
if (model->failed_to_load_data()) {
failed_to_load_region_data_ = true;
// It is safe to update synchronously since the change comes from the model
// and not from the UI.
OnDataChanged(/*synchronous=*/true);
} else {
base::string16 state_value =
GetInitialValueForType(autofill::ADDRESS_HOME_STATE);
if (!state_value.empty()) {
combobox->SelectValue(state_value);
OnPerformAction(combobox);
}
}
}
} // namespace payments