chromium/ios/chrome/browser/ntp/ui_bundled/feed_top_section/feed_top_section_mediator.mm

// Copyright 2022 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/ntp/ui_bundled/feed_top_section/feed_top_section_mediator.h"

#import <UserNotifications/UserNotifications.h>

#import "base/metrics/histogram_functions.h"
#import "base/metrics/user_metrics.h"
#import "base/metrics/user_metrics_action.h"
#import "base/strings/sys_string_conversions.h"
#import "base/time/time.h"
#import "components/prefs/pref_service.h"
#import "components/signin/public/identity_manager/identity_manager.h"
#import "components/signin/public/identity_manager/objc/identity_manager_observer_bridge.h"
#import "ios/chrome/browser/content_notification/model/content_notification_util.h"
#import "ios/chrome/browser/push_notification/model/provisional_push_notification_util.h"
#import "ios/chrome/browser/push_notification/model/push_notification_client_id.h"
#import "ios/chrome/browser/push_notification/model/push_notification_service.h"
#import "ios/chrome/browser/push_notification/model/push_notification_settings_util.h"
#import "ios/chrome/browser/push_notification/model/push_notification_util.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/public/features/system_flags.h"
#import "ios/chrome/browser/signin/model/authentication_service.h"
#import "ios/chrome/browser/signin/model/authentication_service_factory.h"
#import "ios/chrome/browser/signin/model/identity_manager_factory.h"
#import "ios/chrome/browser/ui/authentication/signin_promo_view_mediator.h"
#import "ios/chrome/browser/ui/content_suggestions/set_up_list/utils.h"
#import "ios/chrome/browser/ntp/ui_bundled/feed_top_section/feed_top_section_consumer.h"
#import "ios/chrome/browser/ntp/ui_bundled/new_tab_page_delegate.h"
#import "ios/chrome/browser/ui/push_notification/notifications_alert_presenter.h"
#import "ios/chrome/browser/ui/push_notification/notifications_confirmation_presenter.h"

using base::RecordAction;
using base::UmaHistogramEnumeration;
using base::UserMetricsAction;

@interface FeedTopSectionMediator () <IdentityManagerObserverBridgeDelegate> {
  // Observes changes in identity.
  std::unique_ptr<signin::IdentityManagerObserverBridge>
      _identityObserverBridge;
}

@property(nonatomic, assign) AuthenticationService* authenticationService;
@property(nonatomic, assign) signin::IdentityManager* identityManager;
@property(nonatomic, assign) BOOL isIncognito;
@property(nonatomic, assign) PrefService* prefService;

// Consumer for this mediator.
@property(nonatomic, weak) id<FeedTopSectionConsumer> consumer;

@end

@implementation FeedTopSectionMediator

// FeedTopSectionViewControllerDelegate
@synthesize signinPromoConfigurator = _signinPromoConfigurator;

- (instancetype)initWithConsumer:(id<FeedTopSectionConsumer>)consumer
                 identityManager:(signin::IdentityManager*)identityManager
                     authService:(AuthenticationService*)authenticationService
                     isIncognito:(BOOL)isIncognito
                     prefService:(PrefService*)prefService {
  self = [super init];
  if (self) {
    _authenticationService = authenticationService;
    _identityManager = identityManager;
    _identityObserverBridge.reset(
        new signin::IdentityManagerObserverBridge(_identityManager, self));
    _isIncognito = isIncognito;
    _prefService = prefService;
    _consumer = consumer;
  }
  return self;
}

- (void)setUp {
  [self updateShouldShowPromo];
}

- (void)dealloc {
  [self shutdown];
}

- (void)shutdown {
  _identityObserverBridge.reset();
  self.authenticationService = nullptr;
  self.identityManager = nullptr;
  self.prefService = nullptr;
}

// Handles closing the promo, and the NTP and Feed Top Section layout when the
// promo is closed.
- (void)updateFeedTopSectionWhenClosed {
  [self.NTPDelegate handleFeedTopSectionClosed];
  [self.consumer hidePromo];
  [self.NTPDelegate updateFeedLayout];
}

#pragma mark - FeedTopSectionViewControllerDelegate

- (SigninPromoViewConfigurator*)signinPromoConfigurator {
  if (!_signinPromoConfigurator) {
    _signinPromoConfigurator = [_signinPromoMediator createConfigurator];
  }
  return _signinPromoConfigurator;
}

#pragma mark - IdentityManagerObserverBridgeDelegate

// Called when a user changes the syncing state.
- (void)onPrimaryAccountChanged:
    (const signin::PrimaryAccountChangeEvent&)event {
  switch (event.GetEventTypeFor(signin::ConsentLevel::kSignin)) {
    case signin::PrimaryAccountChangeEvent::Type::kSet:
      if (!self.signinPromoMediator.showSpinner) {
        // User has signed in, stop showing the promo.
        [self updateShouldShowPromo];
      }
      break;
    case signin::PrimaryAccountChangeEvent::Type::kCleared:
      [self updateShouldShowPromo];
      break;
    case signin::PrimaryAccountChangeEvent::Type::kNone:
      break;
  }
}

#pragma mark - SigninPromoViewConsumer

- (void)configureSigninPromoWithConfigurator:
            (SigninPromoViewConfigurator*)configurator
                             identityChanged:(BOOL)identityChanged {
  // No-op: The NTP is always recreated when the identity changes, so this is
  // not needed.
}

- (void)signinPromoViewMediatorCloseButtonWasTapped:
    (SigninPromoViewMediator*)mediator {
  [self updateFeedTopSectionWhenClosed];
}

#pragma mark - FeedTopSectionMutator

- (void)notificationsPromoViewDismissedFromButton:
    (NotificationsPromoButtonType)buttonType {
  [self updateFeedTopSectionWhenClosed];
  // Update prefs that save the dismissed times if the promo conditions are not
  // being overriden.
  int notificationsPromoTimesDismissed =
      self.prefService->GetInteger(prefs::kNotificationsPromoTimesDismissed);
  if (!experimental_flags::ShouldForceContentNotificationsPromo()) {
    self.prefService->SetTime(prefs::kNotificationsPromoLastDismissed,
                              base::Time::Now());
    self.prefService->SetInteger(prefs::kNotificationsPromoTimesDismissed,
                                 notificationsPromoTimesDismissed + 1);
  }
  switch (buttonType) {
    case NotificationsPromoButtonTypeClose:
      [self logHistogramForAction:ContentNotificationTopOfFeedPromoAction::
                                      kDismissedFromCloseButton];
      if (notificationsPromoTimesDismissed >=
          kNotificationsPromoMaxDismissedCount) {
        [self enrollUserToProvisionalNotificationsFromEntrypoint:
                  ContentNotificationPromoProvisionalEntrypoint::kCloseButton];
      }
      break;
    case NotificationsPromoButtonTypeSecondary:
      // If notification is dismissed from secondary button, set TimesDismissed
      // > kNotificationsPromoMaxDismissedCount, to ensure the user doesn't see
      // the notifications promo anymore.
      self.prefService->SetInteger(prefs::kNotificationsPromoTimesDismissed,
                                   kMaxImpressionsForDismissedThreshold);
      [self logHistogramForAction:ContentNotificationTopOfFeedPromoAction::
                                      kDismissedFromSecondaryButton];
      break;
    case NotificationsPromoButtonTypePrimary:
      // This should never be executed as the primary button does not close the
      // promo.
      DCHECK(false);
      break;
  }
}

- (void)notificationsPromoViewMainButtonWasTapped {
  // Show the Notifications promo alert.
  RecordAction(UserMetricsAction(
      "ContentNotifications.Promo.TopOfFeed.MainButtonTapped"));
  [self logHistogramForAction:ContentNotificationTopOfFeedPromoAction::
                                  kMainButtonTapped];
  [self.presenter presentPushNotificationPermissionAlert];
}

#pragma mark - Private

- (BOOL)isUserSignedIn {
  return self.identityManager->HasPrimaryAccount(signin::ConsentLevel::kSignin);
}

// Returns true if notifications are enabled in Chime or at the OS level.
- (BOOL)isNotificationsEnabled {
  DCHECK([self isUserSignedIn]);
  id<SystemIdentity> identity = self.authenticationService->GetPrimaryIdentity(
      signin::ConsentLevel::kSignin);
  // Check if user has notifications enabled at the Chime level.
  BOOL isChimeEnabled =
      push_notification_settings::IsMobileNotificationsEnabledForAnyClient(
          base::SysNSStringToUTF8(identity.gaiaID), self.prefService);
  if (isChimeEnabled) {
    return true;
  }
  // Check the user's OS notification permission status for Chrome.
  __block UNAuthorizationStatus status;
  [PushNotificationUtil
      getPermissionSettings:^(UNNotificationSettings* settings) {
        status = settings.authorizationStatus;
      }];

  if (status != UNAuthorizationStatusNotDetermined &&
      status != UNAuthorizationStatusDenied) {
    return true;
  }
  return false;
}

- (BOOL)shouldShowNotificationsPromo {
  // Check if override is active. Override only works if the user is signed in.
  if (experimental_flags::ShouldForceContentNotificationsPromo()) {
    return true;
  }

  if (!IsContentNotificationExperimentEnabled() ||
      !IsContentNotificationPromoEnabled([self isUserSignedIn],
                                         self.isDefaultSearchEngine,
                                         self.prefService)) {
    return false;
  }

  // Check if notifications are enabled of any type at the Chime level.
  if ([self isNotificationsEnabled]) {
    return false;
  }

  int notificationsPromoTimesShown =
      self.prefService->GetInteger(prefs::kNotificationsPromoTimesShown);
  int notificationsPromoTimesDismissed =
      self.prefService->GetInteger(prefs::kNotificationsPromoTimesDismissed);

  base::Time now = base::Time::Now();
  // Check if promo has been displayed `kNotificationsPromoMaxShownCount`.
  if (notificationsPromoTimesShown >= kNotificationsPromoMaxShownCount) {
    [self enrollUserToProvisionalNotificationsFromEntrypoint:
              ContentNotificationPromoProvisionalEntrypoint::kShownThreshold];
    return false;
  }

  // Check if promo has been dismissed more than the threshold.
  if (notificationsPromoTimesDismissed >=
      kNotificationsPromoMaxDismissedCount) {
    return false;
  }
  // Check if the pref has been initialized before (base::Time() returns the
  // null value for a base::Time type.
  if (self.prefService->GetTime(prefs::kNotificationsPromoLastDismissed) !=
      base::Time()) {
    if (now -
            self.prefService->GetTime(prefs::kNotificationsPromoLastDismissed) <
        kNotificationsPromoDismissedCooldownTime) {
      return false;
    }
  }
  // Check if it has been less than `kNotificationsPromoShownCooldownTime`.
  if (now - self.prefService->GetTime(prefs::kNotificationsPromoLastShown) <
      kNotificationsPromoShownCooldownTime) {
    return false;
  }
  // If all the conditions pass above, update prefs and return true.
  self.prefService->SetTime(prefs::kNotificationsPromoLastShown, now);
  notificationsPromoTimesShown += 1;
  self.prefService->SetTime(prefs::kNotificationsPromoLastShown, now);
  self.prefService->SetInteger(prefs::kNotificationsPromoTimesShown,
                               notificationsPromoTimesShown);
  return true;
}

- (BOOL)shouldShowSigninPromo {
  // Don't show the promo if the account is not eligible for a SigninPromo.
  BOOL isAccountEligibleForSignInPromo = NO;
  if ([SigninPromoViewMediator
          shouldDisplaySigninPromoViewWithAccessPoint:
              signin_metrics::AccessPoint::ACCESS_POINT_NTP_FEED_TOP_PROMO
                                    signinPromoAction:SigninPromoAction::
                                                          kInstantSignin
                                authenticationService:self.authenticationService
                                          prefService:self.prefService]) {
    isAccountEligibleForSignInPromo = ![self isUserSignedIn];
  }
  // Don't show the promo for incognito or start surface or if account is not
  // eligible.
  BOOL isStartSurfaceOrIncognito = self.isIncognito ||
                                   [self.NTPDelegate isStartSurface] ||
                                   !self.isSignInPromoEnabled;
  if (!isStartSurfaceOrIncognito && isAccountEligibleForSignInPromo) {
    return true;
  }
  return false;
}

- (void)updateShouldShowPromo {
  // Don't show any promo if Set Up List is Enabled.
  if (set_up_list_utils::IsSetUpListActive(
          GetApplicationContext()->GetLocalState(), self.prefService)) {
    // Hide promo as a safeguard in case it is being shown.
    [self.consumer hidePromo];
    return;
  }

  if ([self shouldShowSigninPromo]) {
    self.consumer.visiblePromoViewType = PromoViewTypeSignin;
    [self.consumer showPromo];
    return;
  }

  if ([self shouldShowNotificationsPromo]) {
    self.consumer.visiblePromoViewType = PromoViewTypeNotifications;
    [self.consumer showPromo];
    [self logHistogramForAction:ContentNotificationTopOfFeedPromoAction::
                                    kDisplayed];
    return;
  }
}

#pragma mark - Private

- (void)enrollUserToProvisionalNotificationsFromEntrypoint:
    (ContentNotificationPromoProvisionalEntrypoint)entrypoint {
  [self logHistogramForEntrypoint:entrypoint];
  [ProvisionalPushNotificationUtil
      enrollUserToProvisionalNotificationsForClientIds:
          {PushNotificationClientId::kContent,
           PushNotificationClientId::kSports}
                                       withAuthService:
                                           self.authenticationService];
}

#pragma mark - Metrics

- (void)logHistogramForAction:(ContentNotificationTopOfFeedPromoAction)action {
  UmaHistogramEnumeration("ContentNotifications.Promo.TopOfFeed.Action",
                          action);
}

- (void)logHistogramForEntrypoint:
    (ContentNotificationPromoProvisionalEntrypoint)entrypoint {
  UmaHistogramEnumeration(
      "ContentNotifications.Promo.ProvisionalNotifications.Entrypoint",
      entrypoint);
}

@end