| OLD | NEW |
| (Empty) | |
| 1 // Copyright 2017 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 #import "ios/chrome/browser/payments/payment_request_edit_view_controller.h" |
| 6 |
| 7 #include "base/logging.h" |
| 8 #import "base/mac/foundation_util.h" |
| 9 #include "base/strings/sys_string_conversions.h" |
| 10 #include "components/autofill/core/browser/field_types.h" |
| 11 #include "components/strings/grit/components_strings.h" |
| 12 #import "ios/chrome/browser/payments/cells/payments_text_item.h" |
| 13 #import "ios/chrome/browser/payments/payment_request_edit_view_controller+intern
al.h" |
| 14 #import "ios/chrome/browser/payments/payment_request_edit_view_controller_action
s.h" |
| 15 #import "ios/chrome/browser/payments/payment_request_editor_field.h" |
| 16 #import "ios/chrome/browser/ui/collection_view/cells/MDCCollectionViewCell+Chrom
e.h" |
| 17 #import "ios/chrome/browser/ui/collection_view/cells/collection_view_footer_item
.h" |
| 18 #import "ios/chrome/browser/ui/colors/MDCPalette+CrAdditions.h" |
| 19 #import "ios/chrome/browser/ui/settings/autofill_edit_accessory_view.h" |
| 20 #import "ios/chrome/browser/ui/settings/cells/autofill_edit_item.h" |
| 21 #import "ios/chrome/browser/ui/uikit_ui_util.h" |
| 22 #include "ios/chrome/grit/ios_theme_resources.h" |
| 23 #import "ios/third_party/material_components_ios/src/components/Typography/src/M
aterialTypography.h" |
| 24 #include "ui/base/l10n/l10n_util.h" |
| 25 |
| 26 #if !defined(__has_feature) || !__has_feature(objc_arc) |
| 27 #error "This file requires ARC support." |
| 28 #endif |
| 29 |
| 30 namespace { |
| 31 |
| 32 NSString* const kPaymentRequestEditCollectionViewID = |
| 33 @"kPaymentRequestEditCollectionViewID"; |
| 34 |
| 35 const CGFloat kSeparatorEdgeInset = 14; |
| 36 |
| 37 const CGFloat kFooterCellHorizontalPadding = 16; |
| 38 |
| 39 // Returns the AutofillEditCell that is the parent view of the |textField|. |
| 40 AutofillEditCell* AutofillEditCellForTextField(UITextField* textField) { |
| 41 for (UIView* view = textField; view; view = [view superview]) { |
| 42 AutofillEditCell* cell = base::mac::ObjCCast<AutofillEditCell>(view); |
| 43 if (cell) |
| 44 return cell; |
| 45 } |
| 46 |
| 47 // There has to be a cell associated with this text field. |
| 48 NOTREACHED(); |
| 49 return nil; |
| 50 } |
| 51 |
| 52 typedef NS_ENUM(NSInteger, SectionIdentifier) { |
| 53 SectionIdentifierFooter = kSectionIdentifierEnumZero, |
| 54 SectionIdentifierFirstTextField, |
| 55 }; |
| 56 |
| 57 typedef NS_ENUM(NSInteger, ItemType) { |
| 58 ItemTypeFooter = kItemTypeEnumZero, |
| 59 ItemTypeTextField, // This is a repeated item type. |
| 60 ItemTypeErrorMessage, // This is a repeated item type. |
| 61 }; |
| 62 |
| 63 } // namespace |
| 64 |
| 65 @interface PaymentRequestEditViewController ()< |
| 66 AutofillEditAccessoryDelegate, |
| 67 PaymentRequestEditViewControllerActions, |
| 68 PaymentRequestEditViewControllerValidator, |
| 69 UITextFieldDelegate> { |
| 70 NSArray<EditorField*>* _fields; |
| 71 |
| 72 // The currently focused cell. May be nil. |
| 73 __weak AutofillEditCell* _currentEditingCell; |
| 74 |
| 75 AutofillEditAccessoryView* _accessoryView; |
| 76 } |
| 77 |
| 78 // Returns the indexPath for the same row as that of |indexPath| in a section |
| 79 // with the given offset relative to that of |indexPath|. May return nil. |
| 80 - (NSIndexPath*)indexPathWithSectionOffset:(NSInteger)offset |
| 81 fromPath:(NSIndexPath*)indexPath; |
| 82 |
| 83 // Returns the text field with the given offset relative to the currently |
| 84 // focused text field. May return nil. |
| 85 - (AutofillEditCell*)nextTextFieldWithOffset:(NSInteger)offset; |
| 86 |
| 87 // Enables or disables the accessory view's previous and next buttons depending |
| 88 // on whether there is a text field before and after the currently focused text |
| 89 // field. |
| 90 - (void)updateAccessoryViewButtonsStates; |
| 91 |
| 92 @end |
| 93 |
| 94 @implementation PaymentRequestEditViewController |
| 95 |
| 96 @synthesize editorDelegate = _editorDelegate; |
| 97 @synthesize validatorDelegate = _validatorDelegate; |
| 98 |
| 99 - (instancetype)initWithEditorFields:(NSArray<EditorField*>*)fields { |
| 100 self = [super initWithStyle:CollectionViewControllerStyleAppBar]; |
| 101 if (self) { |
| 102 _fields = fields; |
| 103 |
| 104 // Set self as the validator delegate. |
| 105 _validatorDelegate = self; |
| 106 |
| 107 // Set up leading (cancel) button. |
| 108 UIBarButtonItem* cancelButton = [[UIBarButtonItem alloc] |
| 109 initWithTitle:l10n_util::GetNSString(IDS_CANCEL) |
| 110 style:UIBarButtonItemStylePlain |
| 111 target:nil |
| 112 action:@selector(onReturn)]; |
| 113 [cancelButton setTitleTextAttributes:@{ |
| 114 NSForegroundColorAttributeName : [UIColor lightGrayColor] |
| 115 } |
| 116 forState:UIControlStateDisabled]; |
| 117 [cancelButton |
| 118 setAccessibilityLabel:l10n_util::GetNSString(IDS_ACCNAME_CANCEL)]; |
| 119 [self navigationItem].leftBarButtonItem = cancelButton; |
| 120 |
| 121 // Set up trailing (done) button. |
| 122 UIBarButtonItem* doneButton = |
| 123 [[UIBarButtonItem alloc] initWithTitle:l10n_util::GetNSString(IDS_DONE) |
| 124 style:UIBarButtonItemStylePlain |
| 125 target:nil |
| 126 action:@selector(onDone)]; |
| 127 [doneButton setTitleTextAttributes:@{ |
| 128 NSForegroundColorAttributeName : [UIColor lightGrayColor] |
| 129 } |
| 130 forState:UIControlStateDisabled]; |
| 131 [doneButton setAccessibilityLabel:l10n_util::GetNSString(IDS_ACCNAME_DONE)]; |
| 132 [self navigationItem].rightBarButtonItem = doneButton; |
| 133 |
| 134 _accessoryView = [[AutofillEditAccessoryView alloc] initWithDelegate:self]; |
| 135 } |
| 136 return self; |
| 137 } |
| 138 |
| 139 #pragma mark - PaymentRequestEditViewControllerActions methods |
| 140 |
| 141 - (void)onReturn { |
| 142 [_editorDelegate paymentRequestEditViewControllerDidReturn:self]; |
| 143 } |
| 144 |
| 145 - (void)onDone { |
| 146 if ([self validateForm]) { |
| 147 [_editorDelegate paymentRequestEditViewController:self |
| 148 didFinishEditingFields:_fields]; |
| 149 } |
| 150 } |
| 151 |
| 152 - (void)viewDidAppear:(BOOL)animated { |
| 153 [super viewDidAppear:animated]; |
| 154 [[NSNotificationCenter defaultCenter] |
| 155 addObserver:self |
| 156 selector:@selector(keyboardDidShow) |
| 157 name:UIKeyboardDidShowNotification |
| 158 object:nil]; |
| 159 } |
| 160 |
| 161 - (void)viewWillDisappear:(BOOL)animated { |
| 162 [super viewWillDisappear:animated]; |
| 163 [[NSNotificationCenter defaultCenter] |
| 164 removeObserver:self |
| 165 name:UIKeyboardDidShowNotification |
| 166 object:nil]; |
| 167 } |
| 168 |
| 169 #pragma mark - CollectionViewController methods |
| 170 |
| 171 - (void)loadModel { |
| 172 [super loadModel]; |
| 173 CollectionViewModel* model = self.collectionViewModel; |
| 174 |
| 175 // Iterate over the fields and add the respective sections and items. |
| 176 int sectionIdentifier = static_cast<int>(SectionIdentifierFirstTextField); |
| 177 for (EditorField* field in _fields) { |
| 178 [model addSectionWithIdentifier:sectionIdentifier]; |
| 179 AutofillEditItem* item = |
| 180 [[AutofillEditItem alloc] initWithType:ItemTypeTextField]; |
| 181 item.textFieldName = field.label; |
| 182 item.textFieldEnabled = YES; |
| 183 item.textFieldValue = field.value; |
| 184 item.required = field.isRequired; |
| 185 item.autofillType = |
| 186 static_cast<autofill::ServerFieldType>(field.autofillType); |
| 187 [model addItem:item |
| 188 toSectionWithIdentifier:static_cast<NSInteger>(sectionIdentifier)]; |
| 189 field.item = item; |
| 190 field.sectionIdentifier = static_cast<NSInteger>(sectionIdentifier); |
| 191 ++sectionIdentifier; |
| 192 } |
| 193 |
| 194 [self loadFooterItems]; |
| 195 } |
| 196 |
| 197 - (void)viewDidLoad { |
| 198 [super viewDidLoad]; |
| 199 |
| 200 self.collectionView.accessibilityIdentifier = |
| 201 kPaymentRequestEditCollectionViewID; |
| 202 |
| 203 // Customize collection view settings. |
| 204 self.styler.cellStyle = MDCCollectionViewCellStyleCard; |
| 205 self.styler.separatorInset = |
| 206 UIEdgeInsetsMake(0, kSeparatorEdgeInset, 0, kSeparatorEdgeInset); |
| 207 } |
| 208 |
| 209 #pragma mark - UITextFieldDelegate |
| 210 |
| 211 - (void)textFieldDidBeginEditing:(UITextField*)textField { |
| 212 _currentEditingCell = AutofillEditCellForTextField(textField); |
| 213 [textField setInputAccessoryView:_accessoryView]; |
| 214 [self updateAccessoryViewButtonsStates]; |
| 215 } |
| 216 |
| 217 - (void)textFieldDidEndEditing:(UITextField*)textField { |
| 218 DCHECK(_currentEditingCell == AutofillEditCellForTextField(textField)); |
| 219 |
| 220 // Validate the text field. |
| 221 CollectionViewModel* model = self.collectionViewModel; |
| 222 |
| 223 NSIndexPath* indexPath = [self indexPathForCurrentTextField]; |
| 224 AutofillEditItem* item = base::mac::ObjCCastStrict<AutofillEditItem>( |
| 225 [model itemAtIndexPath:indexPath]); |
| 226 |
| 227 NSString* errorMessage = |
| 228 [_validatorDelegate paymentRequestEditViewController:self |
| 229 validateValue:textField.text |
| 230 autofillType:item.autofillType |
| 231 required:item.required]; |
| 232 NSInteger sectionIdentifier = |
| 233 [model sectionIdentifierForSection:[indexPath section]]; |
| 234 [self addOrRemoveErrorMessage:errorMessage |
| 235 inSectionWithIdentifier:sectionIdentifier]; |
| 236 |
| 237 [textField setInputAccessoryView:nil]; |
| 238 _currentEditingCell = nil; |
| 239 } |
| 240 |
| 241 - (BOOL)textFieldShouldReturn:(UITextField*)textField { |
| 242 DCHECK([_currentEditingCell textField] == textField); |
| 243 AutofillEditCell* nextCell = [self nextTextFieldWithOffset:1]; |
| 244 if (nextCell) |
| 245 [self nextPressed]; |
| 246 else |
| 247 [self closePressed]; |
| 248 |
| 249 return NO; |
| 250 } |
| 251 |
| 252 #pragma mark - AutofillEditAccessoryDelegate |
| 253 |
| 254 - (void)nextPressed { |
| 255 AutofillEditCell* nextCell = [self nextTextFieldWithOffset:1]; |
| 256 if (nextCell) |
| 257 [nextCell.textField becomeFirstResponder]; |
| 258 } |
| 259 |
| 260 - (void)previousPressed { |
| 261 AutofillEditCell* previousCell = [self nextTextFieldWithOffset:-1]; |
| 262 if (previousCell) |
| 263 [previousCell.textField becomeFirstResponder]; |
| 264 } |
| 265 |
| 266 - (void)closePressed { |
| 267 [[_currentEditingCell textField] resignFirstResponder]; |
| 268 } |
| 269 |
| 270 #pragma mark - UICollectionViewDataSource |
| 271 |
| 272 - (UICollectionViewCell*)collectionView:(UICollectionView*)collectionView |
| 273 cellForItemAtIndexPath:(NSIndexPath*)indexPath { |
| 274 UICollectionViewCell* cell = |
| 275 [super collectionView:collectionView cellForItemAtIndexPath:indexPath]; |
| 276 |
| 277 CollectionViewItem* item = |
| 278 [self.collectionViewModel itemAtIndexPath:indexPath]; |
| 279 switch (item.type) { |
| 280 case ItemTypeTextField: { |
| 281 AutofillEditCell* autofillEditCell = |
| 282 base::mac::ObjCCast<AutofillEditCell>(cell); |
| 283 autofillEditCell.textField.delegate = self; |
| 284 autofillEditCell.textField.clearButtonMode = UITextFieldViewModeNever; |
| 285 autofillEditCell.textLabel.font = [MDCTypography body2Font]; |
| 286 autofillEditCell.textLabel.textColor = [[MDCPalette greyPalette] tint900]; |
| 287 autofillEditCell.textField.font = [MDCTypography body1Font]; |
| 288 autofillEditCell.textField.textColor = |
| 289 [[MDCPalette cr_bluePalette] tint600]; |
| 290 break; |
| 291 } |
| 292 case ItemTypeErrorMessage: { |
| 293 PaymentsTextCell* errorMessageCell = |
| 294 base::mac::ObjCCastStrict<PaymentsTextCell>(cell); |
| 295 errorMessageCell.textLabel.font = [MDCTypography body1Font]; |
| 296 errorMessageCell.textLabel.textColor = |
| 297 [[MDCPalette cr_redPalette] tint600]; |
| 298 break; |
| 299 } |
| 300 case ItemTypeFooter: { |
| 301 CollectionViewFooterCell* footerCell = |
| 302 base::mac::ObjCCastStrict<CollectionViewFooterCell>(cell); |
| 303 footerCell.textLabel.font = [MDCTypography body2Font]; |
| 304 footerCell.textLabel.textColor = [[MDCPalette greyPalette] tint600]; |
| 305 footerCell.textLabel.shadowColor = nil; // No shadow. |
| 306 footerCell.horizontalPadding = kFooterCellHorizontalPadding; |
| 307 break; |
| 308 } |
| 309 default: |
| 310 break; |
| 311 } |
| 312 |
| 313 return cell; |
| 314 } |
| 315 |
| 316 #pragma mark MDCCollectionViewStylingDelegate |
| 317 |
| 318 - (CGFloat)collectionView:(UICollectionView*)collectionView |
| 319 cellHeightAtIndexPath:(NSIndexPath*)indexPath { |
| 320 CollectionViewItem* item = |
| 321 [self.collectionViewModel itemAtIndexPath:indexPath]; |
| 322 switch (item.type) { |
| 323 case ItemTypeTextField: |
| 324 case ItemTypeErrorMessage: |
| 325 case ItemTypeFooter: |
| 326 return [MDCCollectionViewCell |
| 327 cr_preferredHeightForWidth:CGRectGetWidth(collectionView.bounds) |
| 328 forItem:item]; |
| 329 default: |
| 330 NOTREACHED(); |
| 331 return MDCCellDefaultOneLineHeight; |
| 332 } |
| 333 } |
| 334 |
| 335 - (BOOL)collectionView:(UICollectionView*)collectionView |
| 336 hidesInkViewAtIndexPath:(NSIndexPath*)indexPath { |
| 337 NSInteger type = [self.collectionViewModel itemTypeForIndexPath:indexPath]; |
| 338 switch (type) { |
| 339 case ItemTypeErrorMessage: |
| 340 case ItemTypeFooter: |
| 341 return YES; |
| 342 default: |
| 343 return NO; |
| 344 } |
| 345 } |
| 346 |
| 347 - (BOOL)collectionView:(UICollectionView*)collectionView |
| 348 shouldHideItemBackgroundAtIndexPath:(NSIndexPath*)indexPath { |
| 349 NSInteger type = [self.collectionViewModel itemTypeForIndexPath:indexPath]; |
| 350 switch (type) { |
| 351 case ItemTypeFooter: |
| 352 return YES; |
| 353 default: |
| 354 return NO; |
| 355 } |
| 356 } |
| 357 |
| 358 #pragma mark - PaymentRequestEditViewControllerValidator |
| 359 |
| 360 - (NSString*)paymentRequestEditViewController: |
| 361 (PaymentRequestEditViewController*)controller |
| 362 validateValue:(NSString*)value |
| 363 autofillType:(NSInteger)autofillType |
| 364 required:(BOOL)required { |
| 365 if (required && value.length == 0) { |
| 366 return l10n_util::GetNSString( |
| 367 IDS_PAYMENTS_FIELD_REQUIRED_VALIDATION_MESSAGE); |
| 368 } |
| 369 return @""; |
| 370 } |
| 371 |
| 372 #pragma mark - Helper methods |
| 373 |
| 374 - (NSIndexPath*)indexPathWithSectionOffset:(NSInteger)offset |
| 375 fromPath:(NSIndexPath*)indexPath { |
| 376 DCHECK(indexPath); |
| 377 DCHECK(offset); |
| 378 NSInteger nextSection = [indexPath section] + offset; |
| 379 if (nextSection >= 0 && |
| 380 nextSection < [[self collectionView] numberOfSections]) { |
| 381 return [NSIndexPath indexPathForRow:[indexPath row] inSection:nextSection]; |
| 382 } |
| 383 return nil; |
| 384 } |
| 385 |
| 386 - (AutofillEditCell*)nextTextFieldWithOffset:(NSInteger)offset { |
| 387 UICollectionView* collectionView = [self collectionView]; |
| 388 NSIndexPath* currentCellPath = [self indexPathForCurrentTextField]; |
| 389 DCHECK(currentCellPath); |
| 390 NSIndexPath* nextCellPath = |
| 391 [self indexPathWithSectionOffset:offset fromPath:currentCellPath]; |
| 392 if (nextCellPath) { |
| 393 id nextCell = [collectionView cellForItemAtIndexPath:nextCellPath]; |
| 394 if ([nextCell isKindOfClass:[AutofillEditCell class]]) { |
| 395 return base::mac::ObjCCastStrict<AutofillEditCell>( |
| 396 [collectionView cellForItemAtIndexPath:nextCellPath]); |
| 397 } |
| 398 } |
| 399 return nil; |
| 400 } |
| 401 |
| 402 - (void)updateAccessoryViewButtonsStates { |
| 403 AutofillEditCell* previousCell = [self nextTextFieldWithOffset:-1]; |
| 404 [[_accessoryView previousButton] setEnabled:previousCell != nil]; |
| 405 |
| 406 AutofillEditCell* nextCell = [self nextTextFieldWithOffset:1]; |
| 407 [[_accessoryView nextButton] setEnabled:nextCell != nil]; |
| 408 } |
| 409 |
| 410 #pragma mark - Keyboard handling |
| 411 |
| 412 - (void)keyboardDidShow { |
| 413 [self.collectionView |
| 414 scrollToItemAtIndexPath:[self.collectionView |
| 415 indexPathForCell:_currentEditingCell] |
| 416 atScrollPosition:UICollectionViewScrollPositionCenteredVertically |
| 417 animated:YES]; |
| 418 } |
| 419 |
| 420 @end |
| 421 |
| 422 @implementation PaymentRequestEditViewController (Internal) |
| 423 |
| 424 - (BOOL)validateForm { |
| 425 for (EditorField* field in _fields) { |
| 426 AutofillEditItem* item = field.item; |
| 427 |
| 428 NSString* errorMessage = |
| 429 [_validatorDelegate paymentRequestEditViewController:self |
| 430 validateValue:item.textFieldValue |
| 431 autofillType:field.autofillType |
| 432 required:field.isRequired]; |
| 433 [self addOrRemoveErrorMessage:errorMessage |
| 434 inSectionWithIdentifier:field.sectionIdentifier]; |
| 435 if (errorMessage.length != 0) { |
| 436 return NO; |
| 437 } |
| 438 |
| 439 field.value = item.textFieldValue; |
| 440 } |
| 441 return YES; |
| 442 } |
| 443 |
| 444 - (void)loadFooterItems { |
| 445 CollectionViewModel* model = self.collectionViewModel; |
| 446 |
| 447 [model addSectionWithIdentifier:SectionIdentifierFooter]; |
| 448 CollectionViewFooterItem* footerItem = |
| 449 [[CollectionViewFooterItem alloc] initWithType:ItemTypeFooter]; |
| 450 footerItem.text = l10n_util::GetNSString(IDS_PAYMENTS_REQUIRED_FIELD_MESSAGE); |
| 451 [model addItem:footerItem toSectionWithIdentifier:SectionIdentifierFooter]; |
| 452 } |
| 453 |
| 454 - (NSIndexPath*)indexPathForCurrentTextField { |
| 455 DCHECK(_currentEditingCell); |
| 456 NSIndexPath* indexPath = |
| 457 [[self collectionView] indexPathForCell:_currentEditingCell]; |
| 458 DCHECK(indexPath); |
| 459 return indexPath; |
| 460 } |
| 461 |
| 462 - (void)addOrRemoveErrorMessage:(NSString*)errorMessage |
| 463 inSectionWithIdentifier:(NSInteger)sectionIdentifier { |
| 464 CollectionViewModel* model = self.collectionViewModel; |
| 465 if ([model hasItemForItemType:ItemTypeErrorMessage |
| 466 sectionIdentifier:sectionIdentifier]) { |
| 467 NSIndexPath* indexPath = [model indexPathForItemType:ItemTypeErrorMessage |
| 468 sectionIdentifier:sectionIdentifier]; |
| 469 if (errorMessage.length == 0) { |
| 470 // Remove the item at the index path. |
| 471 [model removeItemWithType:ItemTypeErrorMessage |
| 472 fromSectionWithIdentifier:sectionIdentifier]; |
| 473 [self.collectionView deleteItemsAtIndexPaths:@[ indexPath ]]; |
| 474 } else { |
| 475 // Reload the item at the index path. |
| 476 PaymentsTextItem* item = base::mac::ObjCCastStrict<PaymentsTextItem>( |
| 477 [model itemAtIndexPath:indexPath]); |
| 478 item.text = errorMessage; |
| 479 [self.collectionView reloadItemsAtIndexPaths:@[ indexPath ]]; |
| 480 } |
| 481 } else if (errorMessage.length != 0) { |
| 482 // Insert an item at the index path. |
| 483 PaymentsTextItem* errorMessageItem = |
| 484 [[PaymentsTextItem alloc] initWithType:ItemTypeErrorMessage]; |
| 485 errorMessageItem.text = errorMessage; |
| 486 errorMessageItem.image = NativeImage(IDR_IOS_PAYMENTS_WARNING); |
| 487 [model addItem:errorMessageItem toSectionWithIdentifier:sectionIdentifier]; |
| 488 NSIndexPath* indexPath = [model indexPathForItemType:ItemTypeErrorMessage |
| 489 sectionIdentifier:sectionIdentifier]; |
| 490 [self.collectionView insertItemsAtIndexPaths:@[ indexPath ]]; |
| 491 } |
| 492 } |
| 493 |
| 494 @end |
| OLD | NEW |