chromium/ios/chrome/browser/ui/authentication/account_menu/account_menu_mediator.mm

// Copyright 2024 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/ui/authentication/account_menu/account_menu_mediator.h"

#import <optional>
#import <string>

#import "base/strings/sys_string_conversions.h"
#import "components/prefs/pref_service.h"
#import "components/signin/public/base/consent_level.h"
#import "components/signin/public/identity_manager/objc/identity_manager_observer_bridge.h"
#import "ios/chrome/browser/policy/ui_bundled/management_util.h"
#import "ios/chrome/browser/settings/model/sync/utils/account_error_ui_info.h"
#import "ios/chrome/browser/settings/model/sync/utils/identity_error_util.h"
#import "ios/chrome/browser/signin/model/authentication_service.h"
#import "ios/chrome/browser/signin/model/chrome_account_manager_service_observer_bridge.h"
#import "ios/chrome/browser/sync/model/sync_observer_bridge.h"
#import "ios/chrome/browser/ui/authentication/account_menu/account_menu_consumer.h"
#import "ios/chrome/browser/ui/authentication/account_menu/account_menu_data_source.h"
#import "ios/chrome/browser/ui/authentication/account_menu/account_menu_mediator_delegate.h"
#import "ios/chrome/browser/ui/authentication/cells/table_view_account_item.h"
#import "ios/chrome/browser/ui/settings/settings_table_view_controller_constants.h"

@interface AccountMenuMediator () <ChromeAccountManagerServiceObserver,
                                   IdentityManagerObserverBridgeDelegate,
                                   SyncObserverModelBridge>

@end

@implementation AccountMenuMediator {
  // Account manager service to retrieve Chrome identities.
  raw_ptr<ChromeAccountManagerService> _accountManagerService;
  // Chrome account manager service observer bridge.
  std::unique_ptr<ChromeAccountManagerServiceObserverBridge>
      _accountManagerServiceObserver;
  raw_ptr<AuthenticationService> _authenticationService;
  raw_ptr<signin::IdentityManager> _identityManager;
  std::unique_ptr<signin::IdentityManagerObserverBridge>
      _identityManagerObserver;
  raw_ptr<PrefService> _prefs;
  raw_ptr<syncer::SyncService> _syncService;
  std::unique_ptr<SyncObserverBridge> _syncObserver;
  // The primary identity.
  id<SystemIdentity> _primaryIdentity;
  // The displayed error, if any.
  AccountErrorUIInfo* _error;

  // The list of identities to display and their index in the table view’s
  // identities section
  NSMutableArray<id<SystemIdentity>>* _identities;

  // The type of account error that is being displayed in the error section for
  // signed in accounts. Is set to kNone when there is no error section.
  syncer::SyncService::UserActionableError _diplayedAccountErrorType;

  // Whether an account switching is in progress.
  BOOL _accountSwitchingInProgress;
}

- (instancetype)initWithSyncService:(syncer::SyncService*)syncService
              accountManagerService:
                  (ChromeAccountManagerService*)accountManagerService
                        authService:(AuthenticationService*)authService
                    identityManager:(signin::IdentityManager*)identityManager
                              prefs:(PrefService*)prefs {
  self = [super init];
  if (self) {
    CHECK(syncService);
    CHECK(accountManagerService);
    CHECK(authService);
    CHECK(identityManager);
    _identities = [NSMutableArray array];
    _accountManagerService = accountManagerService;
    _accountManagerServiceObserver =
        std::make_unique<ChromeAccountManagerServiceObserverBridge>(
            self, _accountManagerService);
    _authenticationService = authService;
    _identityManager = identityManager;
    _identityManagerObserver =
        std::make_unique<signin::IdentityManagerObserverBridge>(
            _identityManager, self);
    _prefs = prefs;
    _primaryIdentity = _authenticationService->GetPrimaryIdentity(
        signin::ConsentLevel::kSignin);
    _syncService = syncService;
    _syncObserver = std::make_unique<SyncObserverBridge>(self, _syncService);
    _diplayedAccountErrorType = syncer::SyncService::UserActionableError::kNone;
    _primaryIdentity = _authenticationService->GetPrimaryIdentity(
        signin::ConsentLevel::kSignin);
    [self updateIdentities];
    _error = GetAccountErrorUIInfo(_syncService);
  }
  return self;
}

- (void)disconnect {
  _accountManagerService = nullptr;
  _accountManagerServiceObserver.reset();
  _authenticationService = nullptr;
  _identityManagerObserver.reset();
  _identityManager = nullptr;
  _prefs = nullptr;
  _syncObserver.reset();
  _syncService = nullptr;
  _identities = nil;
  _primaryIdentity = nullptr;
}

#pragma mark - AccountMenuDataSource

- (NSArray<NSString*>*)secondaryAccountsGaiaIDs {
  NSMutableArray<NSString*>* gaiaIDs = [NSMutableArray array];
  for (id<SystemIdentity> identity : _identities) {
    [gaiaIDs addObject:identity.gaiaID];
  }
  return gaiaIDs;
}

- (TableViewAccountItem*)identityItemForGaiaID:(NSString*)gaiaID {
  for (id<SystemIdentity> identity : _identities) {
    if (gaiaID == identity.gaiaID) {
      TableViewAccountItem* item =
          [[TableViewAccountItem alloc] initWithType:SettingsItemTypeAccount];
      item.text = identity.userFullName;
      item.detailText = identity.userEmail;
      item.image = _accountManagerService->GetIdentityAvatarWithIdentity(
          identity, IdentityAvatarSize::TableViewIcon);
      return item;
    }
  }
  NOTREACHED();
}

- (NSString*)primaryAccountEmail {
  return _primaryIdentity.userEmail;
}

- (NSString*)primaryAccountUserFullName {
  return _primaryIdentity.userFullName;
}

- (UIImage*)primaryAccountAvatar {
  return _accountManagerService->GetIdentityAvatarWithIdentity(
      _primaryIdentity, IdentityAvatarSize::Large);
}

- (ManagementState)managementState {
  return GetManagementState(_identityManager, _authenticationService, _prefs);
}

- (AccountErrorUIInfo*)accountErrorUIInfo {
  return _error;
}

#pragma mark - ChromeAccountManagerServiceObserver

- (void)identityListChanged {
  [self updateIdentities];
}

- (void)identityUpdated:(id<SystemIdentity>)identity {
  [self updateIdentities];
}

- (void)onChromeAccountManagerServiceShutdown:
    (ChromeAccountManagerService*)accountManagerService {
  // TODO(crbug.com/40067367): This method can be removed once
  // crbug.com/40067367 is fixed.
  [self disconnect];
}

#pragma mark - IdentityManagerObserverBridgeDelegate

- (void)onPrimaryAccountChanged:
    (const signin::PrimaryAccountChangeEvent&)event {
  switch (event.GetEventTypeFor(signin::ConsentLevel::kSignin)) {
    case signin::PrimaryAccountChangeEvent::Type::kNone:
      return;
    case signin::PrimaryAccountChangeEvent::Type::kSet:
      _primaryIdentity = _authenticationService->GetPrimaryIdentity(
          signin::ConsentLevel::kSignin);
      [self updateIdentities];
      break;
    case signin::PrimaryAccountChangeEvent::Type::kCleared:
      if (_accountSwitchingInProgress) {
        return;
      }
      [self.delegate mediatorWantsToBeDismissed:self];
      break;
  }
}

#pragma mark - SyncObserverModelBridge

- (void)onSyncStateChanged {
  AccountErrorUIInfo* newError = GetAccountErrorUIInfo(_syncService);
  if (newError == _error) {
    return;
  }
  _error = newError;
  [self.consumer updateErrorSection:_error];
}

#pragma mark - AccountMenuMutator

- (void)accountTappedWithGaiaID:(NSString*)gaiaID
                     targetRect:(CGRect)targetRect {
  if (_accountSwitchingInProgress || self.signOutFlowInProgress) {
    return;
  }
  id<SystemIdentity> newIdentity = nil;
  for (id<SystemIdentity> identity : _identities) {
    if (identity.gaiaID == gaiaID) {
      newIdentity = identity;
      break;
    }
  }
  CHECK(newIdentity);
  _accountSwitchingInProgress = YES;
  __weak __typeof(self) weakSelf = self;
  [self.delegate triggerSignoutWithTargetRect:targetRect
                                   completion:^(BOOL success) {
                                     [weakSelf
                                         signoutDoneWithSuccess:success
                                                 systemIdentity:newIdentity];
                                   }];
}

- (void)didTapErrorButton {
  switch (_error.errorType) {
    case syncer::SyncService::UserActionableError::kSignInNeedsUpdate: {
      if (_authenticationService->HasCachedMDMErrorForIdentity(
              _primaryIdentity)) {
        [self.delegate openMDMErrodDialogWithSystemIdentity:_primaryIdentity];
      } else {
        [self.delegate openPrimaryAccountReauthDialog];
      }
      break;
    }
    case syncer::SyncService::UserActionableError::kNeedsPassphrase:
      [self.delegate openPassphraseDialogWithModalPresentation:YES];
      break;
    case syncer::SyncService::UserActionableError::
        kNeedsTrustedVaultKeyForPasswords:
    case syncer::SyncService::UserActionableError::
        kNeedsTrustedVaultKeyForEverything:
      [self.delegate openTrustedVaultReauthForFetchKeys];
      break;
    case syncer::SyncService::UserActionableError::
        kTrustedVaultRecoverabilityDegradedForPasswords:
    case syncer::SyncService::UserActionableError::
        kTrustedVaultRecoverabilityDegradedForEverything:
      [self.delegate openTrustedVaultReauthForDegradedRecoverability];
      break;
    case syncer::SyncService::UserActionableError::kNone:
      NOTREACHED_IN_MIGRATION();
  }
}

#pragma mark - Private

// Updates the identity list in `_identities`, and sends an notification to
// the consumer.
- (void)updateIdentities {
  NSArray<id<SystemIdentity>>* allIdentities =
      _accountManagerService->GetAllIdentities();

  NSMutableArray<NSString*>* gaiaIDsToRemove = [NSMutableArray array];
  NSMutableArray<NSString*>* gaiaIDsToAdd = [NSMutableArray array];

  for (id<SystemIdentity> secondaryIdentity : allIdentities) {
    if (secondaryIdentity == _primaryIdentity) {
      continue;
    }
    BOOL mustAdd = YES;
    for (id<SystemIdentity> displayedIdentity : _identities) {
      if (secondaryIdentity.gaiaID == displayedIdentity.gaiaID) {
        mustAdd = NO;
        break;
      }
    }
    if (mustAdd) {
      [_identities addObject:secondaryIdentity];
      [gaiaIDsToAdd addObject:secondaryIdentity.gaiaID];
    }
  }

  for (NSUInteger i = 0; i < _identities.count; ++i) {
    id<SystemIdentity> identity = _identities[i];
    if (![allIdentities containsObject:identity] ||
        identity == _primaryIdentity) {
      [gaiaIDsToRemove addObject:identity.gaiaID];
      [_identities removeObjectAtIndex:i--];
      // There will be a new object at place `i`. So we must decrease `i`.
    }
  }

  [self.consumer updateAccountListWithGaiaIDsToAdd:gaiaIDsToAdd
                                   gaiaIDsToRemove:gaiaIDsToRemove];
  // In case the primary account information changed.
  [self.consumer updatePrimaryAccount];
}

- (void)signoutDoneWithSuccess:(BOOL)success
                systemIdentity:(id<SystemIdentity>)systemIdentity {
  if (!success) {
    _accountSwitchingInProgress = NO;
    return;
  }
  __weak __typeof(self) weakSelf = self;
  [self.delegate
      triggerSigninWithSystemIdentity:systemIdentity
                           completion:^(id<SystemIdentity> signedInIdentity) {
                             [weakSelf signinDone:signedInIdentity];
                           }];
}

- (void)signinDone:(id<SystemIdentity>)systemIdentity {
  _accountSwitchingInProgress = NO;
  [_delegate triggerAccountSwitchSnackbarWithIdentity:systemIdentity];
  [_delegate mediatorWantsToBeDismissed:self];
}

@end