-
Notifications
You must be signed in to change notification settings - Fork 6.6k
/
password_suggestion_bottom_sheet_view_controller.mm
579 lines (491 loc) · 21.6 KB
/
password_suggestion_bottom_sheet_view_controller.mm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
// 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/passwords/bottom_sheet/password_suggestion_bottom_sheet_view_controller.h"
#import "base/mac/foundation_util.h"
#import "base/memory/raw_ptr.h"
#import "base/strings/sys_string_conversions.h"
#import "components/autofill/ios/browser/form_suggestion.h"
#import "components/password_manager/core/browser/password_ui_utils.h"
#import "components/password_manager/core/browser/ui/credential_ui_entry.h"
#import "components/password_manager/ios/shared_password_controller.h"
#import "ios/chrome/browser/shared/ui/symbols/symbols.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_url_item.h"
#import "ios/chrome/browser/shared/ui/table_view/chrome_table_view_controller.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/ui/passwords/bottom_sheet/password_suggestion_bottom_sheet_delegate.h"
#import "ios/chrome/browser/ui/passwords/bottom_sheet/password_suggestion_bottom_sheet_handler.h"
#import "ios/chrome/browser/ui/settings/password/branded_navigation_item_title_view.h"
#import "ios/chrome/browser/ui/settings/password/create_password_manager_title_view.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/favicon/favicon_attributes.h"
#import "ios/chrome/common/ui/favicon/favicon_view.h"
#import "ios/chrome/common/ui/table_view/table_view_cells_constants.h"
#import "ios/chrome/grit/ios_google_chrome_strings.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ui/base/l10n/l10n_util_mac.h"
#import "url/gurl.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
// Estimated base height value for the bottom sheet without the table view.
CGFloat const kEstimatedBaseHeightForBottomSheet = 195;
// Sets a custom radius for the half sheet presentation.
CGFloat const kHalfSheetCornerRadius = 20;
// Estimated row height for each cell in the table view.
CGFloat const kTableViewEstimatedRowHeight = 75;
// Radius size of the table view.
CGFloat const kTableViewCornerRadius = 10;
// TableView's width constraint multiplier in portrait mode.
CGFloat const kPortraitTableViewWidthMultiplier = 0.95;
// TableView's width constraint multiplier in landscape mode.
CGFloat const kLandscapeTableViewWidthMultiplier = 0.65;
} // namespace
@interface PasswordSuggestionBottomSheetViewController () <
ConfirmationAlertActionHandler,
UIGestureRecognizerDelegate,
UITableViewDataSource,
UITableViewDelegate> {
// Row in the table of suggestions of the use selectesd suggestion.
NSInteger _row;
// If YES: the table view is currently showing a single suggestion
// If NO: the table view is currently showing all suggestions
BOOL _tableViewIsMinimized;
// Height constraint for the bottom sheet when showing a single suggestion.
NSLayoutConstraint* _minimizedHeightConstraint;
// Height constraint for the bottom sheet when showing all suggestions.
NSLayoutConstraint* _fullHeightConstraint;
// Table view for the list of suggestions.
UITableView* _tableView;
// TableView's width constraint in portrait mode.
NSLayoutConstraint* _portraitTableWidthConstraint;
// TableView's width constraint in landscape mode.
NSLayoutConstraint* _landscapeTableWidthConstraint;
// List of suggestions in the bottom sheet
// The property is defined by PasswordSuggestionBottomSheetConsumer protocol.
NSArray<FormSuggestion*>* _suggestions;
// The current's page domain. This is used for the password bottom sheet
// description label.
NSString* _domain;
}
// The password controller handler used to open the password manager.
@property(nonatomic, weak) id<PasswordSuggestionBottomSheetHandler> handler;
@end
@implementation PasswordSuggestionBottomSheetViewController
- (instancetype)initWithHandler:
(id<PasswordSuggestionBottomSheetHandler>)handler {
self = [super init];
if (self) {
self.handler = handler;
}
return self;
}
#pragma mark - UIViewController
- (void)viewDidLoad {
_tableViewIsMinimized = YES;
self.titleView = [self setUpTitleView];
self.underTitleView = [self createTableView];
// Set the properties read by the super when constructing the
// views in `-[ConfirmationAlertViewController viewDidLoad]`.
self.imageHasFixedSize = YES;
self.showsVerticalScrollIndicator = NO;
self.showDismissBarButton = NO;
self.customSpacing = 0;
self.customSpacingAfterImage = 0;
self.titleTextStyle = UIFontTextStyleTitle2;
self.topAlignedLayout = YES;
self.actionHandler = self;
self.scrollEnabled = NO;
self.primaryActionString =
l10n_util::GetNSString(IDS_IOS_PASSWORD_BOTTOM_SHEET_USE_PASSWORD);
self.secondaryActionString =
l10n_util::GetNSString(IDS_IOS_PASSWORD_BOTTOM_SHEET_NO_THANKS);
[super viewDidLoad];
// Assign table view's width anchor now that it is in the same hierarchy as
// the top view.
[self createTableViewWidthConstraint:self.view.layoutMarginsGuide];
}
- (void)viewWillTransitionToSize:(CGSize)size
withTransitionCoordinator:
(id<UIViewControllerTransitionCoordinator>)coordinator {
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
[self adjustTableViewWidthConstraint];
if (!_tableViewIsMinimized) {
// Recompute sheet height and enable/disable scrolling if required.
__weak __typeof(self) weakSelf = self;
[coordinator
animateAlongsideTransition:nil
completion:^(
id<UIViewControllerTransitionCoordinatorContext>
context) {
[weakSelf expand];
}];
}
}
- (void)viewWillAppear:(BOOL)animated {
// Update height constraints for the table view.
[self.view layoutIfNeeded];
CGFloat minimizedTableViewHeight = _tableView.contentSize.height;
if (minimizedTableViewHeight > 0 &&
minimizedTableViewHeight != kTableViewEstimatedRowHeight) {
_minimizedHeightConstraint.constant = minimizedTableViewHeight;
_fullHeightConstraint.constant =
minimizedTableViewHeight * _suggestions.count;
}
[self setUpBottomSheet];
}
- (void)viewWillDisappear:(BOOL)animated {
[self.delegate refocus];
}
#pragma mark - PasswordSuggestionBottomSheetConsumer
- (void)setSuggestions:(NSArray<FormSuggestion*>*)suggestions
andDomain:(NSString*)domain {
_suggestions = suggestions;
_domain = domain;
}
- (void)dismiss {
__weak __typeof(self) weakSelf = self;
[self dismissViewControllerAnimated:NO
completion:^{
[weakSelf.handler stop];
}];
}
#pragma mark - UITableViewDelegate
- (void)tableView:(UITableView*)tableView
didSelectRowAtIndexPath:(NSIndexPath*)indexPath {
_row = indexPath.row;
if (_tableViewIsMinimized) {
_tableViewIsMinimized = NO;
// Update table view height.
__weak __typeof(self) weakSelf = self;
[UIView animateWithDuration:0.1
animations:^{
[weakSelf expandTableView];
}];
[self expand];
}
// Refresh cells to show the checkmark icon next to the selected suggestion.
[_tableView reloadData];
}
// Long press open context menu.
- (UIContextMenuConfiguration*)tableView:(UITableView*)tableView
contextMenuConfigurationForRowAtIndexPath:(NSIndexPath*)indexPath
point:(CGPoint)point {
__weak __typeof(self) weakSelf = self;
UIContextMenuActionProvider actionProvider =
^(NSArray<UIMenuElement*>* suggestedActions) {
NSMutableArray<UIMenuElement*>* menuElements =
[[NSMutableArray alloc] initWithArray:suggestedActions];
PasswordSuggestionBottomSheetViewController* strongSelf = weakSelf;
if (strongSelf) {
[menuElements addObject:[strongSelf openPasswordManagerAction]];
[menuElements
addObject:[strongSelf openPasswordDetailsForIndexPath:indexPath]];
}
return [UIMenu menuWithTitle:@"" children:menuElements];
};
return
[UIContextMenuConfiguration configurationWithIdentifier:nil
previewProvider:nil
actionProvider:actionProvider];
}
#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView*)tableView
numberOfRowsInSection:(NSInteger)section {
return _tableViewIsMinimized ? 1 : _suggestions.count;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView*)theTableView {
return 1;
}
- (UITableViewCell*)tableView:(UITableView*)tableView
cellForRowAtIndexPath:(NSIndexPath*)indexPath {
TableViewURLCell* cell =
[tableView dequeueReusableCellWithIdentifier:@"cell"];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
// Note that both the credentials and URLs will use middle truncation, as it
// generally makes it easier to differentiate between different ones, without
// having to resort to displaying multiple lines to show the full username
// and URL.
cell.titleLabel.text = [self suggestionAtRow:indexPath.row];
cell.titleLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
cell.URLLabel.text = _domain;
cell.URLLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
cell.URLLabel.hidden = NO;
// Make separator invisible on last cell
CGFloat separatorLeftMargin =
(_tableViewIsMinimized || [self isLastRow:indexPath])
? _tableView.bounds.size.width
: kTableViewHorizontalSpacing;
cell.separatorInset = UIEdgeInsetsMake(0.f, separatorLeftMargin, 0.f, 0.f);
[cell setFaviconContainerBackgroundColor:
[UIColor colorNamed:kPrimaryBackgroundColor]];
cell.titleLabel.textColor = [UIColor colorNamed:kTextPrimaryColor];
cell.backgroundColor = [UIColor colorNamed:kSecondaryBackgroundColor];
if (_tableViewIsMinimized && (_suggestions.count > 1)) {
// The table view is showing a single suggestion and the chevron down
// symbol, which can be tapped in order to expand the list of suggestions.
cell.accessoryView = [[UIImageView alloc]
initWithImage:DefaultSymbolTemplateWithPointSize(
kChevronDownSymbol, kSymbolAccessoryPointSize)];
cell.accessoryView.tintColor = [UIColor colorNamed:kTextQuaternaryColor];
} else if (_row == indexPath.row) {
// The table view is showing all suggestions, and this cell contains the
// currently selected suggestion, so we display a checkmark on this cell.
cell.accessoryView = [[UIImageView alloc]
initWithImage:DefaultSymbolTemplateWithPointSize(
kCheckmarkSymbol, kSymbolAccessoryPointSize)];
cell.accessoryView.tintColor = [UIColor colorNamed:kBlueColor];
} else {
// The table view is showing all suggestions, and this cell does not contain
// the currently selected suggestion.
cell.accessoryView = nil;
}
[self loadFaviconAtIndexPath:indexPath forCell:cell];
return cell;
}
#pragma mark - ConfirmationAlertActionHandler
- (void)confirmationAlertPrimaryAction {
// Use password button
[self.delegate disableRefocus];
__weak __typeof(self) weakSelf = self;
[self dismissViewControllerAnimated:NO
completion:^{
// Send a notification to fill the
// username/password fields
[weakSelf didSelectSuggestion];
}];
}
- (void)confirmationAlertSecondaryAction {
// "No thanks" button, which dismisses the bottom sheet.
[self dismiss];
}
#pragma mark - Private
// Configures the bottom sheet's appearance and detents.
- (void)setUpBottomSheet {
self.modalPresentationStyle = UIModalPresentationPageSheet;
UISheetPresentationController* presentationController =
self.sheetPresentationController;
presentationController.prefersEdgeAttachedInCompactHeight = YES;
presentationController.widthFollowsPreferredContentSizeWhenEdgeAttached = YES;
if (@available(iOS 16, *)) {
CGFloat bottomSheetHeight = [self initialHeight];
auto detentBlock = ^CGFloat(
id<UISheetPresentationControllerDetentResolutionContext> context) {
return bottomSheetHeight;
};
UISheetPresentationControllerDetent* customDetent =
[UISheetPresentationControllerDetent
customDetentWithIdentifier:@"customDetent"
resolver:detentBlock];
presentationController.detents = @[ customDetent ];
presentationController.selectedDetentIdentifier = @"customDetent";
} else {
presentationController.detents = @[
[UISheetPresentationControllerDetent mediumDetent],
[UISheetPresentationControllerDetent largeDetent]
];
}
presentationController.preferredCornerRadius = kHalfSheetCornerRadius;
}
// Configures the title view of this ViewController.
- (UIView*)setUpTitleView {
NSString* title = l10n_util::GetNSString(IDS_IOS_PASSWORD_BOTTOM_SHEET_TITLE);
UIView* titleView = password_manager::CreatePasswordManagerTitleView(title);
titleView.backgroundColor = [UIColor colorNamed:kPrimaryBackgroundColor];
return titleView;
}
// Returns the string to display at a given row in the table view.
- (NSString*)suggestionAtRow:(NSInteger)row {
FormSuggestion* formSuggestion = [_suggestions objectAtIndex:row];
// Removing suffix ' ••••••••' appended to the username in the suggestion.
NSString* username = formSuggestion.value;
if ([username containsString:kPasswordFormSuggestionSuffix]) {
username = [username
stringByReplacingOccurrencesOfString:kPasswordFormSuggestionSuffix
withString:@""];
}
return username;
}
// Creates the password bottom sheet's table view, initially at minimized
// height.
- (UITableView*)createTableView {
CGRect frame = [[UIScreen mainScreen] bounds];
_tableView = [[UITableView alloc] initWithFrame:frame
style:UITableViewStylePlain];
_tableView.layer.cornerRadius = kTableViewCornerRadius;
_tableView.estimatedRowHeight = kTableViewEstimatedRowHeight;
_tableView.scrollEnabled = NO;
_tableView.showsVerticalScrollIndicator = NO;
_tableView.delegate = self;
_tableView.dataSource = self;
[_tableView registerClass:TableViewURLCell.class
forCellReuseIdentifier:@"cell"];
_minimizedHeightConstraint = [_tableView.heightAnchor
constraintEqualToConstant:kTableViewEstimatedRowHeight];
_minimizedHeightConstraint.active = YES;
_fullHeightConstraint = [_tableView.heightAnchor
constraintEqualToConstant:kTableViewEstimatedRowHeight *
_suggestions.count];
_fullHeightConstraint.active = NO;
_tableView.translatesAutoresizingMaskIntoConstraints = NO;
return _tableView;
}
// Creates the tableview's width constraints and set their initial active state.
- (void)createTableViewWidthConstraint:(UILayoutGuide*)margins {
_portraitTableWidthConstraint = [_tableView.widthAnchor
constraintGreaterThanOrEqualToAnchor:margins.widthAnchor
multiplier:kPortraitTableViewWidthMultiplier];
_landscapeTableWidthConstraint = [_tableView.widthAnchor
constraintGreaterThanOrEqualToAnchor:margins.widthAnchor
multiplier:kLandscapeTableViewWidthMultiplier];
[self adjustTableViewWidthConstraint];
}
// Change the tableview's width constraint based on the screen's orientation.
- (void)adjustTableViewWidthConstraint {
BOOL isLandscape =
UIDeviceOrientationIsLandscape([UIDevice currentDevice].orientation);
_landscapeTableWidthConstraint.active = isLandscape;
_portraitTableWidthConstraint.active = !isLandscape;
}
// Loads the favicon associated with the provided cell.
// Defaults to the globe symbol if no URL is associated with the cell.
- (void)loadFaviconAtIndexPath:(NSIndexPath*)indexPath
forCell:(UITableViewCell*)cell {
DCHECK(cell);
TableViewURLCell* URLCell = base::mac::ObjCCastStrict<TableViewURLCell>(cell);
auto faviconLoadedBlock = ^(FaviconAttributes* attributes) {
DCHECK(attributes);
// It doesn't matter which cell the user sees here, all the credentials
// listed are for the same page and thus share the same favicon.
[URLCell.faviconView configureWithAttributes:attributes];
};
[self.delegate loadFaviconWithBlockHandler:faviconLoadedBlock];
}
// Sets the password bottom sheet's table view to full height.
- (void)expandTableView {
_minimizedHeightConstraint.active = NO;
_fullHeightConstraint.active = YES;
[self.view layoutIfNeeded];
}
// Notifies the delegate that a password suggestion was selected by the user.
- (void)didSelectSuggestion {
[self.delegate didSelectSuggestion:_row];
}
// Returns whether the provided index path points to the last row of the table
// view.
- (BOOL)isLastRow:(NSIndexPath*)indexPath {
return NSUInteger(indexPath.row) == (_suggestions.count - 1);
}
// Returns the cumulative height of the bottom sheet subviews.
- (CGFloat)cumulativeHeightOfSubviews {
[self.view layoutIfNeeded];
CGFloat subviewsHeight = 0;
// Add height of the bottom sheet subviews. This include the navigation bar,
// the scroll view, the actions stack view and the gradient view.
for (UIView* subview in self.view.subviews) {
subviewsHeight += CGRectGetHeight(subview.frame);
}
return subviewsHeight;
}
// Returns the initial height of the bottom sheet while showing a single row.
- (CGFloat)initialHeight {
CGFloat bottomSheetHeight = [self cumulativeHeightOfSubviews];
if (bottomSheetHeight > 0) {
return bottomSheetHeight;
}
// Return an estimated height if we can't calculate the actual height.
return kEstimatedBaseHeightForBottomSheet + kTableViewEstimatedRowHeight;
}
// Returns the desired height for the bottom sheet (can be larger than the
// screen).
- (CGFloat)fullHeight {
CGFloat bottomSheetHeight = [self cumulativeHeightOfSubviews];
// when this method is called, the table view has only one row and no padding.
CGFloat effectiveRowHeight = _tableView.contentSize.height;
// Add missing row height without calculating the one that is currently
// displayed, hence the -1.
if (bottomSheetHeight > 0 && effectiveRowHeight > 0) {
return bottomSheetHeight + (effectiveRowHeight * (_suggestions.count - 1));
}
// Return an estimated height for the bottom sheet while showing all rows
// (using estimated heights).
return kEstimatedBaseHeightForBottomSheet +
(kTableViewEstimatedRowHeight * _suggestions.count);
}
// Enables scrolling of the table view
- (void)setTableViewScrollEnabled:(BOOL)enabled {
_tableView.scrollEnabled = enabled;
self.scrollEnabled = enabled;
}
// Performs the expand bottom sheet animation.
- (void)expand {
UISheetPresentationController* presentationController =
self.sheetPresentationController;
if (@available(iOS 16, *)) {
// Expand to custom size (only available for iOS 16+).
CGFloat fullHeight = [self fullHeight];
__weak __typeof(self) weakSelf = self;
auto fullHeightBlock = ^CGFloat(
id<UISheetPresentationControllerDetentResolutionContext> context) {
BOOL tooLarge = (fullHeight > context.maximumDetentValue);
[weakSelf setTableViewScrollEnabled:tooLarge];
return tooLarge ? context.maximumDetentValue : fullHeight;
};
UISheetPresentationControllerDetent* customDetentExpand =
[UISheetPresentationControllerDetent
customDetentWithIdentifier:@"customDetentExpand"
resolver:fullHeightBlock];
NSMutableArray* currentDetents =
[presentationController.detents mutableCopy];
[currentDetents addObject:customDetentExpand];
presentationController.detents = [currentDetents copy];
[presentationController animateChanges:^{
presentationController.selectedDetentIdentifier = @"customDetentExpand";
}];
} else {
// Expand to large detent.
[self setTableViewScrollEnabled:YES];
[presentationController animateChanges:^{
presentationController.selectedDetentIdentifier =
UISheetPresentationControllerDetentIdentifierLarge;
}];
}
}
// Creates the UI action used to open the password manager.
- (UIAction*)openPasswordManagerAction {
__weak __typeof(self) weakSelf = self;
void (^passwordManagerButtonTapHandler)(UIAction*) = ^(UIAction* action) {
// Open Password Manager.
[weakSelf.delegate disableRefocus];
[weakSelf.handler displayPasswordManager];
};
UIImage* keyIcon =
CustomSymbolWithPointSize(kPasswordSymbol, kSymbolActionPointSize);
return [UIAction
actionWithTitle:l10n_util::GetNSString(
IDS_IOS_PASSWORD_BOTTOM_SHEET_PASSWORD_MANAGER)
image:keyIcon
identifier:nil
handler:passwordManagerButtonTapHandler];
}
// Creates the UI action used to open the password details for form suggestion
// at index path.
- (UIAction*)openPasswordDetailsForIndexPath:(NSIndexPath*)indexPath {
__weak __typeof(self) weakSelf = self;
FormSuggestion* formSuggestion = [_suggestions objectAtIndex:indexPath.row];
void (^showDetailsButtonTapHandler)(UIAction*) = ^(UIAction* action) {
// Open Password Details.
[weakSelf.delegate disableRefocus];
[weakSelf.handler displayPasswordDetailsForFormSuggestion:formSuggestion];
};
UIImage* infoIcon =
DefaultSymbolWithPointSize(kInfoCircleSymbol, kSymbolActionPointSize);
return
[UIAction actionWithTitle:l10n_util::GetNSString(
IDS_IOS_PASSWORD_BOTTOM_SHEET_SHOW_DETAILS)
image:infoIcon
identifier:nil
handler:showDetailsButtonTapHandler];
}
@end