// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/passwords/model/ios_chrome_save_password_infobar_delegate.h"
#import <string>
#import <utility>
#import "base/memory/ptr_util.h"
#import "base/metrics/histogram_functions.h"
#import "base/notreached.h"
#import "base/strings/strcat.h"
#import "base/strings/sys_string_conversions.h"
#import "components/autofill/ios/common/features.h"
#import "components/infobars/core/infobar.h"
#import "components/infobars/core/infobar_manager.h"
#import "components/password_manager/core/browser/password_form.h"
#import "components/password_manager/core/browser/password_form_manager_for_ui.h"
#import "components/password_manager/core/browser/password_form_metrics_recorder.h"
#import "components/password_manager/core/browser/password_manager_constants.h"
#import "components/password_manager/core/browser/password_ui_utils.h"
#import "components/strings/grit/components_strings.h"
#import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
#import "ios/chrome/grit/ios_branded_strings.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ui/base/l10n/l10n_util.h"
#import "url/gurl.h"
namespace {
inline constexpr std::string_view kInfobarSaveDurationHistogramName =
"PasswordManager.iOS.InfoBar.SaveDuration";
inline constexpr std::string_view kInfobarUpdateDurationHistogramName =
"PasswordManager.iOS.InfoBar.UpdateDuration";
// Moment where the infobar is torn down.
enum class InfobarTearDownMoment {
// Cover all moments.
kAll,
// After dismissing the infobar UI.
kOnDimiss,
// When deleting the infobar delegate.
kOnDeletion
};
// Records Presentation Metrics for the Infobar Delegate.
// `current_password_saved` is true if the Infobar is on read-only mode after a
// Save/Update action has occured.
// `update_infobar` is YES if presenting an Update Infobar, NO if presenting a
// Save Infobar.
// `automatic` is YES the Infobar was presented automatically(e.g. The banner
// was presented), NO if the user triggered it (e.g. Tapped onthe badge).
void RecordPresentationMetrics(
password_manager::PasswordFormManagerForUI* form_to_save,
bool current_password_saved,
bool update_infobar,
bool automatic) {
// TODO(b/318820862): Consider removing this block as it is theoretically
// impossible to save the password (e.g., tap on "Accept") before presenting.
if (current_password_saved) {
// Password was already saved or updated.
form_to_save->GetMetricsRecorder()->RecordPasswordBubbleShown(
form_to_save->GetCredentialSource(),
password_manager::metrics_util::MANUAL_MANAGE_PASSWORDS);
password_manager::metrics_util::LogUIDisplayDisposition(
password_manager::metrics_util::MANUAL_MANAGE_PASSWORDS);
return;
}
if (update_infobar) {
// Update Password.
if (automatic) {
form_to_save->GetMetricsRecorder()->RecordPasswordBubbleShown(
form_to_save->GetCredentialSource(),
password_manager::metrics_util::
AUTOMATIC_WITH_PASSWORD_PENDING_UPDATE);
password_manager::metrics_util::LogUIDisplayDisposition(
password_manager::metrics_util::
AUTOMATIC_WITH_PASSWORD_PENDING_UPDATE);
} else {
form_to_save->GetMetricsRecorder()->RecordPasswordBubbleShown(
form_to_save->GetCredentialSource(),
password_manager::metrics_util::MANUAL_WITH_PASSWORD_PENDING_UPDATE);
password_manager::metrics_util::LogUIDisplayDisposition(
password_manager::metrics_util::MANUAL_WITH_PASSWORD_PENDING_UPDATE);
}
} else {
// Save Password.
if (automatic) {
form_to_save->GetMetricsRecorder()->RecordPasswordBubbleShown(
form_to_save->GetCredentialSource(),
password_manager::metrics_util::AUTOMATIC_WITH_PASSWORD_PENDING);
password_manager::metrics_util::LogUIDisplayDisposition(
password_manager::metrics_util::AUTOMATIC_WITH_PASSWORD_PENDING);
} else {
form_to_save->GetMetricsRecorder()->RecordPasswordBubbleShown(
form_to_save->GetCredentialSource(),
password_manager::metrics_util::MANUAL_WITH_PASSWORD_PENDING);
password_manager::metrics_util::LogUIDisplayDisposition(
password_manager::metrics_util::MANUAL_WITH_PASSWORD_PENDING);
}
}
}
// Records Dismissal Metrics for the Infobar Delegate.
// `infobar_response` is the action that was taken in order to dismiss the
// Infobar.
// `update_infobar` is YES if presenting an Update Infobar, NO if presenting a
// Save Infobar.
void RecordDismissalMetrics(
password_manager::PasswordFormManagerForUI* form_to_save,
password_manager::metrics_util::UIDismissalReason infobar_response,
password_manager::features_util::PasswordAccountStorageUserState
account_storage_user_state,
bool update_infobar) {
form_to_save->GetMetricsRecorder()->RecordUIDismissalReason(infobar_response);
if (update_infobar) {
password_manager::metrics_util::LogUpdateUIDismissalReason(
infobar_response);
} else {
password_manager::metrics_util::LogSaveUIDismissalReason(
infobar_response, account_storage_user_state,
/*log_adoption_metric=*/false);
}
}
bool IsUpdateInfobar(PasswordInfobarType infobar_type) {
switch (infobar_type) {
case PasswordInfobarType::kPasswordInfobarTypeUpdate: {
return YES;
}
case PasswordInfobarType::kPasswordInfobarTypeSave:
return NO;
}
}
// Returns the infobar duration histogram name with the suffix that corresponds
// to `is_update` and `moment`.
std::string DurationHistogramName(bool is_update,
InfobarTearDownMoment moment) {
std::string_view base_name = is_update ? kInfobarUpdateDurationHistogramName
: kInfobarSaveDurationHistogramName;
switch (moment) {
case InfobarTearDownMoment::kAll:
return base::StrCat({base_name, ".All"});
case InfobarTearDownMoment::kOnDimiss:
return base::StrCat({base_name, ".OnDismiss"});
case InfobarTearDownMoment::kOnDeletion:
return base::StrCat({base_name, ".OnDeletion"});
}
NOTREACHED();
}
// Records the infobar duration metric for a given `moment`.
void RecordDurationAtMoment(bool is_update,
InfobarTearDownMoment moment,
base::TimeDelta duration) {
UmaHistogramCustomTimes(DurationHistogramName(is_update, moment), duration,
base::Milliseconds(1), base::Seconds(25), 50);
}
} // namespace
using password_manager::PasswordFormManagerForUI;
IOSChromeSavePasswordInfoBarDelegate::IOSChromeSavePasswordInfoBarDelegate(
std::optional<std::string> account_to_store_password,
bool password_update,
password_manager::features_util::PasswordAccountStorageUserState
account_storage_user_state,
std::unique_ptr<PasswordFormManagerForUI> form_to_save,
CommandDispatcher* dispatcher)
: dispatcher_(dispatcher),
form_to_save_(std::move(form_to_save)),
infobar_type_(password_update
? PasswordInfobarType::kPasswordInfobarTypeUpdate
: PasswordInfobarType::kPasswordInfobarTypeSave),
account_to_store_password_(account_to_store_password),
account_storage_user_state_(account_storage_user_state),
password_update_(password_update) {}
IOSChromeSavePasswordInfoBarDelegate::~IOSChromeSavePasswordInfoBarDelegate() {
if (IsPresenting()) {
// If by any reason this delegate gets dealloc before the Infobar UI is
// dismissed, record the dismissal metrics, which happens when navigating
// away from the page presenting the infobar.
RecordDismissalMetrics(form_to_save_.get(), infobar_response_,
account_storage_user_state_,
IsUpdateInfobar(infobar_type_));
RecordInfobarDuration(/*on_dismiss=*/false);
}
}
// static
IOSChromeSavePasswordInfoBarDelegate*
IOSChromeSavePasswordInfoBarDelegate::FromInfobarDelegate(
infobars::InfoBarDelegate* delegate) {
return delegate->GetIdentifier() == SAVE_PASSWORD_INFOBAR_DELEGATE_MOBILE
? static_cast<IOSChromeSavePasswordInfoBarDelegate*>(delegate)
: nullptr;
}
NSString* IOSChromeSavePasswordInfoBarDelegate::GetUserNameText() const {
return base::SysUTF16ToNSString(
form_to_save_->GetPendingCredentials().username_value);
}
NSString* IOSChromeSavePasswordInfoBarDelegate::GetPasswordText() const {
return base::SysUTF16ToNSString(
form_to_save_->GetPendingCredentials().password_value);
}
NSString* IOSChromeSavePasswordInfoBarDelegate::GetURLHostText() const {
return base::SysUTF8ToNSString(form_to_save_->GetURL().host());
}
std::optional<std::string>
IOSChromeSavePasswordInfoBarDelegate::GetAccountToStorePassword() const {
return account_to_store_password_;
}
bool IOSChromeSavePasswordInfoBarDelegate::ShouldExpire(
const NavigationDetails& details) const {
const bool from_user_gesture =
!base::FeatureList::IsEnabled(kAutofillStickyInfobarIos) ||
details.has_user_gesture;
return !details.is_form_submission && !details.is_redirect &&
from_user_gesture && ConfirmInfoBarDelegate::ShouldExpire(details);
}
std::u16string IOSChromeSavePasswordInfoBarDelegate::GetMessageText() const {
if (IsPasswordUpdate()) {
return l10n_util::GetStringUTF16(IDS_IOS_PASSWORD_MANAGER_UPDATE_PASSWORD);
}
return l10n_util::GetStringUTF16(
IDS_IOS_PASSWORD_MANAGER_SAVE_PASSWORD_PROMPT);
}
std::u16string IOSChromeSavePasswordInfoBarDelegate::GetButtonLabel(
InfoBarButton button) const {
switch (button) {
case BUTTON_OK:
return l10n_util::GetStringUTF16(
IsPasswordUpdate() ? IDS_IOS_PASSWORD_MANAGER_UPDATE_BUTTON
: IDS_IOS_PASSWORD_MANAGER_SAVE_BUTTON);
case BUTTON_CANCEL: {
return IsPasswordUpdate()
? std::u16string()
: l10n_util::GetStringUTF16(
IDS_IOS_PASSWORD_MANAGER_MODAL_BLOCK_BUTTON);
}
case BUTTON_NONE:
NOTREACHED_IN_MIGRATION();
return std::u16string();
}
}
bool IOSChromeSavePasswordInfoBarDelegate::Accept() {
DCHECK(form_to_save_);
form_to_save_->Save();
infobar_response_ = password_manager::metrics_util::CLICKED_ACCEPT;
password_update_ = true;
current_password_saved_ = true;
return true;
}
bool IOSChromeSavePasswordInfoBarDelegate::Cancel() {
DCHECK(form_to_save_);
DCHECK(!password_update_);
form_to_save_->Blocklist();
infobar_response_ = password_manager::metrics_util::CLICKED_NEVER;
return true;
}
void IOSChromeSavePasswordInfoBarDelegate::InfoBarDismissed() {
DCHECK(form_to_save_);
infobar_response_ = password_manager::metrics_util::CLICKED_CANCEL;
}
void IOSChromeSavePasswordInfoBarDelegate::UpdateCredentials(
NSString* username,
NSString* password) {
const std::u16string username_string = base::SysNSStringToUTF16(username);
const std::u16string password_string = base::SysNSStringToUTF16(password);
UpdatePasswordFormUsernameAndPassword(username_string, password_string,
form_to_save_.get());
}
void IOSChromeSavePasswordInfoBarDelegate::InfobarPresenting(bool automatic) {
if (IsPresenting()) {
return;
}
RecordPresentationMetrics(form_to_save_.get(), current_password_saved_,
IsUpdateInfobar(infobar_type_), automatic);
start_timestamp_ = base::TimeTicks::Now();
}
void IOSChromeSavePasswordInfoBarDelegate::InfobarGone() {
if (!IsPresenting()) {
return;
}
RecordDismissalMetrics(form_to_save_.get(), infobar_response_,
account_storage_user_state_,
IsUpdateInfobar(infobar_type_));
RecordInfobarDuration(/*on_dismiss=*/true);
// Reset presentation state.
infobar_response_ = password_manager::metrics_util::NO_DIRECT_INTERACTION;
start_timestamp_ = std::nullopt;
}
bool IOSChromeSavePasswordInfoBarDelegate::IsPasswordUpdate() const {
return password_update_;
}
bool IOSChromeSavePasswordInfoBarDelegate::IsCurrentPasswordSaved() const {
return current_password_saved_;
}
infobars::InfoBarDelegate::InfoBarIdentifier
IOSChromeSavePasswordInfoBarDelegate::GetIdentifier() const {
return SAVE_PASSWORD_INFOBAR_DELEGATE_MOBILE;
}
void IOSChromeSavePasswordInfoBarDelegate::RecordInfobarDuration(
bool on_dismiss) {
// Check that recording the infobar duration is only done when there is a
// start timestamp and the infobar had been presenting.
CHECK(start_timestamp_);
bool is_update = IsUpdateInfobar(infobar_type_);
base::TimeDelta duration = base::TimeTicks::Now() - *start_timestamp_;
RecordDurationAtMoment(is_update, InfobarTearDownMoment::kAll, duration);
if (on_dismiss) {
RecordDurationAtMoment(is_update, InfobarTearDownMoment::kOnDimiss,
duration);
} else {
RecordDurationAtMoment(is_update, InfobarTearDownMoment::kOnDeletion,
duration);
}
}
bool IOSChromeSavePasswordInfoBarDelegate::IsPresenting() const {
return start_timestamp_.has_value();
}