blob: 161a86d9cd0b95507fa474c0b1e62a3ed7a53ebc [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.
#import "ios/chrome/browser/ui/authentication/signin_promo_view_mediator.h"
#include <memory>
#include "base/metrics/histogram_macros.h"
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
#include "base/strings/sys_string_conversions.h"
#include "components/prefs/pref_service.h"
#include "components/signin/core/browser/signin_metrics.h"
#include "ios/chrome/browser/browser_state/chrome_browser_state.h"
#include "ios/chrome/browser/chrome_browser_provider_observer_bridge.h"
#include "ios/chrome/browser/pref_names.h"
#import "ios/chrome/browser/signin/authentication_service.h"
#include "ios/chrome/browser/signin/authentication_service_factory.h"
#include "ios/chrome/browser/signin/chrome_identity_service_observer_bridge.h"
#import "ios/chrome/browser/ui/authentication/cells/signin_promo_view_configurator.h"
#import "ios/chrome/browser/ui/authentication/cells/signin_promo_view_consumer.h"
#import "ios/chrome/browser/ui/commands/application_commands.h"
#import "ios/chrome/browser/ui/commands/show_signin_command.h"
#import "ios/chrome/browser/ui/signin_interaction/public/signin_presenter.h"
#include "ios/chrome/grit/ios_strings.h"
#import "ios/public/provider/chrome/browser/chrome_browser_provider.h"
#import "ios/public/provider/chrome/browser/signin/chrome_identity.h"
#import "ios/public/provider/chrome/browser/signin/signin_resources_provider.h"
#import "ios/third_party/material_components_ios/src/components/Buttons/src/MaterialButtons.h"
#include "ui/base/l10n/l10n_util.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
const int kAutomaticSigninPromoViewDismissCount = 20;
bool IsSupportedAccessPoint(signin_metrics::AccessPoint access_point) {
switch (access_point) {
case signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS:
case signin_metrics::AccessPoint::ACCESS_POINT_BOOKMARK_MANAGER:
case signin_metrics::AccessPoint::ACCESS_POINT_RECENT_TABS:
case signin_metrics::AccessPoint::ACCESS_POINT_TAB_SWITCHER:
return true;
default:
return false;
}
}
void RecordImpressionsTilSigninButtonsHistogramForAccessPoint(
signin_metrics::AccessPoint access_point,
int displayed_count) {
switch (access_point) {
case signin_metrics::AccessPoint::ACCESS_POINT_BOOKMARK_MANAGER:
UMA_HISTOGRAM_COUNTS_100(
"MobileSignInPromo.BookmarkManager.ImpressionsTilSigninButtons",
displayed_count);
break;
case signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS:
UMA_HISTOGRAM_COUNTS_100(
"MobileSignInPromo.SettingsManager.ImpressionsTilSigninButtons",
displayed_count);
break;
default:
NOTREACHED() << "Unexpected value for access point "
<< static_cast<int>(access_point);
break;
}
}
void RecordImpressionsTilDismissHistogramForAccessPoint(
signin_metrics::AccessPoint access_point,
int displayed_count) {
switch (access_point) {
case signin_metrics::AccessPoint::ACCESS_POINT_BOOKMARK_MANAGER:
UMA_HISTOGRAM_COUNTS_100(
"MobileSignInPromo.BookmarkManager.ImpressionsTilDismiss",
displayed_count);
break;
case signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS:
UMA_HISTOGRAM_COUNTS_100(
"MobileSignInPromo.SettingsManager.ImpressionsTilDismiss",
displayed_count);
break;
default:
NOTREACHED() << "Unexpected value for access point "
<< static_cast<int>(access_point);
break;
}
}
void RecordImpressionsTilXButtonHistogramForAccessPoint(
signin_metrics::AccessPoint access_point,
int displayed_count) {
switch (access_point) {
case signin_metrics::AccessPoint::ACCESS_POINT_BOOKMARK_MANAGER:
UMA_HISTOGRAM_COUNTS_100(
"MobileSignInPromo.BookmarkManager.ImpressionsTilXButton",
displayed_count);
break;
case signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS:
UMA_HISTOGRAM_COUNTS_100(
"MobileSignInPromo.SettingsManager.ImpressionsTilXButton",
displayed_count);
break;
default:
NOTREACHED() << "Unexpected value for access point "
<< static_cast<int>(access_point);
break;
}
}
const char* DisplayedCountPreferenceKey(
signin_metrics::AccessPoint access_point) {
switch (access_point) {
case signin_metrics::AccessPoint::ACCESS_POINT_BOOKMARK_MANAGER:
return prefs::kIosBookmarkSigninPromoDisplayedCount;
case signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS:
return prefs::kIosSettingsSigninPromoDisplayedCount;
default:
return nullptr;
}
}
const char* AlreadySeenSigninViewPreferenceKey(
signin_metrics::AccessPoint access_point) {
switch (access_point) {
case signin_metrics::AccessPoint::ACCESS_POINT_BOOKMARK_MANAGER:
return prefs::kIosBookmarkPromoAlreadySeen;
case signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS:
return prefs::kIosSettingsPromoAlreadySeen;
default:
return nullptr;
}
}
} // namespace
@interface SigninPromoViewMediator ()<ChromeIdentityServiceObserver,
ChromeBrowserProviderObserver>
// Presenter which can show signin UI.
@property(nonatomic, readonly, weak) id<SigninPresenter> presenter;
// Redefined to be readwrite.
@property(nonatomic, readwrite, getter=isSigninInProgress)
BOOL signinInProgress;
@end
@implementation SigninPromoViewMediator {
ios::ChromeBrowserState* _browserState;
signin_metrics::AccessPoint _accessPoint;
std::unique_ptr<ChromeIdentityServiceObserverBridge> _identityServiceObserver;
std::unique_ptr<ChromeBrowserProviderObserverBridge> _browserProviderObserver;
UIImage* _identityAvatar;
BOOL _isSigninPromoViewVisible;
}
@synthesize consumer = _consumer;
@synthesize defaultIdentity = _defaultIdentity;
@synthesize signinPromoViewState = _signinPromoViewState;
@synthesize presenter = _presenter;
@synthesize signinInProgress = _signinInProgress;
+ (void)registerBrowserStatePrefs:(user_prefs::PrefRegistrySyncable*)registry {
// Bookmarks
registry->RegisterBooleanPref(prefs::kIosBookmarkPromoAlreadySeen, false);
registry->RegisterIntegerPref(prefs::kIosBookmarkSigninPromoDisplayedCount,
0);
// Settings
registry->RegisterBooleanPref(prefs::kIosSettingsPromoAlreadySeen, false);
registry->RegisterIntegerPref(prefs::kIosSettingsSigninPromoDisplayedCount,
0);
}
+ (BOOL)shouldDisplaySigninPromoViewWithAccessPoint:
(signin_metrics::AccessPoint)accessPoint
browserState:(ios::ChromeBrowserState*)
browserState {
PrefService* prefs = browserState->GetPrefs();
const char* displayedCountPreferenceKey =
DisplayedCountPreferenceKey(accessPoint);
if (displayedCountPreferenceKey &&
prefs->GetInteger(displayedCountPreferenceKey) >=
kAutomaticSigninPromoViewDismissCount) {
return NO;
}
const char* alreadySeenSigninViewPreferenceKey =
AlreadySeenSigninViewPreferenceKey(accessPoint);
if (alreadySeenSigninViewPreferenceKey &&
prefs->GetBoolean(alreadySeenSigninViewPreferenceKey)) {
return NO;
}
return YES;
}
- (instancetype)initWithBrowserState:(ios::ChromeBrowserState*)browserState
accessPoint:(signin_metrics::AccessPoint)accessPoint
presenter:(id<SigninPresenter>)presenter {
self = [super init];
if (self) {
DCHECK(IsSupportedAccessPoint(accessPoint));
_accessPoint = accessPoint;
_browserState = browserState;
_presenter = presenter;
NSArray* identities = ios::GetChromeBrowserProvider()
->GetChromeIdentityService()
->GetAllIdentitiesSortedForDisplay();
if (identities.count != 0) {
[self selectIdentity:identities[0]];
}
_identityServiceObserver =
std::make_unique<ChromeIdentityServiceObserverBridge>(self);
_browserProviderObserver =
std::make_unique<ChromeBrowserProviderObserverBridge>(self);
}
return self;
}
- (void)dealloc {
DCHECK_EQ(ios::SigninPromoViewState::Invalid, _signinPromoViewState);
}
- (BOOL)isInvalidOrClosed {
return _signinPromoViewState == ios::SigninPromoViewState::Closed ||
_signinPromoViewState == ios::SigninPromoViewState::Invalid;
}
- (BOOL)isInvalidClosedOrNeverVisible {
return [self isInvalidOrClosed] ||
_signinPromoViewState == ios::SigninPromoViewState::NeverVisible;
}
- (SigninPromoViewConfigurator*)createConfigurator {
BOOL hasCloseButton =
AlreadySeenSigninViewPreferenceKey(_accessPoint) != nullptr;
if (_defaultIdentity) {
return [[SigninPromoViewConfigurator alloc]
initWithUserEmail:_defaultIdentity.userEmail
userFullName:_defaultIdentity.userFullName
userImage:_identityAvatar
hasCloseButton:hasCloseButton];
}
return [[SigninPromoViewConfigurator alloc] initWithUserEmail:nil
userFullName:nil
userImage:nil
hasCloseButton:hasCloseButton];
}
- (void)selectIdentity:(ChromeIdentity*)identity {
_defaultIdentity = identity;
if (!_defaultIdentity) {
_identityAvatar = nil;
} else {
__weak SigninPromoViewMediator* weakSelf = self;
ios::GetChromeBrowserProvider()
->GetChromeIdentityService()
->GetAvatarForIdentity(identity, ^(UIImage* identityAvatar) {
if (weakSelf.defaultIdentity != identity) {
return;
}
[weakSelf identityAvatarUpdated:identityAvatar];
});
}
}
- (void)identityAvatarUpdated:(UIImage*)identityAvatar {
_identityAvatar = identityAvatar;
[self sendConsumerNotificationWithIdentityChanged:NO];
}
- (void)sendConsumerNotificationWithIdentityChanged:(BOOL)identityChanged {
SigninPromoViewConfigurator* configurator = [self createConfigurator];
[_consumer configureSigninPromoWithConfigurator:configurator
identityChanged:identityChanged];
}
- (void)sendImpressionsTillSigninButtonsHistogram {
DCHECK(![self isInvalidClosedOrNeverVisible]);
const char* displayedCountPreferenceKey =
DisplayedCountPreferenceKey(_accessPoint);
if (!displayedCountPreferenceKey)
return;
PrefService* prefs = _browserState->GetPrefs();
int displayedCount = prefs->GetInteger(displayedCountPreferenceKey);
RecordImpressionsTilSigninButtonsHistogramForAccessPoint(_accessPoint,
displayedCount);
}
- (void)signinPromoViewVisible {
DCHECK(![self isInvalidOrClosed]);
if (_isSigninPromoViewVisible)
return;
if (_signinPromoViewState == ios::SigninPromoViewState::NeverVisible)
_signinPromoViewState = ios::SigninPromoViewState::Unused;
_isSigninPromoViewVisible = YES;
signin_metrics::RecordSigninImpressionUserActionForAccessPoint(_accessPoint);
signin_metrics::RecordSigninImpressionWithAccountUserActionForAccessPoint(
_accessPoint, !!_defaultIdentity);
const char* displayedCountPreferenceKey =
DisplayedCountPreferenceKey(_accessPoint);
if (!displayedCountPreferenceKey)
return;
PrefService* prefs = _browserState->GetPrefs();
int displayedCount = prefs->GetInteger(displayedCountPreferenceKey);
++displayedCount;
prefs->SetInteger(displayedCountPreferenceKey, displayedCount);
}
- (void)signinPromoViewHidden {
DCHECK(![self isInvalidOrClosed]);
_isSigninPromoViewVisible = NO;
}
- (void)signinPromoViewRemoved {
DCHECK_NE(ios::SigninPromoViewState::Invalid, _signinPromoViewState);
DCHECK(!self.signinInProgress);
BOOL wasNeverVisible =
_signinPromoViewState == ios::SigninPromoViewState::NeverVisible;
BOOL wasUnused = _signinPromoViewState == ios::SigninPromoViewState::Unused;
_signinPromoViewState = ios::SigninPromoViewState::Invalid;
_isSigninPromoViewVisible = NO;
if (wasNeverVisible)
return;
// If the sign-in promo view has been used at least once, it should not be
// counted as dismissed (even if the sign-in has been canceled).
const char* displayedCountPreferenceKey =
DisplayedCountPreferenceKey(_accessPoint);
if (!displayedCountPreferenceKey || !wasUnused)
return;
// If the sign-in view is removed when the user is authenticated, then the
// sign-in has been done by another view, and this mediator cannot be counted
// as being dismissed.
AuthenticationService* authService =
AuthenticationServiceFactory::GetForBrowserState(_browserState);
if (authService->IsAuthenticated())
return;
PrefService* prefs = _browserState->GetPrefs();
int displayedCount = prefs->GetInteger(displayedCountPreferenceKey);
RecordImpressionsTilDismissHistogramForAccessPoint(_accessPoint,
displayedCount);
}
- (void)signinCallback {
DCHECK_EQ(ios::SigninPromoViewState::UsedAtLeastOnce, _signinPromoViewState);
DCHECK(self.signinInProgress);
self.signinInProgress = NO;
if ([_consumer respondsToSelector:@selector(signinDidFinish)])
[_consumer signinDidFinish];
}
- (void)showSigninWithIdentity:(ChromeIdentity*)identity
promoAction:(signin_metrics::PromoAction)promoAction {
_signinPromoViewState = ios::SigninPromoViewState::UsedAtLeastOnce;
self.signinInProgress = YES;
__weak SigninPromoViewMediator* weakSelf = self;
ShowSigninCommandCompletionCallback completion = ^(BOOL succeeded) {
[weakSelf signinCallback];
};
if ([self.consumer respondsToSelector:@selector
(signinPromoViewMediator:shouldOpenSigninWithIdentity
:promoAction:completion:)]) {
[self.consumer signinPromoViewMediator:self
shouldOpenSigninWithIdentity:identity
promoAction:promoAction
completion:completion];
} else {
ShowSigninCommand* command = [[ShowSigninCommand alloc]
initWithOperation:AUTHENTICATION_OPERATION_SIGNIN
identity:identity
accessPoint:_accessPoint
promoAction:promoAction
callback:completion];
[self.presenter showSignin:command];
}
}
#pragma mark - ChromeIdentityServiceObserver
- (void)identityListChanged {
// Don't update the sign-in promo view while doing sign-in, to avoid UI
// glitches.
if (self.signinInProgress)
return;
ChromeIdentity* newIdentity = nil;
NSArray* identities = ios::GetChromeBrowserProvider()
->GetChromeIdentityService()
->GetAllIdentitiesSortedForDisplay();
if (identities.count != 0) {
newIdentity = identities[0];
}
if (newIdentity != _defaultIdentity) {
[self selectIdentity:newIdentity];
[self sendConsumerNotificationWithIdentityChanged:YES];
}
}
- (void)profileUpdate:(ChromeIdentity*)identity {
// Don't update the sign-in promo view while doing sign-in, to avoid UI
// glitches.
if (!self.signinInProgress)
return;
if (identity == _defaultIdentity) {
[self sendConsumerNotificationWithIdentityChanged:NO];
}
}
- (void)chromeIdentityServiceWillBeDestroyed {
_identityServiceObserver.reset();
}
#pragma mark - ChromeBrowserProviderObserver
- (void)chromeIdentityServiceDidChange:(ios::ChromeIdentityService*)identity {
DCHECK(!_identityServiceObserver.get());
_identityServiceObserver =
std::make_unique<ChromeIdentityServiceObserverBridge>(self);
}
- (void)chromeBrowserProviderWillBeDestroyed {
_browserProviderObserver.reset();
}
#pragma mark - SigninPromoViewDelegate
- (void)signinPromoViewDidTapSigninWithNewAccount:
(SigninPromoView*)signinPromoView {
DCHECK(!_defaultIdentity);
DCHECK(_isSigninPromoViewVisible);
DCHECK(![self isInvalidClosedOrNeverVisible]);
[self sendImpressionsTillSigninButtonsHistogram];
// On iOS, the promo does not have a button to add and account when there is
// already an account on the device. That flow goes through the NOT_DEFAULT
// promo instead. Always use the NO_EXISTING_ACCOUNT variant.
signin_metrics::PromoAction promo_action =
signin_metrics::PromoAction::PROMO_ACTION_NEW_ACCOUNT_NO_EXISTING_ACCOUNT;
signin_metrics::RecordSigninUserActionForAccessPoint(_accessPoint,
promo_action);
[self showSigninWithIdentity:nil promoAction:promo_action];
}
- (void)signinPromoViewDidTapSigninWithDefaultAccount:
(SigninPromoView*)signinPromoView {
DCHECK(_defaultIdentity);
DCHECK(_isSigninPromoViewVisible);
DCHECK(![self isInvalidClosedOrNeverVisible]);
[self sendImpressionsTillSigninButtonsHistogram];
signin_metrics::PromoAction promo_action =
signin_metrics::PromoAction::PROMO_ACTION_WITH_DEFAULT;
signin_metrics::RecordSigninUserActionForAccessPoint(_accessPoint,
promo_action);
[self showSigninWithIdentity:_defaultIdentity promoAction:promo_action];
}
- (void)signinPromoViewDidTapSigninWithOtherAccount:
(SigninPromoView*)signinPromoView {
DCHECK(_defaultIdentity);
DCHECK(_isSigninPromoViewVisible);
DCHECK(![self isInvalidClosedOrNeverVisible]);
[self sendImpressionsTillSigninButtonsHistogram];
signin_metrics::PromoAction promo_action =
signin_metrics::PromoAction::PROMO_ACTION_NOT_DEFAULT;
signin_metrics::RecordSigninUserActionForAccessPoint(_accessPoint,
promo_action);
[self showSigninWithIdentity:nil promoAction:promo_action];
}
- (void)signinPromoViewCloseButtonWasTapped:(SigninPromoView*)view {
DCHECK(_isSigninPromoViewVisible);
DCHECK(![self isInvalidClosedOrNeverVisible]);
_signinPromoViewState = ios::SigninPromoViewState::Closed;
const char* alreadySeenSigninViewPreferenceKey =
AlreadySeenSigninViewPreferenceKey(_accessPoint);
DCHECK(alreadySeenSigninViewPreferenceKey);
PrefService* prefs = _browserState->GetPrefs();
prefs->SetBoolean(alreadySeenSigninViewPreferenceKey, true);
const char* displayedCountPreferenceKey =
DisplayedCountPreferenceKey(_accessPoint);
if (displayedCountPreferenceKey) {
int displayedCount = prefs->GetInteger(displayedCountPreferenceKey);
RecordImpressionsTilXButtonHistogramForAccessPoint(_accessPoint,
displayedCount);
}
if ([_consumer respondsToSelector:@selector
(signinPromoViewMediatorCloseButtonWasTapped:)]) {
[_consumer signinPromoViewMediatorCloseButtonWasTapped:self];
}
}
@end