blob: 0e2ce217c067cd1f7cff7e1e88d15c1abdec566f [file] [log] [blame]
// Copyright 2016 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/payments/payment_request_view_controller.h"
#include "base/mac/foundation_util.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "components/autofill/core/browser/autofill_data_util.h"
#include "components/autofill/core/browser/autofill_profile.h"
#include "components/autofill/core/browser/credit_card.h"
#include "components/autofill/core/browser/field_types.h"
#include "components/autofill/core/browser/personal_data_manager.h"
#include "components/payments/currency_formatter.h"
#include "components/strings/grit/components_strings.h"
#import "ios/chrome/browser/payments/cells/page_info_item.h"
#import "ios/chrome/browser/payments/cells/payment_method_item.h"
#import "ios/chrome/browser/payments/cells/price_item.h"
#import "ios/chrome/browser/payments/cells/shipping_address_item.h"
#import "ios/chrome/browser/payments/payment_request_util.h"
#import "ios/chrome/browser/ui/collection_view/cells/MDCCollectionViewCell+Chrome.h"
#import "ios/chrome/browser/ui/collection_view/cells/collection_view_detail_item.h"
#import "ios/chrome/browser/ui/collection_view/cells/collection_view_item.h"
#import "ios/chrome/browser/ui/collection_view/cells/collection_view_text_item.h"
#import "ios/chrome/browser/ui/collection_view/collection_view_model.h"
#import "ios/chrome/browser/ui/colors/MDCPalette+CrAdditions.h"
#include "ios/chrome/browser/ui/rtl_geometry.h"
#include "ios/chrome/browser/ui/uikit_ui_util.h"
#include "ios/chrome/grit/ios_strings.h"
#import "ios/third_party/material_components_ios/src/components/Buttons/src/MaterialButtons.h"
#import "ios/third_party/material_components_ios/src/components/CollectionCells/src/MaterialCollectionCells.h"
#import "ios/third_party/material_components_ios/src/components/Typography/src/MaterialTypography.h"
#import "ios/third_party/material_roboto_font_loader_ios/src/src/MaterialRobotoFontLoader.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
using payment_request_util::NameLabelFromAutofillProfile;
using payment_request_util::AddressLabelFromAutofillProfile;
using payment_request_util::PhoneNumberLabelFromAutofillProfile;
NSString* const kPaymentRequestCollectionViewId =
@"kPaymentRequestCollectionViewId";
namespace {
const CGFloat kButtonEdgeInset = 9;
const CGFloat kSeparatorEdgeInset = 14;
typedef NS_ENUM(NSInteger, SectionIdentifier) {
SectionIdentifierSummary = kSectionIdentifierEnumZero,
SectionIdentifierShipping,
SectionIdentifierPayment,
};
typedef NS_ENUM(NSInteger, ItemType) {
ItemTypeSummaryPageInfo = kItemTypeEnumZero,
ItemTypeSummaryTotal,
ItemTypeShippingTitle,
ItemTypeShippingAddress,
ItemTypeAddShippingAddress,
ItemTypeShippingOption,
ItemTypeSelectShippingOption,
ItemTypePaymentTitle,
ItemTypePaymentMethod,
ItemTypeAddPaymentMethod,
};
} // namespace
@interface PaymentRequestViewController () {
UIBarButtonItem* _cancelButton;
MDCFlatButton* _payButton;
// The PaymentRequest object owning an instance of web::PaymentRequest as
// provided by the page invoking the Payment Request API. This is a weak
// pointer and should outlive this class.
PaymentRequest* _paymentRequest;
PriceItem* _paymentSummaryItem;
ShippingAddressItem* _selectedShippingAddressItem;
CollectionViewTextItem* _selectedShippingOptionItem;
PaymentMethodItem* _selectedPaymentMethodItem;
}
// Called when the user presses the cancel button.
- (void)onCancel;
// Called when the user presses the confirm button.
- (void)onConfirm;
@end
@implementation PaymentRequestViewController
@synthesize pageFavicon = _pageFavicon;
@synthesize pageTitle = _pageTitle;
@synthesize pageHost = _pageHost;
@synthesize delegate = _delegate;
- (instancetype)initWithPaymentRequest:(PaymentRequest*)paymentRequest {
DCHECK(paymentRequest);
if ((self = [super initWithStyle:CollectionViewControllerStyleAppBar])) {
[self setTitle:l10n_util::GetNSString(IDS_IOS_PAYMENT_REQUEST_TITLE)];
// Set up left (cancel) button.
_cancelButton = [[UIBarButtonItem alloc]
initWithTitle:l10n_util::GetNSString(
IDS_IOS_PAYMENT_REQUEST_CANCEL_BUTTON)
style:UIBarButtonItemStylePlain
target:nil
action:@selector(onCancel)];
[_cancelButton setTitleTextAttributes:@{
NSForegroundColorAttributeName : [UIColor lightGrayColor]
}
forState:UIControlStateDisabled];
[_cancelButton
setAccessibilityLabel:l10n_util::GetNSString(IDS_ACCNAME_CANCEL)];
[self navigationItem].leftBarButtonItem = _cancelButton;
// Set up right (pay) button.
_payButton = [[MDCFlatButton alloc] init];
[_payButton
setTitle:l10n_util::GetNSString(IDS_IOS_PAYMENT_REQUEST_PAY_BUTTON)
forState:UIControlStateNormal];
[_payButton setBackgroundColor:[[MDCPalette cr_bluePalette] tint500]
forState:UIControlStateNormal];
[_payButton setInkColor:[UIColor colorWithWhite:1 alpha:0.2]];
[_payButton setBackgroundColor:[UIColor grayColor]
forState:UIControlStateDisabled];
[_payButton addTarget:nil
action:@selector(onConfirm)
forControlEvents:UIControlEventTouchUpInside];
[_payButton sizeToFit];
[_payButton setEnabled:(paymentRequest->selected_credit_card() != nil)];
[_payButton setAutoresizingMask:UIViewAutoresizingFlexibleTrailingMargin() |
UIViewAutoresizingFlexibleTopMargin |
UIViewAutoresizingFlexibleBottomMargin];
// The navigation bar will set the rightBarButtonItem's height to the full
// height of the bar. We don't want that for the button so we use a UIView
// here to contain the button instead and the button is vertically centered
// inside the full bar height.
UIView* buttonView = [[UIView alloc] initWithFrame:CGRectZero];
[buttonView addSubview:_payButton];
// Navigation bar button items are aligned with the trailing edge of the
// screen. Make the enclosing view larger here. The pay button will be
// aligned with the leading edge of the enclosing view leaving an inset on
// the trailing edge.
CGRect buttonViewBounds = buttonView.bounds;
buttonViewBounds.size.width =
[_payButton frame].size.width + kButtonEdgeInset;
buttonView.bounds = buttonViewBounds;
UIBarButtonItem* payButtonItem =
[[UIBarButtonItem alloc] initWithCustomView:buttonView];
[self navigationItem].rightBarButtonItem = payButtonItem;
_paymentRequest = paymentRequest;
}
return self;
}
- (void)onCancel {
[_delegate paymentRequestViewControllerDidCancel:self];
}
- (void)onConfirm {
[_delegate paymentRequestViewControllerDidConfirm:self];
}
#pragma mark - CollectionViewController methods
- (void)loadModel {
[super loadModel];
CollectionViewModel* model = self.collectionViewModel;
// Summary section.
[model addSectionWithIdentifier:SectionIdentifierSummary];
PageInfoItem* pageInfo =
[[PageInfoItem alloc] initWithType:ItemTypeSummaryPageInfo];
pageInfo.pageFavicon = _pageFavicon;
pageInfo.pageTitle = _pageTitle;
pageInfo.pageHost = _pageHost;
[model setHeader:pageInfo forSectionWithIdentifier:SectionIdentifierSummary];
_paymentSummaryItem = [[PriceItem alloc] initWithType:ItemTypeSummaryTotal];
[self fillPaymentSummaryItem:_paymentSummaryItem
withPaymentItem:_paymentRequest->payment_details().total
withTotalValueChanged:NO];
if (!_paymentRequest->payment_details().display_items.empty()) {
_paymentSummaryItem.accessoryType =
MDCCollectionViewCellAccessoryDisclosureIndicator;
_paymentSummaryItem.accessibilityTraits |= UIAccessibilityTraitButton;
}
[model addItem:_paymentSummaryItem
toSectionWithIdentifier:SectionIdentifierSummary];
// Shipping section.
[model addSectionWithIdentifier:SectionIdentifierShipping];
CollectionViewTextItem* shippingTitle =
[[CollectionViewTextItem alloc] initWithType:ItemTypeShippingTitle];
shippingTitle.text =
payment_request_util::GetShippingSectionTitle(_paymentRequest);
[model setHeader:shippingTitle
forSectionWithIdentifier:SectionIdentifierShipping];
CollectionViewItem* shippingAddressItem = nil;
if (_paymentRequest->selected_shipping_profile()) {
_selectedShippingAddressItem =
[[ShippingAddressItem alloc] initWithType:ItemTypeShippingAddress];
shippingAddressItem = _selectedShippingAddressItem;
[self fillShippingAddressItem:_selectedShippingAddressItem
withAddress:_paymentRequest->selected_shipping_profile()];
_selectedShippingAddressItem.accessoryType =
MDCCollectionViewCellAccessoryDisclosureIndicator;
} else {
CollectionViewDetailItem* addAddressItem = [[CollectionViewDetailItem alloc]
initWithType:ItemTypeAddShippingAddress];
shippingAddressItem = addAddressItem;
addAddressItem.text =
payment_request_util::GetShippingAddressSelectorTitle(_paymentRequest);
addAddressItem.detailText = [l10n_util::GetNSString(
IDS_IOS_PAYMENT_REQUEST_ADD_SHIPPING_ADDRESS_BUTTON)
uppercaseStringWithLocale:[NSLocale currentLocale]];
}
shippingAddressItem.accessibilityTraits |= UIAccessibilityTraitButton;
[model addItem:shippingAddressItem
toSectionWithIdentifier:SectionIdentifierShipping];
CollectionViewItem* shippingOptionItem = nil;
if (_paymentRequest->selected_shipping_option()) {
_selectedShippingOptionItem =
[[CollectionViewTextItem alloc] initWithType:ItemTypeShippingOption];
shippingOptionItem = _selectedShippingOptionItem;
[self fillShippingOptionItem:_selectedShippingOptionItem
withOption:_paymentRequest->selected_shipping_option()];
_selectedShippingOptionItem.accessoryType =
MDCCollectionViewCellAccessoryDisclosureIndicator;
} else {
CollectionViewDetailItem* selectShippingOptionItem =
[[CollectionViewDetailItem alloc]
initWithType:ItemTypeSelectShippingOption];
shippingOptionItem = selectShippingOptionItem;
selectShippingOptionItem.text =
payment_request_util::GetShippingOptionSelectorTitle(_paymentRequest);
selectShippingOptionItem.accessoryType =
MDCCollectionViewCellAccessoryDisclosureIndicator;
}
shippingOptionItem.accessibilityTraits |= UIAccessibilityTraitButton;
[model addItem:shippingOptionItem
toSectionWithIdentifier:SectionIdentifierShipping];
// Payment method section.
[model addSectionWithIdentifier:SectionIdentifierPayment];
CollectionViewItem* paymentMethodItem = nil;
if (_paymentRequest->selected_credit_card()) {
CollectionViewTextItem* paymentTitle =
[[CollectionViewTextItem alloc] initWithType:ItemTypePaymentTitle];
paymentTitle.text =
l10n_util::GetNSString(IDS_IOS_PAYMENT_REQUEST_PAYMENT_METHOD_HEADER);
[model setHeader:paymentTitle
forSectionWithIdentifier:SectionIdentifierPayment];
_selectedPaymentMethodItem =
[[PaymentMethodItem alloc] initWithType:ItemTypePaymentMethod];
paymentMethodItem = _selectedPaymentMethodItem;
[self fillPaymentMethodItem:_selectedPaymentMethodItem
withPaymentMethod:_paymentRequest->selected_credit_card()];
_selectedPaymentMethodItem.accessoryType =
MDCCollectionViewCellAccessoryDisclosureIndicator;
} else {
CollectionViewDetailItem* addPaymentMethodItem = [
[CollectionViewDetailItem alloc] initWithType:ItemTypeAddPaymentMethod];
paymentMethodItem = addPaymentMethodItem;
addPaymentMethodItem.text =
l10n_util::GetNSString(IDS_IOS_PAYMENT_REQUEST_PAYMENT_METHOD_HEADER);
addPaymentMethodItem.detailText = [l10n_util::GetNSString(
IDS_IOS_PAYMENT_REQUEST_ADD_SHIPPING_ADDRESS_BUTTON)
uppercaseStringWithLocale:[NSLocale currentLocale]];
}
paymentMethodItem.accessibilityTraits |= UIAccessibilityTraitButton;
[model addItem:paymentMethodItem
toSectionWithIdentifier:SectionIdentifierPayment];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.collectionView.accessibilityIdentifier = kPaymentRequestCollectionViewId;
// Customize collection view settings.
self.styler.cellStyle = MDCCollectionViewCellStyleCard;
self.styler.separatorInset =
UIEdgeInsetsMake(0, kSeparatorEdgeInset, 0, kSeparatorEdgeInset);
}
- (void)updatePaymentSummaryWithTotalValueChanged:(BOOL)totalValueChanged {
[self fillPaymentSummaryItem:_paymentSummaryItem
withPaymentItem:_paymentRequest->payment_details().total
withTotalValueChanged:totalValueChanged];
NSIndexPath* indexPath =
[self.collectionViewModel indexPathForItem:_paymentSummaryItem
inSectionWithIdentifier:SectionIdentifierSummary];
[self.collectionView reloadItemsAtIndexPaths:@[ indexPath ]];
}
- (void)updateSelectedShippingAddressUI {
[self fillShippingAddressItem:_selectedShippingAddressItem
withAddress:_paymentRequest->selected_shipping_profile()];
NSIndexPath* indexPath =
[self.collectionViewModel indexPathForItem:_selectedShippingAddressItem
inSectionWithIdentifier:SectionIdentifierShipping];
[self.collectionView reloadItemsAtIndexPaths:@[ indexPath ]];
}
- (void)updateSelectedShippingOptionUI {
[self fillShippingOptionItem:_selectedShippingOptionItem
withOption:_paymentRequest->selected_shipping_option()];
NSIndexPath* indexPath =
[self.collectionViewModel indexPathForItem:_selectedShippingOptionItem
inSectionWithIdentifier:SectionIdentifierShipping];
[self.collectionView reloadItemsAtIndexPaths:@[ indexPath ]];
}
- (void)updateSelectedPaymentMethodUI {
[self fillPaymentMethodItem:_selectedPaymentMethodItem
withPaymentMethod:_paymentRequest->selected_credit_card()];
NSIndexPath* indexPath =
[self.collectionViewModel indexPathForItem:_selectedPaymentMethodItem
inSectionWithIdentifier:SectionIdentifierPayment];
[self.collectionView reloadItemsAtIndexPaths:@[ indexPath ]];
}
#pragma mark - Helper methods
- (void)fillPaymentSummaryItem:(PriceItem*)item
withPaymentItem:(web::PaymentItem)paymentItem
withTotalValueChanged:(BOOL)totalValueChanged {
item.item = l10n_util::GetNSString(IDS_IOS_PAYMENT_REQUEST_TOTAL_HEADER);
payments::CurrencyFormatter* currencyFormatter =
_paymentRequest->GetOrCreateCurrencyFormatter();
item.price = SysUTF16ToNSString(l10n_util::GetStringFUTF16(
IDS_IOS_PAYMENT_REQUEST_PAYMENT_ITEMS_TOTAL_FORMAT,
base::UTF8ToUTF16(currencyFormatter->formatted_currency_code()),
currencyFormatter->Format(base::UTF16ToASCII(paymentItem.amount.value))));
item.notification =
totalValueChanged
? l10n_util::GetNSString(IDS_IOS_PAYMENT_REQUEST_TOTAL_UPDATED_LABEL)
: nil;
}
- (void)fillShippingAddressItem:(ShippingAddressItem*)item
withAddress:(autofill::AutofillProfile*)address {
item.name = NameLabelFromAutofillProfile(address);
item.address = AddressLabelFromAutofillProfile(address);
item.phoneNumber = PhoneNumberLabelFromAutofillProfile(address);
}
- (void)fillShippingOptionItem:(CollectionViewTextItem*)item
withOption:(web::PaymentShippingOption*)option {
item.text = base::SysUTF16ToNSString(option->label);
payments::CurrencyFormatter* currencyFormatter =
_paymentRequest->GetOrCreateCurrencyFormatter();
item.detailText = SysUTF16ToNSString(
currencyFormatter->Format(base::UTF16ToASCII(option->amount.value)));
}
- (void)fillPaymentMethodItem:(PaymentMethodItem*)item
withPaymentMethod:(autofill::CreditCard*)creditCard {
item.methodID = base::SysUTF16ToNSString(creditCard->TypeAndLastFourDigits());
item.methodDetail = base::SysUTF16ToNSString(
creditCard->GetRawInfo(autofill::CREDIT_CARD_NAME_FULL));
int selectedMethodCardTypeIconID =
autofill::data_util::GetPaymentRequestData(creditCard->type())
.icon_resource_id;
item.methodTypeIcon = NativeImage(selectedMethodCardTypeIconID);
}
#pragma mark UICollectionViewDataSource
- (UICollectionViewCell*)collectionView:(UICollectionView*)collectionView
cellForItemAtIndexPath:(nonnull NSIndexPath*)indexPath {
UICollectionViewCell* cell =
[super collectionView:collectionView cellForItemAtIndexPath:indexPath];
NSInteger itemType =
[self.collectionViewModel itemTypeForIndexPath:indexPath];
switch (itemType) {
case ItemTypeAddShippingAddress: {
CollectionViewDetailCell* detailCell =
base::mac::ObjCCastStrict<CollectionViewDetailCell>(cell);
detailCell.detailTextLabel.font = [MDCTypography body2Font];
detailCell.detailTextLabel.textColor =
[[MDCPalette cr_bluePalette] tint700];
break;
}
case ItemTypeShippingOption: {
MDCCollectionViewTextCell* textCell =
base::mac::ObjCCastStrict<MDCCollectionViewTextCell>(cell);
textCell.textLabel.font = [MDCTypography body2Font];
textCell.textLabel.textColor = [[MDCPalette greyPalette] tint900];
textCell.detailTextLabel.font = [MDCTypography body1Font];
textCell.detailTextLabel.textColor = [[MDCPalette greyPalette] tint900];
break;
}
default:
break;
}
return cell;
}
#pragma mark UICollectionViewDelegate
- (void)collectionView:(UICollectionView*)collectionView
didSelectItemAtIndexPath:(NSIndexPath*)indexPath {
[super collectionView:collectionView didSelectItemAtIndexPath:indexPath];
NSInteger itemType =
[self.collectionViewModel itemTypeForIndexPath:indexPath];
switch (itemType) {
case ItemTypeSummaryTotal:
if (!_paymentRequest->payment_details().display_items.empty())
[_delegate
paymentRequestViewControllerDidSelectPaymentSummaryItem:self];
break;
case ItemTypeShippingAddress:
case ItemTypeAddShippingAddress:
[_delegate paymentRequestViewControllerDidSelectShippingAddressItem:self];
break;
case ItemTypeShippingOption:
case ItemTypeSelectShippingOption:
[_delegate paymentRequestViewControllerDidSelectShippingOptionItem:self];
break;
case ItemTypePaymentMethod:
case ItemTypeAddPaymentMethod:
[_delegate paymentRequestViewControllerDidSelectPaymentMethodItem:self];
break;
default:
NOTREACHED();
break;
}
}
#pragma mark MDCCollectionViewStylingDelegate
- (CGFloat)collectionView:(UICollectionView*)collectionView
cellHeightAtIndexPath:(NSIndexPath*)indexPath {
CollectionViewItem* item =
[self.collectionViewModel itemAtIndexPath:indexPath];
switch (item.type) {
case ItemTypeShippingAddress:
case ItemTypePaymentMethod:
return [MDCCollectionViewCell
cr_preferredHeightForWidth:CGRectGetWidth(collectionView.bounds)
forItem:item];
case ItemTypeShippingOption:
return MDCCellDefaultTwoLineHeight;
case ItemTypeSummaryPageInfo:
case ItemTypeSummaryTotal:
case ItemTypeShippingTitle:
case ItemTypeAddShippingAddress:
case ItemTypeSelectShippingOption:
case ItemTypePaymentTitle:
case ItemTypeAddPaymentMethod:
return MDCCellDefaultOneLineHeight;
default:
NOTREACHED();
return MDCCellDefaultOneLineHeight;
}
}
// If there are no payment items to display, there is no effect from touching
// the total so there should not be an ink ripple.
- (BOOL)collectionView:(UICollectionView*)collectionView
hidesInkViewAtIndexPath:(NSIndexPath*)indexPath {
NSInteger type = [self.collectionViewModel itemTypeForIndexPath:indexPath];
if (type == ItemTypeSummaryTotal &&
_paymentRequest->payment_details().display_items.empty()) {
return YES;
} else {
return NO;
}
}
@end