blob: c9b22f7b8a42dee27414be9e0af52316841c23af [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/ui/payments/payment_request_manager.h"
#include <algorithm>
#include <memory>
#include <set>
#include <string>
#include <unordered_map>
#include "base/feature_list.h"
#include "base/ios/block_types.h"
#include "base/ios/ios_util.h"
#include "base/json/json_reader.h"
#include "base/logging.h"
#import "base/mac/bind_objc_block.h"
#include "base/mac/foundation_util.h"
#include "base/memory/ptr_util.h"
#include "base/strings/string16.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#import "base/values.h"
#include "components/autofill/core/browser/autofill_manager.h"
#include "components/autofill/core/browser/credit_card.h"
#include "components/autofill/core/browser/personal_data_manager.h"
#include "components/autofill/ios/browser/autofill_driver_ios.h"
#include "components/payments/core/can_make_payment_query.h"
#include "components/payments/core/features.h"
#include "components/payments/core/journey_logger.h"
#include "components/payments/core/payment_address.h"
#include "components/payments/core/payment_details.h"
#include "components/payments/core/payment_details_validation.h"
#include "components/payments/core/payment_instrument.h"
#include "components/payments/core/payment_prefs.h"
#include "components/payments/core/payment_request_base_delegate.h"
#include "components/payments/core/payment_request_data_util.h"
#include "components/payments/core/payment_shipping_option.h"
#include "components/prefs/pref_service.h"
#include "components/url_formatter/elide_url.h"
#include "ios/chrome/browser/autofill/personal_data_manager_factory.h"
#include "ios/chrome/browser/autofill/validation_rules_storage_factory.h"
#include "ios/chrome/browser/browser_state/chrome_browser_state.h"
#include "ios/chrome/browser/payments/ios_can_make_payment_query_factory.h"
#include "ios/chrome/browser/payments/ios_payment_instrument_launcher.h"
#include "ios/chrome/browser/payments/ios_payment_instrument_launcher_factory.h"
#include "ios/chrome/browser/payments/ios_payment_request_cache_factory.h"
#include "ios/chrome/browser/payments/payment_request.h"
#import "ios/chrome/browser/payments/payment_request_cache.h"
#import "ios/chrome/browser/payments/payment_response_helper.h"
#include "ios/chrome/browser/procedural_block_types.h"
#import "ios/chrome/browser/ui/commands/application_commands.h"
#import "ios/chrome/browser/ui/payments/js_payment_request_manager.h"
#import "ios/chrome/browser/ui/payments/payment_request_coordinator.h"
#include "ios/chrome/browser/ui/toolbar/toolbar_model_ios.h"
#include "ios/web/public/favicon_status.h"
#include "ios/web/public/navigation_item.h"
#include "ios/web/public/navigation_manager.h"
#include "ios/web/public/origin_util.h"
#include "ios/web/public/payments/payment_request.h"
#include "ios/web/public/ssl_status.h"
#import "ios/web/public/url_scheme_util.h"
#import "ios/web/public/web_state/js/crw_js_injection_receiver.h"
#import "ios/web/public/web_state/navigation_context.h"
#import "ios/web/public/web_state/ui/crw_web_view_proxy.h"
#include "ios/web/public/web_state/url_verification_constants.h"
#include "ios/web/public/web_state/web_state.h"
#import "ios/web/public/web_state/web_state_observer_bridge.h"
#include "third_party/libaddressinput/chromium/chrome_metadata_source.h"
#include "third_party/libaddressinput/chromium/chrome_storage_impl.h"
#include "url/gurl.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
NSString* const kAbortError = @"AbortError";
NSString* const kInvalidStateError = @"InvalidStateError";
NSString* const kNotAllowedError = @"NotAllowedError";
NSString* const kNotSupportedError = @"NotSupportedError";
namespace {
// Command prefix for injected JavaScript.
const char kCommandPrefix[] = "paymentRequest";
// Time interval between attempts to unblock the webview's JS event queue.
const NSTimeInterval kNoopInterval = 0.1;
// Time interval before closing the UI if the page has not yet called
// PaymentResponse.complete().
const NSTimeInterval kTimeoutInterval = 60.0;
// Error messages used in Payment Request API.
NSString* const kCancelErrorMessage = @"Request cancelled";
struct PendingPaymentResponse {
std::string methodName;
std::string stringifiedDetails;
autofill::AutofillProfile shippingAddress;
autofill::AutofillProfile contactAddress;
};
} // namespace
@interface PaymentRequestManager ()<CRWWebStateObserver,
PaymentRequestCoordinatorDelegate,
PaymentRequestUIDelegate,
PaymentResponseHelperConsumer> {
// View controller used to present the PaymentRequest view controller.
__weak UIViewController* _baseViewController;
// PersonalDataManager used to manage user credit cards and addresses.
autofill::PersonalDataManager* _personalDataManager;
// The observer for |_activeWebState|.
std::unique_ptr<web::WebStateObserverBridge> _activeWebStateObserver;
// Boolean to track if the active WebState is enabled (JS callback is set
// up).
BOOL _activeWebStateEnabled;
// Timer used to periodically unblock the webview's JS event queue.
NSTimer* _unblockEventQueueTimer;
// Timer used to complete the Payment Request flow and close the UI if the
// page does not call PaymentResponse.complete() in a timely fashion.
NSTimer* _paymentResponseTimeoutTimer;
// Timer used to cancel the Payment Request flow and close the UI if the
// page does not settle the pending update promise in a timely fashion.
NSTimer* _updateEventTimeoutTimer;
// Storage for data to return in the payment response, until we're ready to
// send an actual PaymentResponse.
PendingPaymentResponse _pendingPaymentResponse;
}
// YES if Payment Request is enabled on the active web state.
@property(readonly) BOOL enabled;
// The ios::ChromeBrowserState instance passed to the initializer.
@property(nonatomic, assign) ios::ChromeBrowserState* browserState;
// Coordinator used to create and present the PaymentRequest view controller.
@property(nonatomic, strong)
PaymentRequestCoordinator* paymentRequestCoordinator;
// Object that manages JavaScript injection into the web view.
@property(nonatomic, weak) JSPaymentRequestManager* paymentRequestJsManager;
// Maintains a map of web::WebState to a list of payments::PaymentRequest
// instances maintained for that WebState.
@property(nonatomic, assign) payments::PaymentRequestCache* paymentRequestCache;
// The payments::PaymentRequest instance currently showing, if any.
@property(nonatomic, assign) payments::PaymentRequest* pendingPaymentRequest;
// The dispatcher for Payment Requests.
@property(nonatomic, weak, readonly) id<ApplicationCommands> dispatcher;
// A block to be run when the |_paymentRequestCoordinator| stops.
@property(nonatomic, strong, nullable)
ProceduralBlock coordinatorDidStopCallback;
// Dismisses the UI, rejects the pending request promise with "AbortError" and
// |errorMessage|, and records |reason| for the pending request. Invokes
// |callback| once request promise is rejected.
- (void)abortPendingRequestWithReason:
(payments::JourneyLogger::AbortReason)reason
errorMessage:(NSString*)errorMessage
callback:(ProceduralBlockWithBool)callback;
// Rejects the pending request promise with |errorName| and |errorMessage|, and
// records |reason| for |paymentRequest|. Invokes |callback| once request
// promise is rejected. |paymentRequest| may be nil.
- (void)abortPaymentRequest:(payments::PaymentRequest*)paymentRequest
reason:(payments::JourneyLogger::AbortReason)reason
errorName:(NSString*)errorName
errorMessage:(NSString*)errorMessage
callback:(ProceduralBlockWithBool)callback;
// Handler for injected JavaScript callbacks.
- (BOOL)handleScriptCommand:(const base::DictionaryValue&)JSONCommand;
// Handles creation of a PaymentRequest instance. The value of the JavaScript
// PaymentRequest object should be provided in |message|. Returns YES if the
// invocation was successful.
- (BOOL)handleCreatePaymentRequest:(const base::DictionaryValue&)message;
// Handles invocations of PaymentRequest.show(). The value of the JavaScript
// PaymentRequest object should be provided in |message|. Returns YES if the
// invocation was successful.
- (BOOL)handleRequestShow:(const base::DictionaryValue&)message;
// Handles invocations of PaymentRequest.abort(). Returns YES if the invocation
// was successful.
- (BOOL)handleRequestAbort:(const base::DictionaryValue&)message;
// Handles invocations of PaymentRequest.canMakePayment(). Returns YES if the
// invocation was successful.
- (BOOL)handleCanMakePayment:(const base::DictionaryValue&)message;
// Called by |_updateEventTimeoutTimer|, displays an error message. Upon
// dismissal of the error message, cancels the Payment Request as if it was
// performed by the user.
- (BOOL)displayErrorThenCancelRequest;
// Called by |_paymentResponseTimeoutTimer|, invokes handleResponseComplete:
// as if PaymentResponse.complete() was invoked with the default "unknown"
// argument.
- (BOOL)doResponseComplete;
// Handles invocations of PaymentResponse.complete(). Returns YES if the
// invocation was successful.
- (BOOL)handleResponseComplete:(const base::DictionaryValue&)message;
// Handles setting the "updating" state of the pending request. Returns YES if
// the invocation was successful.
- (BOOL)handleSetPendingRequestUpdating:(const base::DictionaryValue&)message;
// Handles invocations of PaymentRequestUpdateEvent.updateWith(). Returns YES if
// the invocation was successful.
- (BOOL)handleUpdatePaymentDetails:(const base::DictionaryValue&)message;
// Establishes a timer that periodically prompts the JS manager to execute a
// noop. This works around an issue where the JS event queue is blocked while
// presenting the Payment Request UI.
- (void)setUnblockEventQueueTimer;
// Establishes a timer that calls doResponseComplete when it times out. Per
// the spec, if the page does not call PaymentResponse.complete() within some
// timeout period, user agents may behave as if the complete() method was
// called with no arguments.
- (void)setPaymentResponseTimeoutTimer;
// Establishes a timer that dismisses the Payment Request UI when it times out.
// Per the spec, implementations may choose to consider a timeout for the
// promise provided with the PaymentRequestUpdateEvent.updateWith() call. If the
// promise doesn't get settled in a reasonable amount of time, it is as if it
// was rejected.
- (void)setUpdateEventTimeoutTimer;
// Returns the instance of payments::PaymentRequest for self.activeWebState that
// has the identifier |paymentRequestId|, if any. Otherwise returns nullptr.
- (payments::PaymentRequest*)paymentRequestWithId:(std::string)paymentRequestId;
@end
@implementation PaymentRequestManager
@synthesize toolbarModel = _toolbarModel;
@synthesize browserState = _browserState;
@synthesize enabled = _enabled;
@synthesize activeWebState = _activeWebState;
@synthesize paymentRequestCoordinator = _paymentRequestCoordinator;
@synthesize paymentRequestJsManager = _paymentRequestJsManager;
@synthesize paymentRequestCache = _paymentRequestCache;
@synthesize pendingPaymentRequest = _pendingPaymentRequest;
@synthesize dispatcher = _dispatcher;
@synthesize coordinatorDidStopCallback = _coordinatorDidStopCallback;
- (instancetype)initWithBaseViewController:(UIViewController*)viewController
browserState:
(ios::ChromeBrowserState*)browserState
dispatcher:(id<ApplicationCommands>)dispatcher {
if ((self = [super init])) {
_baseViewController = viewController;
_browserState = browserState;
_dispatcher = dispatcher;
_personalDataManager =
autofill::PersonalDataManagerFactory::GetForBrowserState(
browserState->GetOriginalChromeBrowserState());
_paymentRequestCache =
payments::IOSPaymentRequestCacheFactory::GetForBrowserState(
browserState->GetOriginalChromeBrowserState());
}
return self;
}
- (instancetype)init {
NOTREACHED();
return nil;
}
- (void)setActiveWebState:(web::WebState*)webState {
[self disableActiveWebState];
_paymentRequestJsManager = nil;
_activeWebStateObserver.reset();
_activeWebState = webState;
[self enableActiveWebState];
if (_activeWebState) {
_paymentRequestJsManager =
base::mac::ObjCCastStrict<JSPaymentRequestManager>(
[_activeWebState->GetJSInjectionReceiver()
instanceOfClass:[JSPaymentRequestManager class]]);
_activeWebStateObserver =
base::MakeUnique<web::WebStateObserverBridge>(_activeWebState, self);
}
}
- (void)stopTrackingWebState:(web::WebState*)webState {
__weak PaymentRequestManager* weakSelf = self;
ProceduralBlockWithBool callback = ^(BOOL) {
for (const auto& paymentRequest :
weakSelf.paymentRequestCache->GetPaymentRequests(webState)) {
if (paymentRequest->state() != payments::PaymentRequest::State::CLOSED) {
paymentRequest->journey_logger().SetAborted(
payments::JourneyLogger::ABORT_REASON_USER_NAVIGATION);
paymentRequest->set_updating(false);
paymentRequest->set_state(payments::PaymentRequest::State::CLOSED);
}
}
// The lifetime of a PaymentRequest is tied to the WebState it is associated
// with and the current URL. Therefore, PaymentRequest instances should get
// destroyed when the WebState goes away.
weakSelf.paymentRequestCache->ClearPaymentRequests(webState);
};
// Abort any pending request.
if (_pendingPaymentRequest) {
[self abortPendingRequestWithReason:payments::JourneyLogger::
ABORT_REASON_MERCHANT_NAVIGATION
errorMessage:kCancelErrorMessage
callback:callback];
} else {
callback(YES);
}
}
- (void)enablePaymentRequest:(BOOL)enabled {
if (_enabled == enabled)
return;
_enabled = enabled;
if (!enabled)
[self cancelRequest];
}
- (void)cancelRequest {
if (!_pendingPaymentRequest ||
_pendingPaymentRequest->state() !=
payments::PaymentRequest::State::INTERACTIVE) {
return;
}
[self abortPendingRequestWithReason:payments::JourneyLogger::
ABORT_REASON_MERCHANT_NAVIGATION
errorMessage:kCancelErrorMessage
callback:nil];
}
- (void)abortPendingRequestWithReason:
(payments::JourneyLogger::AbortReason)reason
errorMessage:(NSString*)errorMessage
callback:(ProceduralBlockWithBool)callback {
DCHECK(_pendingPaymentRequest);
DCHECK(_pendingPaymentRequest->state() ==
payments::PaymentRequest::State::INTERACTIVE);
[self resetIOSPaymentInstrumentLauncherDelegate];
__weak PaymentRequestManager* weakSelf = self;
ProceduralBlock dismissUICallback = ^() {
[weakSelf abortPaymentRequest:weakSelf.pendingPaymentRequest
reason:reason
errorName:kAbortError
errorMessage:errorMessage
callback:callback];
weakSelf.pendingPaymentRequest = nullptr;
};
[self dismissUIWithCallback:dismissUICallback];
}
- (void)abortPaymentRequest:(payments::PaymentRequest*)paymentRequest
reason:(payments::JourneyLogger::AbortReason)reason
errorName:(NSString*)errorName
errorMessage:(NSString*)errorMessage
callback:(ProceduralBlockWithBool)callback {
if (paymentRequest &&
paymentRequest->state() != payments::PaymentRequest::State::CLOSED) {
paymentRequest->journey_logger().SetAborted(reason);
paymentRequest->set_updating(false);
paymentRequest->set_state(payments::PaymentRequest::State::CLOSED);
}
[_paymentRequestJsManager rejectRequestPromiseWithErrorName:errorName
errorMessage:errorMessage
completionHandler:callback];
}
- (void)resetIOSPaymentInstrumentLauncherDelegate {
payments::IOSPaymentInstrumentLauncher* paymentAppLauncher =
payments::IOSPaymentInstrumentLauncherFactory::GetForBrowserState(
_browserState->GetOriginalChromeBrowserState());
DCHECK(paymentAppLauncher);
paymentAppLauncher->set_delegate(nullptr);
}
- (void)close {
[self setActiveWebState:nullptr];
}
- (void)enableActiveWebState {
if (!_activeWebState)
return;
DCHECK(!_activeWebStateEnabled);
__weak PaymentRequestManager* weakSelf = self;
auto callback =
base::BindBlockArc(^bool(const base::DictionaryValue& JSON,
const GURL& originURL, bool userIsInteracting) {
// |originURL| and |userIsInteracting| aren't used.
return [weakSelf handleScriptCommand:JSON];
});
_activeWebState->AddScriptCommandCallback(callback, kCommandPrefix);
_activeWebStateEnabled = YES;
}
- (void)disableActiveWebState {
if (!_activeWebState)
return;
DCHECK(_activeWebStateEnabled);
_activeWebState->RemoveScriptCommandCallback(kCommandPrefix);
_activeWebStateEnabled = NO;
}
- (BOOL)handleScriptCommand:(const base::DictionaryValue&)JSONCommand {
// Early return if the Payment Request is not enabled.
if (!_enabled)
return NO;
std::string command;
if (!JSONCommand.GetString("command", &command)) {
LOG(ERROR) << "Received bad JSON: No 'command' field";
return NO;
}
if (command == "paymentRequest.createPaymentRequest") {
return [self handleCreatePaymentRequest:JSONCommand];
}
if (command == "paymentRequest.requestShow") {
return [self handleRequestShow:JSONCommand];
}
if (command == "paymentRequest.requestAbort") {
return [self handleRequestAbort:JSONCommand];
}
if (command == "paymentRequest.requestCanMakePayment") {
return [self handleCanMakePayment:JSONCommand];
}
if (command == "paymentRequest.responseComplete") {
return [self handleResponseComplete:JSONCommand];
}
if (command == "paymentRequest.setPendingRequestUpdating") {
return [self handleSetPendingRequestUpdating:JSONCommand];
}
if (command == "paymentRequest.updatePaymentDetails") {
return [self handleUpdatePaymentDetails:JSONCommand];
}
return NO;
}
// Extracts a web::PaymentRequest from |message|. Creates and returns an
// instance of payments::PaymentRequest which is initialized with the
// web::PaymentRequest object. Returns nullptr and populates |errorMessage| with
// the appropriate error message if it cannot extract a web::PaymentRequest from
// |message| or the web::PaymentRequest instance is invalid.
- (payments::PaymentRequest*)
newPaymentRequestFromMessage:(const base::DictionaryValue&)message
errorMessage:(std::string*)errorMessage {
DCHECK(errorMessage);
const base::DictionaryValue* paymentRequestData;
web::PaymentRequest webPaymentRequest;
if (!message.GetDictionary("payment_request", &paymentRequestData)) {
*errorMessage = "JS message parameter 'payment_request' is missing";
return nullptr;
}
if (!webPaymentRequest.FromDictionaryValue(*paymentRequestData)) {
*errorMessage = "Cannot create payment request";
return nullptr;
}
if (!payments::ValidatePaymentDetails(webPaymentRequest.details,
errorMessage)) {
return nullptr;
}
return _paymentRequestCache->AddPaymentRequest(
_activeWebState, base::MakeUnique<payments::PaymentRequest>(
webPaymentRequest, _browserState, _activeWebState,
_personalDataManager, self));
}
// Extracts a web::PaymentRequest from |message|. Returns the cached instance of
// payments::PaymentRequest that corresponds to the extracted
// web::PaymentRequest object, if one exists. Otherwise, creates and returns a
// new one which is initialized with the web::PaymentRequest object. Returns
// nullptr and populates |errorMessage| with the appropriate error message if it
// cannot extract a web::PaymentRequest from |message|, cannot find the
// payments::PaymentRequest instance or the web::PaymentRequest instance is
// invalid.
- (payments::PaymentRequest*)
paymentRequestFromMessage:(const base::DictionaryValue&)message
errorMessage:(std::string*)errorMessage {
DCHECK(errorMessage);
const base::DictionaryValue* paymentRequestData;
web::PaymentRequest webPaymentRequest;
if (!message.GetDictionary("payment_request", &paymentRequestData)) {
*errorMessage = "JS message parameter 'payment_request' is missing";
return nullptr;
}
if (!webPaymentRequest.FromDictionaryValue(*paymentRequestData)) {
*errorMessage = "Cannot create payment request";
return nullptr;
}
if (!payments::ValidatePaymentDetails(webPaymentRequest.details,
errorMessage)) {
return nullptr;
}
return [self paymentRequestWithId:webPaymentRequest.payment_request_id];
}
- (BOOL)handleCreatePaymentRequest:(const base::DictionaryValue&)message {
std::string errorMessage;
payments::PaymentRequest* paymentRequest =
[self newPaymentRequestFromMessage:message errorMessage:&errorMessage];
if (!paymentRequest) {
LOG(ERROR) << errorMessage;
[_paymentRequestJsManager
throwDOMExceptionWithErrorName:kInvalidStateError
errorMessage:base::SysUTF8ToNSString(errorMessage)
completionHandler:nil];
}
return YES;
}
- (BOOL)handleRequestShow:(const base::DictionaryValue&)message {
std::string errorMessage;
payments::PaymentRequest* paymentRequest =
[self paymentRequestFromMessage:message errorMessage:&errorMessage];
if (!paymentRequest) {
LOG(ERROR) << "Request promise rejected: "
<< base::SysNSStringToUTF16(kInvalidStateError) << errorMessage;
[self abortPaymentRequest:nil
reason:payments::JourneyLogger::ABORT_REASON_OTHER
errorName:kInvalidStateError
errorMessage:base::SysUTF8ToNSString(errorMessage)
callback:nil];
return YES;
}
if (![self webStateContentIsSecureHTML]) {
paymentRequest->journey_logger().SetNotShown(
payments::JourneyLogger::NOT_SHOWN_REASON_OTHER);
LOG(ERROR) << "Request promise rejected: "
<< base::SysNSStringToUTF16(kNotSupportedError)
<< "Must be in a secure context";
[self abortPaymentRequest:paymentRequest
reason:payments::JourneyLogger::ABORT_REASON_OTHER
errorName:kNotSupportedError
errorMessage:@"Must be in a secure context"
callback:nil];
return YES;
}
if (paymentRequest->state() != payments::PaymentRequest::State::CREATED) {
paymentRequest->journey_logger().SetNotShown(
payments::JourneyLogger::NOT_SHOWN_REASON_OTHER);
LOG(ERROR) << "Request promise rejected: "
<< base::SysNSStringToUTF16(kInvalidStateError)
<< "Already called show() once";
[self abortPaymentRequest:paymentRequest
reason:payments::JourneyLogger::ABORT_REASON_OTHER
errorName:kInvalidStateError
errorMessage:@"Already called show() once"
callback:nil];
return YES;
}
if (_pendingPaymentRequest) {
paymentRequest->journey_logger().SetNotShown(
payments::JourneyLogger::NOT_SHOWN_REASON_CONCURRENT_REQUESTS);
LOG(ERROR) << "Request promise rejected: "
<< base::SysNSStringToUTF16(kAbortError)
<< "Only one PaymentRequest may be shown at a time";
[self abortPendingRequestWithReason:payments::JourneyLogger::
ABORT_REASON_OTHER
errorMessage:
@"Only one PaymentRequest may be shown at a time"
callback:nil];
return YES;
}
if (paymentRequest->supported_card_networks().empty() &&
(!base::FeatureList::IsEnabled(
payments::features::kWebPaymentsNativeApps) ||
paymentRequest->url_payment_method_identifiers().empty())) {
paymentRequest->journey_logger().SetNotShown(
payments::JourneyLogger::NOT_SHOWN_REASON_NO_SUPPORTED_PAYMENT_METHOD);
LOG(ERROR) << "Request promise rejected: "
<< base::SysNSStringToUTF16(kNotSupportedError)
<< "The payment method is not supported";
[self abortPaymentRequest:paymentRequest
reason:payments::JourneyLogger::ABORT_REASON_OTHER
errorName:kNotSupportedError
errorMessage:@"The payment method is not supported"
callback:nil];
return YES;
}
_pendingPaymentRequest = paymentRequest;
paymentRequest->set_state(payments::PaymentRequest::State::INTERACTIVE);
paymentRequest->journey_logger().SetEventOccurred(
payments::JourneyLogger::EVENT_SHOWN);
UIImage* pageFavicon = nil;
web::NavigationItem* navigationItem =
_activeWebState->GetNavigationManager()->GetVisibleItem();
if (navigationItem && !navigationItem->GetFavicon().image.IsEmpty())
pageFavicon = navigationItem->GetFavicon().image.ToUIImage();
NSString* pageTitle = base::SysUTF16ToNSString(_activeWebState->GetTitle());
NSString* pageHost =
base::SysUTF16ToNSString(url_formatter::FormatUrlForSecurityDisplay(
_activeWebState->GetLastCommittedURL()));
BOOL connectionSecure =
_activeWebState->GetLastCommittedURL().SchemeIs(url::kHttpsScheme);
autofill::AutofillManager* autofillManager =
autofill::AutofillDriverIOS::FromWebState(_activeWebState)
->autofill_manager();
_paymentRequestCoordinator = [[PaymentRequestCoordinator alloc]
initWithBaseViewController:_baseViewController];
[_paymentRequestCoordinator setPaymentRequest:paymentRequest];
[_paymentRequestCoordinator setAutofillManager:autofillManager];
[_paymentRequestCoordinator setBrowserState:_browserState];
[_paymentRequestCoordinator setPageFavicon:pageFavicon];
[_paymentRequestCoordinator setPageTitle:pageTitle];
[_paymentRequestCoordinator setPageHost:pageHost];
[_paymentRequestCoordinator setConnectionSecure:connectionSecure];
[_paymentRequestCoordinator setDelegate:self];
[_paymentRequestCoordinator start];
return YES;
}
- (BOOL)handleRequestAbort:(const base::DictionaryValue&)message {
if (!_pendingPaymentRequest ||
_pendingPaymentRequest->state() !=
payments::PaymentRequest::State::INTERACTIVE) {
return YES;
}
[_unblockEventQueueTimer invalidate];
[_paymentResponseTimeoutTimer invalidate];
[_updateEventTimeoutTimer invalidate];
__weak PaymentRequestManager* weakSelf = self;
ProceduralBlockWithBool cancellationCallback = ^(BOOL) {
[[weakSelf paymentRequestJsManager]
resolveAbortPromiseWithCompletionHandler:nil];
};
ProceduralBlock callback = ^{
[weakSelf
abortPendingRequestWithReason:payments::JourneyLogger::
ABORT_REASON_ABORTED_BY_MERCHANT
errorMessage:@"The website has aborted the payment"
callback:cancellationCallback];
};
[_paymentRequestCoordinator displayErrorWithCallback:callback];
return YES;
}
- (BOOL)handleCanMakePayment:(const base::DictionaryValue&)message {
std::string errorMessage;
payments::PaymentRequest* paymentRequest =
[self paymentRequestFromMessage:message errorMessage:&errorMessage];
if (!paymentRequest) {
LOG(ERROR) << errorMessage;
[_paymentRequestJsManager
rejectCanMakePaymentPromiseWithErrorName:kInvalidStateError
errorMessage:base::SysUTF8ToNSString(
errorMessage)
completionHandler:nil];
return YES;
}
if (paymentRequest->state() != payments::PaymentRequest::State::CREATED) {
[_paymentRequestJsManager
rejectCanMakePaymentPromiseWithErrorName:kInvalidStateError
errorMessage:@"Cannot query payment request"
completionHandler:nil];
return YES;
}
if (![self webStateContentIsSecureHTML]) {
[_paymentRequestJsManager resolveCanMakePaymentPromiseWithValue:NO
completionHandler:nil];
paymentRequest->journey_logger().SetCanMakePaymentValue(false);
return YES;
}
BOOL canMakePayment = paymentRequest->CanMakePayment();
payments::CanMakePaymentQuery* canMakePaymentQuery =
IOSCanMakePaymentQueryFactory::GetForBrowserState(
_browserState->GetOriginalChromeBrowserState());
DCHECK(canMakePaymentQuery);
// iOS PaymentRequest does not support iframes.
if (canMakePaymentQuery->CanQuery(
GURL(url_formatter::FormatUrlForSecurityDisplay(
_activeWebState->GetLastCommittedURL())),
GURL(url_formatter::FormatUrlForSecurityDisplay(
_activeWebState->GetLastCommittedURL())),
paymentRequest->stringified_method_data())) {
if (paymentRequest->IsIncognito()) {
canMakePayment = !paymentRequest->supported_card_networks_set().empty() ||
base::FeatureList::IsEnabled(
payments::features::kWebPaymentsNativeApps);
}
[_paymentRequestJsManager
resolveCanMakePaymentPromiseWithValue:canMakePayment
completionHandler:nil];
paymentRequest->journey_logger().SetCanMakePaymentValue(canMakePayment);
// TODO(crbug.com/602666): Warn on console if origin is localhost or file.
} else {
[_paymentRequestJsManager
rejectCanMakePaymentPromiseWithErrorName:kNotAllowedError
errorMessage:
@"Not allowed to check whether can "
@"make payment"
completionHandler:nil];
}
return YES;
}
- (BOOL)displayErrorThenCancelRequest {
if (!_pendingPaymentRequest ||
_pendingPaymentRequest->state() !=
payments::PaymentRequest::State::INTERACTIVE) {
return YES;
}
[_unblockEventQueueTimer invalidate];
[_paymentResponseTimeoutTimer invalidate];
[_updateEventTimeoutTimer invalidate];
__weak PaymentRequestManager* weakSelf = self;
ProceduralBlock callback = ^{
[weakSelf abortPendingRequestWithReason:payments::JourneyLogger::
ABORT_REASON_ABORTED_BY_USER
errorMessage:kCancelErrorMessage
callback:nil];
};
[_paymentRequestCoordinator displayErrorWithCallback:callback];
return YES;
}
- (BOOL)doResponseComplete {
base::DictionaryValue command;
command.SetString("result", "unknown");
return [self handleResponseComplete:command];
}
- (BOOL)handleResponseComplete:(const base::DictionaryValue&)message {
DCHECK(_pendingPaymentRequest);
// TODO(crbug.com/602666): Check that there *is* a pending response here.
[_unblockEventQueueTimer invalidate];
[_paymentResponseTimeoutTimer invalidate];
[_updateEventTimeoutTimer invalidate];
std::string result;
if (!message.GetString("result", &result)) {
LOG(ERROR) << "JS message parameter 'result' is missing";
return NO;
}
__weak PaymentRequestManager* weakSelf = self;
ProceduralBlock callback = ^{
weakSelf.pendingPaymentRequest = nullptr;
ProceduralBlock dismissUICallback = ^() {
[weakSelf.paymentRequestJsManager
resolveResponsePromiseWithCompletionHandler:nil];
};
[weakSelf dismissUIWithCallback:dismissUICallback];
};
// Display UI indicating failure if the value of |result| is "fail".
if (result == "fail") {
[_paymentRequestCoordinator displayErrorWithCallback:callback];
} else {
_pendingPaymentRequest->journey_logger().SetCompleted();
_pendingPaymentRequest->set_updating(false);
_pendingPaymentRequest->set_state(payments::PaymentRequest::State::CLOSED);
_pendingPaymentRequest->RecordUseStats();
_pendingPaymentRequest->GetPrefService()->SetBoolean(
payments::kPaymentsFirstTransactionCompleted, true);
callback();
}
return YES;
}
- (BOOL)handleSetPendingRequestUpdating:(const base::DictionaryValue&)message {
if (!_pendingPaymentRequest ||
_pendingPaymentRequest->state() !=
payments::PaymentRequest::State::INTERACTIVE ||
_pendingPaymentRequest->updating()) {
return YES;
}
bool updating;
if (!message.GetBoolean("updating", &updating)) {
LOG(ERROR) << "JS message parameter 'updating' is missing";
return NO;
}
_pendingPaymentRequest->set_updating(updating);
return YES;
}
- (BOOL)handleUpdatePaymentDetails:(const base::DictionaryValue&)message {
if (!_pendingPaymentRequest ||
_pendingPaymentRequest->state() !=
payments::PaymentRequest::State::INTERACTIVE ||
!_pendingPaymentRequest->updating()) {
return YES;
}
[_unblockEventQueueTimer invalidate];
[_updateEventTimeoutTimer invalidate];
const base::DictionaryValue* paymentDetailsData = nullptr;
payments::PaymentDetails paymentDetails;
if (!message.GetDictionary("payment_details", &paymentDetailsData)) {
LOG(ERROR) << "JS message parameter 'payment_details' is missing";
return NO;
}
if (!paymentDetails.FromDictionaryValue(*paymentDetailsData,
/*requires_total=*/false)) {
LOG(ERROR) << "Cannot create payment details";
return NO;
}
std::string errorMessage;
if (!payments::ValidatePaymentDetails(paymentDetails, &errorMessage)) {
LOG(ERROR) << errorMessage;
return NO;
}
[_paymentRequestCoordinator updatePaymentDetails:paymentDetails];
_pendingPaymentRequest->set_updating(false);
return YES;
}
- (void)setUnblockEventQueueTimer {
_unblockEventQueueTimer =
[NSTimer scheduledTimerWithTimeInterval:kNoopInterval
target:_paymentRequestJsManager
selector:@selector(executeNoop)
userInfo:nil
repeats:YES];
}
- (void)setPaymentResponseTimeoutTimer {
_paymentResponseTimeoutTimer =
[NSTimer scheduledTimerWithTimeInterval:kTimeoutInterval
target:self
selector:@selector(doResponseComplete)
userInfo:nil
repeats:NO];
}
- (void)setUpdateEventTimeoutTimer {
_updateEventTimeoutTimer = [NSTimer
scheduledTimerWithTimeInterval:kTimeoutInterval
target:self
selector:@selector(displayErrorThenCancelRequest)
userInfo:nil
repeats:NO];
}
- (void)dismissUIWithCallback:(ProceduralBlock)callback {
_coordinatorDidStopCallback = callback;
[_paymentRequestCoordinator stop];
}
- (BOOL)webStateContentIsSecureHTML {
if (!_activeWebState) {
return NO;
}
if (!_toolbarModel) {
return NO;
}
if (!_activeWebState->ContentIsHTML()) {
LOG(ERROR) << "Not a web view with HTML.";
return NO;
}
const GURL lastCommittedURL = _activeWebState->GetLastCommittedURL();
if (!web::IsOriginSecure(lastCommittedURL) ||
lastCommittedURL.scheme() == url::kDataScheme) {
DLOG(ERROR) << "Not in a secure context.";
return NO;
}
if (!security_state::IsSchemeCryptographic(lastCommittedURL) &&
!security_state::IsOriginLocalhostOrFile(lastCommittedURL)) {
DLOG(ERROR) << "Not localhost, or with file or cryptographic scheme.";
return NO;
}
// If the scheme is cryptographic, the SSL certificate must also be valid.
return !security_state::IsSchemeCryptographic(lastCommittedURL) ||
security_state::IsSslCertificateValid(
_toolbarModel->GetToolbarModel()->GetSecurityLevel(true));
}
#pragma mark - PaymentRequestUIDelegate
- (void)paymentRequestDidFetchPaymentMethods:
(payments::PaymentRequest*)paymentRequest {
[_paymentRequestCoordinator setPending:NO];
[_paymentRequestCoordinator setCancellable:YES];
}
- (void)
paymentRequest:(payments::PaymentRequest*)paymentRequest
requestFullCreditCard:(const autofill::CreditCard&)creditCard
resultDelegate:
(base::WeakPtr<autofill::payments::FullCardRequest::ResultDelegate>)
delegate {
[_paymentRequestCoordinator requestFullCreditCard:creditCard
resultDelegate:delegate];
}
- (void)paymentInstrument:(payments::IOSPaymentInstrument*)paymentInstrument
launchAppWithUniversalLink:(GURL)universalLink
instrumentDelegate:
(payments::PaymentInstrument::Delegate*)delegate {
DCHECK(_pendingPaymentRequest);
DCHECK(_activeWebState);
[_paymentRequestCoordinator setPending:YES];
[_paymentRequestCoordinator setCancellable:YES];
payments::IOSPaymentInstrumentLauncher* paymentAppLauncher =
payments::IOSPaymentInstrumentLauncherFactory::GetForBrowserState(
_browserState->GetOriginalChromeBrowserState());
DCHECK(paymentAppLauncher);
if (!paymentAppLauncher->LaunchIOSPaymentInstrument(
_pendingPaymentRequest, _activeWebState, universalLink, delegate)) {
[_paymentRequestCoordinator setPending:NO];
[_paymentRequestCoordinator setCancellable:YES];
}
}
#pragma mark - PaymentRequestCoordinatorDelegate methods
- (void)paymentRequestCoordinatorDidConfirm:
(PaymentRequestCoordinator*)coordinator {
DCHECK(coordinator.paymentRequest->selected_payment_method());
coordinator.paymentRequest->journey_logger().SetEventOccurred(
payments::JourneyLogger::EVENT_PAY_CLICKED);
coordinator.paymentRequest->journey_logger().SetEventOccurred(
coordinator.paymentRequest->selected_payment_method()->type() ==
payments::PaymentInstrument::Type::AUTOFILL
? payments::JourneyLogger::EVENT_SELECTED_CREDIT_CARD
: payments::JourneyLogger::EVENT_SELECTED_OTHER);
coordinator.paymentRequest->InvokePaymentApp(self);
}
- (void)paymentRequestCoordinatorDidCancel:
(PaymentRequestCoordinator*)coordinator {
[self abortPendingRequestWithReason:payments::JourneyLogger::
ABORT_REASON_ABORTED_BY_USER
errorMessage:kCancelErrorMessage
callback:nil];
}
- (void)paymentRequestCoordinatorDidSelectSettings:
(PaymentRequestCoordinator*)coordinator {
__weak PaymentRequestManager* weakSelf = self;
ProceduralBlockWithBool callback = ^(BOOL) {
[weakSelf.dispatcher showAutofillSettings];
};
[self abortPendingRequestWithReason:payments::JourneyLogger::
ABORT_REASON_ABORTED_BY_USER
errorMessage:kCancelErrorMessage
callback:callback];
}
- (void)paymentRequestCoordinator:(PaymentRequestCoordinator*)coordinator
didReceiveFullMethodName:(const std::string&)methodName
stringifiedDetails:(const std::string&)stringifiedDetails {
}
- (void)paymentRequestCoordinator:(PaymentRequestCoordinator*)coordinator
didSelectShippingAddress:
(const autofill::AutofillProfile&)shippingAddress {
if (coordinator.paymentRequest->state() !=
payments::PaymentRequest::State::INTERACTIVE ||
coordinator.paymentRequest->updating()) {
return;
}
payments::PaymentAddress address =
payments::data_util::GetPaymentAddressFromAutofillProfile(
shippingAddress, coordinator.paymentRequest->GetApplicationLocale());
[_paymentRequestJsManager updateShippingAddress:address
completionHandler:nil];
[self setUnblockEventQueueTimer];
[self setUpdateEventTimeoutTimer];
}
- (void)paymentRequestCoordinator:(PaymentRequestCoordinator*)coordinator
didSelectShippingOption:
(const payments::PaymentShippingOption&)shippingOption {
if (coordinator.paymentRequest->state() !=
payments::PaymentRequest::State::INTERACTIVE ||
coordinator.paymentRequest->updating()) {
return;
}
[_paymentRequestJsManager updateShippingOption:shippingOption
completionHandler:nil];
[self setUnblockEventQueueTimer];
[self setUpdateEventTimeoutTimer];
}
- (void)paymentRequestCoordinatorDidStop:
(PaymentRequestCoordinator*)coordinator {
// The coordinator can now be safely set to nil.
_paymentRequestCoordinator = nil;
if (_coordinatorDidStopCallback) {
_coordinatorDidStopCallback();
_coordinatorDidStopCallback = nil;
}
}
#pragma mark - PaymentResponseHelperConsumer methods
- (void)paymentResponseHelperDidReceivePaymentMethodDetails {
[_paymentRequestCoordinator setPending:YES];
}
- (void)paymentResponseHelperDidFailToReceivePaymentMethodDetails {
[_paymentRequestCoordinator setPending:NO];
[_paymentRequestCoordinator setCancellable:YES];
}
- (void)paymentResponseHelperDidCompleteWithPaymentResponse:
(const web::PaymentResponse&)paymentResponse {
if (!_pendingPaymentRequest ||
_pendingPaymentRequest->state() !=
payments::PaymentRequest::State::INTERACTIVE ||
_pendingPaymentRequest->updating()) {
return;
}
[_paymentRequestCoordinator setCancellable:NO];
[_paymentRequestJsManager
resolveRequestPromiseWithPaymentResponse:paymentResponse
completionHandler:nil];
[self setUnblockEventQueueTimer];
[self setPaymentResponseTimeoutTimer];
}
#pragma mark - CRWWebStateObserver methods
- (void)webState:(web::WebState*)webState
didStartNavigation:(web::NavigationContext*)navigation {
payments::JourneyLogger::AbortReason abortReason =
navigation->IsRendererInitiated()
? payments::JourneyLogger::ABORT_REASON_MERCHANT_NAVIGATION
: payments::JourneyLogger::ABORT_REASON_USER_NAVIGATION;
__weak PaymentRequestManager* weakSelf = self;
ProceduralBlockWithBool callback = ^(BOOL) {
for (const auto& paymentRequest :
weakSelf.paymentRequestCache->GetPaymentRequests(
weakSelf.activeWebState)) {
if (paymentRequest->state() != payments::PaymentRequest::State::CLOSED) {
paymentRequest->journey_logger().SetAborted(abortReason);
paymentRequest->set_updating(false);
paymentRequest->set_state(payments::PaymentRequest::State::CLOSED);
}
}
// The lifetime of a PaymentRequest is tied to the WebState it is associated
// with and the current URL. Therefore, PaymentRequest instances should get
// destroyed when the user navigates to a URL.
weakSelf.paymentRequestCache->ClearPaymentRequests(weakSelf.activeWebState);
};
// Abort any pending request.
if (_pendingPaymentRequest) {
[self abortPendingRequestWithReason:abortReason
errorMessage:kCancelErrorMessage
callback:callback];
} else {
callback(YES);
}
// Set the JS isContextSecure global variable at the earliest opportunity.
[_paymentRequestJsManager
setContextSecure:(web::IsOriginSecure(
_activeWebState->GetLastCommittedURL()) &&
_activeWebState->GetLastCommittedURL().scheme() !=
url::kDataScheme)
completionHandler:nil];
}
#pragma mark - Helper methods
- (payments::PaymentRequest*)paymentRequestWithId:
(std::string)paymentRequestId {
const payments::PaymentRequestCache::PaymentRequestSet& paymentRequests =
_paymentRequestCache->GetPaymentRequests(_activeWebState);
const auto found = std::find_if(
paymentRequests.begin(), paymentRequests.end(),
[&paymentRequestId](
const std::unique_ptr<payments::PaymentRequest>& request) {
return request.get()->web_payment_request().payment_request_id ==
paymentRequestId;
});
return found != paymentRequests.end() ? found->get() : nullptr;
}
@end