chromium/ios/chrome/browser/ui/settings/password/password_sharing/sharing_status_view_controller.mm

// Copyright 2023 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/settings/password/password_sharing/sharing_status_view_controller.h"

#import "base/strings/sys_string_conversions.h"
#import "components/strings/grit/components_strings.h"
#import "ios/chrome/browser/net/model/crurl.h"
#import "ios/chrome/browser/shared/ui/symbols/symbols.h"
#import "ios/chrome/browser/shared/ui/table_view/table_view_favicon_data_source.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/ui/authentication/authentication_constants.h"
#import "ios/chrome/browser/ui/settings/password/password_sharing/password_sharing_constants.h"
#import "ios/chrome/browser/ui/settings/password/password_sharing/password_sharing_metrics.h"
#import "ios/chrome/browser/ui/settings/password/password_sharing/sharing_status_view_controller_presentation_delegate.h"
#import "ios/chrome/common/string_util.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/favicon/favicon_container_view.h"
#import "ios/chrome/common/ui/favicon/favicon_view.h"
#import "ios/chrome/common/ui/util/button_util.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ui/base/l10n/l10n_util.h"
#import "ui/base/l10n/l10n_util_mac.h"
#import "url/gurl.h"

namespace {

// Progress bar dimensions.
const CGFloat kProgressBarWidth = 100.0;
const CGFloat kProgressBarHeight = 30.0;
const CGFloat kProgressBarCircleDiameter = 3.0;
const CGFloat kProgressBarCircleSpacing = 2.0;
const NSInteger kProgressBarCirclesAmount = 20;

// Loaded images size dimensions.
const CGFloat kLockSymbolPointSize = 22.0;
const CGFloat kFaviconContainerSize = 30.0;
const CGFloat kFaviconSize = 22.0;
const CGFloat kProfileImageSize = 60.0;

// Spacing and padding constraints.
const CGFloat kVerticalSpacing = 16.0;
const CGFloat kTopPadding = 20.0;
const CGFloat kBottomPadding = 42.0;
const CGFloat kHorizontalPadding = 16.0;
const CGFloat kFaviconProfileImageVerticalOverlap = 10.0;

// Durations of specific parts of the animation in seconds.
const CGFloat kImagesSlidingOutDelay = 0.35;
const CGFloat kImagesSlidingOutDuration = 0.5;
const CGFloat kLockAppearingDuration = 0.15;
const CGFloat kProgressBarLoadingDuration = 3.25;
const CGFloat kImagesSlidingInDuration = 0.5;
const CGFloat kFaviconAppearingDelay = 0.1;
const CGFloat kFaviconAppearingDuration = 0.15;
const CGFloat kSharingCancelledDuration = 0.5;

// Distance by which the profile images x-center should be away from the middle
// of the view in different parts of the animation.
const CGFloat kImagesSlidedOutCenterXConstant = 78;
const CGFloat kImagesSlidedInCenterXConstant = 27;

// Tags marking parts of string that should have a bold font.
NSString* const kBeginBoldTag = @"BEGIN_BOLD[ \t]*";
NSString* const kEndBoldTag = @"[ \t]*END_BOLD";

// Accessibility identifiers of text views with links.
NSString* const kSharingStatusFooterId = @"SharingStatusViewFooter";

}  // namespace

@interface SharingStatusViewController () <UITextViewDelegate>

// Container view for the animation.
@property(nonatomic, strong) UIView* animationView;

// Profile image of the sender.
@property(nonatomic, strong) UIImageView* senderImageView;
@property(nonatomic, strong) UIImage* senderImage;

// Profile image of the recipient (or merged avatar of multiple recipients).
@property(nonatomic, strong) UIImageView* recipientImageView;
@property(nonatomic, strong) UIImage* recipientImage;

// Lock image displayed in the animation.
@property(nonatomic, strong) UIImageView* lockImage;

// Rectangle view with fixed length and height containing fixed amount of
// circles.
@property(nonatomic, strong) UIView* progressBarView;

// The container for the favicon view that is displayed below the recipient and
// sender images in successful status view.
@property(nonatomic, strong) FaviconContainerView* faviconContainerView;

// Stack view containing animation container view, title, subtitle and footer.
@property(nonatomic, strong) UIStackView* stackView;

// Animates profile image of the sender sliding to the left and profile images
// of recipients sliding to the right.
@property(nonatomic, strong) UIViewPropertyAnimator* imagesSlidingOutAnimation;

// Animates lock appearing in the middle between profile images.
@property(nonatomic, strong) UIViewPropertyAnimator* lockAppearingAnimation;

// Animates the progress bar going from the left to right image.
@property(nonatomic, strong)
    UIViewPropertyAnimator* progressBarLoadingAnimation;

// Animates progress bar and lock disappearing and profile images sliding to the
// middle.
@property(nonatomic, strong) UIViewPropertyAnimator* imagesSlidingInAnimation;

// Animates favicon appearing below recipient and sender image.
@property(nonatomic, strong) UIViewPropertyAnimator* faviconAppearingAnimation;

// Animates profile images sliding to the middle on cancel button tap.
@property(nonatomic, strong) UIViewPropertyAnimator* sharingCancelledAnimation;

// Contains the information that sharing is in progress at first and then is
// modified to convey the result status.
@property(nonatomic, strong) UILabel* titleLabel;

// Subtitle string that will be displayed when the sharing is succesful.
@property(nonatomic, strong) NSString* subtitleString;

// Footer string that will be displayed when the sharing is succesful.
@property(nonatomic, strong) NSString* footerString;

// The button that cancels the sharing process.
@property(nonatomic, strong) UIButton* cancelButton;

// Url of the site for which the password is being shared.
@property(nonatomic, readonly) GURL URL;

@end

@implementation SharingStatusViewController {
  // CenterX constraints for the images of sender and recipients.
  NSLayoutConstraint* _senderImageCenterXConstraint;
  NSLayoutConstraint* _recipientImageCenterXConstraint;
}

#pragma mark - UIViewController

- (void)viewDidLoad {
  [super viewDidLoad];

  UIView* view = self.view;
  view.accessibilityIdentifier = kSharingStatusViewID;
  view.backgroundColor = [UIColor colorNamed:kPrimaryBackgroundColor];

  // Add vertical stack view for the animation and all labels.
  UIStackView* verticalStack = [[UIStackView alloc] initWithArrangedSubviews:@[
    [self createAnimationContainerView], [self createTitleLabel]
  ]];
  verticalStack.axis = UILayoutConstraintAxisVertical;
  verticalStack.spacing = kVerticalSpacing;
  verticalStack.translatesAutoresizingMaskIntoConstraints = NO;
  self.stackView = verticalStack;
  [view addSubview:verticalStack];

  // Add cancel button below the stack.
  UIButton* cancelButton = [self createCancelButton];
  [view addSubview:cancelButton];

  [NSLayoutConstraint activateConstraints:@[
    // Vertical stack constraints.
    [verticalStack.topAnchor constraintEqualToAnchor:view.topAnchor
                                            constant:kTopPadding],
    [verticalStack.leadingAnchor constraintEqualToAnchor:view.leadingAnchor
                                                constant:kHorizontalPadding],
    [verticalStack.trailingAnchor constraintEqualToAnchor:view.trailingAnchor
                                                 constant:-kHorizontalPadding],
    [verticalStack.centerXAnchor constraintEqualToAnchor:view.centerXAnchor],

    // Cancel button constraints.
    [cancelButton.topAnchor
        constraintGreaterThanOrEqualToAnchor:verticalStack.bottomAnchor
                                    constant:kVerticalSpacing],
    [cancelButton.bottomAnchor constraintEqualToAnchor:view.bottomAnchor
                                              constant:-kBottomPadding],
    [cancelButton.centerXAnchor
        constraintEqualToAnchor:verticalStack.centerXAnchor],
  ]];

  [self createAnimations];
}

- (void)viewDidAppear:(BOOL)animated {
  [super viewDidAppear:animated];

  // Make sure that the title is focused when the view appears.
  UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification,
                                  self.titleLabel);
  [self.imagesSlidingOutAnimation
      startAnimationAfterDelay:kImagesSlidingOutDelay];
}

- (void)viewDidDisappear:(BOOL)animated {
  // Stop the ongoing animations so that their completion is not called.
  [self.imagesSlidingOutAnimation stopAnimation:YES];
  [self.lockAppearingAnimation stopAnimation:YES];
  [self.progressBarLoadingAnimation stopAnimation:YES];
  [self.imagesSlidingInAnimation stopAnimation:YES];
  [super viewDidDisappear:animated];
}

#pragma mark - Public

- (UISheetPresentationControllerDetent*)preferredHeightDetent {
  __typeof(self) __weak weakSelf = self;
  auto resolver = ^CGFloat(
      id<UISheetPresentationControllerDetentResolutionContext> context) {
    return [weakSelf detentForPreferredHeightInContext:context];
  };
  return [UISheetPresentationControllerDetent
      customDetentWithIdentifier:@"preferred_height"
                        resolver:resolver];
}

#pragma mark - SharingStatusConsumer

- (void)setSenderImage:(UIImage*)senderImage {
  _senderImage = senderImage;
}

- (void)setRecipientImage:(UIImage*)recipientImage {
  _recipientImage = recipientImage;
}

- (void)setSubtitleString:(NSString*)subtitleString {
  _subtitleString = subtitleString;
}

- (void)setFooterString:(NSString*)footerString {
  _footerString = footerString;
}

- (void)setURL:(const GURL&)URL {
  _URL = URL;
}

#pragma mark - UITextViewDelegate

- (BOOL)textView:(UITextView*)textView
    shouldInteractWithURL:(NSURL*)URL
                  inRange:(NSRange)characterRange
              interaction:(UITextItemInteraction)interaction {
  [self.delegate changePasswordLinkWasTapped];
  return NO;
}

#pragma mark - Private

- (CGFloat)detentForPreferredHeightInContext:
    (id<UISheetPresentationControllerDetentResolutionContext>)context {
  UIView* containerView = self.sheetPresentationController.containerView;
  CGFloat width = containerView.bounds.size.width;
  CGSize fittingSize = CGSizeMake(width, UILayoutFittingCompressedSize.height);
  CGFloat height = [self.view systemLayoutSizeFittingSize:fittingSize].height;

  // Measure height without the safeAreaInsets.bottom in portrait orientation on
  // iPhone (as it is added anyway to the result in edge-attached sheets).
  UITraitCollection* traitCollection = context.containerTraitCollection;
  if (traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact &&
      traitCollection.verticalSizeClass == UIUserInterfaceSizeClassRegular) {
    height -= containerView.safeAreaInsets.bottom;
  }
  return height;
}

// Helper for creating sender image view.
- (UIImageView*)createSenderImageView {
  UIImageView* senderImageView =
      [[UIImageView alloc] initWithImage:self.senderImage];
  senderImageView.translatesAutoresizingMaskIntoConstraints = NO;
  self.senderImageView = senderImageView;
  return senderImageView;
}

// Helper for creating recipient image view.
- (UIImageView*)createRecipientImageView {
  UIImageView* recipientImageView =
      [[UIImageView alloc] initWithImage:self.recipientImage];
  recipientImageView.translatesAutoresizingMaskIntoConstraints = NO;
  recipientImageView.backgroundColor =
      [UIColor colorNamed:kPrimaryBackgroundColor];
  self.recipientImageView = recipientImageView;
  return recipientImageView;
}

// Helper for creating progress bar view.
- (UIView*)createProgressBarView {
  UIView* progressBarView = [[UIView alloc] init];
  progressBarView.translatesAutoresizingMaskIntoConstraints = NO;
  progressBarView.backgroundColor =
      [UIColor colorNamed:kPrimaryBackgroundColor];
  self.progressBarView = progressBarView;
  return progressBarView;
}

// Helper for creating the lock image view.
- (UIImageView*)createLockImage {
  UIImageView* lockImage = [[UIImageView alloc]
      initWithImage:DefaultSymbolWithPointSize(kLockSymbol,
                                               kLockSymbolPointSize)];
  lockImage.translatesAutoresizingMaskIntoConstraints = NO;
  lockImage.backgroundColor = [UIColor colorNamed:kPrimaryBackgroundColor];
  lockImage.hidden = YES;
  self.lockImage = lockImage;
  return lockImage;
}

// Creates `kProgressBarCirclesAmount` blue circles in the progress bar view.
- (void)createProgressBarSubviews {
  UIView* progressBarView = self.progressBarView;
  for (NSInteger i = 0; i < kProgressBarCirclesAmount; i++) {
    UIView* circleView =
        [[UIView alloc] initWithFrame:CGRectMake((kProgressBarCircleDiameter +
                                                  kProgressBarCircleSpacing) *
                                                     i,
                                                 kProgressBarHeight / 2,
                                                 kProgressBarCircleDiameter,
                                                 kProgressBarCircleDiameter)];
    circleView.backgroundColor = [UIColor colorNamed:kBlueColor];
    circleView.alpha = 0.0;
    circleView.layer.cornerRadius = kProgressBarCircleDiameter / 2;
    [progressBarView addSubview:circleView];
  }
}

// Creates favicon view and fetches the actual favicon, while setting the
// default world icon as well as a fallback.
- (FaviconView*)createFaviconView {
  FaviconView* faviconView = [[FaviconView alloc] init];
  faviconView.translatesAutoresizingMaskIntoConstraints = NO;
  faviconView.contentMode = UIViewContentModeScaleAspectFill;

  // Use the default world icon as a fallback.
  FaviconAttributes* defaultFaviconAttributes = [FaviconAttributes
      attributesWithImage:[UIImage imageNamed:@"default_world_favicon"]];
  [faviconView configureWithAttributes:defaultFaviconAttributes];

  // Fetch the actual favicon.
  [self.imageDataSource
      faviconForPageURL:[[CrURL alloc] initWithGURL:_URL]
             completion:^(FaviconAttributes* attributes) {
               [faviconView configureWithAttributes:attributes];
             }];

  return faviconView;
}

// Creates and returns the container for the favicon view.
- (FaviconContainerView*)createFaviconContainerView {
  FaviconContainerView* faviconContainerView =
      [[FaviconContainerView alloc] init];
  faviconContainerView.translatesAutoresizingMaskIntoConstraints = NO;
  [faviconContainerView
      setFaviconBackgroundColor:[UIColor colorNamed:kPrimaryBackgroundColor]];
  faviconContainerView.hidden = YES;
  self.faviconContainerView = faviconContainerView;
  return faviconContainerView;
}

// Creates the container view for the animation.
- (UIView*)createAnimationContainerView {
  UIView* animationView = [[UIView alloc] init];
  animationView = [[UIView alloc] init];
  animationView.translatesAutoresizingMaskIntoConstraints = NO;

  // Add progress bar view.
  UIView* progressBarView = [self createProgressBarView];
  [animationView addSubview:progressBarView];

  // Add progress bar circles.
  [self createProgressBarSubviews];

  // Add lock image.
  UIImageView* lockImage = [self createLockImage];
  [progressBarView addSubview:lockImage];

  // Add sender profile image.
  UIImageView* senderImageView = [self createSenderImageView];
  [animationView addSubview:senderImageView];

  // Add recipient profile image.
  UIImageView* recipientImageView = [self createRecipientImageView];
  [animationView addSubview:recipientImageView];

  // Add favicon and its container.
  FaviconContainerView* faviconContainerView =
      [self createFaviconContainerView];
  [animationView addSubview:faviconContainerView];
  FaviconView* faviconView = [self createFaviconView];
  [faviconContainerView addSubview:faviconView];

  [NSLayoutConstraint activateConstraints:@[
    // Sender image constraints.
    [senderImageView.topAnchor constraintEqualToAnchor:animationView.topAnchor
                                              constant:kVerticalSpacing],
    [senderImageView.bottomAnchor
        constraintEqualToAnchor:animationView.bottomAnchor
                       constant:-kVerticalSpacing],
    [senderImageView.widthAnchor constraintEqualToConstant:kProfileImageSize],
    [senderImageView.heightAnchor constraintEqualToConstant:kProfileImageSize],

    // Recipient image constraints.
    [recipientImageView.centerYAnchor
        constraintEqualToAnchor:senderImageView.centerYAnchor],
    [recipientImageView.widthAnchor
        constraintEqualToConstant:kProfileImageSize],
    [recipientImageView.heightAnchor
        constraintEqualToConstant:kProfileImageSize],

    // Progress bar constraints.
    [progressBarView.centerXAnchor
        constraintEqualToAnchor:animationView.centerXAnchor],
    [progressBarView.centerYAnchor
        constraintEqualToAnchor:senderImageView.centerYAnchor],
    [progressBarView.widthAnchor constraintEqualToConstant:kProgressBarWidth],
    [progressBarView.heightAnchor constraintEqualToConstant:kProgressBarHeight],

    // Lock image constraints.
    [lockImage.centerYAnchor
        constraintEqualToAnchor:senderImageView.centerYAnchor],
    [lockImage.centerXAnchor
        constraintEqualToAnchor:animationView.centerXAnchor],

    // Favicon constraints.
    [faviconContainerView.topAnchor
        constraintEqualToAnchor:senderImageView.bottomAnchor
                       constant:-kFaviconProfileImageVerticalOverlap],
    [faviconContainerView.centerXAnchor
        constraintEqualToAnchor:animationView.centerXAnchor],
    [faviconContainerView.widthAnchor
        constraintEqualToConstant:kFaviconContainerSize],
    [faviconContainerView.heightAnchor
        constraintEqualToConstant:kFaviconContainerSize],
    [faviconView.centerXAnchor
        constraintEqualToAnchor:faviconContainerView.centerXAnchor],
    [faviconView.centerYAnchor
        constraintEqualToAnchor:faviconContainerView.centerYAnchor],
    [faviconView.widthAnchor constraintEqualToConstant:kFaviconSize],
    [faviconView.heightAnchor constraintEqualToConstant:kFaviconSize],
  ]];

  _senderImageCenterXConstraint = [senderImageView.centerXAnchor
      constraintEqualToAnchor:animationView.centerXAnchor];
  _senderImageCenterXConstraint.active = YES;
  _recipientImageCenterXConstraint = [recipientImageView.centerXAnchor
      constraintEqualToAnchor:animationView.centerXAnchor];
  _recipientImageCenterXConstraint.active = YES;

  self.animationView = animationView;
  return animationView;
}

// Creates title label.
- (UILabel*)createTitleLabel {
  UILabel* title = [[UILabel alloc] init];
  title.numberOfLines = 0;
  title.translatesAutoresizingMaskIntoConstraints = NO;
  title.text =
      l10n_util::GetNSString(IDS_IOS_PASSWORD_SHARING_STATUS_PROGRESS_TITLE);
  title.font = CreateDynamicFont(UIFontTextStyleTitle1, UIFontWeightBold);
  title.adjustsFontForContentSizeCategory = YES;
  title.textColor = [UIColor colorNamed:kTextPrimaryColor];
  title.textAlignment = NSTextAlignmentCenter;
  self.titleLabel = title;
  return title;
}

// Helper for creating the cancel button
- (UIButton*)createCancelButton {
  UIButton* cancelButton = [UIButton buttonWithType:UIButtonTypeSystem];
  cancelButton.translatesAutoresizingMaskIntoConstraints = NO;
  [cancelButton setTitle:l10n_util::GetNSString(IDS_CANCEL)
                forState:UIControlStateNormal];
  [cancelButton addTarget:self
                   action:@selector(cancelButtonTapped)
         forControlEvents:UIControlEventTouchUpInside];
  self.cancelButton = cancelButton;
  return cancelButton;
}

// Creates sharing status animations that are started one by one.
- (void)createAnimations {
  UICubicTimingParameters* imagesSlidingTimingParams =
      [[UICubicTimingParameters alloc]
          initWithControlPoint1:CGPointMake(0.7, 0.0)
                  controlPoint2:CGPointMake(0.45, 1.45)];
  self.imagesSlidingOutAnimation = [[UIViewPropertyAnimator alloc]
      initWithDuration:kImagesSlidingOutDuration
      timingParameters:imagesSlidingTimingParams];
  __weak __typeof(self) weakSelf = self;
  [self.imagesSlidingOutAnimation addAnimations:^{
    [weakSelf setImagesCenterXConstraint:kImagesSlidedOutCenterXConstant];
  }];
  [self.imagesSlidingOutAnimation
      addCompletion:^(UIViewAnimatingPosition finalPosition) {
        [weakSelf.lockAppearingAnimation startAnimation];
      }];

  self.lockAppearingAnimation = [[UIViewPropertyAnimator alloc]
      initWithDuration:kLockAppearingDuration
                 curve:UIViewAnimationCurveEaseIn
            animations:^{
              weakSelf.lockImage.hidden = NO;
            }];
  [self.lockAppearingAnimation
      addCompletion:^(UIViewAnimatingPosition finalPosition) {
        [weakSelf.progressBarLoadingAnimation startAnimation];
      }];

  self.progressBarLoadingAnimation = [[UIViewPropertyAnimator alloc]
      initWithDuration:kProgressBarLoadingDuration
                 curve:UIViewAnimationCurveLinear
            animations:^{
              [weakSelf animateProgressBarLoading];
            }];
  [self.progressBarLoadingAnimation
      addCompletion:^(UIViewAnimatingPosition finalPosition) {
        [weakSelf.imagesSlidingInAnimation startAnimation];
      }];

  self.imagesSlidingInAnimation = [[UIViewPropertyAnimator alloc]
      initWithDuration:kImagesSlidingInDuration
      timingParameters:imagesSlidingTimingParams];
  [self.imagesSlidingInAnimation addAnimations:^{
    weakSelf.progressBarView.hidden = YES;
    [weakSelf sendRecipientImageToBack];
    [weakSelf setImagesCenterXConstraint:kImagesSlidedInCenterXConstant];
  }];
  [self.imagesSlidingInAnimation
      addCompletion:^(UIViewAnimatingPosition finalPosition) {
        [weakSelf.faviconAppearingAnimation
            startAnimationAfterDelay:kFaviconAppearingDelay];
      }];

  self.faviconAppearingAnimation = [[UIViewPropertyAnimator alloc]
      initWithDuration:kFaviconAppearingDuration
                 curve:UIViewAnimationCurveEaseIn
            animations:^{
              weakSelf.faviconContainerView.hidden = NO;
            }];
  __weak __typeof(self.delegate) weakDelegate = self.delegate;
  [self.faviconAppearingAnimation
      addCompletion:^(UIViewAnimatingPosition finalPosition) {
        [weakSelf displaySuccessStatus];
        [weakDelegate startPasswordSharing];
      }];

  UICubicTimingParameters* animationCancelledTimingParams =
      [[UICubicTimingParameters alloc]
          initWithControlPoint1:CGPointMake(0.7, -0.45)
                  controlPoint2:CGPointMake(0.45, 1.0)];
  self.sharingCancelledAnimation = [[UIViewPropertyAnimator alloc]
      initWithDuration:kSharingCancelledDuration
      timingParameters:animationCancelledTimingParams];
  [self.sharingCancelledAnimation addAnimations:^{
    weakSelf.progressBarView.hidden = YES;
    [weakSelf sendRecipientImageToBack];
    [weakSelf setImagesCenterXConstraint:0];
  }];
  [self.sharingCancelledAnimation
      addCompletion:^(UIViewAnimatingPosition finalPosition) {
        [weakSelf displayCancelledStatus];
      }];
}

// Animates consecutive circles of the progress bar appearing.
- (void)animateProgressBarLoading {
  __weak __typeof(self) weakSelf = self;
  for (NSUInteger i = 0; i < kProgressBarCirclesAmount; i++) {
    [UIView animateWithDuration:0
                          delay:(kProgressBarLoadingDuration /
                                 kProgressBarCirclesAmount) *
                                i
                        options:UIViewAnimationOptionCurveEaseInOut
                     animations:^{
                       weakSelf.progressBarView.subviews[i].alpha = 1.0;
                     }
                     completion:nil];
  }
}

// Moves the recipient image to the back so that it's below the sender image
// when they overlap.
- (void)sendRecipientImageToBack {
  [self.animationView sendSubviewToBack:self.recipientImageView];
}

// Sets constant for sender and recipients centerX constraint so that the sender
// is on the left from the middle of the view and the recipients on the right.
- (void)setImagesCenterXConstraint:(CGFloat)constant {
  _senderImageCenterXConstraint.constant = -constant;
  _recipientImageCenterXConstraint.constant = constant;
  [self.view layoutIfNeeded];
}

// Calculates and sets detent based on the height of content.
- (void)recalculatePreferredHeightDetent {
  self.sheetPresentationController.detents = @[
    [self preferredHeightDetent],
    UISheetPresentationControllerDetent.largeDetent
  ];
}

// Creates a UITextView with subtitle and footer defaults.
- (UITextView*)createTextView {
  UITextView* view = [[UITextView alloc] init];
  view.textAlignment = NSTextAlignmentCenter;
  view.translatesAutoresizingMaskIntoConstraints = NO;
  view.adjustsFontForContentSizeCategory = YES;
  view.delegate = self;
  view.editable = NO;
  view.selectable = YES;
  view.scrollEnabled = NO;
  view.backgroundColor = [UIColor colorNamed:kPrimaryBackgroundColor];
  return view;
}

// Adds link attribute to the specified `range` of the `view`.
- (void)addLinkAttributeToTextView:(UITextView*)view range:(NSRange)range {
  NSMutableAttributedString* linkText = [[NSMutableAttributedString alloc]
      initWithAttributedString:view.attributedText];
  NSDictionary* linkAttributes = @{
    NSLinkAttributeName : @"",
    NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle)
  };
  [linkText addAttributes:linkAttributes range:range];
  view.attributedText = linkText;
}

// Adds bold attribute to the specified `range` of the `view`.
- (void)addBoldAttributeToTextView:(UITextView*)view range:(NSRange)range {
  NSMutableAttributedString* boldText = [[NSMutableAttributedString alloc]
      initWithAttributedString:view.attributedText];
  UIFontDescriptor* boldDescriptor = [[UIFontDescriptor
      preferredFontDescriptorWithTextStyle:UIFontTextStyleBody]
      fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitBold];
  [boldText addAttribute:NSFontAttributeName
                   value:[UIFont fontWithDescriptor:boldDescriptor size:0.0]
                   range:range];
  view.attributedText = boldText;
}

// Helper to create the subtitle.
- (UITextView*)createSubtitle {
  UITextView* subtitle = [self createTextView];
  subtitle.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
  subtitle.textColor = [UIColor colorNamed:kTextPrimaryColor];

  StringWithTags stringWithBolds =
      ParseStringWithTags(self.subtitleString, kBeginBoldTag, kEndBoldTag);
  subtitle.text = stringWithBolds.string;

  for (const NSRange& range : stringWithBolds.ranges) {
    [self addBoldAttributeToTextView:subtitle range:range];
  }

  return subtitle;
}

// Helper to create the footer.
- (UITextView*)createFooter {
  UITextView* footer = [self createTextView];
  footer.font = [UIFont preferredFontForTextStyle:UIFontTextStyleFootnote];
  footer.textColor = [UIColor colorNamed:kTextSecondaryColor];
  footer.accessibilityIdentifier = kSharingStatusFooterId;

  StringWithTags stringWithTags = ParseStringWithLinks(self.footerString);
  footer.text = stringWithTags.string;
  if (!stringWithTags.ranges.empty()) {
    [self addLinkAttributeToTextView:footer range:stringWithTags.ranges[0]];
  }

  return footer;
}

// Helper for creating the done button.
- (UIButton*)createDoneButton {
  UIButton* doneButton = PrimaryActionButton(YES);
  [doneButton addTarget:self
                 action:@selector(doneButtonTapped)
       forControlEvents:UIControlEventTouchUpInside];
  SetConfigurationTitle(doneButton, l10n_util::GetNSString(IDS_DONE));
  doneButton.accessibilityIdentifier = kSharingStatusDoneButtonID;
  return doneButton;
}

// Creates done button, adds it to the view and sets its constraints.
- (void)addDoneButtonWithBottomPadding {
  UIView* view = self.view;
  UIButton* doneButton = [self createDoneButton];
  [view addSubview:doneButton];

  [NSLayoutConstraint activateConstraints:@[
    [doneButton.leadingAnchor constraintEqualToAnchor:view.leadingAnchor
                                             constant:kHorizontalPadding],
    [doneButton.trailingAnchor constraintEqualToAnchor:view.trailingAnchor
                                              constant:-kHorizontalPadding],
    [doneButton.topAnchor
        constraintGreaterThanOrEqualToAnchor:self.stackView.bottomAnchor
                                    constant:kVerticalSpacing],
    [doneButton.bottomAnchor constraintEqualToAnchor:view.bottomAnchor
                                            constant:-kBottomPadding],
  ]];
}

// Replaces text of the title label, cancel button with done button and adds a
// subtitle and a footer.
- (void)displaySuccessStatus {
  self.titleLabel.text =
      l10n_util::GetNSString(IDS_IOS_PASSWORD_SHARING_SUCCESS_TITLE);
  self.cancelButton.hidden = YES;

  UIStackView* stackView = self.stackView;
  [stackView addArrangedSubview:[self createSubtitle]];
  [stackView addArrangedSubview:[self createFooter]];

  [self addDoneButtonWithBottomPadding];
  [self recalculatePreferredHeightDetent];
  UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification,
                                  self.titleLabel);
}

// Replaces text of the title label and adds a done button.
// TODO(crbug.com/40275395): Add test.
- (void)displayCancelledStatus {
  self.titleLabel.text =
      l10n_util::GetNSString(IDS_IOS_PASSWORD_SHARING_CANCELLED_TITLE);
  self.cancelButton.hidden = YES;

  [self addDoneButtonWithBottomPadding];
  [self recalculatePreferredHeightDetent];
  UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification,
                                  self.titleLabel);
}

// Stops any ongoing animations and starts a new one (profile images sliding to
// the middle).
- (void)cancelButtonTapped {
  [self.imagesSlidingOutAnimation stopAnimation:YES];
  [self.progressBarLoadingAnimation stopAnimation:YES];
  [self.imagesSlidingInAnimation stopAnimation:YES];
  [self.faviconAppearingAnimation stopAnimation:YES];

  [self.sharingCancelledAnimation startAnimation];

  LogPasswordSharingInteraction(
      PasswordSharingInteraction::kSharingConfirmationCancelClicked);
}

// Handles done buttons clicks by dismissing the view.
- (void)doneButtonTapped {
  [self.delegate sharingStatusWasDismissed:self];
}

@end