| // 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_manager.h" |
| |
| #include "base/ios/ios_util.h" |
| #import "base/mac/bind_objc_block.h" |
| #include "base/mac/foundation_util.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/strings/sys_string_conversions.h" |
| #import "base/values.h" |
| #include "components/autofill/core/browser/autofill_manager.h" |
| #include "components/autofill/core/browser/personal_data_manager.h" |
| #include "components/autofill/ios/browser/autofill_driver_ios.h" |
| #include "ios/chrome/browser/autofill/personal_data_manager_factory.h" |
| #include "ios/chrome/browser/browser_state/chrome_browser_state.h" |
| #import "ios/chrome/browser/payments/js_payment_request_manager.h" |
| #include "ios/chrome/browser/payments/payment_request.h" |
| #import "ios/chrome/browser/payments/payment_request_coordinator.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/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/crw_web_view_proxy.h" |
| #import "ios/web/public/web_state/js/crw_js_injection_receiver.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" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| namespace { |
| // Command prefix for injected JavaScript. |
| const std::string 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; |
| |
| } // namespace |
| |
| @interface PaymentRequestManager ()<CRWWebStateObserver, |
| PaymentRequestCoordinatorDelegate> { |
| // View controller used to present the PaymentRequest view controller. |
| __weak UIViewController* _baseViewController; |
| |
| // PersonalDataManager used to manage user credit cards and addresses. |
| autofill::PersonalDataManager* _personalDataManager; |
| |
| // Object that owns an instance of web::PaymentRequest as provided by the page |
| // invoking the PaymentRequest API. Also caches credit cards and addresses |
| // provided by the _personalDataManager and manages selected ones for the |
| // current PaymentRequest flow. |
| std::unique_ptr<PaymentRequest> _paymentRequest; |
| |
| // WebState for the tab this object is attached to. |
| web::WebState* _webState; |
| |
| // Observer for |_webState|. |
| std::unique_ptr<web::WebStateObserverBridge> _webStateObserver; |
| |
| // Object that manages JavaScript injection into the web view. |
| __weak JSPaymentRequestManager* _paymentRequestJsManager; |
| |
| // Boolean to track if the current WebState is enabled (JS callback is set |
| // up). |
| BOOL _webStateEnabled; |
| |
| // Boolean to track if the script has been injected in the current page. This |
| // is a faster check than asking the JS controller. |
| BOOL _isScriptInjected; |
| |
| // True when close has been called and the PaymentRequest coordinator has |
| // been destroyed. |
| BOOL _closed; |
| |
| // Coordinator used to create and present the PaymentRequest view controller. |
| PaymentRequestCoordinator* _paymentRequestCoordinator; |
| |
| // Timer used to periodically unblock the webview's JS event queue. |
| NSTimer* _unblockEventQueueTimer; |
| |
| // Timer used to close the UI if the page does not call |
| // PaymentResponse.complete() in a timely fashion. |
| NSTimer* _paymentResponseTimeoutTimer; |
| } |
| |
| // Synchronous method executed by -asynchronouslyEnablePaymentRequest: |
| - (void)doEnablePaymentRequest:(BOOL)enabled; |
| |
| // Initialize the PaymentRequest JavaScript. |
| - (void)initializeWebViewForPaymentRequest; |
| |
| // Handler for injected JavaScript callbacks. |
| - (BOOL)handleScriptCommand:(const base::DictionaryValue&)JSONCommand; |
| |
| // 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 PaymentResponse.complete(). Returns YES if the |
| // invocation was successful. |
| - (BOOL)handleResponseComplete; |
| |
| // 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 handleResponseComplete 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; |
| |
| @end |
| |
| @implementation PaymentRequestManager |
| |
| @synthesize enabled = _enabled; |
| @synthesize webState = _webState; |
| @synthesize browserState = _browserState; |
| |
| - (instancetype)initWithBaseViewController:(UIViewController*)viewController |
| browserState: |
| (ios::ChromeBrowserState*)browserState { |
| if ((self = [super init])) { |
| _baseViewController = viewController; |
| |
| _browserState = browserState; |
| |
| _personalDataManager = |
| autofill::PersonalDataManagerFactory::GetForBrowserState( |
| browserState->GetOriginalChromeBrowserState()); |
| } |
| return self; |
| } |
| |
| - (instancetype)init { |
| NOTREACHED(); |
| return nil; |
| } |
| |
| - (void)setWebState:(web::WebState*)webState { |
| [self disconnectWebState]; |
| if (webState) { |
| _paymentRequestJsManager = |
| base::mac::ObjCCastStrict<JSPaymentRequestManager>( |
| [webState->GetJSInjectionReceiver() |
| instanceOfClass:[JSPaymentRequestManager class]]); |
| _webState = webState; |
| _webStateObserver.reset(new web::WebStateObserverBridge(webState, self)); |
| [self enableCurrentWebState]; |
| } else { |
| _webState = nullptr; |
| } |
| } |
| |
| - (void)enablePaymentRequest:(BOOL)enabled { |
| // Asynchronously enables PaymentRequest, so that some preferences |
| // (UIAccessibilityIsVoiceOverRunning(), for example) have time to synchronize |
| // with their own notifications. |
| __weak PaymentRequestManager* weakSelf = self; |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| [weakSelf doEnablePaymentRequest:enabled]; |
| }); |
| } |
| |
| - (void)doEnablePaymentRequest:(BOOL)enabled { |
| BOOL changing = _enabled != enabled; |
| if (changing) { |
| if (!enabled) { |
| [self dismissUI]; |
| } |
| _enabled = enabled; |
| [self enableCurrentWebState]; |
| } |
| } |
| |
| - (void)cancelRequest { |
| [self cancelRequestWithErrorMessage:@"Request canceled."]; |
| } |
| |
| - (void)cancelRequestWithErrorMessage:(NSString*)errorMessage { |
| [self dismissUI]; |
| [_paymentRequestJsManager rejectRequestPromiseWithErrorMessage:errorMessage |
| completionHandler:nil]; |
| } |
| |
| - (void)close { |
| if (_closed) |
| return; |
| |
| _closed = YES; |
| [self disableCurrentWebState]; |
| [self setWebState:nil]; |
| [self dismissUI]; |
| } |
| |
| - (void)enableCurrentWebState { |
| if (![self webState]) { |
| return; |
| } |
| |
| if (_enabled) { |
| if (!_webStateEnabled) { |
| __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]; |
| }); |
| [self webState]->AddScriptCommandCallback(callback, kCommandPrefix); |
| |
| _webStateEnabled = YES; |
| } |
| } else { |
| [self disableCurrentWebState]; |
| } |
| } |
| |
| - (void)disableCurrentWebState { |
| if (_webStateEnabled) { |
| _webState->RemoveScriptCommandCallback(kCommandPrefix); |
| _webStateEnabled = NO; |
| } |
| } |
| |
| - (void)disconnectWebState { |
| if (_webState) { |
| _paymentRequestJsManager = nil; |
| _webStateObserver.reset(); |
| [self disableCurrentWebState]; |
| } |
| } |
| |
| - (void)initializeWebViewForPaymentRequest { |
| if (_enabled) { |
| DCHECK(_webStateEnabled); |
| |
| [_paymentRequestJsManager inject]; |
| _isScriptInjected = YES; |
| } |
| } |
| |
| - (BOOL)handleScriptCommand:(const base::DictionaryValue&)JSONCommand { |
| if (![self webStateContentIsSecureHTML]) { |
| return NO; |
| } |
| |
| std::string command; |
| if (!JSONCommand.GetString("command", &command)) { |
| DLOG(ERROR) << "RECEIVED BAD JSON - NO 'command' field"; |
| return NO; |
| } |
| |
| if (command == "paymentRequest.requestShow") { |
| return [self handleRequestShow:JSONCommand]; |
| } |
| if (command == "paymentRequest.requestCancel") { |
| return [self handleRequestCancel]; |
| } |
| if (command == "paymentRequest.responseComplete") { |
| return [self handleResponseComplete]; |
| } |
| if (command == "paymentRequest.updatePaymentDetails") { |
| return [self handleUpdatePaymentDetails:JSONCommand]; |
| } |
| return NO; |
| } |
| |
| - (BOOL)handleRequestShow:(const base::DictionaryValue&)message { |
| // TODO(crbug.com/602666): check that there's not already a pending request. |
| // TODO(crbug.com/602666): compare our supported payment types (i.e. autofill |
| // credit card types) against the merchant supported types and return NO |
| // if the intersection is empty. |
| |
| const base::DictionaryValue* paymentRequestData; |
| web::PaymentRequest paymentRequest; |
| if (!message.GetDictionary("payment_request", &paymentRequestData)) { |
| DLOG(ERROR) << "JS message parameter 'payment_request' is missing"; |
| return NO; |
| } |
| if (!paymentRequest.FromDictionaryValue(*paymentRequestData)) { |
| DLOG(ERROR) << "JS message parameter 'payment_request' is invalid"; |
| return NO; |
| } |
| |
| _paymentRequest.reset( |
| new PaymentRequest(base::MakeUnique<web::PaymentRequest>(paymentRequest), |
| _personalDataManager)); |
| |
| UIImage* pageFavicon = nil; |
| web::NavigationItem* navigationItem = |
| [self webState]->GetNavigationManager()->GetVisibleItem(); |
| if (navigationItem && !navigationItem->GetFavicon().image.IsEmpty()) |
| pageFavicon = navigationItem->GetFavicon().image.ToUIImage(); |
| NSString* pageTitle = base::SysUTF16ToNSString([self webState]->GetTitle()); |
| NSString* pageHost = |
| base::SysUTF8ToNSString([self webState]->GetLastCommittedURL().host()); |
| autofill::AutofillManager* autofillManager = |
| autofill::AutofillDriverIOS::FromWebState(_webState)->autofill_manager(); |
| _paymentRequestCoordinator = [[PaymentRequestCoordinator alloc] |
| initWithBaseViewController:_baseViewController]; |
| [_paymentRequestCoordinator setPaymentRequest:_paymentRequest.get()]; |
| [_paymentRequestCoordinator setAutofillManager:autofillManager]; |
| [_paymentRequestCoordinator setBrowserState:_browserState]; |
| [_paymentRequestCoordinator setPageFavicon:pageFavicon]; |
| [_paymentRequestCoordinator setPageTitle:pageTitle]; |
| [_paymentRequestCoordinator setPageHost:pageHost]; |
| [_paymentRequestCoordinator setDelegate:self]; |
| |
| [_paymentRequestCoordinator start]; |
| |
| return YES; |
| } |
| |
| - (BOOL)handleRequestCancel { |
| // TODO(crbug.com/602666): Check that there is already a pending request. |
| |
| [_unblockEventQueueTimer invalidate]; |
| [_paymentResponseTimeoutTimer invalidate]; |
| |
| [self cancelRequestWithErrorMessage:@"Request canceled by the page."]; |
| |
| return YES; |
| } |
| |
| - (BOOL)handleResponseComplete { |
| // TODO(crbug.com/602666): Check that there *is* a pending response here. |
| // TODO(crbug.com/602666): Indicate success or failure in the UI. |
| |
| [_unblockEventQueueTimer invalidate]; |
| [_paymentResponseTimeoutTimer invalidate]; |
| |
| [self dismissUI]; |
| |
| [_paymentRequestJsManager resolveResponsePromiseWithCompletionHandler:nil]; |
| |
| return YES; |
| } |
| |
| - (BOOL)handleUpdatePaymentDetails:(const base::DictionaryValue&)message { |
| // TODO(crbug.com/602666): Check that there is already a pending request. |
| |
| [_unblockEventQueueTimer invalidate]; |
| |
| const base::DictionaryValue* paymentDetailsData = nullptr; |
| web::PaymentDetails paymentDetails; |
| if (!message.GetDictionary("payment_details", &paymentDetailsData)) { |
| DLOG(ERROR) << "JS message parameter 'payment_details' is missing"; |
| return NO; |
| } |
| if (!paymentDetails.FromDictionaryValue(*paymentDetailsData)) { |
| DLOG(ERROR) << "JS message parameter 'payment_details' is invalid"; |
| return NO; |
| } |
| |
| [_paymentRequestCoordinator updatePaymentDetails:paymentDetails]; |
| |
| 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(handleResponseComplete) |
| userInfo:nil |
| repeats:NO]; |
| } |
| |
| - (void)dismissUI { |
| [_paymentRequestCoordinator stop]; |
| _paymentRequestCoordinator = nil; |
| } |
| |
| - (BOOL)webStateContentIsSecureHTML { |
| if (![self webState]) { |
| return NO; |
| } |
| |
| if (!web::UrlHasWebScheme([self webState]->GetLastCommittedURL()) || |
| ![self webState]->ContentIsHTML()) { |
| return NO; |
| } |
| |
| const web::NavigationItem* navigationItem = |
| [self webState]->GetNavigationManager()->GetLastCommittedItem(); |
| return navigationItem && |
| navigationItem->GetSSL().security_style == |
| web::SECURITY_STYLE_AUTHENTICATED; |
| } |
| |
| #pragma mark - PaymentRequestCoordinatorDelegate methods |
| |
| - (void)paymentRequestCoordinatorDidCancel: |
| (PaymentRequestCoordinator*)coordinator { |
| [self cancelRequestWithErrorMessage:@"Request canceled by user."]; |
| } |
| |
| - (void)paymentRequestCoordinator:(PaymentRequestCoordinator*)coordinator |
| didConfirmWithPaymentResponse:(web::PaymentResponse)paymentResponse { |
| [_paymentRequestJsManager |
| resolveRequestPromiseWithPaymentResponse:paymentResponse |
| completionHandler:nil]; |
| [self setUnblockEventQueueTimer]; |
| [self setPaymentResponseTimeoutTimer]; |
| } |
| |
| - (void)paymentRequestCoordinator:(PaymentRequestCoordinator*)coordinator |
| didSelectShippingAddress:(web::PaymentAddress)shippingAddress { |
| [_paymentRequestJsManager updateShippingAddress:shippingAddress |
| completionHandler:nil]; |
| [self setUnblockEventQueueTimer]; |
| } |
| |
| - (void)paymentRequestCoordinator:(PaymentRequestCoordinator*)coordinator |
| didSelectShippingOption:(web::PaymentShippingOption)shippingOption { |
| [_paymentRequestJsManager updateShippingOption:shippingOption |
| completionHandler:nil]; |
| [self setUnblockEventQueueTimer]; |
| } |
| |
| #pragma mark - CRWWebStateObserver methods |
| |
| - (void)webState:(web::WebState*)webState |
| didCommitNavigationWithDetails: |
| (const web::LoadCommittedDetails&)load_details { |
| [self dismissUI]; |
| _isScriptInjected = NO; |
| [self enableCurrentWebState]; |
| [self initializeWebViewForPaymentRequest]; |
| } |
| |
| @end |