blob: 1f2f49003d08d57e1dcc08ca673051f0e857e091 [file] [log] [blame]
// Copyright 2018 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/unified_consent/unified_consent_view_controller.h"
#include "base/check_op.h"
#include "components/google/core/common/google_util.h"
#include "ios/chrome/browser/application_context.h"
#import "ios/chrome/browser/ui/authentication/authentication_constants.h"
#import "ios/chrome/browser/ui/authentication/unified_consent/identity_picker_view.h"
#import "ios/chrome/browser/ui/authentication/unified_consent/unified_consent_constants.h"
#import "ios/chrome/browser/ui/authentication/unified_consent/unified_consent_view_controller_delegate.h"
#import "ios/chrome/browser/ui/colors/MDCPalette+CrAdditions.h"
#import "ios/chrome/browser/ui/util/label_link_controller.h"
#import "ios/chrome/browser/ui/util/uikit_ui_util.h"
#include "ios/chrome/common/string_util.h"
#import "ios/chrome/common/ui/colors/UIColor+cr_semantic_colors.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
#include "ios/chrome/grit/ios_chromium_strings.h"
#include "ios/chrome/grit/ios_strings.h"
#include "ui/base/l10n/l10n_util_mac.h"
#include "url/gurl.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
// Vertical margin the main title and the identity picker.
const CGFloat kTitlePickerMargin = 16.;
// Vertical margin above the first text and after the last text.
const CGFloat kVerticalTextMargin = 22.;
// Vertical margin between texts.
const CGFloat kVerticalBetweenTextMargin = 25.;
// Vertical margin between separator and text.
const CGFloat kVerticalSeparatorTextMargin = 16.;
// URL for the Settings link.
const char* const kSettingsSyncURL = "internal://settings-sync";
} // namespace
@interface UnifiedConsentViewController ()<UIScrollViewDelegate> {
std::vector<int> _consentStringIds;
}
// Read/write internal.
@property(nonatomic, readwrite) int openSettingsStringId;
// Main view.
@property(nonatomic, strong) UIScrollView* scrollView;
// Identity picker to change the identity to sign-in.
@property(nonatomic, strong) IdentityPickerView* identityPickerView;
// Vertical constraint on imageBackgroundView to have it over non-safe area.
@property(nonatomic, strong)
NSLayoutConstraint* imageBackgroundViewHeightConstraint;
// Constraint when identityPickerView is hidden.
@property(nonatomic, strong) NSLayoutConstraint* noIdentityConstraint;
// Constraint when identityPickerView is visible.
@property(nonatomic, strong) NSLayoutConstraint* withIdentityConstraint;
// Constraint for the maximum height of the header view (also used to hide the
// the header view if needed).
@property(nonatomic, strong) NSLayoutConstraint* headerViewMaxHeightConstraint;
// Settings link controller.
@property(nonatomic, strong) LabelLinkController* settingsLinkController;
// Label related to customize sync text.
@property(nonatomic, strong) UILabel* customizeSyncLabel;
@end
@implementation UnifiedConsentViewController
@synthesize delegate = _delegate;
@synthesize identityPickerView = _identityPickerView;
@synthesize imageBackgroundViewHeightConstraint =
_imageBackgroundViewHeightConstraint;
@synthesize noIdentityConstraint = _noIdentityConstraint;
@synthesize openSettingsStringId = _openSettingsStringId;
@synthesize scrollView = _scrollView;
@synthesize settingsLinkController = _settingsLinkController;
@synthesize withIdentityConstraint = _withIdentityConstraint;
@synthesize customizeSyncLabel = _customizeSyncLabel;
- (const std::vector<int>&)consentStringIds {
return _consentStringIds;
}
- (void)updateIdentityPickerViewWithUserFullName:(NSString*)fullName
email:(NSString*)email {
DCHECK(email);
self.identityPickerView.hidden = NO;
self.noIdentityConstraint.active = NO;
self.withIdentityConstraint.active = YES;
[self.identityPickerView setIdentityName:fullName email:email];
[self setSettingsLinkURLShown:YES];
}
- (void)updateIdentityPickerViewWithAvatar:(UIImage*)avatar {
DCHECK(!self.identityPickerView.hidden);
[self.identityPickerView setIdentityAvatar:avatar];
}
- (void)hideIdentityPickerView {
self.identityPickerView.hidden = YES;
self.withIdentityConstraint.active = NO;
self.noIdentityConstraint.active = YES;
[self setSettingsLinkURLShown:NO];
}
- (void)scrollToBottom {
// Add one point to make sure that it is actually scrolled to the bottom (as
// there are some issues when the fonts are increased).
CGPoint bottomOffset =
CGPointMake(0, self.scrollView.contentSize.height -
self.scrollView.bounds.size.height +
self.scrollView.contentInset.bottom + 1);
[self.scrollView setContentOffset:bottomOffset animated:YES];
}
- (BOOL)isScrolledToBottom {
CGFloat scrollPosition =
self.scrollView.contentOffset.y + self.scrollView.frame.size.height;
CGFloat scrollLimit =
self.scrollView.contentSize.height + self.scrollView.contentInset.bottom;
return scrollPosition >= scrollLimit;
}
#pragma mark - UIViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Main scroll view.
self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
self.scrollView.translatesAutoresizingMaskIntoConstraints = NO;
self.scrollView.accessibilityIdentifier = kUnifiedConsentScrollViewIdentifier;
// The observed behavior was buggy. When the view appears on the screen,
// the scrollview was not scrolled all the way to the top. Adjusting the
// safe area manually fixes the issue.
self.scrollView.contentInsetAdjustmentBehavior =
UIScrollViewContentInsetAdjustmentNever;
[self.view addSubview:self.scrollView];
// Scroll view container.
UIView* container = [[UIView alloc] initWithFrame:CGRectZero];
container.translatesAutoresizingMaskIntoConstraints = NO;
[self.scrollView addSubview:container];
// View used to draw the background color of the header image, in the non-safe
// areas (like the status bar).
UIView* imageBackgroundView = [[UIView alloc] initWithFrame:CGRectZero];
imageBackgroundView.translatesAutoresizingMaskIntoConstraints = NO;
[container addSubview:imageBackgroundView];
// Header image.
UIImageView* headerImageView = [[UIImageView alloc] initWithFrame:CGRectZero];
headerImageView.translatesAutoresizingMaskIntoConstraints = NO;
headerImageView.image = [UIImage imageNamed:kAuthenticationHeaderImageName];
headerImageView.contentMode = UIViewContentModeScaleAspectFit;
headerImageView.clipsToBounds = YES;
[container addSubview:headerImageView];
// Title.
UILabel* title =
[self addLabelWithStringId:IDS_IOS_ACCOUNT_UNIFIED_CONSENT_TITLE
fontStyle:kAuthenticationTitleFontStyle
textColor:UIColor.cr_labelColor
parentView:container];
// Identity picker view.
self.identityPickerView =
[[IdentityPickerView alloc] initWithFrame:CGRectZero];
self.identityPickerView.translatesAutoresizingMaskIntoConstraints = NO;
[self.identityPickerView addTarget:self
action:@selector(identityPickerAction:forEvent:)
forControlEvents:UIControlEventTouchUpInside];
[container addSubview:self.identityPickerView];
// Sync title and subtitle.
UILabel* syncTitleLabel =
[self addLabelWithStringId:IDS_IOS_ACCOUNT_UNIFIED_CONSENT_SYNC_TITLE
fontStyle:kAuthenticationTextFontStyle
textColor:UIColor.cr_labelColor
parentView:container];
UILabel* syncSubtitleLabel =
[self addLabelWithStringId:IDS_IOS_ACCOUNT_UNIFIED_CONSENT_SYNC_SUBTITLE
fontStyle:kAuthenticationTextFontStyle
textColor:UIColor.cr_secondaryLabelColor
parentView:container];
// Separator.
UIView* separator = [[UIView alloc] initWithFrame:CGRectZero];
separator.translatesAutoresizingMaskIntoConstraints = NO;
separator.backgroundColor = UIColor.cr_secondarySystemBackgroundColor;
[container addSubview:separator];
// Customize label.
self.openSettingsStringId = IDS_IOS_ACCOUNT_UNIFIED_CONSENT_SETTINGS;
self.customizeSyncLabel =
[self addLabelWithStringId:self.openSettingsStringId
fontStyle:kAuthenticationTextFontStyle
textColor:UIColor.cr_secondaryLabelColor
parentView:container];
// Layouts
NSDictionary* views = @{
@"header" : headerImageView,
@"title" : title,
@"picker" : self.identityPickerView,
@"container" : container,
@"scrollview" : self.scrollView,
@"separator" : separator,
@"synctitle" : syncTitleLabel,
@"syncsubtitle" : syncSubtitleLabel,
@"customizesync" : self.customizeSyncLabel,
};
NSDictionary* metrics = @{
@"TitlePickerMargin" : @(kTitlePickerMargin),
@"HMargin" : @(kAuthenticationHorizontalMargin),
@"VBetweenText" : @(kVerticalBetweenTextMargin),
@"VSeparatorText" : @(kVerticalSeparatorTextMargin),
@"VTextMargin" : @(kVerticalTextMargin),
@"SeparatorHeight" : @(kAuthenticationSeparatorHeight),
@"HeaderTitleMargin" : @(kAuthenticationHeaderTitleMargin),
};
NSArray* constraints = @[
// Horitizontal constraints.
@"H:|[scrollview]|",
@"H:|[container]|",
@"H:|-(HMargin)-[title]-(HMargin)-|",
@"H:|-(HMargin)-[picker]-(HMargin)-|",
@"H:|-(HMargin)-[separator]-(HMargin)-|",
@"H:|-(HMargin)-[synctitle]-(HMargin)-|",
@"H:|-(HMargin)-[syncsubtitle]-(HMargin)-|",
@"H:|-(HMargin)-[customizesync]-(HMargin)-|",
// Vertical constraints.
@"V:|[scrollview]|",
@"V:|[container]|",
@"V:|[header]-(HeaderTitleMargin)-[title]-(TitlePickerMargin)-[picker]",
@"V:[synctitle]-[syncsubtitle]-(VBetweenText)-[separator]",
@"V:[separator]-(VSeparatorText)-[customizesync]-(VTextMargin)-|",
// Size constraints.
@"V:[separator(SeparatorHeight)]",
];
ApplyVisualConstraintsWithMetrics(constraints, views, metrics);
// Adding constraints for header image.
AddSameCenterXConstraint(self.view, headerImageView);
// |headerView| fills 20% of |view|, capped at
// |kAuthenticationHeaderImageHeight|.
[headerImageView.heightAnchor
constraintLessThanOrEqualToAnchor:self.view.heightAnchor
multiplier:0.2]
.active = YES;
self.headerViewMaxHeightConstraint = [headerImageView.heightAnchor
constraintLessThanOrEqualToConstant:kAuthenticationHeaderImageHeight];
self.headerViewMaxHeightConstraint.active = YES;
[self updateHeaderViewConstraints];
// Adding constraints with or without identity.
self.noIdentityConstraint =
[syncTitleLabel.topAnchor constraintEqualToAnchor:title.bottomAnchor
constant:kVerticalTextMargin];
self.withIdentityConstraint = [syncTitleLabel.topAnchor
constraintEqualToAnchor:self.identityPickerView.bottomAnchor
constant:kVerticalTextMargin];
// Adding constraints for the container.
id<LayoutGuideProvider> safeArea = self.view.safeAreaLayoutGuide;
[container.widthAnchor constraintEqualToAnchor:safeArea.widthAnchor].active =
YES;
// Adding constraints for |imageBackgroundView|.
AddSameCenterXConstraint(self.view, imageBackgroundView);
[imageBackgroundView.widthAnchor
constraintEqualToAnchor:self.view.widthAnchor]
.active = YES;
self.imageBackgroundViewHeightConstraint = [imageBackgroundView.heightAnchor
constraintEqualToAnchor:headerImageView.heightAnchor];
self.imageBackgroundViewHeightConstraint.active = YES;
[imageBackgroundView.bottomAnchor
constraintEqualToAnchor:headerImageView.bottomAnchor]
.active = YES;
// Update UI.
[self hideIdentityPickerView];
[self updateScrollViewAndImageBackgroundView];
}
- (void)viewSafeAreaInsetsDidChange {
// Updates the scroll view content inset, used by iOS 11 or later.
[super viewSafeAreaInsetsDidChange];
[self updateScrollViewAndImageBackgroundView];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self.delegate unifiedConsentViewControllerViewDidAppear:self];
}
- (void)viewWillTransitionToSize:(CGSize)size
withTransitionCoordinator:
(id<UIViewControllerTransitionCoordinator>)coordinator {
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
[coordinator
animateAlongsideTransition:^(
id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
[self updateScrollViewAndImageBackgroundView];
}
completion:nil];
}
- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
[self updateHeaderViewConstraints];
}
- (void)didMoveToParentViewController:(UIViewController*)parent {
if (!parent)
return;
[parent.view layoutIfNeeded];
// Needs to add the scroll view delegate only when all the view layouts are
// fully done.
dispatch_async(dispatch_get_main_queue(), ^{
// Having a layout of the parent view makes the scroll view not being
// presented at the top. Scrolling to the top is required.
CGPoint topOffset = CGPointMake(self.scrollView.contentOffset.x,
-self.scrollView.contentInset.top);
[self.scrollView setContentOffset:topOffset animated:NO];
self.scrollView.delegate = self;
[self sendDidReachBottomIfReached];
});
}
#pragma mark - UI actions
- (void)identityPickerAction:(id)sender forEvent:(UIEvent*)event {
UITouch* touch = event.allTouches.anyObject;
[self.delegate
unifiedConsentViewControllerDidTapIdentityPickerView:self
atPoint:
[touch
locationInView:nil]];
}
#pragma mark - Private
// Adds label with title |stringId| into |parentView|.
- (UILabel*)addLabelWithStringId:(int)stringId
fontStyle:(UIFontTextStyle)fontStyle
textColor:(UIColor*)textColor
parentView:(UIView*)parentView {
DCHECK(stringId);
DCHECK(parentView);
UILabel* label = [[UILabel alloc] initWithFrame:CGRectZero];
label.adjustsFontForContentSizeCategory = YES;
label.translatesAutoresizingMaskIntoConstraints = NO;
label.font = [UIFont preferredFontForTextStyle:fontStyle];
label.textColor = textColor;
label.text = l10n_util::GetNSString(stringId);
_consentStringIds.push_back(stringId);
label.numberOfLines = 0;
[parentView addSubview:label];
return label;
}
// Adds or removes the Settings link in |self.customizeSyncLabel|.
- (void)setSettingsLinkURLShown:(BOOL)showLink {
self.customizeSyncLabel.text =
l10n_util::GetNSString(self.openSettingsStringId);
GURL URL = google_util::AppendGoogleLocaleParam(
GURL(kSettingsSyncURL), GetApplicationContext()->GetApplicationLocale());
NSRange range;
NSString* text = self.customizeSyncLabel.text;
self.customizeSyncLabel.text = ParseStringWithLink(text, &range);
DCHECK(range.location != NSNotFound && range.length != 0);
if (!showLink) {
self.settingsLinkController = nil;
} else {
__weak UnifiedConsentViewController* weakSelf = self;
self.settingsLinkController =
[[LabelLinkController alloc] initWithLabel:self.customizeSyncLabel
action:^(const GURL& URL) {
[weakSelf openSettings];
}];
[self.settingsLinkController setLinkColor:[UIColor colorNamed:kBlueColor]];
[self.settingsLinkController
addLinkWithRange:range
url:URL
accessibilityID:kAdvancedSigninSettingsLinkIdentifier];
}
}
// Updates constraints and content insets for the |scrollView| and
// |imageBackgroundView| related to non-safe area.
- (void)updateScrollViewAndImageBackgroundView {
self.scrollView.contentInset = self.view.safeAreaInsets;
self.imageBackgroundViewHeightConstraint.constant =
self.view.safeAreaInsets.top;
if (self.scrollView.delegate == self) {
// Don't send the notification if the delegate is not configured yet.
[self sendDidReachBottomIfReached];
}
}
// Notifies |delegate| that the user tapped on "Settings" link.
- (void)openSettings {
[self.delegate unifiedConsentViewControllerDidTapSettingsLink:self];
}
// Sends notification to the delegate if the scroll view is scrolled to the
// bottom.
- (void)sendDidReachBottomIfReached {
if (self.isScrolledToBottom) {
[self.delegate unifiedConsentViewControllerDidReachBottom:self];
}
}
// Updates the header view constraints based on the height class traits of
// |view|.
- (void)updateHeaderViewConstraints {
if (IsCompactHeight(self)) {
self.headerViewMaxHeightConstraint.constant = 0;
} else {
self.headerViewMaxHeightConstraint.constant =
kAuthenticationHeaderImageHeight;
}
}
#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView*)scrollView {
DCHECK_EQ(self.scrollView, scrollView);
[self sendDidReachBottomIfReached];
}
@end