diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-10-29 10:46:47 +0100 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-11-02 12:02:10 +0000 |
commit | 99677208ff3b216fdfec551fbe548da5520cd6fb (patch) | |
tree | 476a4865c10320249360e859d8fdd3e01833b03a /chromium/components/payments | |
parent | c30a6232df03e1efbd9f3b226777b07e087a1122 (diff) | |
download | qtwebengine-chromium-99677208ff3b216fdfec551fbe548da5520cd6fb.tar.gz |
BASELINE: Update Chromium to 86.0.4240.124
Change-Id: Ide0ff151e94cd665ae6521a446995d34a9d1d644
Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
Diffstat (limited to 'chromium/components/payments')
139 files changed, 8735 insertions, 360 deletions
diff --git a/chromium/components/payments/content/BUILD.gn b/chromium/components/payments/content/BUILD.gn index ab9091d996b..16c9e56ab38 100644 --- a/chromium/components/payments/content/BUILD.gn +++ b/chromium/components/payments/content/BUILD.gn @@ -2,10 +2,14 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -import("//build/config/jumbo.gni") - -jumbo_static_library("content") { +static_library("content") { sources = [ + "android_app_communication.cc", + "android_app_communication.h", + "android_payment_app.cc", + "android_payment_app.h", + "android_payment_app_factory.cc", + "android_payment_app_factory.h", "autofill_payment_app.cc", "autofill_payment_app.h", "autofill_payment_app_factory.cc", @@ -32,6 +36,11 @@ jumbo_static_library("content") { "payment_request_converter.h", "payment_request_spec.cc", "payment_request_spec.h", + "secure_payment_confirmation_app.cc", + "secure_payment_confirmation_app.h", + "secure_payment_confirmation_app_factory.cc", + "secure_payment_confirmation_app_factory.h", + "secure_payment_confirmation_view.h", "service_worker_payment_app.cc", "service_worker_payment_app.h", "service_worker_payment_app_factory.cc", @@ -41,6 +50,7 @@ jumbo_static_library("content") { deps = [ ":content_common", ":utils", + "//base", "//components/autofill/core/browser", "//components/keyed_service/content", "//components/payments/content/utility", @@ -52,14 +62,36 @@ jumbo_static_library("content") { "//components/strings:components_strings_grit", "//components/ukm/content", "//components/url_formatter", + "//components/webdata/common", "//content/public/browser", + "//crypto", + "//device/fido", + "//services/data_decoder/public/cpp", "//third_party/blink/public:blink_headers", "//url", ] - if (!is_android) { + if (is_chromeos) { + sources += [ "android_app_communication_chrome_os.cc" ] + + deps += [ + "//components/arc", + "//components/arc/mojom", + ] + } + + if (!is_chromeos) { + sources += [ "android_app_communication_stub.cc" ] + } + + if (is_android) { + sources += [ "secure_payment_confirmation_view_stub.cc" ] + } else { sources += [ + "content_payment_request_delegate.cc", "content_payment_request_delegate.h", + "payment_credential.cc", + "payment_credential.h", "payment_request.cc", "payment_request.h", "payment_request_dialog.h", @@ -71,12 +103,16 @@ jumbo_static_library("content") { "payment_request_web_contents_manager.h", "payment_response_helper.cc", "payment_response_helper.h", + "secure_payment_confirmation_controller.cc", + "secure_payment_confirmation_controller.h", + "secure_payment_confirmation_model.cc", + "secure_payment_confirmation_model.h", ] } } # Files used by content and utility. -jumbo_static_library("content_common") { +static_library("content_common") { sources = [ "web_app_manifest.cc", "web_app_manifest.h", @@ -88,7 +124,7 @@ jumbo_static_library("content_common") { ] } -jumbo_static_library("utils") { +static_library("utils") { sources = [ "developer_console_logger.cc", "developer_console_logger.h", @@ -129,13 +165,15 @@ jumbo_static_library("utils") { ] } -jumbo_source_set("unit_tests") { +source_set("unit_tests") { testonly = true sources = [ + "android_app_communication_test_support.h", + "android_app_communication_unittest.cc", + "android_payment_app_factory_unittest.cc", + "android_payment_app_unittest.cc", "payment_method_manifest_table_unittest.cc", "service_worker_payment_app_finder_unittest.cc", - "test_content_payment_request_delegate.cc", - "test_content_payment_request_delegate.h", "web_app_manifest_section_table_unittest.cc", ] @@ -146,7 +184,11 @@ jumbo_source_set("unit_tests") { "payment_request_spec_unittest.cc", "payment_request_state_unittest.cc", "payment_response_helper_unittest.cc", + "secure_payment_confirmation_app_unittest.cc", + "secure_payment_confirmation_model_unittest.cc", "service_worker_payment_app_unittest.cc", + "test_content_payment_request_delegate.cc", + "test_content_payment_request_delegate.h", ] } @@ -159,6 +201,7 @@ jumbo_source_set("unit_tests") { "//components/autofill/core/browser:test_support", "//components/payments/core", "//components/payments/core:error_strings", + "//components/payments/core:method_strings", "//components/payments/core:test_support", "//components/strings:components_strings_grit", "//components/webdata/common", @@ -172,4 +215,18 @@ jumbo_source_set("unit_tests") { "//third_party/icu", "//third_party/libaddressinput:test_support", ] + + if (is_chromeos) { + sources += [ "android_app_communication_test_support_chrome_os.cc" ] + + deps += [ + "//components/arc", + "//components/arc:arc_test_support", + "//components/arc/mojom", + ] + } + + if (!is_chromeos) { + sources += [ "android_app_communication_test_support_stub.cc" ] + } } diff --git a/chromium/components/payments/content/DEPS b/chromium/components/payments/content/DEPS index 117c4ac539b..35b832b03d5 100644 --- a/chromium/components/payments/content/DEPS +++ b/chromium/components/payments/content/DEPS @@ -1,5 +1,6 @@ include_rules = [ "-components/payments/content/android", + "+components/arc", "+components/autofill", "+components/keyed_service/content", "+components/keyed_service/core", @@ -9,6 +10,8 @@ include_rules = [ "+components/url_formatter", "+components/webdata/common", "+content/public", + "+crypto", + "+device/fido", "+mojo/public/cpp", "+net", "+services/data_decoder/public/cpp", @@ -20,6 +23,7 @@ include_rules = [ "+third_party/blink/public/common", "+third_party/blink/public/mojom/devtools/console_message.mojom.h", "+third_party/blink/public/mojom/payments", + "+third_party/blink/public/mojom/webauthn", "+third_party/blink/public/platform/modules/payments", "+third_party/skia/include/core/SkBitmap.h", "+ui/base", diff --git a/chromium/components/payments/content/android/BUILD.gn b/chromium/components/payments/content/android/BUILD.gn index ba4855549cb..e73979c2543 100644 --- a/chromium/components/payments/content/android/BUILD.gn +++ b/chromium/components/payments/content/android/BUILD.gn @@ -4,12 +4,14 @@ import("//build/config/android/config.gni") import("//build/config/android/rules.gni") +import("//components/payments/content/android/payments_java_resources.gni") import("//mojo/public/tools/bindings/mojom.gni") static_library("android") { sources = [ "byte_buffer_helper.cc", "byte_buffer_helper.h", + "can_make_payment_query_android.cc", "currency_formatter_android.cc", "currency_formatter_android.h", "error_message_util.cc", @@ -48,6 +50,7 @@ static_library("android") { generate_jni("jni_headers") { sources = [ + "java/src/org/chromium/components/payments/CanMakePaymentQuery.java", "java/src/org/chromium/components/payments/CurrencyFormatter.java", "java/src/org/chromium/components/payments/ErrorMessageUtil.java", "java/src/org/chromium/components/payments/JniPaymentApp.java", @@ -57,27 +60,39 @@ generate_jni("jni_headers") { "java/src/org/chromium/components/payments/PaymentHandlerHost.java", "java/src/org/chromium/components/payments/PaymentManifestDownloader.java", "java/src/org/chromium/components/payments/PaymentManifestParser.java", + "java/src/org/chromium/components/payments/PaymentManifestWebDataService.java", "java/src/org/chromium/components/payments/PaymentRequestSpec.java", "java/src/org/chromium/components/payments/PaymentRequestUpdateEventListener.java", "java/src/org/chromium/components/payments/PaymentValidator.java", + "java/src/org/chromium/components/payments/SslValidityChecker.java", "java/src/org/chromium/components/payments/UrlUtil.java", ] } +android_resources("java_resources") { + create_srcjar = false + sources = payments_java_resources +} + android_library("java") { annotation_processor_deps = [ "//base/android/jni_generator:jni_processor" ] sources = [ "java/src/org/chromium/components/payments/Address.java", + "java/src/org/chromium/components/payments/BrowserPaymentRequest.java", + "java/src/org/chromium/components/payments/CanMakePaymentQuery.java", + "java/src/org/chromium/components/payments/ComponentPaymentRequestImpl.java", "java/src/org/chromium/components/payments/CurrencyFormatter.java", "java/src/org/chromium/components/payments/ErrorMessageUtil.java", "java/src/org/chromium/components/payments/JniPaymentApp.java", "java/src/org/chromium/components/payments/JourneyLogger.java", + "java/src/org/chromium/components/payments/MojoPaymentRequestGateKeeper.java", "java/src/org/chromium/components/payments/MojoStructCollection.java", "java/src/org/chromium/components/payments/OriginSecurityChecker.java", "java/src/org/chromium/components/payments/PackageManagerDelegate.java", "java/src/org/chromium/components/payments/PayerData.java", "java/src/org/chromium/components/payments/PaymentAddressTypeConverter.java", "java/src/org/chromium/components/payments/PaymentApp.java", + "java/src/org/chromium/components/payments/PaymentAppFactoryParams.java", "java/src/org/chromium/components/payments/PaymentDetailsConverter.java", "java/src/org/chromium/components/payments/PaymentDetailsUpdateService.java", "java/src/org/chromium/components/payments/PaymentDetailsUpdateServiceHelper.java", @@ -85,22 +100,32 @@ android_library("java") { "java/src/org/chromium/components/payments/PaymentHandlerHost.java", "java/src/org/chromium/components/payments/PaymentManifestDownloader.java", "java/src/org/chromium/components/payments/PaymentManifestParser.java", + "java/src/org/chromium/components/payments/PaymentManifestWebDataService.java", + "java/src/org/chromium/components/payments/PaymentOptionsUtils.java", + "java/src/org/chromium/components/payments/PaymentRequestLifecycleObserver.java", + "java/src/org/chromium/components/payments/PaymentRequestParams.java", "java/src/org/chromium/components/payments/PaymentRequestSpec.java", "java/src/org/chromium/components/payments/PaymentRequestUpdateEventListener.java", "java/src/org/chromium/components/payments/PaymentValidator.java", + "java/src/org/chromium/components/payments/SslValidityChecker.java", + "java/src/org/chromium/components/payments/SupportedDelegations.java", "java/src/org/chromium/components/payments/UrlUtil.java", "java/src/org/chromium/components/payments/WebAppManifestSection.java", "java/src/org/chromium/components/payments/intent/IsReadyToPayServiceHelper.java", "java/src/org/chromium/components/payments/intent/WebPaymentIntentHelper.java", "java/src/org/chromium/components/payments/intent/WebPaymentIntentHelperType.java", + "java/src/org/chromium/components/payments/intent/WebPaymentIntentHelperTypeConverter.java", ] deps = [ + ":java_resources", "//base:base_java", "//base:jni_java", "//components/autofill/android:autofill_java", "//components/payments/mojom:mojom_java", "//content/public/android:content_java", "//mojo/public/java:bindings_java", + "//mojo/public/java:system_java", + "//third_party/android_deps:androidx_annotation_annotation_java", "//third_party/blink/public/mojom:android_mojo_bindings_java", "//url:gurl_java", "//url:origin_java", @@ -111,6 +136,7 @@ android_library("java") { ":payment_details_update_service_aidl", ":payments_journey_logger_enum_javagen", ] + resources_package = "org.chromium.components.payments" } android_aidl("payment_details_update_service_aidl") { diff --git a/chromium/components/payments/content/android/can_make_payment_query_android.cc b/chromium/components/payments/content/android/can_make_payment_query_android.cc new file mode 100644 index 00000000000..cf0535a7a40 --- /dev/null +++ b/chromium/components/payments/content/android/can_make_payment_query_android.cc @@ -0,0 +1,56 @@ +// Copyright 2018 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. + +#include <map> +#include <set> +#include <string> +#include <vector> + +#include "base/android/jni_array.h" +#include "base/android/jni_string.h" +#include "base/android/scoped_java_ref.h" +#include "components/payments/content/android/jni_headers/CanMakePaymentQuery_jni.h" +#include "components/payments/content/can_make_payment_query_factory.h" +#include "components/payments/core/can_make_payment_query.h" +#include "content/public/browser/web_contents.h" +#include "url/gurl.h" + +namespace payments { + +// static +jboolean JNI_CanMakePaymentQuery_CanQuery( + JNIEnv* env, + const base::android::JavaParamRef<jobject>& jweb_contents, + const base::android::JavaParamRef<jstring>& jtop_level_origin, + const base::android::JavaParamRef<jstring>& jframe_origin, + const base::android::JavaParamRef<jobject>& jquery_map) { + auto* web_contents = content::WebContents::FromJavaWebContents(jweb_contents); + if (!web_contents) + return false; + + std::vector<std::string> method_identifiers; + base::android::AppendJavaStringArrayToStringVector( + env, Java_CanMakePaymentQuery_getMethodIdentifiers(env, jquery_map), + &method_identifiers); + + std::map<std::string, std::set<std::string>> query; + for (const auto& method_identifier : method_identifiers) { + std::set<std::string> method_specific_parameters = { + base::android::ConvertJavaStringToUTF8( + Java_CanMakePaymentQuery_getStringifiedMethodData( + env, jquery_map, + base::android::ConvertUTF8ToJavaString(env, + method_identifier)))}; + query.insert(std::make_pair(method_identifier, method_specific_parameters)); + } + + return CanMakePaymentQueryFactory::GetInstance() + ->GetForContext(web_contents->GetBrowserContext()) + ->CanQuery( + GURL(base::android::ConvertJavaStringToUTF8(env, jtop_level_origin)), + GURL(base::android::ConvertJavaStringToUTF8(env, jframe_origin)), + query); +} + +} // namespace payments diff --git a/chromium/components/payments/content/android/java/res/layout/payment_handler_content.xml b/chromium/components/payments/content/android/java/res/layout/payment_handler_content.xml new file mode 100644 index 00000000000..b3af8a30a9d --- /dev/null +++ b/chromium/components/payments/content/android/java/res/layout/payment_handler_content.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2019 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. --> +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> + +</FrameLayout> diff --git a/chromium/components/payments/content/android/java/res/layout/payment_minimal_ui_content.xml b/chromium/components/payments/content/android/java/res/layout/payment_minimal_ui_content.xml new file mode 100644 index 00000000000..71707d60e2e --- /dev/null +++ b/chromium/components/payments/content/android/java/res/layout/payment_minimal_ui_content.xml @@ -0,0 +1,133 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2019 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. --> +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:background="@color/sheet_bg_color" + android:layout_height="wrap_content" + android:layout_width="match_parent"> + + <!-- "Payment" label. --> + <TextView + android:id="@+id/payment_label" + android:layout_alignParentStart="true" + android:layout_height="wrap_content" + android:layout_toStartOf="@+id/payment_currency" + android:layout_width="wrap_content" + android:paddingBottom="@dimen/payment_minimal_ui_large_text_vertical_spacing" + android:paddingEnd="@dimen/payment_minimal_ui_spacing" + android:paddingStart="@dimen/payment_minimal_ui_spacing" + android:paddingTop="@dimen/payment_minimal_ui_content_top_spacing" + android:textAppearance="@style/TextAppearance.TextMedium.Secondary" + android:text="@string/payment_request_payment_method_section_name"/> + + <!-- Currency label, e.g., "USD". --> + <TextView + android:id="@id/payment_currency" + android:layout_height="wrap_content" + android:layout_toStartOf="@+id/payment_amount" + android:layout_width="wrap_content" + android:paddingTop="@dimen/payment_minimal_ui_content_top_spacing" + android:textAppearance="@style/TextAppearance.TextMedium.Primary"/> + + <!-- Bold formatted amount, e.g., "$1.00". --> + <TextView + android:id="@id/payment_amount" + android:layout_alignParentEnd="true" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:paddingEnd="@dimen/payment_minimal_ui_spacing" + android:paddingStart="@dimen/payment_minimal_ui_amount_currency_spacing" + android:paddingTop="@dimen/payment_minimal_ui_content_top_spacing" + android:textAppearance="@style/TextAppearance.TextMediumThick.Primary"/> + + <!-- Line item separator. --> + <View + android:background="@color/divider_line_bg_color" + android:id="@+id/line_item_separator" + android:layout_below="@id/payment_label" + android:layout_height="@dimen/payment_minimal_ui_separator_width" + android:layout_marginEnd="@dimen/payment_minimal_ui_spacing" + android:layout_marginStart="@dimen/payment_minimal_ui_spacing" + android:layout_width="match_parent"/> + + <!-- "Account Balance" label. --> + <TextView + android:id="@+id/account_balance_label" + android:layout_alignParentStart="true" + android:layout_below="@id/line_item_separator" + android:layout_height="wrap_content" + android:layout_toStartOf="@+id/account_balance_currency" + android:layout_width="wrap_content" + android:paddingBottom="@dimen/payment_minimal_ui_large_text_vertical_spacing" + android:paddingEnd="@dimen/payment_minimal_ui_spacing" + android:paddingStart="@dimen/payment_minimal_ui_spacing" + android:paddingTop="@dimen/payment_minimal_ui_large_text_vertical_spacing" + android:textAppearance="@style/TextAppearance.TextMedium.Secondary" + android:text="@string/payment_account_balance"/> + + <!-- Currency label, e.g., "USD". --> + <TextView + android:id="@id/account_balance_currency" + android:layout_below="@id/line_item_separator" + android:layout_height="wrap_content" + android:layout_toStartOf="@+id/account_balance" + android:layout_width="wrap_content" + android:paddingTop="@dimen/payment_minimal_ui_large_text_vertical_spacing" + android:textAppearance="@style/TextAppearance.TextMedium.Primary"/> + + <!-- Bold formatted account balance, e.g., "$18.00". --> + <TextView + android:id="@id/account_balance" + android:layout_alignParentEnd="true" + android:layout_below="@id/line_item_separator" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:paddingEnd="@dimen/payment_minimal_ui_spacing" + android:paddingStart="@dimen/payment_minimal_ui_amount_currency_spacing" + android:paddingTop="@dimen/payment_minimal_ui_large_text_vertical_spacing" + android:textAppearance="@style/TextAppearance.TextMediumThick.Primary"/> + + <!-- Status icon, e.g., the fingerprint icon. --> + <ImageView + android:id="@+id/status_icon" + android:importantForAccessibility="no" + android:layout_below="@id/account_balance_label" + android:layout_centerHorizontal="true" + android:layout_height="@dimen/payment_minimal_ui_content_icon_size" + android:layout_margin="@dimen/payment_minimal_ui_content_icon_spacing" + android:layout_width="@dimen/payment_minimal_ui_content_icon_size" + android:scaleType="centerCrop"/> + + <!-- Processing spinner. --> + <ProgressBar + android:id="@+id/processing_spinner" + android:layout_below="@id/account_balance_label" + android:layout_centerHorizontal="true" + android:layout_height="@dimen/payment_minimal_ui_content_icon_size" + android:layout_margin="@dimen/payment_minimal_ui_content_icon_spacing" + android:layout_width="@dimen/payment_minimal_ui_content_icon_size"/> + + <!-- Status message, e.g., "Touch sensor to pay". --> + <TextView + android:id="@+id/status_message" + android:layout_below="@id/status_icon" + android:layout_centerHorizontal="true" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:paddingBottom="@dimen/payment_minimal_ui_content_bottom_spacing" + android:textAppearance="@style/TextAppearance.TextSmall.Secondary"/> + + <!-- "Pay" button. --> + <org.chromium.ui.widget.ButtonCompat + android:id="@+id/pay_button" + android:layout_alignParentEnd="true" + android:layout_below="@id/account_balance" + android:layout_height="wrap_content" + android:layout_margin="@dimen/payment_minimal_ui_spacing" + android:layout_width="wrap_content" + android:text="@string/payments_pay_button" + style="@style/FilledButton.Flat"/> + +</RelativeLayout> diff --git a/chromium/components/payments/content/android/java/res/layout/payment_minimal_ui_toolbar.xml b/chromium/components/payments/content/android/java/res/layout/payment_minimal_ui_toolbar.xml new file mode 100644 index 00000000000..e8daeb1449a --- /dev/null +++ b/chromium/components/payments/content/android/java/res/layout/payment_minimal_ui_toolbar.xml @@ -0,0 +1,119 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2019 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. --> +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:background="@color/sheet_bg_color" + android:layout_height="wrap_content" + android:layout_width="match_parent"> + + <!-- Drag handle. --> + <ImageView + android:id="@+id/drag_handle" + android:importantForAccessibility="no" + android:layout_centerHorizontal="true" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:padding="@dimen/payment_minimal_ui_drag_handle_spacing" + android:src="@drawable/drag_handlebar"/> + + <!-- Payment app icon. --> + <ImageView + android:id="@+id/payment_app_icon" + android:importantForAccessibility="no" + android:layout_alignParentStart="true" + android:layout_below="@id/drag_handle" + android:layout_height="@dimen/payment_minimal_ui_toolbar_icon_size" + android:layout_marginBottom="@dimen/payment_minimal_ui_app_icon_bottom_spacing" + android:layout_marginEnd="@dimen/payment_minimal_ui_app_icon_horizontal_spacing" + android:layout_marginStart="@dimen/payment_minimal_ui_app_icon_horizontal_spacing" + android:layout_width="@dimen/payment_minimal_ui_toolbar_icon_size" + android:scaleType="centerCrop"/> + + <!-- Large status message, e.g., "Not recognized" or "Complete". --> + <TextView + android:alpha="0" + android:id="@+id/large_status_message" + android:layout_below="@id/drag_handle" + android:layout_height="wrap_content" + android:layout_toEndOf="@id/payment_app_icon" + android:layout_width="wrap_content" + android:paddingTop="@dimen/payment_minimal_ui_large_text_vertical_spacing" + android:textAppearance="@style/TextAppearance.TextMedium.Primary"/> + + <!-- Payment app name, e.g., "Bob Pay". --> + <TextView + android:alpha="0" + android:id="@+id/payment_app_name" + android:layout_below="@id/drag_handle" + android:layout_height="wrap_content" + android:layout_toEndOf="@id/payment_app_icon" + android:layout_width="wrap_content" + android:paddingTop="@dimen/payment_minimal_ui_large_text_vertical_spacing" + android:textAppearance="@style/TextAppearance.TextLarge.Primary"/> + + <!-- Small emphasized status message, e.g., "Touch sensor to pay". --> + <TextView + android:id="@+id/small_emphasized_status_message" + android:layout_below="@id/drag_handle" + android:layout_height="wrap_content" + android:layout_toEndOf="@id/payment_app_icon" + android:layout_width="wrap_content" + android:paddingTop="@dimen/payment_minimal_ui_toolbar_small_status_text_top_spacing" + android:textAppearance="@style/TextAppearance.TextSmall.Blue"/> + + <!-- Currency label, e.g., "USD". --> + <TextView + android:id="@+id/currency" + android:layout_below="@id/small_emphasized_status_message" + android:layout_height="wrap_content" + android:layout_toEndOf="@id/payment_app_icon" + android:layout_width="wrap_content" + android:textAppearance="@style/TextAppearance.TextMedium.Primary"/> + + <!-- Bold formatted amount, e.g., "$1.00". --> + <TextView + android:id="@+id/amount" + android:layout_below="@id/small_emphasized_status_message" + android:layout_height="wrap_content" + android:layout_toEndOf="@id/currency" + android:layout_width="wrap_content" + android:paddingStart="@dimen/payment_minimal_ui_amount_currency_spacing" + android:textAppearance="@style/TextAppearance.TextMediumThick.Primary"/> + + <!-- Status icon, e.g., the fingerprint icon. --> + <ImageView + android:id="@+id/status_icon" + android:importantForAccessibility="no" + android:layout_alignParentEnd="true" + android:layout_below="@id/drag_handle" + android:layout_height="@dimen/payment_minimal_ui_toolbar_icon_size" + android:layout_marginEnd="@dimen/payment_minimal_ui_spacing" + android:layout_marginStart="@dimen/payment_minimal_ui_spacing" + android:layout_width="@dimen/payment_minimal_ui_toolbar_icon_size" + android:scaleType="centerCrop"/> + + <!-- Processing spinner. --> + <ProgressBar + android:id="@+id/processing_spinner" + android:layout_alignParentEnd="true" + android:layout_below="@id/drag_handle" + android:layout_height="@dimen/payment_minimal_ui_toolbar_icon_size" + android:layout_marginEnd="@dimen/payment_minimal_ui_spacing" + android:layout_marginStart="@dimen/payment_minimal_ui_spacing" + android:layout_width="@dimen/payment_minimal_ui_toolbar_icon_size"/> + + <!-- "Pay" button. --> + <org.chromium.ui.widget.ButtonCompat + android:id="@+id/pay_button" + android:layout_alignParentEnd="true" + android:layout_below="@id/drag_handle" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/payment_minimal_ui_spacing" + android:layout_marginStart="@dimen/payment_minimal_ui_spacing" + android:layout_width="wrap_content" + android:text="@string/payments_pay_button" + style="@style/FilledButton.Flat"/> + +</RelativeLayout> diff --git a/chromium/components/payments/content/android/java/res/layout/payment_option_edit_icon.xml b/chromium/components/payments/content/android/java/res/layout/payment_option_edit_icon.xml new file mode 100644 index 00000000000..d26c8040fb1 --- /dev/null +++ b/chromium/components/payments/content/android/java/res/layout/payment_option_edit_icon.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. --> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/payments_open_editor_pencil_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:contentDescription="@string/payments_edit_button"> + + <View + android:layout_width="1dp" + android:layout_height="48dp" + android:layout_gravity="center_vertical" + android:layout_marginStart="@dimen/editor_dialog_section_large_spacing" + android:layout_marginEnd="@dimen/editor_dialog_section_large_spacing" + style="@style/VerticalDivider" /> + + <org.chromium.ui.widget.ChromeImageView + android:layout_width="24dp" + android:layout_height="24dp" + android:layout_gravity="center_vertical" + android:src="@drawable/ic_edit_24dp" + android:importantForAccessibility="no" + app:tint="@color/default_icon_color"/> +</LinearLayout> diff --git a/chromium/components/payments/content/android/java/res/layout/payment_request.xml b/chromium/components/payments/content/android/java/res/layout/payment_request.xml new file mode 100644 index 00000000000..d6f6c44d6a5 --- /dev/null +++ b/chromium/components/payments/content/android/java/res/layout/payment_request.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. --> + +<!-- PaymentRequestUI dialog + Sits at the bottom of the screen like a Bottom Sheet. +--> +<org.chromium.components.browser_ui.widget.BoundedLinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/payment_request" + android:orientation="vertical" + android:gravity="center" + app:maxWidthLandscape="@dimen/payments_ui_max_dialog_width" + app:maxWidthPortrait="@dimen/payments_ui_max_dialog_width" + android:background="@color/payment_request_bg" > + + <include layout="@layout/payment_request_header" /> + <include layout="@layout/payment_request_spinny" /> + + <org.chromium.components.browser_ui.widget.FadingEdgeScrollView + android:id="@+id/option_container" + android:layout_height="0dp" + android:layout_width="match_parent" + android:layout_weight="1" + android:visibility="gone" > + + <LinearLayout + android:id="@+id/payment_container_layout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + <TextView + android:id="@+id/retry_error" + android:layout_height="wrap_content" + android:layout_width="match_parent" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:visibility="gone" + android:textAppearance="@style/TextAppearance.PaymentsUiSectionWarningText" /> + </LinearLayout> + + </org.chromium.components.browser_ui.widget.FadingEdgeScrollView> + + <include layout="@layout/payment_request_bottom_bar" /> + +</org.chromium.components.browser_ui.widget.BoundedLinearLayout> diff --git a/chromium/components/payments/content/android/java/res/layout/payment_request_bottom_bar.xml b/chromium/components/payments/content/android/java/res/layout/payment_request_bottom_bar.xml new file mode 100644 index 00000000000..f91344db848 --- /dev/null +++ b/chromium/components/payments/content/android/java/res/layout/payment_request_bottom_bar.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2017 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. --> + +<!-- Sits at the bottom of the payment request UI. --> +<org.chromium.chrome.browser.payments.ui.PaymentRequestBottomBar + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/bottom_bar" + android:layout_height="wrap_content" + android:layout_width="match_parent" + android:paddingTop="@dimen/payments_request_bottom_bar_vertical_padding" + android:paddingBottom="@dimen/payments_request_bottom_bar_vertical_padding" + android:paddingStart="@dimen/payments_request_bottom_bar_horizontal_padding" + android:paddingEnd="@dimen/payments_request_bottom_bar_horizontal_padding" + android:background="@color/payment_request_bg" + android:visibility="gone" > + + <ImageView + android:id="@+id/logo_name" + android:layout_width="72dp" + android:layout_height="20dp" + android:src="@drawable/product_logo_name" + tools:ignore="ContentDescription" /> + + <ImageView + android:id="@+id/logo" + android:layout_width="20dp" + android:layout_height="20dp" + android:src="@drawable/fre_product_logo" + tools:ignore="ContentDescription" /> + + <Space + android:id="@+id/space" + android:layout_width="0dp" + android:layout_height="0dp" /> + + <org.chromium.ui.widget.ButtonCompat + android:id="@+id/button_secondary" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/payments_edit_button" + style="@style/TextButton" /> + + <org.chromium.ui.widget.ButtonCompat + android:id="@+id/button_primary" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/payments_pay_button" + style="@style/FilledButton.Flat" /> +</org.chromium.chrome.browser.payments.ui.PaymentRequestBottomBar>
\ No newline at end of file diff --git a/chromium/components/payments/content/android/java/res/layout/payment_request_dropdown_item.xml b/chromium/components/payments/content/android/java/res/layout/payment_request_dropdown_item.xml new file mode 100644 index 00000000000..67be52073b9 --- /dev/null +++ b/chromium/components/payments/content/android/java/res/layout/payment_request_dropdown_item.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <TextView + android:id="@+id/spinner_item" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingStart="24dp" + android:paddingEnd="24dp" + android:minHeight="48dp" + style="?android:attr/spinnerDropDownItemStyle" /> + +</FrameLayout>
\ No newline at end of file diff --git a/chromium/components/payments/content/android/java/res/layout/payment_request_editor.xml b/chromium/components/payments/content/android/java/res/layout/payment_request_editor.xml new file mode 100644 index 00000000000..65f225edfdd --- /dev/null +++ b/chromium/components/payments/content/android/java/res/layout/payment_request_editor.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. --> + +<!-- PaymentRequestUI editor dialog. --> +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/editor_container" + android:background="@color/default_bg_color"> + + <!-- Toolbar --> + <org.chromium.chrome.browser.autofill.prefeditor.EditorDialogToolbar + android:id="@+id/action_bar" + android:layout_width="match_parent" + android:layout_height="?attr/actionBarSize" + android:layout_alignParentTop="true" + android:layout_alignParentStart="true" + android:layout_alignParentEnd="true" /> + + <include layout="@layout/settings_action_bar_shadow" /> + + <!-- All the page content in scrollable form. --> + <org.chromium.components.browser_ui.widget.FadingEdgeScrollView + android:id="@+id/scroll_view" + android:layout_height="0dp" + android:layout_width="match_parent" + android:layout_below="@id/action_bar" + android:layout_alignParentBottom="true" + android:layout_alignParentStart="true" + android:layout_alignParentEnd="true" > + + <LinearLayout + android:id="@+id/contents" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingStart="@dimen/pref_autofill_content_spacing" + android:paddingEnd="@dimen/pref_autofill_content_spacing" + android:orientation="vertical" /> + + </org.chromium.components.browser_ui.widget.FadingEdgeScrollView> + +</RelativeLayout> diff --git a/chromium/components/payments/content/android/java/res/layout/payment_request_editor_dropdown.xml b/chromium/components/payments/content/android/java/res/layout/payment_request_editor_dropdown.xml new file mode 100644 index 00000000000..4dd75f2ba64 --- /dev/null +++ b/chromium/components/payments/content/android/java/res/layout/payment_request_editor_dropdown.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. --> + +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/editor_dialog_section_small_spacing" + android:layout_marginBottom="@dimen/editor_dialog_section_large_spacing" + android:layout_marginStart="@dimen/pref_autofill_field_horizontal_padding" + android:layout_marginEnd="@dimen/pref_autofill_field_horizontal_padding" + android:orientation="vertical"> + + <TextView + android:id="@+id/spinner_label" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textAppearance="@style/TextAppearance.TextSmall.Secondary" /> + + <androidx.appcompat.widget.AppCompatSpinner + android:id="@+id/spinner" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/pref_autofill_field_top_margin" + android:padding="0dp" + android:dropDownWidth="match_parent" /> + + <View android:id="@+id/spinner_underline" style="@style/PreferenceSpinnerUnderlineView" /> + + <TextView + android:id="@+id/spinner_error" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/pref_autofill_field_top_margin" + android:visibility="gone" + android:textAppearance="@style/TextAppearance.ErrorCaption" /> + +</LinearLayout> diff --git a/chromium/components/payments/content/android/java/res/layout/payment_request_editor_label.xml b/chromium/components/payments/content/android/java/res/layout/payment_request_editor_label.xml new file mode 100644 index 00000000000..ad7fab13cd4 --- /dev/null +++ b/chromium/components/payments/content/android/java/res/layout/payment_request_editor_label.xml @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. --> + +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/editor_dialog_section_large_spacing" + android:layout_marginBottom="@dimen/editor_dialog_section_large_spacing" + android:layout_marginStart="@dimen/pref_autofill_field_horizontal_padding" + android:layout_marginEnd="@dimen/pref_autofill_field_horizontal_padding"> + + <ImageView + android:id="@+id/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentEnd="true" + android:layout_centerVertical="true" + android:importantForAccessibility="no" /> + + <TextView + android:id="@+id/top_label" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_toStartOf="@id/icon" + android:layout_alignParentStart="true" + android:layout_alignParentTop="true" + style="@style/PreferenceTitle" /> + + <TextView + android:id="@+id/mid_label" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@id/top_label" + android:layout_toStartOf="@id/icon" + android:layout_alignParentStart="true" + style="@style/PreferenceSummary" /> + + <TextView + android:id="@+id/bottom_label" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@id/mid_label" + android:layout_toStartOf="@id/icon" + android:layout_alignParentStart="true" + android:textAppearance="@style/TextAppearance.TextSmall.Secondary" /> + +</RelativeLayout>
\ No newline at end of file diff --git a/chromium/components/payments/content/android/java/res/layout/payment_request_header.xml b/chromium/components/payments/content/android/java/res/layout/payment_request_header.xml new file mode 100644 index 00000000000..581ab28d2c6 --- /dev/null +++ b/chromium/components/payments/content/android/java/res/layout/payment_request_header.xml @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. --> +<!-- Header containing information about the site. + Java code inflating this layout manages the hiding and adjustment of elements in the layout. + Request dialog: Displays an X in the top right corner, allowing the user to close it. + Result dialog: Displays no X. Title goes all the way to the end. +--> +<merge xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + <org.chromium.chrome.browser.payments.ui.PaymentRequestHeader + android:id="@+id/header" + android:layout_height="wrap_content" + android:layout_width="match_parent" + android:minHeight="64dp"> + <ImageView + android:id="@+id/icon_view" + android:layout_height="24dp" + android:layout_width="24dp" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:layout_gravity="start|center_vertical" + android:importantForAccessibility="no" + android:scaleType="centerInside" /> + <LinearLayout + android:id="@+id/page_info" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:layout_marginStart="50dp" + android:layout_marginEnd="50dp" + android:layout_marginBottom="@dimen/payments_section_vertical_spacing" + android:layout_marginTop="@dimen/payments_section_vertical_spacing" + android:layout_gravity="center_vertical" + android:orientation="vertical"> + <TextView + android:id="@+id/page_title" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:paddingStart="6dp" + android:ellipsize="end" + android:maxLines="1" + android:singleLine="true" + android:textAppearance="@style/TextAppearance.PaymentRequestHeaderTitle" /> + <TextView + android:id="@+id/hostname" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:paddingStart="6dp" + android:gravity="center_vertical" + android:ellipsize="start" + android:maxLines="1" + android:singleLine="true" + android:textAppearance="@style/TextAppearance.TextMedium.Secondary" /> + </LinearLayout> + <ImageView + android:id="@+id/close_button" + android:layout_gravity="end|center_vertical" + android:layout_height="56dp" + android:layout_width="56dp" + android:src="@drawable/btn_close" + android:contentDescription="@string/close" + android:background="?attr/selectableItemBackground" + android:scaleType="center" + app:tint="@color/default_icon_color_tint_list" /> + </org.chromium.chrome.browser.payments.ui.PaymentRequestHeader> +</merge> diff --git a/chromium/components/payments/content/android/java/res/layout/payment_request_spinny.xml b/chromium/components/payments/content/android/java/res/layout/payment_request_spinny.xml new file mode 100644 index 00000000000..bd96a3da115 --- /dev/null +++ b/chromium/components/payments/content/android/java/res/layout/payment_request_spinny.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. --> + +<!-- Indeterminate spinny used to show that things are being loaded in the PaymentRequestUi. + Margins in this file are assigned so that it can be either included or inflated and have + the correct margins for the situation. When included in the main bottom sheet, the + FrameLayout's margins are applied, but they are ignored when this is inflated without + a parent. +--> +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + tools:ignore="MergeRootFrame" + android:id="@+id/payment_request_spinny" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="@dimen/editor_dialog_section_large_spacing" > + + <!-- Indeterminate spinny to show that things are processing. --> + <ProgressBar + android:id="@+id/waiting_progress" + android:layout_width="24dp" + android:layout_height="24dp" + android:layout_marginTop="28dp" + android:layout_gravity="center_horizontal|top" /> + + <!-- Message displayed to the user. + The top margin is computed assuming there's a 28dp margin above and below the progress bar, + which is itself 24dp tall (28 + 28 + 24 = 80). + --> + <TextView + android:id="@+id/message" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:layout_marginTop="80dp" + android:layout_marginStart="@dimen/editor_dialog_section_large_spacing" + android:layout_marginEnd="@dimen/editor_dialog_section_large_spacing" + android:layout_marginBottom="@dimen/editor_dialog_section_large_spacing" + android:gravity="center_horizontal" + android:textAppearance="@style/TextAppearance.TextLarge.Secondary" /> + +</FrameLayout> diff --git a/chromium/components/payments/content/android/java/res/layout/payments_request_editor_textview.xml b/chromium/components/payments/content/android/java/res/layout/payments_request_editor_textview.xml new file mode 100644 index 00000000000..fb9ec39b7ed --- /dev/null +++ b/chromium/components/payments/content/android/java/res/layout/payments_request_editor_textview.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. --> + +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/editor_dialog_section_small_spacing" + android:layout_marginBottom="@dimen/editor_dialog_section_small_spacing"> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/text_input_layout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:errorTextAppearance="@style/TextAppearance.ErrorCaption"> + + <!-- TODO(crbug.com/900912): Fix and remove lint ignore --> + <AutoCompleteTextView + tools:ignore="LabelFor" + android:id="@+id/text_view" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:singleLine="true" + android:imeOptions="flagNoExtractUi" /> + </com.google.android.material.textfield.TextInputLayout> + + <LinearLayout + android:id="@+id/icons_layer" + android:layout_width="wrap_content" + android:layout_height="48dp" + android:layout_gravity="end" + android:orientation="horizontal"> + + <ImageView + android:id="@+id/value_icon" + android:layout_width="wrap_content" + android:layout_height="24dp" + android:layout_gravity="center_vertical" + android:layout_marginStart="8dp" + android:adjustViewBounds="true" + tools:ignore="ContentDescription" + android:clickable="false" + android:visibility="gone"/> + + <ImageView + android:id="@+id/action_icon" + android:layout_width="48dp" + android:layout_height="48dp" + android:padding="12dp" + tools:ignore="ContentDescription" + android:visibility="gone"/> + </LinearLayout> +</FrameLayout> diff --git a/chromium/components/payments/content/android/java/res/values-sw600dp/dimens.xml b/chromium/components/payments/content/android/java/res/values-sw600dp/dimens.xml new file mode 100644 index 00000000000..fe8914de7bf --- /dev/null +++ b/chromium/components/payments/content/android/java/res/values-sw600dp/dimens.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2020 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. --> + +<resources> + <!-- Payments UI --> + <dimen name="payments_ui_max_dialog_width">600dp</dimen> +</resources> diff --git a/chromium/components/payments/content/android/java/res/values/dimens.xml b/chromium/components/payments/content/android/java/res/values/dimens.xml new file mode 100644 index 00000000000..692b2a344ee --- /dev/null +++ b/chromium/components/payments/content/android/java/res/values/dimens.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2020 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. --> + +<resources xmlns:tools="http://schemas.android.com/tools"> + <!-- Payments UI + * payments_section_checking_spacing: + The spec says that the "Checking" text should be 32dp above the bottom of its section. + We improvise by using 6dp because sections also have a 10dp padding, so we end up with + a 16dp bottom margin on the "Checking" text + 10dp section padding + 6dp leftover. + --> + <dimen name="payments_section_vertical_spacing">10dp</dimen> + <dimen name="payments_section_checking_spacing">6dp</dimen> + <dimen name="payments_section_descriptive_item_spacing">40dp</dimen> + <dimen name="payments_section_add_button_height">48dp</dimen> + <dimen name="payments_section_dropdown_top_padding">5dp</dimen> + <dimen name="payments_ui_max_dialog_width">0dp</dimen> + <dimen name="payments_ui_translation">100dp</dimen> + <dimen name="payments_favicon_size">24dp</dimen> + <dimen name="payments_handler_window_minimum_height">500dp</dimen> + <dimen name="payments_request_bottom_bar_vertical_padding">10dp</dimen> + <dimen name="payments_request_bottom_bar_horizontal_padding">16dp</dimen> + + <!-- Payment minimal UI spacing --> + <dimen name="payment_minimal_ui_amount_currency_spacing">4dp</dimen> + <dimen name="payment_minimal_ui_app_icon_bottom_spacing">20dp</dimen> + <dimen name="payment_minimal_ui_app_icon_horizontal_spacing">10dp</dimen> + <dimen name="payment_minimal_ui_content_bottom_spacing">26dp</dimen> + <dimen name="payment_minimal_ui_content_icon_spacing">8dp</dimen> + <dimen name="payment_minimal_ui_content_top_spacing">78dp</dimen> + <dimen name="payment_minimal_ui_drag_handle_spacing">8dp</dimen> + <dimen name="payment_minimal_ui_large_text_vertical_spacing">8dp</dimen> + <dimen name="payment_minimal_ui_spacing">16dp</dimen> + <dimen name="payment_minimal_ui_toolbar_small_status_text_top_spacing">2dp</dimen> + + <!-- Payment minimal UI element sizes --> + <dimen name="payment_minimal_ui_content_icon_size">56dp</dimen> + <dimen name="payment_minimal_ui_separator_width">0.5dp</dimen> + <dimen name="payment_minimal_ui_toolbar_icon_size">36dp</dimen> +</resources> diff --git a/chromium/components/payments/content/android/java/src/org/chromium/components/payments/BrowserPaymentRequest.java b/chromium/components/payments/content/android/java/src/org/chromium/components/payments/BrowserPaymentRequest.java new file mode 100644 index 00000000000..2132ce27cbf --- /dev/null +++ b/chromium/components/payments/content/android/java/src/org/chromium/components/payments/BrowserPaymentRequest.java @@ -0,0 +1,98 @@ +// Copyright 2020 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. + +package org.chromium.components.payments; + +import androidx.annotation.Nullable; + +import org.chromium.content_public.browser.RenderFrameHost; +import org.chromium.payments.mojom.PaymentDetails; +import org.chromium.payments.mojom.PaymentMethodData; +import org.chromium.payments.mojom.PaymentOptions; +import org.chromium.payments.mojom.PaymentRequest; +import org.chromium.payments.mojom.PaymentValidationErrors; + +/** + * The browser part of the PaymentRequest implementation. The browser here can be either the + * Android Chrome browser or the WebLayer "browser". + */ +public interface BrowserPaymentRequest { + /** The factory that creates an instance of {@link BrowserPaymentRequest}. */ + interface Factory { + /** + * Create an instance of {@link BrowserPaymentRequest}. + * @param renderFrameHost The RenderFrameHost of the merchant page. + * @param componentPaymentRequestImpl The ComponentPaymentRequestImpl to work together with + * the BrowserPaymentRequest instance. + * @param isOffTheRecord Whether the merchant page is in an OffTheRecord (e.g., incognito, + * guest mode) Tab. + * @return An instance of BrowserPaymentRequest, cannot be null. + */ + BrowserPaymentRequest createBrowserPaymentRequest(RenderFrameHost renderFrameHost, + ComponentPaymentRequestImpl componentPaymentRequestImpl, boolean isOffTheRecord); + } + + /** + * Initialize the browser part of the {@link PaymentRequest} implementation and validate the raw + * payment request data coming from the untrusted mojo. + * @param methodData The supported methods specified by the merchant. + * @param details The payment details specified by the merchant. + * @param options The payment options specified by the merchant, can be null. + * @param googlePayBridgeEligible True when the renderer process deems the current request + * eligible for the skip-to-GPay experimental flow. It is ultimately up to the browser + * process to determine whether to trigger it + * @return whether the initialization is successful. + */ + boolean initAndValidate(PaymentMethodData[] methodData, PaymentDetails details, + @Nullable PaymentOptions options, boolean googlePayBridgeEligible); + + /** + * The browser part of the {@link PaymentRequest#show} implementation. + * @param isUserGesture Whether this method is triggered from a user gesture. + * @param waitForUpdatedDetails Whether to wait for updated details. It's true when merchant + * passed in a promise into PaymentRequest.show(), so Chrome should disregard the + * initial payment details and show a spinner until the promise resolves with the + * correct payment details. + */ + void show(boolean isUserGesture, boolean waitForUpdatedDetails); + + /** + * The browser part of the {@link PaymentRequest#updateWith} implementation. + * @param details The details that the merchant provides to update the payment request. + */ + void updateWith(PaymentDetails details); + + /** The browser part of the {@link PaymentRequest#onPaymentDetailsNotUpdated} implementation. */ + void onPaymentDetailsNotUpdated(); + + /** The browser part of the {@link PaymentRequest#abort} implementation. */ + void abort(); + + /** The browser part of the {@link PaymentRequest#complete} implementation. */ + void complete(int result); + + /** + * The browser part of the {@link PaymentRequest#retry} implementation. + * @param errors The merchant-defined error message strings, which are used to indicate to the + * end-user that something is wrong with the data of the payment response. + */ + void retry(PaymentValidationErrors errors); + + /** + * The browser part of the {@link PaymentRequest#hasEnrolledInstrument} implementation. + */ + void hasEnrolledInstrument(); + + /** The browser part of the {@link PaymentRequest#canMakePayment} implementation. */ + void canMakePayment(); + + /** Delegate to the same method of PaymentRequestImpl. */ + void disconnectFromClientWithDebugMessage(String debugMessage); + + /** + * Close this instance. The callers of this method should stop referencing this instance upon + * calling it. This method can be called within itself without causing infinite loop. + */ + void close(); +} diff --git a/chromium/components/payments/content/android/java/src/org/chromium/components/payments/CanMakePaymentQuery.java b/chromium/components/payments/content/android/java/src/org/chromium/components/payments/CanMakePaymentQuery.java new file mode 100644 index 00000000000..fb3f28a7fad --- /dev/null +++ b/chromium/components/payments/content/android/java/src/org/chromium/components/payments/CanMakePaymentQuery.java @@ -0,0 +1,53 @@ +// Copyright 2018 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. + +package org.chromium.components.payments; + +import org.chromium.base.annotations.CalledByNative; +import org.chromium.base.annotations.JNINamespace; +import org.chromium.base.annotations.NativeMethods; +import org.chromium.content_public.browser.WebContents; +import org.chromium.payments.mojom.PaymentMethodData; + +import java.util.Map; + +/** Checks whether canMakePayment() can be queried. */ +@JNINamespace("payments") +public class CanMakePaymentQuery { + /** + * Checks whether the given canMakePayment() query is allowed. + * + * @param webContents The web contents where the query is being performed. + * @param topLevelOrigin The top level origin using the Payment Request API. + * @param frameOrigin The frame origin using the Payment Request API. + * @param query The payment method identifiers and payment method specific data. + * + * @return True if the given query for canMakePayment() is allowed. + */ + public static boolean canQuery(WebContents webContents, String topLevelOrigin, + String frameOrigin, Map<String, PaymentMethodData> query) { + return CanMakePaymentQueryJni.get().canQuery( + webContents, topLevelOrigin, frameOrigin, query); + } + + @CalledByNative + private static String[] getMethodIdentifiers(Map<String, PaymentMethodData> query) { + return query.keySet().toArray(new String[query.size()]); + } + + @CalledByNative + private static String getStringifiedMethodData( + Map<String, PaymentMethodData> query, String methodIdentifier) { + assert query.containsKey(methodIdentifier); + return query.get(methodIdentifier).stringifiedData; + } + + private CanMakePaymentQuery() {} // Do not instantiate. + + @NativeMethods + interface Natives { + boolean canQuery(WebContents webContents, String topLevelOrigin, String frameOrigin, + Map<String, PaymentMethodData> query); + } +} diff --git a/chromium/components/payments/content/android/java/src/org/chromium/components/payments/ComponentPaymentRequestImpl.java b/chromium/components/payments/content/android/java/src/org/chromium/components/payments/ComponentPaymentRequestImpl.java new file mode 100644 index 00000000000..b1713acb177 --- /dev/null +++ b/chromium/components/payments/content/android/java/src/org/chromium/components/payments/ComponentPaymentRequestImpl.java @@ -0,0 +1,567 @@ +// Copyright 2020 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. + +package org.chromium.components.payments; + +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import org.chromium.base.Log; +import org.chromium.components.autofill.EditableOption; +import org.chromium.content_public.browser.RenderFrameHost; +import org.chromium.content_public.browser.WebContents; +import org.chromium.content_public.browser.WebContentsStatics; +import org.chromium.mojo.system.MojoException; +import org.chromium.payments.mojom.PayerDetail; +import org.chromium.payments.mojom.PaymentAddress; +import org.chromium.payments.mojom.PaymentDetails; +import org.chromium.payments.mojom.PaymentItem; +import org.chromium.payments.mojom.PaymentMethodData; +import org.chromium.payments.mojom.PaymentOptions; +import org.chromium.payments.mojom.PaymentRequest; +import org.chromium.payments.mojom.PaymentRequestClient; +import org.chromium.payments.mojom.PaymentResponse; +import org.chromium.payments.mojom.PaymentValidationErrors; + +import java.util.List; + +/** + * {@link ComponentPaymentRequestImpl}, {@link MojoPaymentRequestGateKeeper} and PaymentRequestImpl + * together make up the PaymentRequest service defined in + * third_party/blink/public/mojom/payments/payment_request.mojom. This class provides the parts + * shareable between Clank and WebLayer. The Clank specific logic lives in + * org.chromium.chrome.browser.payments.PaymentRequestImpl. + * TODO(crbug.com/1102522): PaymentRequestImpl is under refactoring, with the purpose of moving the + * business logic of PaymentRequestImpl into ComponentPaymentRequestImpl and eventually moving + * PaymentRequestImpl. Note that the callers of the instances of this class need to close them with + * {@link ComponentPaymentRequestImpl#close()}, after which no usage is allowed. + */ +public class ComponentPaymentRequestImpl { + private static final String TAG = "CompPaymentRequest"; + private static PaymentRequestServiceObserverForTest sObserverForTest; + private static NativeObserverForTest sNativeObserverForTest; + private final Runnable mOnClosedListener; + private final WebContents mWebContents; + private final JourneyLogger mJourneyLogger; + private boolean mSkipUiForNonUrlPaymentMethodIdentifiers; + private PaymentRequestLifecycleObserver mPaymentRequestLifecycleObserver; + private boolean mHasClosed; + + // mClient is null only when it has closed. + private PaymentRequestClient mClient; + + // mBrowserPaymentRequest is null when it has closed or is uninitiated. + private BrowserPaymentRequest mBrowserPaymentRequest; + + /** + * An observer interface injected when running tests to allow them to observe events. + * This interface holds events that should be passed back to the native C++ test + * harness and mirrors the C++ PaymentRequest::ObserverForTest() interface. Its methods + * should be called in the same places that the C++ PaymentRequest object will call its + * ObserverForTest. + */ + public interface NativeObserverForTest { + void onCanMakePaymentCalled(); + void onCanMakePaymentReturned(); + void onHasEnrolledInstrumentCalled(); + void onHasEnrolledInstrumentReturned(); + void onAppListReady(@Nullable List<EditableOption> paymentApps, PaymentItem total); + void onNotSupportedError(); + void onConnectionTerminated(); + void onAbortCalled(); + void onCompleteCalled(); + void onMinimalUIReady(); + } + + /** + * A test-only observer for the PaymentRequest service implementation. + */ + public interface PaymentRequestServiceObserverForTest { + /** + * Called after an instance of {@link ComponentPaymentRequestImpl} has been created. + * + * @param componentPaymentRequest The newly created instance of ComponentPaymentRequestImpl. + */ + void onPaymentRequestCreated(ComponentPaymentRequestImpl componentPaymentRequest); + + /** + * Called when an abort request was denied. + */ + void onPaymentRequestServiceUnableToAbort(); + + /** + * Called when the controller is notified of billing address change, but does not alter the + * editor UI. + */ + void onPaymentRequestServiceBillingAddressChangeProcessed(); + + /** + * Called when the controller is notified of an expiration month change. + */ + void onPaymentRequestServiceExpirationMonthChange(); + + /** + * Called when a show request failed. This can happen when: + * <ul> + * <li>The merchant requests only unsupported payment methods.</li> + * <li>The merchant requests only payment methods that don't have corresponding apps and + * are not able to add a credit card from PaymentRequest UI.</li> + * </ul> + */ + void onPaymentRequestServiceShowFailed(); + + /** + * Called when the canMakePayment() request has been responded to. + */ + void onPaymentRequestServiceCanMakePaymentQueryResponded(); + + /** + * Called when the hasEnrolledInstrument() request has been responded to. + */ + void onPaymentRequestServiceHasEnrolledInstrumentQueryResponded(); + + /** + * Called when the payment response is ready. + */ + void onPaymentResponseReady(); + + /** + * Called when the browser acknowledges the renderer's complete call, which indicates that + * the browser UI has closed. + */ + void onCompleteReplied(); + + /** + * Called when the renderer is closing the mojo connection (e.g. upon show promise + * rejection). + */ + void onRendererClosedMojoConnection(); + } + + /** + * Create an instance of {@link PaymentRequest} that provides the Android PaymentRequest + * service. + * @param renderFrameHost The RenderFrameHost of the merchant page. + * @param isOffTheRecord Whether the merchant page is in a off-the-record (e.g., incognito, + * guest mode) Tab. + * @param skipUiForBasicCard True if the PaymentRequest UI should be skipped when the request + * only supports basic-card methods. + * @param browserPaymentRequestFactory The factory that generates BrowserPaymentRequest. + * @return The created instance. + */ + public static PaymentRequest createPaymentRequest(RenderFrameHost renderFrameHost, + boolean isOffTheRecord, boolean skipUiForBasicCard, + BrowserPaymentRequest.Factory browserPaymentRequestFactory) { + return new MojoPaymentRequestGateKeeper( + (client, methodData, details, options, googlePayBridgeEligible, onClosedListener) + -> ComponentPaymentRequestImpl.createIfParamsValid(renderFrameHost, + isOffTheRecord, skipUiForBasicCard, browserPaymentRequestFactory, + client, methodData, details, options, googlePayBridgeEligible, + onClosedListener)); + } + + /** + * @return An instance of {@link ComponentPaymentRequestImpl} only if the parameters are deemed + * valid; Otherwise, null. + */ + @Nullable + private static ComponentPaymentRequestImpl createIfParamsValid(RenderFrameHost renderFrameHost, + boolean isOffTheRecord, boolean skipUiForBasicCard, + BrowserPaymentRequest.Factory browserPaymentRequestFactory, + @Nullable PaymentRequestClient client, @Nullable PaymentMethodData[] methodData, + @Nullable PaymentDetails details, @Nullable PaymentOptions options, + boolean googlePayBridgeEligible, Runnable onClosedListener) { + assert renderFrameHost != null; + assert browserPaymentRequestFactory != null; + assert onClosedListener != null; + + WebContents webContents = WebContentsStatics.fromRenderFrameHost(renderFrameHost); + if (webContents == null || webContents.isDestroyed()) { + abortBeforeInstantiation(/*client=*/null, /*journeyLogger=*/null, + ErrorStrings.NO_WEB_CONTENTS, AbortReason.INVALID_DATA_FROM_RENDERER); + return null; + } + + JourneyLogger journeyLogger = new JourneyLogger(isOffTheRecord, webContents); + + if (client == null) { + abortBeforeInstantiation(/*client=*/null, journeyLogger, ErrorStrings.INVALID_STATE, + AbortReason.INVALID_DATA_FROM_RENDERER); + return null; + } + + if (!OriginSecurityChecker.isOriginSecure(webContents.getLastCommittedUrl())) { + abortBeforeInstantiation(client, journeyLogger, ErrorStrings.NOT_IN_A_SECURE_ORIGIN, + AbortReason.INVALID_DATA_FROM_RENDERER); + return null; + } + + if (methodData == null) { + abortBeforeInstantiation(client, journeyLogger, + ErrorStrings.INVALID_PAYMENT_METHODS_OR_DATA, + AbortReason.INVALID_DATA_FROM_RENDERER); + return null; + } + + if (details == null) { + abortBeforeInstantiation(client, journeyLogger, ErrorStrings.INVALID_PAYMENT_DETAILS, + AbortReason.INVALID_DATA_FROM_RENDERER); + return null; + } + + ComponentPaymentRequestImpl instance = new ComponentPaymentRequestImpl(client, + renderFrameHost, webContents, journeyLogger, skipUiForBasicCard, onClosedListener); + instance.onCreated(); + boolean valid = instance.initAndValidate(renderFrameHost, browserPaymentRequestFactory, + methodData, details, options, googlePayBridgeEligible, isOffTheRecord); + if (!valid) { + instance.close(); + return null; + } + return instance; + } + + private void onCreated() { + if (sObserverForTest == null) return; + sObserverForTest.onPaymentRequestCreated(this); + } + + /** Abort the request, used before this class's instantiation. */ + private static void abortBeforeInstantiation(@Nullable PaymentRequestClient client, + @Nullable JourneyLogger journeyLogger, String debugMessage, int reason) { + Log.d(TAG, debugMessage); + if (client != null) client.onError(reason, debugMessage); + if (journeyLogger != null) journeyLogger.setAborted(reason); + if (sNativeObserverForTest != null) sNativeObserverForTest.onConnectionTerminated(); + } + + private ComponentPaymentRequestImpl(PaymentRequestClient client, + RenderFrameHost renderFrameHost, WebContents webContents, JourneyLogger journeyLogger, + boolean skipUiForBasicCard, Runnable onClosedListener) { + assert client != null; + assert renderFrameHost != null; + assert webContents != null; + assert journeyLogger != null; + + mSkipUiForNonUrlPaymentMethodIdentifiers = skipUiForBasicCard; + mClient = client; + mWebContents = webContents; + mJourneyLogger = journeyLogger; + mOnClosedListener = onClosedListener; + mHasClosed = false; + } + + /** + * Set a native-side observer for PaymentRequest implementations. This observer should be set + * before PaymentRequest implementations are instantiated. + * @param nativeObserverForTest The native-side observer. + */ + @VisibleForTesting + public static void setNativeObserverForTest(NativeObserverForTest nativeObserverForTest) { + sNativeObserverForTest = nativeObserverForTest; + } + + /** @return Get the native=side observer, for testing purpose only. */ + @Nullable + public static NativeObserverForTest getNativeObserverForTest() { + return sNativeObserverForTest; + } + + private boolean initAndValidate(RenderFrameHost renderFrameHost, + BrowserPaymentRequest.Factory factory, @Nullable PaymentMethodData[] methodData, + @Nullable PaymentDetails details, @Nullable PaymentOptions options, + boolean googlePayBridgeEligible, boolean isOffTheRecord) { + mBrowserPaymentRequest = + factory.createBrowserPaymentRequest(renderFrameHost, this, isOffTheRecord); + return mBrowserPaymentRequest.initAndValidate( + methodData, details, options, googlePayBridgeEligible); + } + + /** + * The component part of the {@link PaymentRequest#show} implementation. Check {@link + * PaymentRequest#show} for the parameters' specification. + */ + /* package */ void show(boolean isUserGesture, boolean waitForUpdatedDetails) { + // Every caller should stop referencing this class once close() is called. + assert mBrowserPaymentRequest != null; + + mBrowserPaymentRequest.show(isUserGesture, waitForUpdatedDetails); + } + + /** + * The component part of the {@link PaymentRequest#updateWith} implementation. + * @param details The details that the merchant provides to update the payment request. + */ + /* package */ void updateWith(PaymentDetails details) { + // Every caller should stop referencing this class once close() is called. + assert mBrowserPaymentRequest != null; + + mBrowserPaymentRequest.updateWith(details); + } + + /** + * The component part of the {@link PaymentRequest#onPaymentDetailsNotUpdated} implementation. + */ + /* package */ void onPaymentDetailsNotUpdated() { + // Every caller should stop referencing this class once close() is called. + assert mBrowserPaymentRequest != null; + + mBrowserPaymentRequest.onPaymentDetailsNotUpdated(); + } + + /** The component part of the {@link PaymentRequest#abort} implementation. */ + /* package */ void abort() { + // Every caller should stop referencing this class once close() is called. + assert mBrowserPaymentRequest != null; + mBrowserPaymentRequest.abort(); + } + + /** The component part of the {@link PaymentRequest#complete} implementation. */ + /* package */ void complete(int result) { + // Every caller should stop referencing this class once close() is called. + assert mBrowserPaymentRequest != null; + + mBrowserPaymentRequest.complete(result); + } + + /** + * The component part of the {@link PaymentRequest#retry} implementation. Check {@link + * PaymentRequest#retry} for the parameters' specification. + */ + /* package */ void retry(PaymentValidationErrors errors) { + // Every caller should stop referencing this class once close() is called. + assert mBrowserPaymentRequest != null; + + mBrowserPaymentRequest.retry(errors); + } + + /** The component part of the {@link PaymentRequest#canMakePayment} implementation. */ + /* package */ void canMakePayment() { + // Every caller should stop referencing this class once close() is called. + assert mBrowserPaymentRequest != null; + + mBrowserPaymentRequest.canMakePayment(); + } + + /** + * The component part of the {@link PaymentRequest#hasEnrolledInstrument} implementation. + */ + /* package */ void hasEnrolledInstrument() { + // Every caller should stop referencing this class once close() is called. + assert mBrowserPaymentRequest != null; + + mBrowserPaymentRequest.hasEnrolledInstrument(); + } + + /** + * Implement {@link PaymentRequest#close}. This should be called by the renderer only. The + * closing triggered by other classes should call {@link #close} instead. The caller should + * stop referencing this class after calling this method. + */ + /* package */ void closeByRenderer() { + // Every caller should stop referencing this class once close() is called. + assert mBrowserPaymentRequest != null; + + mJourneyLogger.setAborted(AbortReason.MOJO_RENDERER_CLOSING); + close(); + if (sObserverForTest != null) { + sObserverForTest.onRendererClosedMojoConnection(); + } + if (sNativeObserverForTest != null) { + sNativeObserverForTest.onConnectionTerminated(); + } + } + + /** + * Called when the mojo connection with the renderer PaymentRequest has an error. The caller + * should stop referencing this class after calling this method. + * @param e The mojo exception. + */ + /* package */ void onConnectionError(MojoException e) { + // Every caller should stop referencing this class once close() is called. + assert mBrowserPaymentRequest != null; + + mJourneyLogger.setAborted(AbortReason.MOJO_CONNECTION_ERROR); + close(); + if (sNativeObserverForTest != null) { + sNativeObserverForTest.onConnectionTerminated(); + } + } + + /** + * Abort the request because the (untrusted) renderer passes invalid data. + * @param debugMessage The debug message to be sent to the renderer. + */ + /* package */ void abortForInvalidDataFromRenderer(String debugMessage) { + // Every caller should stop referencing this class once close() is called. + assert mBrowserPaymentRequest != null; + + mJourneyLogger.setAborted(AbortReason.INVALID_DATA_FROM_RENDERER); + mBrowserPaymentRequest.disconnectFromClientWithDebugMessage(debugMessage); + } + + /** + * Close this instance and release all of the retained resources. The external callers of this + * method should stop referencing this instance upon calling. This method can be called within + * itself without causing infinite loops. + */ + public void close() { + if (mHasClosed) return; + mHasClosed = true; + + if (mBrowserPaymentRequest == null) return; + mBrowserPaymentRequest.close(); + mBrowserPaymentRequest = null; + + // mClient can be null only when this method is called from + // ComponentPaymentRequestImpl#create(). + if (mClient != null) mClient.close(); + mClient = null; + + mOnClosedListener.run(); + } + + /** + * Register an observer for the PaymentRequest lifecycle. + * @param paymentRequestLifecycleObserver The observer, cannot be null. + */ + public void registerPaymentRequestLifecycleObserver( + PaymentRequestLifecycleObserver paymentRequestLifecycleObserver) { + assert paymentRequestLifecycleObserver != null; + mPaymentRequestLifecycleObserver = paymentRequestLifecycleObserver; + } + + /** @return The observer for the PaymentRequest lifecycle, can be null. */ + @Nullable + public PaymentRequestLifecycleObserver getPaymentRequestLifecycleObserver() { + return mPaymentRequestLifecycleObserver; + } + + /** @return An observer for the payment request service, if any; otherwise, null. */ + @Nullable + public static PaymentRequestServiceObserverForTest getObserverForTest() { + return sObserverForTest; + } + + /** Set an observer for the payment request service, cannot be null. */ + @VisibleForTesting + public static void setObserverForTest(PaymentRequestServiceObserverForTest observerForTest) { + assert observerForTest != null; + sObserverForTest = observerForTest; + } + + /** + * @return True when skip UI is available for non-url based payment method identifiers (e.g. + * basic-card). + */ + public boolean skipUiForNonUrlPaymentMethodIdentifiers() { + return mSkipUiForNonUrlPaymentMethodIdentifiers; + } + + @VisibleForTesting + public void setSkipUiForNonUrlPaymentMethodIdentifiersForTest() { + mSkipUiForNonUrlPaymentMethodIdentifiers = true; + } + + /** Invokes {@link PaymentRequest.onPaymentMethodChange}. */ + public void onPaymentMethodChange(String methodName, String stringifiedDetails) { + // Every caller should stop referencing this class once close() is called. + assert mClient != null; + + mClient.onPaymentMethodChange(methodName, stringifiedDetails); + } + + /** Invokes {@link PaymentRequest.onShippingAddressChange}. */ + public void onShippingAddressChange(PaymentAddress address) { + // Every caller should stop referencing this class once close() is called. + assert mClient != null; + + mClient.onShippingAddressChange(address); + } + + /** Invokes {@link PaymentRequest.onShippingOptionChange}. */ + public void onShippingOptionChange(String shippingOptionId) { + // Every caller should stop referencing this class once close() is called. + assert mClient != null; + + mClient.onShippingOptionChange(shippingOptionId); + } + + /** Invokes {@link PaymentRequest.onPayerDetailChange}. */ + public void onPayerDetailChange(PayerDetail detail) { + // Every caller should stop referencing this class once close() is called. + assert mClient != null; + + mClient.onPayerDetailChange(detail); + } + + /** Invokes {@link PaymentRequest.onPaymentResponse}. */ + public void onPaymentResponse(PaymentResponse response) { + // Every caller should stop referencing this class once close() is called. + assert mClient != null; + + mClient.onPaymentResponse(response); + } + + /** Invokes {@link PaymentRequest.onError}. */ + public void onError(int error, String errorMessage) { + // Every caller should stop referencing this class once close() is called. + assert mClient != null; + + mClient.onError(error, errorMessage); + } + + /** Invokes {@link PaymentRequest.onComplete}. */ + public void onComplete() { + // Every caller should stop referencing this class once close() is called. + assert mClient != null; + + mClient.onComplete(); + } + + /** Invokes {@link PaymentRequest.onAbort}. */ + public void onAbort(boolean abortedSuccessfully) { + // Every caller should stop referencing this class once close() is called. + assert mClient != null; + + mClient.onAbort(abortedSuccessfully); + } + + /** Invokes {@link PaymentRequest.onCanMakePayment}. */ + public void onCanMakePayment(int result) { + // Every caller should stop referencing this class once close() is called. + assert mClient != null; + + mClient.onCanMakePayment(result); + } + + /** Invokes {@link PaymentRequest.onHasEnrolledInstrument}. */ + public void onHasEnrolledInstrument(int result) { + // Every caller should stop referencing this class once close() is called. + assert mClient != null; + + mClient.onHasEnrolledInstrument(result); + } + + /** Invokes {@link PaymentRequest.warnNoFavicon}. */ + public void warnNoFavicon() { + // Every caller should stop referencing this class once close() is called. + assert mClient != null; + + mClient.warnNoFavicon(); + } + + /** + * @return The logger of the user journey of the Android PaymentRequest service, cannot be + * null. + */ + public JourneyLogger getJourneyLogger() { + return mJourneyLogger; + } + + /** @return The WebContents of the merchant's page, cannot be null. */ + public WebContents getWebContents() { + return mWebContents; + } +} diff --git a/chromium/components/payments/content/android/java/src/org/chromium/components/payments/JourneyLogger.java b/chromium/components/payments/content/android/java/src/org/chromium/components/payments/JourneyLogger.java index 93a29b3b056..bc519530283 100644 --- a/chromium/components/payments/content/android/java/src/org/chromium/components/payments/JourneyLogger.java +++ b/chromium/components/payments/content/android/java/src/org/chromium/components/payments/JourneyLogger.java @@ -20,7 +20,14 @@ public class JourneyLogger { private boolean mHasRecorded; + /** + * Creates the journey logger. + * @param isIncognito Whether the user profile is incognito. + * @param webContents The web contents where PaymentRequest API is invoked. Should not be null. + */ public JourneyLogger(boolean isIncognito, WebContents webContents) { + assert webContents != null; + assert !webContents.isDestroyed(); // Note that this pointer could leak the native object. The called must call destroy() to // ensure that the native object is destroyed. mJourneyLoggerAndroid = JourneyLoggerJni.get().initJourneyLoggerAndroid( @@ -109,9 +116,6 @@ public class JourneyLogger { * @param event The event that occurred. */ public void setEventOccurred(int event) { - assert event >= 0; - assert event < Event.ENUM_MAX; - JourneyLoggerJni.get().setEventOccurred(mJourneyLoggerAndroid, JourneyLogger.this, event); } @@ -202,6 +206,14 @@ public class JourneyLogger { } /** + * Records that the payment request has entered the given checkout step. + * @param step An int indicating the step to be recorded. + */ + public void recordCheckoutStep(int step) { + JourneyLoggerJni.get().recordCheckoutStep(mJourneyLoggerAndroid, JourneyLogger.this, step); + } + + /** * Records the time when request.show() is called. */ public void setTriggerTime() { @@ -246,6 +258,7 @@ public class JourneyLogger { void setNotShown(long nativeJourneyLoggerAndroid, JourneyLogger caller, int reason); void recordTransactionAmount(long nativeJourneyLoggerAndroid, JourneyLogger caller, String currency, String value, boolean completed); + void recordCheckoutStep(long nativeJourneyLoggerAndroid, JourneyLogger caller, int step); void setTriggerTime(long nativeJourneyLoggerAndroid, JourneyLogger caller); void setPaymentAppUkmSourceId( long nativeJourneyLoggerAndroid, JourneyLogger caller, long sourceId); diff --git a/chromium/components/payments/content/android/java/src/org/chromium/components/payments/MojoPaymentRequestGateKeeper.java b/chromium/components/payments/content/android/java/src/org/chromium/components/payments/MojoPaymentRequestGateKeeper.java new file mode 100644 index 00000000000..c4c500be045 --- /dev/null +++ b/chromium/components/payments/content/android/java/src/org/chromium/components/payments/MojoPaymentRequestGateKeeper.java @@ -0,0 +1,147 @@ +// Copyright 2020 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. + +package org.chromium.components.payments; + +import org.chromium.mojo.system.MojoException; +import org.chromium.payments.mojom.PaymentDetails; +import org.chromium.payments.mojom.PaymentMethodData; +import org.chromium.payments.mojom.PaymentOptions; +import org.chromium.payments.mojom.PaymentRequest; +import org.chromium.payments.mojom.PaymentRequestClient; +import org.chromium.payments.mojom.PaymentValidationErrors; + +/** + * Guards against invalid mojo parameters and enforces correct call sequence from mojo IPC in the + * untrusted renderer, so ComponentPaymentRequestImpl does not have to. + */ +/* package */ class MojoPaymentRequestGateKeeper implements PaymentRequest { + private final ComponentPaymentRequestImplCreator mComponentPaymentRequestImplCreator; + private ComponentPaymentRequestImpl mComponentPaymentRequestImpl; + + /** The creator of ComponentPaymentRequestImpl. */ + /* package */ interface ComponentPaymentRequestImplCreator { + /** + * Create an instance of ComponentPaymentRequestImpl if the parameters are valid. + * @param client The client of the renderer PaymentRequest, need validation before usage. + * @param methodData The supported methods specified by the merchant, need validation before + * usage. + * @param details The payment details specified by the merchant, need validation before + * usage. + * @param options The payment options specified by the merchant, need validation before + * usage. + * @param googlePayBridgeEligible True when the renderer process deems the current request + * eligible for the skip-to-GPay experimental flow. It is ultimately up to the + * browser process to determine whether to trigger it. + * @param onClosedListener The listener that's invoked when ComponentPaymentRequestImpl has + * just closed. + * @return The created instance, if the parameters are valid; otherwise, null. + */ + ComponentPaymentRequestImpl createComponentPaymentRequestImplIfParamsValid( + PaymentRequestClient client, PaymentMethodData[] methodData, PaymentDetails details, + PaymentOptions options, boolean googlePayBridgeEligible, Runnable onClosedListener); + } + + /** + * Create an instance of MojoPaymentRequestGateKeeper. + * @param creator The creator of ComponentPaymentRequestImpl. + */ + /* package */ MojoPaymentRequestGateKeeper(ComponentPaymentRequestImplCreator creator) { + mComponentPaymentRequestImplCreator = creator; + } + + // Implement PaymentRequest: + @Override + public void init(PaymentRequestClient client, PaymentMethodData[] methodData, + PaymentDetails details, PaymentOptions options, boolean googlePayBridgeEligible) { + if (mComponentPaymentRequestImpl != null) { + mComponentPaymentRequestImpl.abortForInvalidDataFromRenderer( + ErrorStrings.ATTEMPTED_INITIALIZATION_TWICE); + mComponentPaymentRequestImpl = null; + return; + } + + // Note that a null value would be assigned when the params are invalid. + mComponentPaymentRequestImpl = + mComponentPaymentRequestImplCreator.createComponentPaymentRequestImplIfParamsValid( + client, methodData, details, options, googlePayBridgeEligible, + this::onComponentPaymentRequestImplClosed); + } + + private void onComponentPaymentRequestImplClosed() { + mComponentPaymentRequestImpl = null; + } + + // Implement PaymentRequest: + @Override + public void show(boolean isUserGesture, boolean waitForUpdatedDetails) { + if (mComponentPaymentRequestImpl == null) return; + mComponentPaymentRequestImpl.show(isUserGesture, waitForUpdatedDetails); + } + + // Implement PaymentRequest: + @Override + public void updateWith(PaymentDetails details) { + if (mComponentPaymentRequestImpl == null) return; + mComponentPaymentRequestImpl.updateWith(details); + } + + // Implement PaymentRequest: + @Override + public void onPaymentDetailsNotUpdated() { + if (mComponentPaymentRequestImpl == null) return; + mComponentPaymentRequestImpl.onPaymentDetailsNotUpdated(); + } + + // Implement PaymentRequest: + @Override + public void abort() { + if (mComponentPaymentRequestImpl == null) return; + mComponentPaymentRequestImpl.abort(); + } + + // Implement PaymentRequest: + @Override + public void complete(int result) { + if (mComponentPaymentRequestImpl == null) return; + mComponentPaymentRequestImpl.complete(result); + } + + // Implement PaymentRequest: + @Override + public void retry(PaymentValidationErrors errors) { + if (mComponentPaymentRequestImpl == null) return; + mComponentPaymentRequestImpl.retry(errors); + } + + // Implement PaymentRequest: + @Override + public void canMakePayment() { + if (mComponentPaymentRequestImpl == null) return; + mComponentPaymentRequestImpl.canMakePayment(); + } + + // Implement PaymentRequest: + @Override + public void hasEnrolledInstrument() { + if (mComponentPaymentRequestImpl == null) return; + mComponentPaymentRequestImpl.hasEnrolledInstrument(); + } + + // Implement PaymentRequest: + @Override + public void close() { + if (mComponentPaymentRequestImpl == null) return; + mComponentPaymentRequestImpl.closeByRenderer(); + mComponentPaymentRequestImpl = null; + } + + // Implement PaymentRequest: + @Override + public void onConnectionError(MojoException e) { + if (mComponentPaymentRequestImpl == null) return; + mComponentPaymentRequestImpl.onConnectionError(e); + mComponentPaymentRequestImpl = null; + } +} diff --git a/chromium/components/payments/content/android/java/src/org/chromium/components/payments/OWNERS b/chromium/components/payments/content/android/java/src/org/chromium/components/payments/OWNERS index db4625d4570..54fab122ac4 100644 --- a/chromium/components/payments/content/android/java/src/org/chromium/components/payments/OWNERS +++ b/chromium/components/payments/content/android/java/src/org/chromium/components/payments/OWNERS @@ -1,5 +1,6 @@ # TEAM: payments-dev@chromium.org # COMPONENT: UI>Browser>Payments +# OS: Android file://components/payments/OWNERS diff --git a/chromium/components/payments/content/android/java/src/org/chromium/components/payments/PaymentApp.java b/chromium/components/payments/content/android/java/src/org/chromium/components/payments/PaymentApp.java index b719873aa8c..e465261712f 100644 --- a/chromium/components/payments/content/android/java/src/org/chromium/components/payments/PaymentApp.java +++ b/chromium/components/payments/content/android/java/src/org/chromium/components/payments/PaymentApp.java @@ -306,4 +306,12 @@ public abstract class PaymentApp extends EditableOption { public @PaymentAppType int getPaymentAppType() { return PaymentAppType.UNDEFINED; } + + /** + * @return Whether this app should be chosen over other available payment apps. For example, + * when the Play Billing payment app is available in a TWA. + */ + public boolean isPreferred() { + return false; + } } diff --git a/chromium/components/payments/content/android/java/src/org/chromium/components/payments/PaymentAppFactoryParams.java b/chromium/components/payments/content/android/java/src/org/chromium/components/payments/PaymentAppFactoryParams.java new file mode 100644 index 00000000000..57a9817f783 --- /dev/null +++ b/chromium/components/payments/content/android/java/src/org/chromium/components/payments/PaymentAppFactoryParams.java @@ -0,0 +1,87 @@ +// Copyright 2019 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. + +package org.chromium.components.payments; + +import androidx.annotation.Nullable; + +import org.chromium.content_public.browser.RenderFrameHost; +import org.chromium.content_public.browser.WebContents; +import org.chromium.url.Origin; + +/** Interface for providing information to a payment app factory. */ +public interface PaymentAppFactoryParams extends PaymentRequestParams { + /** @return The web contents where the payment is being requested. */ + WebContents getWebContents(); + + /** @return The RenderFrameHost for the frame that initiates the payment request. */ + RenderFrameHost getRenderFrameHost(); + + /** @return The PaymentRequest object identifier. */ + default String getId() { + return null; + } + + /** + * @return The scheme, host, and port of the last committed URL of the top-level context as + * formatted by UrlFormatter.formatUrlForSecurityDisplay(). + */ + default String getTopLevelOrigin() { + return null; + } + + /** + * @return The scheme, host, and port of the last committed URL of the iframe that invoked the + * PaymentRequest API as formatted by UrlFormatter.formatUrlForSecurityDisplay(). + */ + default String getPaymentRequestOrigin() { + return null; + } + + /** + * @return The origin of the iframe that invoked the PaymentRequest API. Can be opaque. Used by + * security features like 'Sec-Fetch-Site' and 'Cross-Origin-Resource-Policy'. Should not be + * null. + */ + default Origin getPaymentRequestSecurityOrigin() { + return null; + } + + /** + * @return The certificate chain of the top-level context as returned by + * CertificateChainHelper.getCertificateChain(). Can be null. + */ + @Nullable + default byte[][] getCertificateChain() { + return null; + } + + /** + * @return Whether crawling the web for just-in-time installable payment handlers is enabled. + */ + default boolean getMayCrawl() { + return false; + } + + /** + * @return The listener for payment method, shipping address, and shipping option change events. + */ + default PaymentRequestUpdateEventListener getPaymentRequestUpdateEventListener() { + return null; + } + + /** @return The Payment Request information received from the merchant. */ + default PaymentRequestSpec getSpec() { + return null; + } + + /** + * @return The Android package name of the Trusted Web Activity that invoked Chrome, if running + * in TWA mode. Otherwise null or empty string. + */ + @Nullable + default String getTwaPackageName() { + return null; + } +} diff --git a/chromium/components/payments/content/android/java/src/org/chromium/components/payments/PaymentFeatureList.java b/chromium/components/payments/content/android/java/src/org/chromium/components/payments/PaymentFeatureList.java index 28bf46d2499..79b22ff47b7 100644 --- a/chromium/components/payments/content/android/java/src/org/chromium/components/payments/PaymentFeatureList.java +++ b/chromium/components/payments/content/android/java/src/org/chromium/components/payments/PaymentFeatureList.java @@ -17,6 +17,7 @@ import org.chromium.base.annotations.NativeMethods; public class PaymentFeatureList { /** Alphabetical: */ public static final String ANDROID_APP_PAYMENT_UPDATE_EVENTS = "AndroidAppPaymentUpdateEvents"; + public static final String ENFORCE_FULL_DELEGATION = "EnforceFullDelegation"; public static final String PAYMENT_REQUEST_SKIP_TO_GPAY = "PaymentRequestSkipToGPay"; public static final String PAYMENT_REQUEST_SKIP_TO_GPAY_IF_NO_CARD = "PaymentRequestSkipToGPayIfNoCard"; @@ -27,6 +28,7 @@ public class PaymentFeatureList { public static final String WEB_PAYMENTS = "WebPayments"; public static final String WEB_PAYMENTS_ALWAYS_ALLOW_JUST_IN_TIME_PAYMENT_APP = "AlwaysAllowJustInTimePaymentApp"; + public static final String WEB_PAYMENTS_APP_STORE_BILLING = "AppStoreBilling"; public static final String WEB_PAYMENTS_APP_STORE_BILLING_DEBUG = "AppStoreBillingDebug"; public static final String WEB_PAYMENTS_EXPERIMENTAL_FEATURES = "WebPaymentsExperimentalFeatures"; diff --git a/chromium/components/payments/content/android/java/src/org/chromium/components/payments/PaymentManifestWebDataService.java b/chromium/components/payments/content/android/java/src/org/chromium/components/payments/PaymentManifestWebDataService.java new file mode 100644 index 00000000000..2999c039226 --- /dev/null +++ b/chromium/components/payments/content/android/java/src/org/chromium/components/payments/PaymentManifestWebDataService.java @@ -0,0 +1,151 @@ +// Copyright 2017 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. + +package org.chromium.components.payments; + +import org.chromium.base.annotations.CalledByNative; +import org.chromium.base.annotations.JNINamespace; +import org.chromium.base.annotations.NativeMethods; + +/** Java wrapper of the payment manifest web data service. */ +@JNINamespace("payments") +public class PaymentManifestWebDataService { + /** Interface for the callback to invoke when getting data from the web data service. */ + public interface PaymentManifestWebDataServiceCallback { + /** + * Called when getPaymentMethodManifest success. + * + * @param appIdentifiers The list of package names and origins of the supported apps in the + * payment method manifest. May also contain "*" to indicate that + * all origins are supported. + */ + @CalledByNative("PaymentManifestWebDataServiceCallback") + void onPaymentMethodManifestFetched(String[] appIdentifiers); + + /** + * Called when getPaymentWebAppManifest success. + * + * @param manifest The web app manifest sections. + */ + @CalledByNative("PaymentManifestWebDataServiceCallback") + void onPaymentWebAppManifestFetched(WebAppManifestSection[] manifest); + } + + /** Holds the native counterpart of this class object. */ + private long mManifestWebDataServiceAndroid; + + public PaymentManifestWebDataService() { + mManifestWebDataServiceAndroid = + PaymentManifestWebDataServiceJni.get().init(PaymentManifestWebDataService.this); + } + + /** + * Destroy this class object. It destroys the native counterpart. + */ + public void destroy() { + PaymentManifestWebDataServiceJni.get().destroy( + mManifestWebDataServiceAndroid, PaymentManifestWebDataService.this); + mManifestWebDataServiceAndroid = 0; + } + + /** + * Gets the payment method's manifest. + * + * @param methodName The payment method name. + * @param callback The callback to invoke when finishing the request. + * @return True if the result will be returned through callback. + */ + public boolean getPaymentMethodManifest( + String methodName, PaymentManifestWebDataServiceCallback callback) { + return PaymentManifestWebDataServiceJni.get().getPaymentMethodManifest( + mManifestWebDataServiceAndroid, PaymentManifestWebDataService.this, methodName, + callback); + } + + /** + * Gets the corresponding payment web app's manifest. + * + * @param appPackageName The package name of the Android payment app. + * @param callback The callback to invoke when finishing the request. + * @return True if the result will be returned through callback. + */ + public boolean getPaymentWebAppManifest( + String appPackageName, PaymentManifestWebDataServiceCallback callback) { + return PaymentManifestWebDataServiceJni.get().getPaymentWebAppManifest( + mManifestWebDataServiceAndroid, PaymentManifestWebDataService.this, appPackageName, + callback); + } + + /** + * Adds the supported Android apps' package names of the method. + * + * @param methodName The method name. + * @param appPackageNames The supported app package names and origins. Also possibly "*" if + * applicable. + */ + public void addPaymentMethodManifest(String methodName, String[] appIdentifiers) { + PaymentManifestWebDataServiceJni.get().addPaymentMethodManifest( + mManifestWebDataServiceAndroid, PaymentManifestWebDataService.this, methodName, + appIdentifiers); + } + + /** + * Adds web app's manifest. + * + * @param manifest The manifest. + */ + public void addPaymentWebAppManifest(WebAppManifestSection[] manifest) { + PaymentManifestWebDataServiceJni.get().addPaymentWebAppManifest( + mManifestWebDataServiceAndroid, PaymentManifestWebDataService.this, manifest); + } + + @CalledByNative + private static WebAppManifestSection[] createManifest(int numberOfsections) { + return new WebAppManifestSection[numberOfsections]; + } + + @CalledByNative + private static void addSectionToManifest(WebAppManifestSection[] manifest, int sectionIndex, + String id, long minVersion, int numberOfFingerprints) { + manifest[sectionIndex] = new WebAppManifestSection(id, minVersion, numberOfFingerprints); + } + + @CalledByNative + private static void addFingerprintToSection(WebAppManifestSection[] manifest, int sectionIndex, + int fingerprintIndex, byte[] fingerprint) { + manifest[sectionIndex].fingerprints[fingerprintIndex] = fingerprint; + } + + @CalledByNative + private static String getIdFromSection(WebAppManifestSection manifestSection) { + return manifestSection.id; + } + + @CalledByNative + private static long getMinVersionFromSection(WebAppManifestSection manifestSection) { + return manifestSection.minVersion; + } + + @CalledByNative + private static byte[][] getFingerprintsFromSection(WebAppManifestSection manifestSection) { + return manifestSection.fingerprints; + } + + @NativeMethods + interface Natives { + long init(PaymentManifestWebDataService caller); + void destroy(long nativePaymentManifestWebDataServiceAndroid, + PaymentManifestWebDataService caller); + boolean getPaymentMethodManifest(long nativePaymentManifestWebDataServiceAndroid, + PaymentManifestWebDataService caller, String methodName, + PaymentManifestWebDataServiceCallback callback); + boolean getPaymentWebAppManifest(long nativePaymentManifestWebDataServiceAndroid, + PaymentManifestWebDataService caller, String appPackageName, + PaymentManifestWebDataServiceCallback callback); + void addPaymentMethodManifest(long nativePaymentManifestWebDataServiceAndroid, + PaymentManifestWebDataService caller, String methodName, String[] appPackageNames); + void addPaymentWebAppManifest(long nativePaymentManifestWebDataServiceAndroid, + PaymentManifestWebDataService caller, WebAppManifestSection[] manifest); + } +} diff --git a/chromium/components/payments/content/android/java/src/org/chromium/components/payments/PaymentOptionsUtils.java b/chromium/components/payments/content/android/java/src/org/chromium/components/payments/PaymentOptionsUtils.java new file mode 100644 index 00000000000..de0ee8fa45c --- /dev/null +++ b/chromium/components/payments/content/android/java/src/org/chromium/components/payments/PaymentOptionsUtils.java @@ -0,0 +1,90 @@ +// Copyright 2019 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. + +package org.chromium.components.payments; + +import androidx.annotation.Nullable; + +import org.chromium.payments.mojom.PaymentOptions; + +/** + * A collection of utility methods for PaymentOptions. + */ +public class PaymentOptionsUtils { + /** + * @param options Any PaymentOption, can be null. + * @return Whether a PaymentOptions has requested any information (shipping, payer's email, + * payer's phone, payer's name). + */ + public static boolean requestAnyInformation(@Nullable PaymentOptions options) { + if (options == null) return false; + return options.requestShipping || options.requestPayerEmail || options.requestPayerPhone + || options.requestPayerName; + } + + /** + * @param options Any PaymentOption, can be null. + * @return Whether a PaymentOptions has requested any payer's information (email, phone, name). + */ + public static boolean requestAnyContactInformation(@Nullable PaymentOptions options) { + if (options == null) return false; + return options.requestPayerEmail || options.requestPayerPhone || options.requestPayerName; + } + + /** + * @param options Any PaymentOptions, can be null. + * @return Return a JSON string indicating whether each information is requested in the + * PaymentOptions. + */ + public static String stringifyRequestedInformation(@Nullable PaymentOptions options) { + boolean requestPayerEmail = false; + boolean requestPayerName = false; + boolean requestPayerPhone = false; + boolean requestShipping = false; + if (options != null) { + requestPayerEmail = options.requestPayerEmail; + requestPayerName = options.requestPayerName; + requestPayerPhone = options.requestPayerPhone; + requestShipping = options.requestShipping; + } + return String.format("{payerEmail:%s,payerName:%s,payerPhone:%s,shipping:%s}", + requestPayerEmail, requestPayerName, requestPayerPhone, requestShipping); + } + + /** + * @param paymentOptions The PaymentOptions of the payment request. + * @return Whether requestShipping is specified in the payment request. + */ + public static boolean requestShipping( + org.chromium.payments.mojom.PaymentOptions paymentOptions) { + return paymentOptions != null && paymentOptions.requestShipping; + } + + /** + * @param paymentOptions The PaymentOptions of the payment request. + * @return Whether requestPayerName is specified in the payment request. + */ + public static boolean requestPayerName( + org.chromium.payments.mojom.PaymentOptions paymentOptions) { + return paymentOptions != null && paymentOptions.requestPayerName; + } + + /** + * @param paymentOptions The PaymentOptions of the payment request. + * @return Whether requestPayerPhone is specified in the payment request. + */ + public static boolean requestPayerPhone( + org.chromium.payments.mojom.PaymentOptions paymentOptions) { + return paymentOptions != null && paymentOptions.requestPayerPhone; + } + + /** + * @param paymentOptions The PaymentOptions of the payment request. + * @return Whether requestPayerEmail is specified in the payment request. + */ + public static boolean requestPayerEmail( + org.chromium.payments.mojom.PaymentOptions paymentOptions) { + return paymentOptions != null && paymentOptions.requestPayerEmail; + } +} diff --git a/chromium/components/payments/content/android/java/src/org/chromium/components/payments/PaymentRequestLifecycleObserver.java b/chromium/components/payments/content/android/java/src/org/chromium/components/payments/PaymentRequestLifecycleObserver.java new file mode 100644 index 00000000000..5b8a18dc28c --- /dev/null +++ b/chromium/components/payments/content/android/java/src/org/chromium/components/payments/PaymentRequestLifecycleObserver.java @@ -0,0 +1,23 @@ +// Copyright 2020 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. + +package org.chromium.components.payments; + +import org.chromium.payments.mojom.PaymentRequest; +import org.chromium.payments.mojom.PaymentValidationErrors; + +/** Observe the lifecycle of the PaymentRequest. */ +public interface PaymentRequestLifecycleObserver { + /** + * Called when all of the PaymentRequest parameters have been initiated and validated. + * @param params The parameters. + */ + void onPaymentRequestParamsInitiated(PaymentRequestParams params); + + /** + * Called after {@link PaymentRequest#retry} is invoked. + * @param errors The payment validation errors. + */ + void onRetry(PaymentValidationErrors errors); +} diff --git a/chromium/components/payments/content/android/java/src/org/chromium/components/payments/PaymentRequestParams.java b/chromium/components/payments/content/android/java/src/org/chromium/components/payments/PaymentRequestParams.java new file mode 100644 index 00000000000..70e77c5eccd --- /dev/null +++ b/chromium/components/payments/content/android/java/src/org/chromium/components/payments/PaymentRequestParams.java @@ -0,0 +1,39 @@ +// Copyright 2020 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. + +package org.chromium.components.payments; + +import org.chromium.payments.mojom.PaymentDetailsModifier; +import org.chromium.payments.mojom.PaymentItem; +import org.chromium.payments.mojom.PaymentMethodData; +import org.chromium.payments.mojom.PaymentOptions; + +import java.util.Map; + +/** + * The parameters of PaymentRequest specified by the merchant. + */ +public interface PaymentRequestParams { + /** @return The PaymentOptions set by the merchant. */ + PaymentOptions getPaymentOptions(); + + /** + * @return The unmodifiable mapping of method names to modifiers, which include modified totals + * and additional line items. Used to display modified totals for each payment app, modified + * total in order summary, and additional line items in order summary. Should not be null. + */ + Map<String, PaymentDetailsModifier> getUnmodifiableModifiers(); + + /** + * @return The unmodifiable mapping of payment method identifier to the method-specific data in + * the payment request. + */ + Map<String, PaymentMethodData> getMethodData(); + + /** + * @return The raw total amount being charged - the total property of the PaymentDetails of + * payment request. + */ + PaymentItem getRawTotal(); +} diff --git a/chromium/components/payments/content/android/java/src/org/chromium/components/payments/SslValidityChecker.java b/chromium/components/payments/content/android/java/src/org/chromium/components/payments/SslValidityChecker.java new file mode 100644 index 00000000000..0820b8d727c --- /dev/null +++ b/chromium/components/payments/content/android/java/src/org/chromium/components/payments/SslValidityChecker.java @@ -0,0 +1,47 @@ +// Copyright 2017 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. + +package org.chromium.components.payments; + +import org.chromium.base.annotations.JNINamespace; +import org.chromium.base.annotations.NativeMethods; +import org.chromium.content_public.browser.WebContents; + +/** SSL validity checker. */ +@JNINamespace("payments") +public class SslValidityChecker { + /** + * Returns a developer-facing error message for invalid SSL certificate state or an empty + * string when the SSL certificate is valid. Only SECURE and + * SECURE_WITH_POLICY_INSTALLED_CERT are considered valid for web payments, unless + * --ignore-certificate-errors is specified on the command line. + * + * @param webContents The web contents whose SSL certificate state will be used for the error + * message. Should not be null. A null |web_contents| parameter will return + * an "Invalid certificate" error message. + * @return A developer-facing error message about the SSL certificate state in the given web + * contents or an empty string when the SSL certificate is valid. + */ + public static String getInvalidSslCertificateErrorMessage(WebContents webContents) { + return SslValidityCheckerJni.get().getInvalidSslCertificateErrorMessage(webContents); + } + + /** + * Returns true for web contents that is allowed in a payment handler window. + * + * @param webContents The web contents to check. + * @return Whether the web contents is a allowed in a payment handler window. + */ + public static boolean isValidPageInPaymentHandlerWindow(WebContents webContents) { + return SslValidityCheckerJni.get().isValidPageInPaymentHandlerWindow(webContents); + } + + private SslValidityChecker() {} + + @NativeMethods + interface Natives { + String getInvalidSslCertificateErrorMessage(WebContents webContents); + boolean isValidPageInPaymentHandlerWindow(WebContents webContents); + } +} diff --git a/chromium/components/payments/content/android/java/src/org/chromium/components/payments/SupportedDelegations.java b/chromium/components/payments/content/android/java/src/org/chromium/components/payments/SupportedDelegations.java new file mode 100644 index 00000000000..0844fb49f11 --- /dev/null +++ b/chromium/components/payments/content/android/java/src/org/chromium/components/payments/SupportedDelegations.java @@ -0,0 +1,99 @@ +// Copyright 2019 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. + +package org.chromium.components.payments; + +import androidx.annotation.Nullable; + +import org.chromium.base.Log; + +/** + * This class represents the supported delegations of a service worker based payment app. + */ +public class SupportedDelegations { + private static final String TAG = "SupportedDelegations"; + private final boolean mShippingAddress; + private final boolean mPayerName; + private final boolean mPayerPhone; + private final boolean mPayerEmail; + + public SupportedDelegations( + boolean shippingAddress, boolean payerName, boolean payerPhone, boolean payerEmail) { + mShippingAddress = shippingAddress; + mPayerName = payerName; + mPayerPhone = payerPhone; + mPayerEmail = payerEmail; + } + public SupportedDelegations() { + mShippingAddress = false; + mPayerName = false; + mPayerPhone = false; + mPayerEmail = false; + } + + public boolean providesAll(org.chromium.payments.mojom.PaymentOptions options) { + if (options == null) return true; + if (options.requestShipping && !mShippingAddress) return false; + if (options.requestPayerName && !mPayerName) return false; + if (options.requestPayerPhone && !mPayerPhone) return false; + if (options.requestPayerEmail && !mPayerEmail) return false; + return true; + } + + public boolean getShippingAddress() { + return mShippingAddress; + } + + public boolean getPayerName() { + return mPayerName; + } + + public boolean getPayerPhone() { + return mPayerPhone; + } + + public boolean getPayerEmail() { + return mPayerEmail; + } + + public static SupportedDelegations createFromStringArray( + @Nullable String[] supportedDelegationsNames) throws IllegalArgumentException { + if (supportedDelegationsNames == null || supportedDelegationsNames.length == 0) { + return new SupportedDelegations(); + } + + boolean shippingAddress = false; + boolean payerName = false; + boolean payerPhone = false; + boolean payerEmail = false; + + // At most check the first 4 elements since there are only 4 different valid delegation + // types. + final int cappedArraySize = + Math.min(supportedDelegationsNames.length, /*MAX_DELEGATION_SIZE =*/4); + for (int i = 0; i < cappedArraySize; i++) { + if (supportedDelegationsNames[i] == null) { + Log.e(TAG, + "null is an invalid delegation value. Only [\"shippingAddress\", " + + "\"payerName\", \"payerPhone\", \"payerEmail\"] values " + + "are possible."); + } else if (supportedDelegationsNames[i].equals("shippingAddress")) { + shippingAddress = true; + } else if (supportedDelegationsNames[i].equals("payerName")) { + payerName = true; + } else if (supportedDelegationsNames[i].equals("payerPhone")) { + payerPhone = true; + } else if (supportedDelegationsNames[i].equals("payerEmail")) { + payerEmail = true; + } else { + Log.e(TAG, + "\"%s\" is an invalid delegation value. Only [\"shippingAddress\", " + + "\"payerName\", \"payerPhone\", \"payerEmail\"] values are " + + "possible.", + supportedDelegationsNames[i]); + } + } + return new SupportedDelegations(shippingAddress, payerName, payerPhone, payerEmail); + } +} diff --git a/chromium/components/payments/content/android/java/src/org/chromium/components/payments/intent/OWNERS b/chromium/components/payments/content/android/java/src/org/chromium/components/payments/intent/OWNERS new file mode 100644 index 00000000000..d6811b543c1 --- /dev/null +++ b/chromium/components/payments/content/android/java/src/org/chromium/components/payments/intent/OWNERS @@ -0,0 +1,8 @@ +# TEAM: payments-dev@chromium.org +# COMPONENT: UI>Browser>Payments +# OS: Android + +file://components/payments/OWNERS + +per-file *TypeConverter*.*=set noparent +per-file *TypeConverter*.*=file://ipc/SECURITY_OWNERS
\ No newline at end of file diff --git a/chromium/components/payments/content/android/java/src/org/chromium/components/payments/intent/WebPaymentIntentHelper.java b/chromium/components/payments/content/android/java/src/org/chromium/components/payments/intent/WebPaymentIntentHelper.java index b035c031668..247375dd5f3 100644 --- a/chromium/components/payments/content/android/java/src/org/chromium/components/payments/intent/WebPaymentIntentHelper.java +++ b/chromium/components/payments/content/android/java/src/org/chromium/components/payments/intent/WebPaymentIntentHelper.java @@ -96,6 +96,22 @@ public class WebPaymentIntentHelper { } /** + * Get stringified payment details from a payment intent. + * + * @param data The payment intent data. + * @return The stringified payment details, if any. + */ + @Nullable + public static String getPaymentIntentDetails(Intent data) { + String details = data.getExtras().getString(EXTRA_RESPONSE_DETAILS); + if (details == null) { + // try to get deprecated details rather than early returning. + details = data.getExtras().getString(EXTRA_DEPRECATED_RESPONSE_INSTRUMENT_DETAILS); + } + return details; + } + + /** * Parse the Payment Intent response. * @param resultCode Result code of the requested intent. * @param data The intent response data. @@ -125,11 +141,7 @@ public class WebPaymentIntentHelper { return; } - String details = data.getExtras().getString(EXTRA_RESPONSE_DETAILS); - if (details == null) { - // try to get deprecated details rather than early returning. - details = data.getExtras().getString(EXTRA_DEPRECATED_RESPONSE_INSTRUMENT_DETAILS); - } + String details = getPaymentIntentDetails(data); if (TextUtils.isEmpty(details)) { errorCallback.onPaymentError(ErrorStrings.MISSING_DETAILS_FROM_PAYMENT_APP); return; diff --git a/chromium/components/payments/content/android/java/src/org/chromium/components/payments/intent/WebPaymentIntentHelperTypeConverter.java b/chromium/components/payments/content/android/java/src/org/chromium/components/payments/intent/WebPaymentIntentHelperTypeConverter.java new file mode 100644 index 00000000000..b862a1d63c7 --- /dev/null +++ b/chromium/components/payments/content/android/java/src/org/chromium/components/payments/intent/WebPaymentIntentHelperTypeConverter.java @@ -0,0 +1,195 @@ +// Copyright 2020 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. + +package org.chromium.components.payments.intent; + +import android.os.Bundle; +import android.text.TextUtils; + +import androidx.annotation.Nullable; + +import org.chromium.base.CollectionUtil; +import org.chromium.payments.mojom.AddressErrors; +import org.chromium.payments.mojom.PaymentCurrencyAmount; +import org.chromium.payments.mojom.PaymentDetailsModifier; +import org.chromium.payments.mojom.PaymentItem; +import org.chromium.payments.mojom.PaymentMethodData; +import org.chromium.payments.mojom.PaymentOptions; +import org.chromium.payments.mojom.PaymentRequestDetailsUpdate; +import org.chromium.payments.mojom.PaymentShippingOption; +import org.chromium.payments.mojom.PaymentShippingType; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * This class defines the utility functions that convert the payment info types in + * org.chromium.payments.mojom to their counterparts in WebPaymentIntentHelperType. + */ +public final class WebPaymentIntentHelperTypeConverter { + @Nullable + public static WebPaymentIntentHelperType.PaymentCurrencyAmount fromMojoPaymentCurrencyAmount( + @Nullable PaymentCurrencyAmount currencyAmount) { + if (currencyAmount == null) return null; + return new WebPaymentIntentHelperType.PaymentCurrencyAmount( + /*currency=*/currencyAmount.currency, /*value=*/currencyAmount.value); + } + + @Nullable + public static WebPaymentIntentHelperType.PaymentItem fromMojoPaymentItem( + @Nullable PaymentItem item) { + if (item == null) return null; + return new WebPaymentIntentHelperType.PaymentItem( + fromMojoPaymentCurrencyAmount(item.amount)); + } + + @Nullable + public static WebPaymentIntentHelperType.PaymentDetailsModifier fromMojoPaymentDetailsModifier( + @Nullable PaymentDetailsModifier detailsModifier) { + if (detailsModifier == null) return null; + return new WebPaymentIntentHelperType.PaymentDetailsModifier( + fromMojoPaymentItem(detailsModifier.total), + fromMojoPaymentMethodData(detailsModifier.methodData)); + } + + @Nullable + public static WebPaymentIntentHelperType.PaymentMethodData fromMojoPaymentMethodData( + @Nullable PaymentMethodData methodData) { + if (methodData == null) return null; + return new WebPaymentIntentHelperType.PaymentMethodData( + /*supportedMethod=*/methodData.supportedMethod, + /*stringifiedData=*/methodData.stringifiedData); + } + + @Nullable + public static Map<String, WebPaymentIntentHelperType.PaymentMethodData> + fromMojoPaymentMethodDataMap(@Nullable Map<String, PaymentMethodData> methodDataMap) { + if (methodDataMap == null) return null; + Map<String, WebPaymentIntentHelperType.PaymentMethodData> compatibleMethodDataMap = + new HashMap<>(); + CollectionUtil.forEach(methodDataMap, + entry + -> compatibleMethodDataMap.put(entry.getKey(), + WebPaymentIntentHelperTypeConverter.fromMojoPaymentMethodData( + entry.getValue()))); + return compatibleMethodDataMap; + } + + @Nullable + public static Map<String, WebPaymentIntentHelperType.PaymentDetailsModifier> + fromMojoPaymentDetailsModifierMap(@Nullable Map<String, PaymentDetailsModifier> modifiers) { + if (modifiers == null) return null; + Map<String, WebPaymentIntentHelperType.PaymentDetailsModifier> compatibleModifiers = + new HashMap<>(); + CollectionUtil.forEach(modifiers, + entry + -> compatibleModifiers.put(entry.getKey(), + WebPaymentIntentHelperTypeConverter.fromMojoPaymentDetailsModifier( + entry.getValue()))); + return compatibleModifiers; + } + + @Nullable + public static List<WebPaymentIntentHelperType.PaymentItem> fromMojoPaymentItems( + @Nullable List<PaymentItem> paymentItems) { + if (paymentItems == null) return null; + List<WebPaymentIntentHelperType.PaymentItem> compatiblePaymentItems = new ArrayList<>(); + CollectionUtil.forEach(paymentItems, + element + -> compatiblePaymentItems.add( + WebPaymentIntentHelperTypeConverter.fromMojoPaymentItem(element))); + return compatiblePaymentItems; + } + + @Nullable + public static WebPaymentIntentHelperType.PaymentShippingOption fromMojoPaymentShippingOption( + @Nullable PaymentShippingOption shippingOption) { + if (shippingOption == null) return null; + return new WebPaymentIntentHelperType.PaymentShippingOption(shippingOption.id, + shippingOption.label, fromMojoPaymentCurrencyAmount(shippingOption.amount), + shippingOption.selected); + } + + @Nullable + public static List<WebPaymentIntentHelperType.PaymentShippingOption> fromMojoShippingOptionList( + @Nullable List<PaymentShippingOption> shippingOptions) { + if (shippingOptions == null) return null; + List<WebPaymentIntentHelperType.PaymentShippingOption> shippingOptionList = + new ArrayList<>(); + CollectionUtil.forEach(shippingOptions, + element + -> shippingOptionList.add( + WebPaymentIntentHelperTypeConverter.fromMojoPaymentShippingOption( + element))); + return shippingOptionList; + } + + @Nullable + public static WebPaymentIntentHelperType.PaymentOptions fromMojoPaymentOptions( + @Nullable PaymentOptions paymentOptions) { + if (paymentOptions == null) return null; + String shippingType = null; + if (paymentOptions.requestShipping) { + switch (paymentOptions.shippingType) { + case PaymentShippingType.SHIPPING: + shippingType = "shipping"; + break; + case PaymentShippingType.DELIVERY: + shippingType = "delivery"; + break; + case PaymentShippingType.PICKUP: + shippingType = "pickup"; + break; + } + } + return new WebPaymentIntentHelperType.PaymentOptions(paymentOptions.requestPayerName, + paymentOptions.requestPayerEmail, paymentOptions.requestPayerPhone, + paymentOptions.requestShipping, shippingType); + } + + @Nullable + private static Bundle fromMojoShippingAddressErrors(@Nullable AddressErrors addressErrors) { + if (addressErrors == null) return null; + Bundle bundle = new Bundle(); + putIfNonEmpty("addressLine", addressErrors.addressLine, bundle); + putIfNonEmpty("city", addressErrors.city, bundle); + putIfNonEmpty("countryCode", addressErrors.country, bundle); + putIfNonEmpty("dependentLocality", addressErrors.dependentLocality, bundle); + putIfNonEmpty("organization", addressErrors.organization, bundle); + putIfNonEmpty("phone", addressErrors.phone, bundle); + putIfNonEmpty("postalCode", addressErrors.postalCode, bundle); + putIfNonEmpty("recipient", addressErrors.recipient, bundle); + putIfNonEmpty("region", addressErrors.region, bundle); + putIfNonEmpty("sortingCode", addressErrors.sortingCode, bundle); + return bundle; + } + + private static void putIfNonEmpty(String key, String value, Bundle bundle) { + if (!TextUtils.isEmpty(value)) bundle.putString(key, value); + } + + /** + * Converts PaymentRequestDetailsUpdate from mojo to + * WebPaymentIntentHelperType.PaymentRequestDetailsUpdate. + * @param update The mojo PaymentRequestDetailsUpdate to be converted. + * @return The converted update. + */ + @Nullable + public static WebPaymentIntentHelperType.PaymentRequestDetailsUpdate + fromMojoPaymentRequestDetailsUpdate(@Nullable PaymentRequestDetailsUpdate update) { + if (update == null) return null; + return new WebPaymentIntentHelperType.PaymentRequestDetailsUpdate( + fromMojoPaymentCurrencyAmount(update.total), + // Arrays.asList does not accept null. + update.shippingOptions == null + ? null + : fromMojoShippingOptionList(Arrays.asList(update.shippingOptions)), + // update.modifiers is intentionally redacted. + update.error, update.stringifiedPaymentMethodErrors, + fromMojoShippingAddressErrors(update.shippingAddressErrors)); + } +} diff --git a/chromium/components/payments/content/android/payment_feature_list.cc b/chromium/components/payments/content/android/payment_feature_list.cc index c21c16bac3b..3a105c078ab 100644 --- a/chromium/components/payments/content/android/payment_feature_list.cc +++ b/chromium/components/payments/content/android/payment_feature_list.cc @@ -24,7 +24,9 @@ const base::Feature* kFeaturesExposedToJava[] = { &::features::kWebPayments, &::features::kWebPaymentsMinimalUI, &features::kAlwaysAllowJustInTimePaymentApp, + &features::kAppStoreBilling, &features::kAppStoreBillingDebug, + &features::kEnforceFullDelegation, &features::kPaymentRequestSkipToGPay, &features::kPaymentRequestSkipToGPayIfNoCard, &features::kReturnGooglePayInBasicCard, diff --git a/chromium/components/payments/content/android/payments_java_resources.gni b/chromium/components/payments/content/android/payments_java_resources.gni new file mode 100644 index 00000000000..47a780d59c4 --- /dev/null +++ b/chromium/components/payments/content/android/payments_java_resources.gni @@ -0,0 +1,21 @@ +# Copyright 2020 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. + +payments_java_resources = [ + "//components/payments/content/android/java/res/layout/payment_handler_content.xml", + "//components/payments/content/android/java/res/layout/payment_minimal_ui_content.xml", + "//components/payments/content/android/java/res/layout/payment_minimal_ui_toolbar.xml", + "//components/payments/content/android/java/res/layout/payment_option_edit_icon.xml", + "//components/payments/content/android/java/res/layout/payment_request.xml", + "//components/payments/content/android/java/res/layout/payment_request_bottom_bar.xml", + "//components/payments/content/android/java/res/layout/payment_request_dropdown_item.xml", + "//components/payments/content/android/java/res/layout/payment_request_editor.xml", + "//components/payments/content/android/java/res/layout/payment_request_editor_dropdown.xml", + "//components/payments/content/android/java/res/layout/payment_request_editor_label.xml", + "//components/payments/content/android/java/res/layout/payment_request_header.xml", + "//components/payments/content/android/java/res/layout/payment_request_spinny.xml", + "//components/payments/content/android/java/res/layout/payments_request_editor_textview.xml", + "//components/payments/content/android/java/res/values-sw600dp/dimens.xml", + "//components/payments/content/android/java/res/values/dimens.xml", +] diff --git a/chromium/components/payments/content/android_app_communication.cc b/chromium/components/payments/content/android_app_communication.cc new file mode 100644 index 00000000000..212ccba2510 --- /dev/null +++ b/chromium/components/payments/content/android_app_communication.cc @@ -0,0 +1,48 @@ +// Copyright 2020 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. + +#include "components/payments/content/android_app_communication.h" + +#include "content/public/browser/browser_context.h" +#include "content/public/browser/browser_thread.h" + +namespace payments { +namespace { + +const char kAndroidAppCommunicationKeyName[] = + "payment_android_app_communication"; + +} // namespace + +// static +base::WeakPtr<AndroidAppCommunication> +AndroidAppCommunication::GetForBrowserContext( + content::BrowserContext* context) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + DCHECK(context); + + base::SupportsUserData::Data* data = + context->GetUserData(kAndroidAppCommunicationKeyName); + if (!data) { + auto communication = AndroidAppCommunication::Create(context); + data = communication.get(); + context->SetUserData(kAndroidAppCommunicationKeyName, + std::move(communication)); + } + return static_cast<AndroidAppCommunication*>(data) + ->weak_ptr_factory_.GetWeakPtr(); +} + +AndroidAppCommunication::~AndroidAppCommunication() { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); +} + +AndroidAppCommunication::AndroidAppCommunication( + content::BrowserContext* context) + : context_(context) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + DCHECK(context_); +} + +} // namespace payments diff --git a/chromium/components/payments/content/android_app_communication.h b/chromium/components/payments/content/android_app_communication.h new file mode 100644 index 00000000000..d4707c860bd --- /dev/null +++ b/chromium/components/payments/content/android_app_communication.h @@ -0,0 +1,114 @@ +// Copyright 2020 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. + +#ifndef COMPONENTS_PAYMENTS_CONTENT_ANDROID_APP_COMMUNICATION_H_ +#define COMPONENTS_PAYMENTS_CONTENT_ANDROID_APP_COMMUNICATION_H_ + +#include <map> +#include <memory> +#include <set> +#include <string> +#include <vector> + +#include "base/callback_forward.h" +#include "base/memory/weak_ptr.h" +#include "base/optional.h" +#include "base/supports_user_data.h" +#include "components/payments/core/android_app_description.h" + +class GURL; + +namespace content { +class BrowserContext; +} // namespace content + +namespace payments { + +// Invokes Android payment apps. This object is owned by BrowserContext, so it +// should only be accessed on UI thread, where BrowserContext lives. +class AndroidAppCommunication : public base::SupportsUserData::Data { + public: + using GetAppDescriptionsCallback = base::OnceCallback<void( + const base::Optional<std::string>& error_message, + std::vector<std::unique_ptr<AndroidAppDescription>> app_descriptions)>; + + using IsReadyToPayCallback = + base::OnceCallback<void(const base::Optional<std::string>& error_message, + bool is_ready_to_pay)>; + + using InvokePaymentAppCallback = + base::OnceCallback<void(const base::Optional<std::string>& error_message, + bool is_activity_result_ok, + const std::string& payment_method_identifier, + const std::string& stringified_details)>; + + // Returns a weak pointer to the instance of AndroidAppCommunication that is + // owned by the given |context|, which should not be null. + static base::WeakPtr<AndroidAppCommunication> GetForBrowserContext( + content::BrowserContext* context); + + ~AndroidAppCommunication() override; + + // Disallow copy and assign. + AndroidAppCommunication(const AndroidAppCommunication& other) = delete; + AndroidAppCommunication& operator=(const AndroidAppCommunication& other) = + delete; + + // Looks up installed Android apps that support making payments. If running in + // TWA mode, the |twa_package_name| parameter is the name of the Android + // package of the TWA that invoked Chrome, or an empty string otherwise. + virtual void GetAppDescriptions(const std::string& twa_package_name, + GetAppDescriptionsCallback callback) = 0; + + // Queries the IS_READY_TO_PAY service to check whether the payment app can + // perform payments. + virtual void IsReadyToPay(const std::string& package_name, + const std::string& service_name, + const std::map<std::string, std::set<std::string>>& + stringified_method_data, + const GURL& top_level_origin, + const GURL& payment_request_origin, + const std::string& payment_request_id, + IsReadyToPayCallback callback) = 0; + + // Invokes the PAY activity to initiate the payment flow. + virtual void InvokePaymentApp( + const std::string& package_name, + const std::string& activity_name, + const std::map<std::string, std::set<std::string>>& + stringified_method_data, + const GURL& top_level_origin, + const GURL& payment_request_origin, + const std::string& payment_request_id, + InvokePaymentAppCallback callback) = 0; + + // Allows usage of a test browser context. + virtual void SetForTesting() = 0; + + // Simulates having this payment app. + virtual void SetAppForTesting(const std::string& package_name, + const std::string& method, + const std::string& response) = 0; + + protected: + explicit AndroidAppCommunication(content::BrowserContext* context); + + content::BrowserContext* context() { return context_; } + + private: + // Defined in platform-specific implementation files. See: + // components/payments/content/android_app_communication_chromeos.cc + // components/payments/content/android_app_communication_stub.cc + static std::unique_ptr<AndroidAppCommunication> Create( + content::BrowserContext* context); + + // Owns this object, so always valid. + content::BrowserContext* context_; + + base::WeakPtrFactory<AndroidAppCommunication> weak_ptr_factory_{this}; +}; + +} // namespace payments + +#endif // COMPONENTS_PAYMENTS_CONTENT_ANDROID_APP_COMMUNICATION_H_ diff --git a/chromium/components/payments/content/android_app_communication_chrome_os.cc b/chromium/components/payments/content/android_app_communication_chrome_os.cc new file mode 100644 index 00000000000..1012d35b2ed --- /dev/null +++ b/chromium/components/payments/content/android_app_communication_chrome_os.cc @@ -0,0 +1,345 @@ +// Copyright 2020 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. + +#include "components/payments/content/android_app_communication.h" + +#include <utility> + +#include "components/arc/mojom/payment_app.mojom.h" +#include "components/arc/pay/arc_payment_app_bridge.h" +#include "components/payments/core/android_app_description.h" +#include "components/payments/core/chrome_os_error_strings.h" +#include "components/payments/core/method_strings.h" +#include "components/payments/core/native_error_strings.h" +#include "content/public/browser/browser_thread.h" +#include "url/gurl.h" + +namespace payments { +namespace { + +static constexpr char kEmptyDictionaryJson[] = "{}"; + +void OnIsImplemented( + const std::string& twa_package_name, + AndroidAppCommunication::GetAppDescriptionsCallback callback, + arc::mojom::IsPaymentImplementedResultPtr response) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + DCHECK(!twa_package_name.empty()); + + if (response.is_null()) { + std::move(callback).Run(errors::kEmptyResponse, /*app_descriptions=*/{}); + return; + } + + if (response->is_error()) { + std::move(callback).Run(response->get_error(), /*app_descriptions=*/{}); + return; + } + + if (!response->is_valid()) { + std::move(callback).Run(errors::kInvalidResponse, /*app_descriptions=*/{}); + return; + } + + if (response->get_valid()->activity_names.empty()) { + // If a TWA does not implement PAY intent in any of its activities, then + // |activity_names| is empty, which is not an error. + std::move(callback).Run(/*error_message=*/base::nullopt, + /*app_descriptions=*/{}); + return; + } + + if (response->get_valid()->activity_names.size() != 1U) { + std::move(callback).Run(errors::kMoreThanOneActivity, + /*app_descriptions=*/{}); + return; + } + + auto activity = std::make_unique<AndroidActivityDescription>(); + activity->name = response->get_valid()->activity_names.front(); + + // The only available payment method identifier in a Chrome OS TWA at this + // time. + activity->default_payment_method = methods::kGooglePlayBilling; + + auto app = std::make_unique<AndroidAppDescription>(); + app->package = twa_package_name; + app->activities.emplace_back(std::move(activity)); + app->service_names = response->get_valid()->service_names; + + std::vector<std::unique_ptr<AndroidAppDescription>> app_descriptions; + app_descriptions.emplace_back(std::move(app)); + + std::move(callback).Run(/*error_message=*/base::nullopt, + std::move(app_descriptions)); +} + +void OnIsReadyToPay(AndroidAppCommunication::IsReadyToPayCallback callback, + arc::mojom::IsReadyToPayResultPtr response) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + if (response.is_null()) { + std::move(callback).Run(errors::kEmptyResponse, /*is_ready_to_pay=*/false); + return; + } + + if (response->is_error()) { + std::move(callback).Run(response->get_error(), /*is_ready_to_pay=*/false); + return; + } + + if (!response->is_response()) { + std::move(callback).Run(errors::kInvalidResponse, + /*is_ready_to_pay=*/false); + return; + } + + std::move(callback).Run(/*error_message=*/base::nullopt, + response->get_response()); +} + +void OnPaymentAppResponse( + AndroidAppCommunication::InvokePaymentAppCallback callback, + arc::mojom::InvokePaymentAppResultPtr response) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + if (response.is_null()) { + std::move(callback).Run(errors::kEmptyResponse, + /*is_activity_result_ok=*/false, + /*payment_method_identifier=*/"", + /*stringified_details=*/kEmptyDictionaryJson); + return; + } + + if (response->is_error()) { + std::move(callback).Run(response->get_error(), + /*is_activity_result_ok=*/false, + /*payment_method_identifier=*/"", + /*stringified_details=*/kEmptyDictionaryJson); + return; + } + + if (!response->is_valid()) { + std::move(callback).Run(errors::kInvalidResponse, + /*is_activity_result_ok=*/false, + /*payment_method_identifier=*/"", + /*stringified_details=*/kEmptyDictionaryJson); + return; + } + + // Chrome OS TWA currently supports only methods::kGooglePlayBilling payment + // method identifier. + std::move(callback).Run( + /*error_message=*/base::nullopt, + response->get_valid()->is_activity_result_ok, + /*payment_method_identifier=*/methods::kGooglePlayBilling, + response->get_valid()->stringified_details); +} + +arc::mojom::PaymentParametersPtr CreatePaymentParameters( + const std::string& package_name, + const std::string& activity_or_service_name, + const std::map<std::string, std::set<std::string>>& stringified_method_data, + const GURL& top_level_origin, + const GURL& payment_request_origin, + const std::string& payment_request_id, + base::Optional<std::string>* error_message) { + // Chrome OS TWA supports only kGooglePlayBilling payment method identifier + // at this time. + auto supported_method_iterator = + stringified_method_data.find(methods::kGooglePlayBilling); + if (supported_method_iterator == stringified_method_data.end()) + return nullptr; + + // Chrome OS TWA supports only one set of payment method specific data. + if (supported_method_iterator->second.size() > 1) { + *error_message = errors::kMoreThanOneMethodData; + return nullptr; + } + + auto parameters = arc::mojom::PaymentParameters::New(); + parameters->stringified_method_data = + supported_method_iterator->second.empty() + ? kEmptyDictionaryJson + : *supported_method_iterator->second.begin(); + parameters->package_name = package_name; + parameters->activity_or_service_name = activity_or_service_name; + parameters->top_level_origin = top_level_origin.spec(); + parameters->payment_request_origin = payment_request_origin.spec(); + parameters->payment_request_id = payment_request_id; + + return parameters; +} + +std::vector<std::unique_ptr<AndroidAppDescription>> CreateAppForTesting( + const std::string& package_name, + const std::string& method_name) { + auto activity = std::make_unique<AndroidActivityDescription>(); + activity->name = package_name + ".Activity"; + activity->default_payment_method = method_name; + auto app = std::make_unique<AndroidAppDescription>(); + app->package = package_name; + app->activities.emplace_back(std::move(activity)); + std::vector<std::unique_ptr<AndroidAppDescription>> app_descriptions; + app_descriptions.emplace_back(std::move(app)); + return app_descriptions; +} + +// Invokes the TWA Android app in Android subsystem on Chrome OS. +class AndroidAppCommunicationChromeOS : public AndroidAppCommunication { + public: + explicit AndroidAppCommunicationChromeOS(content::BrowserContext* context) + : AndroidAppCommunication(context), + get_app_service_(base::BindRepeating( + &arc::ArcPaymentAppBridge::GetForBrowserContext)) {} + + ~AndroidAppCommunicationChromeOS() override = default; + + // Disallow copy and assign. + AndroidAppCommunicationChromeOS( + const AndroidAppCommunicationChromeOS& other) = delete; + AndroidAppCommunicationChromeOS& operator=( + const AndroidAppCommunicationChromeOS& other) = delete; + + // AndroidAppCommunication implementation: + void GetAppDescriptions(const std::string& twa_package_name, + GetAppDescriptionsCallback callback) override { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + if (twa_package_name.empty()) { + // Chrome OS supports Android app payment only through a TWA. An empty + // |twa_package_name| indicates that Chrome was not launched from a TWA, + // so there're no payment apps available. + std::move(callback).Run(/*error_message=*/base::nullopt, + /*app_descriptions=*/{}); + return; + } + + if (!package_name_for_testing_.empty()) { + std::move(callback).Run( + /*error_message=*/base::nullopt, + CreateAppForTesting(package_name_for_testing_, method_for_testing_)); + return; + } + + auto* payment_app_service = get_app_service_.Run(context()); + if (!payment_app_service) { + std::move(callback).Run(errors::kUnableToInvokeAndroidPaymentApps, + /*app_descriptions=*/{}); + return; + } + + payment_app_service->IsPaymentImplemented( + twa_package_name, base::BindOnce(&OnIsImplemented, twa_package_name, + std::move(callback))); + } + + // AndroidAppCommunication implementation. + void IsReadyToPay(const std::string& package_name, + const std::string& service_name, + const std::map<std::string, std::set<std::string>>& + stringified_method_data, + const GURL& top_level_origin, + const GURL& payment_request_origin, + const std::string& payment_request_id, + IsReadyToPayCallback callback) override { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + auto* payment_app_service = get_app_service_.Run(context()); + if (!payment_app_service) { + std::move(callback).Run(errors::kUnableToInvokeAndroidPaymentApps, + /*is_ready_to_pay=*/false); + return; + } + + base::Optional<std::string> error_message; + auto parameters = CreatePaymentParameters( + package_name, service_name, stringified_method_data, top_level_origin, + payment_request_origin, payment_request_id, &error_message); + if (!parameters) { + std::move(callback).Run(error_message, /*is_ready_to_pay=*/false); + return; + } + + payment_app_service->IsReadyToPay( + std::move(parameters), + base::BindOnce(&OnIsReadyToPay, std::move(callback))); + } + + // AndroidAppCommunication implementation. + void InvokePaymentApp(const std::string& package_name, + const std::string& activity_name, + const std::map<std::string, std::set<std::string>>& + stringified_method_data, + const GURL& top_level_origin, + const GURL& payment_request_origin, + const std::string& payment_request_id, + InvokePaymentAppCallback callback) override { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + + base::Optional<std::string> error_message; + if (package_name_for_testing_ == package_name) { + std::move(callback).Run(error_message, + /*is_activity_result_ok=*/true, + method_for_testing_, response_for_testing_); + return; + } + + auto* payment_app_service = get_app_service_.Run(context()); + if (!payment_app_service) { + std::move(callback).Run(errors::kUnableToInvokeAndroidPaymentApps, + /*is_activity_result_ok=*/false, + /*payment_method_identifier=*/"", + /*stringified_details=*/kEmptyDictionaryJson); + return; + } + + auto parameters = CreatePaymentParameters( + package_name, activity_name, stringified_method_data, top_level_origin, + payment_request_origin, payment_request_id, &error_message); + if (!parameters) { + std::move(callback).Run(error_message, + /*is_activity_result_ok=*/false, + /*payment_method_identifier=*/"", + /*stringified_details=*/kEmptyDictionaryJson); + return; + } + + payment_app_service->InvokePaymentApp( + std::move(parameters), + base::BindOnce(&OnPaymentAppResponse, std::move(callback))); + } + + // AndroidAppCommunication implementation. + void SetForTesting() override { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + get_app_service_ = base::BindRepeating( + &arc::ArcPaymentAppBridge::GetForBrowserContextForTesting); + } + + // AndroidAppCommunication implementation. + void SetAppForTesting(const std::string& package_name, + const std::string& method, + const std::string& response) override { + package_name_for_testing_ = package_name; + method_for_testing_ = method; + response_for_testing_ = response; + } + + private: + std::string package_name_for_testing_; + std::string method_for_testing_; + std::string response_for_testing_; + + base::RepeatingCallback<arc::ArcPaymentAppBridge*(content::BrowserContext*)> + get_app_service_; +}; + +} // namespace + +// Declared in cross-platform header file. See: +// //components/payments/content/android_app_communication.h +// static +std::unique_ptr<AndroidAppCommunication> AndroidAppCommunication::Create( + content::BrowserContext* context) { + return std::make_unique<AndroidAppCommunicationChromeOS>(context); +} + +} // namespace payments diff --git a/chromium/components/payments/content/android_app_communication_stub.cc b/chromium/components/payments/content/android_app_communication_stub.cc new file mode 100644 index 00000000000..b03ab067705 --- /dev/null +++ b/chromium/components/payments/content/android_app_communication_stub.cc @@ -0,0 +1,77 @@ +// Copyright 2020 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. + +#include "components/payments/content/android_app_communication.h" + +#include <utility> + +#include "base/callback.h" +#include "base/optional.h" +#include "components/payments/core/native_error_strings.h" + +namespace payments { +namespace { + +class AndroidAppCommunicationStub : public AndroidAppCommunication { + public: + explicit AndroidAppCommunicationStub(content::BrowserContext* context) + : AndroidAppCommunication(context) {} + + ~AndroidAppCommunicationStub() override = default; + + // AndroidAppCommunication implementation. + void GetAppDescriptions(const std::string& twa_package_name, + GetAppDescriptionsCallback callback) override { + std::move(callback).Run(/*error_message=*/base::nullopt, + /*app_descriptions=*/{}); + } + + // AndroidAppCommunication implementation. + void IsReadyToPay(const std::string& package_name, + const std::string& service_name, + const std::map<std::string, std::set<std::string>>& + stringified_method_data, + const GURL& top_level_origin, + const GURL& payment_request_origin, + const std::string& payment_request_id, + IsReadyToPayCallback callback) override { + std::move(callback).Run(errors::kUnableToInvokeAndroidPaymentApps, + /*is_ready_to_pay=*/false); + } + + // AndroidAppCommunication implementation. + void InvokePaymentApp(const std::string& package_name, + const std::string& activity_name, + const std::map<std::string, std::set<std::string>>& + stringified_method_data, + const GURL& top_level_origin, + const GURL& payment_request_origin, + const std::string& payment_request_id, + InvokePaymentAppCallback callback) override { + std::move(callback).Run(errors::kUnableToInvokeAndroidPaymentApps, + /*is_activity_result_ok=*/false, + /*payment_method_identifier=*/"", + /*stringified_details=*/"{}"); + } + + // AndroidAppCommunication implementation. + void SetForTesting() override {} + + // AndroidAppCommunication implementation. + void SetAppForTesting(const std::string& package_name, + const std::string& method, + const std::string& response) override {} +}; + +} // namespace + +// Declared in cross-platform header file. See: +// components/payments/content/android_app_communication.h +// static +std::unique_ptr<AndroidAppCommunication> AndroidAppCommunication::Create( + content::BrowserContext* context) { + return std::make_unique<AndroidAppCommunicationStub>(context); +} + +} // namespace payments diff --git a/chromium/components/payments/content/android_app_communication_test_support.h b/chromium/components/payments/content/android_app_communication_test_support.h new file mode 100644 index 00000000000..22617d8960b --- /dev/null +++ b/chromium/components/payments/content/android_app_communication_test_support.h @@ -0,0 +1,102 @@ +// Copyright 2020 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. + +#ifndef COMPONENTS_PAYMENTS_CONTENT_ANDROID_APP_COMMUNICATION_TEST_SUPPORT_H_ +#define COMPONENTS_PAYMENTS_CONTENT_ANDROID_APP_COMMUNICATION_TEST_SUPPORT_H_ + +#include <memory> +#include <string> +#include <vector> + +#include "components/payments/core/android_app_description.h" + +namespace content { +class BrowserContext; +} // namespace content + +namespace payments { + +// Cross-platform test support for Android payment app communication. On Chrome +// OS, this connects to the Android subsystem. +// +// The test expectations are platform-specific. For example, on Chrome OS, +// expectations are setup in the mock Mojo IPC service. +class AndroidAppCommunicationTestSupport { + public: + // The object that initializes the ability to invoke Android apps in tests. + // For example, on Chrome OS, this object creates a mock Mojo IPC connection + // for the Android subsystem. This is meant to be placed on the stack, so it + // can perform clean up in its destructor. + class ScopedInitialization { + public: + ScopedInitialization() = default; + virtual ~ScopedInitialization() = default; + + ScopedInitialization(const ScopedInitialization& other) = delete; + ScopedInitialization& operator=(const ScopedInitialization& other) = delete; + }; + + // Defined in platform-specific files. + static std::unique_ptr<AndroidAppCommunicationTestSupport> Create(); + + virtual ~AndroidAppCommunicationTestSupport() = default; + + // Disallow copy and assign. + AndroidAppCommunicationTestSupport( + const AndroidAppCommunicationTestSupport& other) = delete; + AndroidAppCommunicationTestSupport& operator=( + const AndroidAppCommunicationTestSupport& other) = delete; + + // Whether this platform supports Android apps. Used in tests to determine the + // expected outcome when attempting to query and invoke Android payment apps. + virtual bool AreAndroidAppsSupportedOnThisPlatform() const = 0; + + // Creates the object that initializes the ability to invoke Android apps + // in tests. Places this on the stack, so it can perform clean up in its + // destructor. It's also useful to have a test case that does not invoke this + // method, so the code path that handles inability to invoke Android apps is + // tested. + virtual std::unique_ptr<ScopedInitialization> + CreateScopedInitialization() = 0; + + // Sets up the expectation that the test case will not query the list of + // Android payment apps. This can happen, for example, when there is no + // ScopedInitialization object in scope. + virtual void ExpectNoListOfPaymentAppsQuery() = 0; + + // Sets up the expectation that the test case will not query an + // IS_READY_TO_PAY service. + virtual void ExpectNoIsReadyToPayQuery() = 0; + + // Sets up the expectation that the test case will not invoke a PAY activity. + virtual void ExpectNoPaymentAppInvoke() = 0; + + // Sets up the expectation that the test case will query the list of Android + // payment apps. When that happens, the given list of |apps| will be used for + // the response. + virtual void ExpectQueryListOfPaymentAppsAndRespond( + std::vector<std::unique_ptr<AndroidAppDescription>> apps) = 0; + + // Sets up the expectation that the test case will query an IS_READY_TO_PAY + // service. When that happens, the service will reply with the given + // |is_ready_to_pay| answer. + virtual void ExpectQueryIsReadyToPayAndRespond(bool is_ready_to_pay) = 0; + + // Sets up the expectation that the test case will invoke a PAY activity. When + // that happens, the activity will reply with the given parameters. + virtual void ExpectInvokePaymentAppAndRespond( + bool is_activity_result_ok, + const std::string& payment_method_identifier, + const std::string& stringified_details) = 0; + + // Returns the browser context to use. + virtual content::BrowserContext* context() = 0; + + protected: + AndroidAppCommunicationTestSupport() = default; +}; + +} // namespace payments + +#endif // COMPONENTS_PAYMENTS_CONTENT_ANDROID_APP_COMMUNICATION_TEST_SUPPORT_H_ diff --git a/chromium/components/payments/content/android_app_communication_test_support_chrome_os.cc b/chromium/components/payments/content/android_app_communication_test_support_chrome_os.cc new file mode 100644 index 00000000000..5cae13e86db --- /dev/null +++ b/chromium/components/payments/content/android_app_communication_test_support_chrome_os.cc @@ -0,0 +1,160 @@ +// Copyright 2020 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. + +#include "components/payments/content/android_app_communication_test_support.h" + +#include <utility> + +#include "components/arc/mojom/payment_app.mojom.h" +#include "components/arc/pay/arc_payment_app_bridge.h" +#include "components/arc/test/arc_payment_app_bridge_test_support.h" +#include "components/payments/core/method_strings.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace payments { +namespace { + +class ScopedInitializationChromeOS + : public AndroidAppCommunicationTestSupport::ScopedInitialization { + public: + ScopedInitializationChromeOS(arc::ArcServiceManager* manager, + arc::mojom::PaymentAppInstance* instance) + : scoped_set_instance_(manager, instance) {} + ~ScopedInitializationChromeOS() override = default; + + ScopedInitializationChromeOS(const ScopedInitializationChromeOS& other) = + delete; + ScopedInitializationChromeOS& operator=( + const ScopedInitializationChromeOS& other) = delete; + + private: + arc::ArcPaymentAppBridgeTestSupport::ScopedSetInstance scoped_set_instance_; +}; + +class AndroidAppCommunicationTestSupportChromeOS + : public AndroidAppCommunicationTestSupport { + public: + AndroidAppCommunicationTestSupportChromeOS() = default; + ~AndroidAppCommunicationTestSupportChromeOS() override = default; + + AndroidAppCommunicationTestSupportChromeOS( + const AndroidAppCommunicationTestSupportChromeOS& other) = delete; + AndroidAppCommunicationTestSupportChromeOS& operator=( + const AndroidAppCommunicationTestSupportChromeOS& other) = delete; + + bool AreAndroidAppsSupportedOnThisPlatform() const override { return true; } + + std::unique_ptr<ScopedInitialization> CreateScopedInitialization() override { + return std::make_unique<ScopedInitializationChromeOS>(support_.manager(), + support_.instance()); + } + + void ExpectNoListOfPaymentAppsQuery() override { + EXPECT_CALL(*support_.instance(), + IsPaymentImplemented(testing::_, testing::_)) + .Times(0); + } + + void ExpectNoIsReadyToPayQuery() override { + EXPECT_CALL(*support_.instance(), IsReadyToPay(testing::_, testing::_)) + .Times(0); + } + + void ExpectNoPaymentAppInvoke() override { + EXPECT_CALL(*support_.instance(), InvokePaymentApp(testing::_, testing::_)) + .Times(0); + } + + void ExpectQueryListOfPaymentAppsAndRespond( + std::vector<std::unique_ptr<AndroidAppDescription>> apps) override { + // Move |apps| into a member variable, so it's still alive by the time the + // RespondToGetAppDescriptions() method is executed at some point in the + // future. + apps_ = std::move(apps); + EXPECT_CALL(*support_.instance(), + IsPaymentImplemented(testing::_, testing::_)) + .WillOnce(testing::Invoke( + [&](const std::string& package_name, + arc::ArcPaymentAppBridge::IsPaymentImplementedCallback + callback) { + RespondToGetAppDescriptions(package_name, std::move(callback)); + })); + } + + void ExpectQueryIsReadyToPayAndRespond(bool is_ready_to_pay) override { + EXPECT_CALL(*support_.instance(), IsReadyToPay(testing::_, testing::_)) + .WillOnce(testing::Invoke( + [is_ready_to_pay]( + arc::mojom::PaymentParametersPtr parameters, + arc::ArcPaymentAppBridge::IsReadyToPayCallback callback) { + std::move(callback).Run( + arc::mojom::IsReadyToPayResult::NewResponse(is_ready_to_pay)); + })); + } + + void ExpectInvokePaymentAppAndRespond( + bool is_activity_result_ok, + const std::string& payment_method_identifier, + const std::string& stringified_details) override { + EXPECT_CALL(*support_.instance(), InvokePaymentApp(testing::_, testing::_)) + .WillOnce(testing::Invoke( + [is_activity_result_ok, stringified_details]( + arc::mojom::PaymentParametersPtr parameters, + arc::ArcPaymentAppBridge::InvokePaymentAppCallback callback) { + // Chrome OS supports only kGooglePlayBilling payment method + // identifier at this time, so the |payment_method_identifier| + // parameter is ignored here. + auto valid = arc::mojom::InvokePaymentAppValidResult::New(); + valid->is_activity_result_ok = is_activity_result_ok; + valid->stringified_details = stringified_details; + std::move(callback).Run( + arc::mojom::InvokePaymentAppResult::NewValid( + std::move(valid))); + })); + } + + content::BrowserContext* context() override { return support_.context(); } + + private: + void RespondToGetAppDescriptions( + const std::string& package_name, + arc::ArcPaymentAppBridge::IsPaymentImplementedCallback callback) { + auto valid = arc::mojom::IsPaymentImplementedValidResult::New(); + for (const auto& app : apps_) { + if (app->package == package_name) { + for (const auto& activity : app->activities) { + // Chrome OS supports only kGooglePlayBilling method at this time. + if (activity->default_payment_method == methods::kGooglePlayBilling) { + valid->activity_names.push_back(activity->name); + } + } + + valid->service_names = app->service_names; + + // Chrome OS supports only one payment app in Android subsystem at this + // time, i.e., the TWA that invoked Chrome. + break; + } + } + + std::move(callback).Run( + arc::mojom::IsPaymentImplementedResult::NewValid(std::move(valid))); + } + + arc::ArcPaymentAppBridgeTestSupport support_; + std::vector<std::unique_ptr<AndroidAppDescription>> apps_; +}; + +} // namespace + +// Declared in cross-platform file +// //components/payments/content/android_app_communication_test_support.h +// static +std::unique_ptr<AndroidAppCommunicationTestSupport> +AndroidAppCommunicationTestSupport::Create() { + return std::make_unique<AndroidAppCommunicationTestSupportChromeOS>(); +} + +} // namespace payments diff --git a/chromium/components/payments/content/android_app_communication_test_support_stub.cc b/chromium/components/payments/content/android_app_communication_test_support_stub.cc new file mode 100644 index 00000000000..93df4287e60 --- /dev/null +++ b/chromium/components/payments/content/android_app_communication_test_support_stub.cc @@ -0,0 +1,65 @@ +// Copyright 2020 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. + +#include "components/payments/content/android_app_communication_test_support.h" + +#include <utility> + +#include "content/public/test/browser_task_environment.h" +#include "content/public/test/test_browser_context.h" + +namespace payments { +namespace { + +class AndroidAppCommunicationTestSupportStub + : public AndroidAppCommunicationTestSupport { + public: + AndroidAppCommunicationTestSupportStub() = default; + ~AndroidAppCommunicationTestSupportStub() override = default; + + AndroidAppCommunicationTestSupportStub( + const AndroidAppCommunicationTestSupportStub& other) = delete; + AndroidAppCommunicationTestSupportStub& operator=( + const AndroidAppCommunicationTestSupportStub& other) = delete; + + bool AreAndroidAppsSupportedOnThisPlatform() const override { return false; } + + std::unique_ptr<ScopedInitialization> CreateScopedInitialization() override { + return std::make_unique<ScopedInitialization>(); + } + + void ExpectNoListOfPaymentAppsQuery() override {} + + void ExpectNoIsReadyToPayQuery() override {} + + void ExpectNoPaymentAppInvoke() override {} + + void ExpectQueryListOfPaymentAppsAndRespond( + std::vector<std::unique_ptr<AndroidAppDescription>> apps) override {} + + void ExpectQueryIsReadyToPayAndRespond(bool is_ready_to_pay) override {} + + void ExpectInvokePaymentAppAndRespond( + bool is_activity_result_ok, + const std::string& payment_method_identifier, + const std::string& stringified_details) override {} + + content::BrowserContext* context() override { return &context_; } + + private: + content::BrowserTaskEnvironment environment_; + content::TestBrowserContext context_; +}; + +} // namespace + +// Declared in cross-platform file +// //components/payments/content/android_app_communication_test_support.h +// static +std::unique_ptr<AndroidAppCommunicationTestSupport> +AndroidAppCommunicationTestSupport::Create() { + return std::make_unique<AndroidAppCommunicationTestSupportStub>(); +} + +} // namespace payments diff --git a/chromium/components/payments/content/android_app_communication_unittest.cc b/chromium/components/payments/content/android_app_communication_unittest.cc new file mode 100644 index 00000000000..552e46b3234 --- /dev/null +++ b/chromium/components/payments/content/android_app_communication_unittest.cc @@ -0,0 +1,639 @@ +// Copyright 2020 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. + +#include "components/payments/content/android_app_communication.h" + +#include <memory> +#include <string> +#include <utility> +#include <vector> + +#include "base/bind.h" +#include "base/callback.h" +#include "base/optional.h" +#include "components/payments/content/android_app_communication_test_support.h" +#include "components/payments/core/android_app_description.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +namespace payments { +namespace { + +std::vector<std::unique_ptr<AndroidAppDescription>> createApp( + const std::vector<std::string>& activity_names, + const std::string& default_payment_method, + const std::vector<std::string>& service_names) { + auto app = std::make_unique<AndroidAppDescription>(); + + for (const auto& activity_name : activity_names) { + auto activity = std::make_unique<AndroidActivityDescription>(); + activity->name = activity_name; + activity->default_payment_method = default_payment_method; + app->activities.emplace_back(std::move(activity)); + } + + app->package = "com.example.app"; + app->service_names = service_names; + + std::vector<std::unique_ptr<AndroidAppDescription>> apps; + apps.emplace_back(std::move(app)); + + return apps; +} + +class AndroidAppCommunicationTest : public testing::Test { + public: + AndroidAppCommunicationTest() + : support_(AndroidAppCommunicationTestSupport::Create()) {} + ~AndroidAppCommunicationTest() override = default; + + AndroidAppCommunicationTest(const AndroidAppCommunicationTest& other) = + delete; + AndroidAppCommunicationTest& operator=( + const AndroidAppCommunicationTest& other) = delete; + + void OnGetAppDescriptionsResponse( + const base::Optional<std::string>& error, + std::vector<std::unique_ptr<AndroidAppDescription>> apps) { + error_ = error; + apps_ = std::move(apps); + } + + void OnIsReadyToPayResponse(const base::Optional<std::string>& error, + bool is_ready_to_pay) { + error_ = error; + is_ready_to_pay_ = is_ready_to_pay; + } + + void OnPaymentAppResponse(const base::Optional<std::string>& error, + bool is_activity_result_ok, + const std::string& payment_method_identifier, + const std::string& stringified_details) { + error_ = error; + is_activity_result_ok_ = is_activity_result_ok; + payment_method_identifier_ = payment_method_identifier; + stringified_details_ = stringified_details; + } + + std::unique_ptr<AndroidAppCommunicationTestSupport> support_; + base::Optional<std::string> error_; + std::vector<std::unique_ptr<AndroidAppDescription>> apps_; + bool is_ready_to_pay_ = false; + bool is_activity_result_ok_ = false; + std::string payment_method_identifier_; + std::string stringified_details_; +}; + +TEST_F(AndroidAppCommunicationTest, OneInstancePerBrowserContext) { + auto communication_one = + AndroidAppCommunication::GetForBrowserContext(support_->context()); + auto communication_two = + AndroidAppCommunication::GetForBrowserContext(support_->context()); + EXPECT_EQ(communication_one.get(), communication_two.get()); +} + +TEST_F(AndroidAppCommunicationTest, NoArcForGetAppDescriptions) { + // Intentionally do not set an instance. + + support_->ExpectNoListOfPaymentAppsQuery(); + + auto communication = + AndroidAppCommunication::GetForBrowserContext(support_->context()); + communication->SetForTesting(); + communication->GetAppDescriptions( + "com.example.app", + base::BindOnce(&AndroidAppCommunicationTest::OnGetAppDescriptionsResponse, + base::Unretained(this))); + + if (support_->AreAndroidAppsSupportedOnThisPlatform()) { + ASSERT_TRUE(error_.has_value()); + EXPECT_EQ("Unable to invoke Android apps.", error_.value()); + } else { + EXPECT_FALSE(error_.has_value()); + } + + EXPECT_TRUE(apps_.empty()); +} + +TEST_F(AndroidAppCommunicationTest, NoAppDescriptions) { + auto scoped_initialization = support_->CreateScopedInitialization(); + + support_->ExpectQueryListOfPaymentAppsAndRespond({}); + + auto communication = + AndroidAppCommunication::GetForBrowserContext(support_->context()); + communication->SetForTesting(); + communication->GetAppDescriptions( + "com.example.app", + base::BindOnce(&AndroidAppCommunicationTest::OnGetAppDescriptionsResponse, + base::Unretained(this))); + + EXPECT_FALSE(error_.has_value()); + EXPECT_TRUE(apps_.empty()); +} + +TEST_F(AndroidAppCommunicationTest, TwoActivitiesInPackage) { + auto scoped_initialization = support_->CreateScopedInitialization(); + + support_->ExpectQueryListOfPaymentAppsAndRespond( + createApp({"com.example.app.ActivityOne", "com.example.app.ActivityTwo"}, + "https://play.google.com/billing", {})); + + auto communication = + AndroidAppCommunication::GetForBrowserContext(support_->context()); + communication->SetForTesting(); + communication->GetAppDescriptions( + "com.example.app", + base::BindOnce(&AndroidAppCommunicationTest::OnGetAppDescriptionsResponse, + base::Unretained(this))); + + if (support_->AreAndroidAppsSupportedOnThisPlatform()) { + ASSERT_TRUE(error_.has_value()); + EXPECT_EQ( + "Found more than one PAY activity in the Trusted Web Activity, but at " + "most one activity is supported.", + error_.value()); + } else { + EXPECT_FALSE(error_.has_value()); + } + EXPECT_TRUE(apps_.empty()); +} + +TEST_F(AndroidAppCommunicationTest, TwoServicesInPackage) { + auto scoped_initialization = support_->CreateScopedInitialization(); + + support_->ExpectQueryListOfPaymentAppsAndRespond( + createApp({"com.example.app.Activity"}, "https://play.google.com/billing", + {"com.example.app.ServiceOne", "com.example.app.ServiceTwo"})); + + auto communication = + AndroidAppCommunication::GetForBrowserContext(support_->context()); + communication->SetForTesting(); + communication->GetAppDescriptions( + "com.example.app", + base::BindOnce(&AndroidAppCommunicationTest::OnGetAppDescriptionsResponse, + base::Unretained(this))); + + EXPECT_FALSE(error_.has_value()); + if (support_->AreAndroidAppsSupportedOnThisPlatform()) { + ASSERT_EQ(1u, apps_.size()); + ASSERT_NE(nullptr, apps_.front().get()); + EXPECT_EQ("com.example.app", apps_.front()->package); + + // The logic for checking for multiple services is cross-platform in + // android_payment_app_factory.cc, so the platform-specific implementations + // of android_app_communication.h do not check for this error condition. + std::vector<std::string> expected_service_names = { + "com.example.app.ServiceOne", "com.example.app.ServiceTwo"}; + EXPECT_EQ(expected_service_names, apps_.front()->service_names); + + ASSERT_EQ(1u, apps_.front()->activities.size()); + ASSERT_NE(nullptr, apps_.front()->activities.front().get()); + EXPECT_EQ("com.example.app.Activity", + apps_.front()->activities.front()->name); + EXPECT_EQ("https://play.google.com/billing", + apps_.front()->activities.front()->default_payment_method); + } else { + EXPECT_TRUE(apps_.empty()); + } +} + +TEST_F(AndroidAppCommunicationTest, ActivityAndService) { + auto scoped_initialization = support_->CreateScopedInitialization(); + + support_->ExpectQueryListOfPaymentAppsAndRespond( + createApp({"com.example.app.Activity"}, "https://play.google.com/billing", + {"com.example.app.Service"})); + + auto communication = + AndroidAppCommunication::GetForBrowserContext(support_->context()); + communication->SetForTesting(); + communication->GetAppDescriptions( + "com.example.app", + base::BindOnce(&AndroidAppCommunicationTest::OnGetAppDescriptionsResponse, + base::Unretained(this))); + + EXPECT_FALSE(error_.has_value()); + + if (support_->AreAndroidAppsSupportedOnThisPlatform()) { + ASSERT_EQ(1u, apps_.size()); + ASSERT_NE(nullptr, apps_.front().get()); + EXPECT_EQ("com.example.app", apps_.front()->package); + EXPECT_EQ(std::vector<std::string>{"com.example.app.Service"}, + apps_.front()->service_names); + ASSERT_EQ(1u, apps_.front()->activities.size()); + ASSERT_NE(nullptr, apps_.front()->activities.front().get()); + EXPECT_EQ("com.example.app.Activity", + apps_.front()->activities.front()->name); + EXPECT_EQ("https://play.google.com/billing", + apps_.front()->activities.front()->default_payment_method); + } else { + EXPECT_TRUE(apps_.empty()); + } +} + +TEST_F(AndroidAppCommunicationTest, OnlyActivity) { + auto scoped_initialization = support_->CreateScopedInitialization(); + + support_->ExpectQueryListOfPaymentAppsAndRespond(createApp( + {"com.example.app.Activity"}, "https://play.google.com/billing", {})); + + auto communication = + AndroidAppCommunication::GetForBrowserContext(support_->context()); + communication->SetForTesting(); + communication->GetAppDescriptions( + "com.example.app", + base::BindOnce(&AndroidAppCommunicationTest::OnGetAppDescriptionsResponse, + base::Unretained(this))); + + EXPECT_FALSE(error_.has_value()); + + if (support_->AreAndroidAppsSupportedOnThisPlatform()) { + ASSERT_EQ(1u, apps_.size()); + ASSERT_NE(nullptr, apps_.front().get()); + EXPECT_EQ("com.example.app", apps_.front()->package); + EXPECT_TRUE(apps_.front()->service_names.empty()); + ASSERT_EQ(1u, apps_.front()->activities.size()); + ASSERT_NE(nullptr, apps_.front()->activities.front().get()); + EXPECT_EQ("com.example.app.Activity", + apps_.front()->activities.front()->name); + EXPECT_EQ("https://play.google.com/billing", + apps_.front()->activities.front()->default_payment_method); + } else { + EXPECT_TRUE(apps_.empty()); + } +} + +TEST_F(AndroidAppCommunicationTest, OutsideOfTwa) { + auto scoped_initialization = support_->CreateScopedInitialization(); + + support_->ExpectNoListOfPaymentAppsQuery(); + + auto communication = + AndroidAppCommunication::GetForBrowserContext(support_->context()); + communication->SetForTesting(); + communication->GetAppDescriptions( + /*twa_package_name=*/"", // Empty string means this is not TWA. + base::BindOnce(&AndroidAppCommunicationTest::OnGetAppDescriptionsResponse, + base::Unretained(this))); + + EXPECT_FALSE(error_.has_value()); + EXPECT_TRUE(apps_.empty()); +} + +TEST_F(AndroidAppCommunicationTest, NoArcForIsReadyToPay) { + // Intentionally do not set an instance. + + support_->ExpectNoIsReadyToPayQuery(); + + auto communication = + AndroidAppCommunication::GetForBrowserContext(support_->context()); + communication->SetForTesting(); + + std::map<std::string, std::set<std::string>> stringified_method_data; + stringified_method_data["https://play.google.com/billing"].insert("{}"); + communication->IsReadyToPay( + "com.example.app", "com.example.app.Service", stringified_method_data, + GURL("https://top-level-origin.com"), + GURL("https://payment-request-origin.com"), "payment-request-id", + base::BindOnce(&AndroidAppCommunicationTest::OnIsReadyToPayResponse, + base::Unretained(this))); + + ASSERT_TRUE(error_.has_value()); + EXPECT_EQ("Unable to invoke Android apps.", error_.value()); + EXPECT_FALSE(is_ready_to_pay_); +} + +TEST_F(AndroidAppCommunicationTest, TwaIsReadyToPayOnlyWithPlayBilling) { + auto scoped_initialization = support_->CreateScopedInitialization(); + + support_->ExpectNoIsReadyToPayQuery(); + + auto communication = + AndroidAppCommunication::GetForBrowserContext(support_->context()); + communication->SetForTesting(); + + std::map<std::string, std::set<std::string>> stringified_method_data; + stringified_method_data["https://example.com"].insert("{}"); + communication->IsReadyToPay( + "com.example.app", "com.example.app.Service", stringified_method_data, + GURL("https://top-level-origin.com"), + GURL("https://payment-request-origin.com"), "payment-request-id", + base::BindOnce(&AndroidAppCommunicationTest::OnIsReadyToPayResponse, + base::Unretained(this))); + + if (support_->AreAndroidAppsSupportedOnThisPlatform()) { + EXPECT_FALSE(error_.has_value()); + } else { + ASSERT_TRUE(error_.has_value()); + EXPECT_EQ("Unable to invoke Android apps.", error_.value()); + } + + EXPECT_FALSE(is_ready_to_pay_); +} + +TEST_F(AndroidAppCommunicationTest, MoreThanOnePaymentMethodDataNotReadyToPay) { + auto scoped_initialization = support_->CreateScopedInitialization(); + + support_->ExpectNoIsReadyToPayQuery(); + + auto communication = + AndroidAppCommunication::GetForBrowserContext(support_->context()); + communication->SetForTesting(); + + std::map<std::string, std::set<std::string>> stringified_method_data; + stringified_method_data["https://play.google.com/billing"].insert( + "{\"product_id\": \"1\"}"); + stringified_method_data["https://play.google.com/billing"].insert( + "{\"product_id\": \"2\"}"); + communication->IsReadyToPay( + "com.example.app", "com.example.app.Service", stringified_method_data, + GURL("https://top-level-origin.com"), + GURL("https://payment-request-origin.com"), "payment-request-id", + base::BindOnce(&AndroidAppCommunicationTest::OnIsReadyToPayResponse, + base::Unretained(this))); + + ASSERT_TRUE(error_.has_value()); + + if (support_->AreAndroidAppsSupportedOnThisPlatform()) { + EXPECT_EQ("At most one payment method specific data is supported.", + error_.value()); + } else { + EXPECT_EQ("Unable to invoke Android apps.", error_.value()); + } + + EXPECT_FALSE(is_ready_to_pay_); +} + +TEST_F(AndroidAppCommunicationTest, EmptyMethodDataIsReadyToPay) { + auto scoped_initialization = support_->CreateScopedInitialization(); + + support_->ExpectQueryIsReadyToPayAndRespond(true); + + auto communication = + AndroidAppCommunication::GetForBrowserContext(support_->context()); + communication->SetForTesting(); + + std::map<std::string, std::set<std::string>> stringified_method_data; + stringified_method_data.insert(std::make_pair( + "https://play.google.com/billing", std::set<std::string>())); + communication->IsReadyToPay( + "com.example.app", "com.example.app.Service", stringified_method_data, + GURL("https://top-level-origin.com"), + GURL("https://payment-request-origin.com"), "payment-request-id", + base::BindOnce(&AndroidAppCommunicationTest::OnIsReadyToPayResponse, + base::Unretained(this))); + + if (support_->AreAndroidAppsSupportedOnThisPlatform()) { + EXPECT_FALSE(error_.has_value()); + EXPECT_TRUE(is_ready_to_pay_); + } else { + ASSERT_TRUE(error_.has_value()); + EXPECT_EQ("Unable to invoke Android apps.", error_.value()); + EXPECT_FALSE(is_ready_to_pay_); + } +} + +TEST_F(AndroidAppCommunicationTest, NotReadyToPay) { + auto scoped_initialization = support_->CreateScopedInitialization(); + + support_->ExpectQueryIsReadyToPayAndRespond(false); + + auto communication = + AndroidAppCommunication::GetForBrowserContext(support_->context()); + communication->SetForTesting(); + + std::map<std::string, std::set<std::string>> stringified_method_data; + stringified_method_data["https://play.google.com/billing"].insert("{}"); + communication->IsReadyToPay( + "com.example.app", "com.example.app.Service", stringified_method_data, + GURL("https://top-level-origin.com"), + GURL("https://payment-request-origin.com"), "payment-request-id", + base::BindOnce(&AndroidAppCommunicationTest::OnIsReadyToPayResponse, + base::Unretained(this))); + + if (support_->AreAndroidAppsSupportedOnThisPlatform()) { + EXPECT_FALSE(error_.has_value()); + } else { + ASSERT_TRUE(error_.has_value()); + EXPECT_EQ("Unable to invoke Android apps.", error_.value()); + } + + EXPECT_FALSE(is_ready_to_pay_); +} + +TEST_F(AndroidAppCommunicationTest, ReadyToPay) { + auto scoped_initialization = support_->CreateScopedInitialization(); + + support_->ExpectQueryIsReadyToPayAndRespond(true); + + auto communication = + AndroidAppCommunication::GetForBrowserContext(support_->context()); + communication->SetForTesting(); + + std::map<std::string, std::set<std::string>> stringified_method_data; + stringified_method_data["https://play.google.com/billing"].insert("{}"); + communication->IsReadyToPay( + "com.example.app", "com.example.app.Service", stringified_method_data, + GURL("https://top-level-origin.com"), + GURL("https://payment-request-origin.com"), "payment-request-id", + base::BindOnce(&AndroidAppCommunicationTest::OnIsReadyToPayResponse, + base::Unretained(this))); + + if (support_->AreAndroidAppsSupportedOnThisPlatform()) { + EXPECT_FALSE(error_.has_value()); + EXPECT_TRUE(is_ready_to_pay_); + } else { + ASSERT_TRUE(error_.has_value()); + EXPECT_EQ("Unable to invoke Android apps.", error_.value()); + EXPECT_FALSE(is_ready_to_pay_); + } +} + +TEST_F(AndroidAppCommunicationTest, NoArcForInvokePaymentApp) { + // Intentionally do not set an instance. + + support_->ExpectNoPaymentAppInvoke(); + + auto communication = + AndroidAppCommunication::GetForBrowserContext(support_->context()); + communication->SetForTesting(); + + std::map<std::string, std::set<std::string>> stringified_method_data; + stringified_method_data["https://play.google.com/billing"].insert("{}"); + communication->InvokePaymentApp( + "com.example.app", "com.example.app.Activity", stringified_method_data, + GURL("https://top-level-origin.com"), + GURL("https://payment-request-origin.com"), "payment-request-id", + base::BindOnce(&AndroidAppCommunicationTest::OnPaymentAppResponse, + base::Unretained(this))); + + EXPECT_EQ("Unable to invoke Android apps.", error_.value()); + EXPECT_FALSE(is_activity_result_ok_); + EXPECT_TRUE(payment_method_identifier_.empty()); + EXPECT_EQ("{}", stringified_details_); +} + +TEST_F(AndroidAppCommunicationTest, TwaPaymentOnlyWithPlayBilling) { + auto scoped_initialization = support_->CreateScopedInitialization(); + + support_->ExpectNoPaymentAppInvoke(); + + auto communication = + AndroidAppCommunication::GetForBrowserContext(support_->context()); + communication->SetForTesting(); + + std::map<std::string, std::set<std::string>> stringified_method_data; + stringified_method_data["https://example.com"].insert("{}"); + communication->InvokePaymentApp( + "com.example.app", "com.example.app.Activity", stringified_method_data, + GURL("https://top-level-origin.com"), + GURL("https://payment-request-origin.com"), "payment-request-id", + base::BindOnce(&AndroidAppCommunicationTest::OnPaymentAppResponse, + base::Unretained(this))); + + if (support_->AreAndroidAppsSupportedOnThisPlatform()) { + EXPECT_FALSE(error_.has_value()); + EXPECT_FALSE(is_activity_result_ok_); + EXPECT_TRUE(payment_method_identifier_.empty()); + EXPECT_EQ("{}", stringified_details_); + } else { + ASSERT_TRUE(error_.has_value()); + EXPECT_EQ("Unable to invoke Android apps.", error_.value()); + } +} + +TEST_F(AndroidAppCommunicationTest, NoPaymentWithMoreThanOnePaymentMethodData) { + auto scoped_initialization = support_->CreateScopedInitialization(); + + support_->ExpectNoPaymentAppInvoke(); + + auto communication = + AndroidAppCommunication::GetForBrowserContext(support_->context()); + communication->SetForTesting(); + + std::map<std::string, std::set<std::string>> stringified_method_data; + stringified_method_data["https://play.google.com/billing"].insert( + "{\"product_id\": \"1\"}"); + stringified_method_data["https://play.google.com/billing"].insert( + "{\"product_id\": \"2\"}"); + communication->InvokePaymentApp( + "com.example.app", "com.example.app.Activity", stringified_method_data, + GURL("https://top-level-origin.com"), + GURL("https://payment-request-origin.com"), "payment-request-id", + base::BindOnce(&AndroidAppCommunicationTest::OnPaymentAppResponse, + base::Unretained(this))); + + EXPECT_FALSE(is_activity_result_ok_); + EXPECT_EQ("{}", stringified_details_); + EXPECT_TRUE(payment_method_identifier_.empty()); + ASSERT_TRUE(error_.has_value()); + + if (support_->AreAndroidAppsSupportedOnThisPlatform()) { + EXPECT_EQ("At most one payment method specific data is supported.", + error_.value()); + } else { + EXPECT_EQ("Unable to invoke Android apps.", error_.value()); + } +} + +TEST_F(AndroidAppCommunicationTest, PaymentWithEmptyMethodData) { + auto scoped_initialization = support_->CreateScopedInitialization(); + + support_->ExpectInvokePaymentAppAndRespond( + /*is_activity_result_ok=*/true, + /*payment_method_identifier=*/"https://play.google.com/billing", + /*stringified_details*/ "{\"status\": \"ok\"}"); + + auto communication = + AndroidAppCommunication::GetForBrowserContext(support_->context()); + communication->SetForTesting(); + + std::map<std::string, std::set<std::string>> stringified_method_data; + stringified_method_data.insert(std::make_pair( + "https://play.google.com/billing", std::set<std::string>())); + communication->InvokePaymentApp( + "com.example.app", "com.example.app.Activity", stringified_method_data, + GURL("https://top-level-origin.com"), + GURL("https://payment-request-origin.com"), "payment-request-id", + base::BindOnce(&AndroidAppCommunicationTest::OnPaymentAppResponse, + base::Unretained(this))); + + if (support_->AreAndroidAppsSupportedOnThisPlatform()) { + EXPECT_FALSE(error_.has_value()); + EXPECT_TRUE(is_activity_result_ok_); + EXPECT_EQ("https://play.google.com/billing", payment_method_identifier_); + EXPECT_EQ("{\"status\": \"ok\"}", stringified_details_); + } else { + ASSERT_TRUE(error_.has_value()); + EXPECT_EQ("Unable to invoke Android apps.", error_.value()); + } +} + +TEST_F(AndroidAppCommunicationTest, UserCancelInvokePaymentApp) { + auto scoped_initialization = support_->CreateScopedInitialization(); + + support_->ExpectInvokePaymentAppAndRespond( + /*is_activity_result_ok=*/false, + /*payment_method_identifier=*/"https://play.google.com/billing", + /*stringified_details*/ "{}"); + + auto communication = + AndroidAppCommunication::GetForBrowserContext(support_->context()); + communication->SetForTesting(); + + std::map<std::string, std::set<std::string>> stringified_method_data; + stringified_method_data["https://play.google.com/billing"].insert("{}"); + communication->InvokePaymentApp( + "com.example.app", "com.example.app.Activity", stringified_method_data, + GURL("https://top-level-origin.com"), + GURL("https://payment-request-origin.com"), "payment-request-id", + base::BindOnce(&AndroidAppCommunicationTest::OnPaymentAppResponse, + base::Unretained(this))); + + if (support_->AreAndroidAppsSupportedOnThisPlatform()) { + EXPECT_FALSE(error_.has_value()); + EXPECT_FALSE(is_activity_result_ok_); + EXPECT_EQ("https://play.google.com/billing", payment_method_identifier_); + EXPECT_EQ("{}", stringified_details_); + } else { + ASSERT_TRUE(error_.has_value()); + EXPECT_EQ("Unable to invoke Android apps.", error_.value()); + } +} + +TEST_F(AndroidAppCommunicationTest, UserConfirmInvokePaymentApp) { + auto scoped_initialization = support_->CreateScopedInitialization(); + + support_->ExpectInvokePaymentAppAndRespond( + /*is_activity_result_ok=*/true, + /*payment_method_identifier=*/"https://play.google.com/billing", + /*stringified_details*/ "{\"status\": \"ok\"}"); + + auto communication = + AndroidAppCommunication::GetForBrowserContext(support_->context()); + communication->SetForTesting(); + + std::map<std::string, std::set<std::string>> stringified_method_data; + stringified_method_data["https://play.google.com/billing"].insert("{}"); + communication->InvokePaymentApp( + "com.example.app", "com.example.app.Activity", stringified_method_data, + GURL("https://top-level-origin.com"), + GURL("https://payment-request-origin.com"), "payment-request-id", + base::BindOnce(&AndroidAppCommunicationTest::OnPaymentAppResponse, + base::Unretained(this))); + + if (support_->AreAndroidAppsSupportedOnThisPlatform()) { + EXPECT_FALSE(error_.has_value()); + EXPECT_TRUE(is_activity_result_ok_); + EXPECT_EQ("https://play.google.com/billing", payment_method_identifier_); + EXPECT_EQ("{\"status\": \"ok\"}", stringified_details_); + } else { + ASSERT_TRUE(error_.has_value()); + EXPECT_EQ("Unable to invoke Android apps.", error_.value()); + } +} + +} // namespace +} // namespace payments diff --git a/chromium/components/payments/content/android_payment_app.cc b/chromium/components/payments/content/android_payment_app.cc new file mode 100644 index 00000000000..6871e68e00e --- /dev/null +++ b/chromium/components/payments/content/android_payment_app.cc @@ -0,0 +1,178 @@ +// Copyright 2020 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. + +#include "components/payments/content/android_payment_app.h" + +#include <utility> + +#include "base/check.h" +#include "components/payments/core/method_strings.h" +#include "components/payments/core/native_error_strings.h" +#include "components/payments/core/payer_data.h" + +namespace payments { + +AndroidPaymentApp::AndroidPaymentApp( + const std::set<std::string>& payment_method_names, + std::unique_ptr<std::map<std::string, std::set<std::string>>> + stringified_method_data, + const GURL& top_level_origin, + const GURL& payment_request_origin, + const std::string& payment_request_id, + std::unique_ptr<AndroidAppDescription> description, + base::WeakPtr<AndroidAppCommunication> communication) + : PaymentApp(/*icon_resource_id=*/0, PaymentApp::Type::NATIVE_MOBILE_APP), + stringified_method_data_(std::move(stringified_method_data)), + top_level_origin_(top_level_origin), + payment_request_origin_(payment_request_origin), + payment_request_id_(payment_request_id), + description_(std::move(description)), + communication_(communication) { + DCHECK(!payment_method_names.empty()); + DCHECK_EQ(payment_method_names.size(), stringified_method_data_->size()); + DCHECK_EQ(*payment_method_names.begin(), + stringified_method_data_->begin()->first); + DCHECK(description_); + DCHECK(!description_->package.empty()); + DCHECK_EQ(1U, description_->activities.size()); + DCHECK(!description_->activities.front()->name.empty()); + + app_method_names_ = payment_method_names; +} + +AndroidPaymentApp::~AndroidPaymentApp() = default; + +void AndroidPaymentApp::InvokePaymentApp(Delegate* delegate) { + // Browser is closing, so no need to invoke a callback. + if (!communication_) + return; + + communication_->InvokePaymentApp( + description_->package, description_->activities.front()->name, + *stringified_method_data_, top_level_origin_, payment_request_origin_, + payment_request_id_, + base::BindOnce(&AndroidPaymentApp::OnPaymentAppResponse, + weak_ptr_factory_.GetWeakPtr(), delegate)); +} + +bool AndroidPaymentApp::IsCompleteForPayment() const { + return true; +} + +uint32_t AndroidPaymentApp::GetCompletenessScore() const { + return 0; +} + +bool AndroidPaymentApp::CanPreselect() const { + return true; +} + +base::string16 AndroidPaymentApp::GetMissingInfoLabel() const { + NOTREACHED(); + return base::string16(); +} + +bool AndroidPaymentApp::HasEnrolledInstrument() const { + return true; +} + +void AndroidPaymentApp::RecordUse() { + NOTIMPLEMENTED(); +} + +bool AndroidPaymentApp::NeedsInstallation() const { + return false; +} + +std::string AndroidPaymentApp::GetId() const { + return description_->package; +} + +base::string16 AndroidPaymentApp::GetLabel() const { + return base::string16(); +} + +base::string16 AndroidPaymentApp::GetSublabel() const { + return base::string16(); +} + +const SkBitmap* AndroidPaymentApp::icon_bitmap() const { + return nullptr; +} + +bool AndroidPaymentApp::IsValidForModifier( + const std::string& method, + bool supported_networks_specified, + const std::set<std::string>& supported_networks) const { + bool is_valid = false; + IsValidForPaymentMethodIdentifier(method, &is_valid); + return is_valid; +} + +base::WeakPtr<PaymentApp> AndroidPaymentApp::AsWeakPtr() { + return weak_ptr_factory_.GetWeakPtr(); +} + +bool AndroidPaymentApp::HandlesShippingAddress() const { + return false; +} + +bool AndroidPaymentApp::HandlesPayerName() const { + return false; +} + +bool AndroidPaymentApp::HandlesPayerEmail() const { + return false; +} + +bool AndroidPaymentApp::HandlesPayerPhone() const { + return false; +} + +bool AndroidPaymentApp::IsWaitingForPaymentDetailsUpdate() const { + return false; +} + +void AndroidPaymentApp::UpdateWith( + mojom::PaymentRequestDetailsUpdatePtr details_update) { + // TODO(crbug.com/1022512): Support payment method, shipping address, and + // shipping option change events. +} + +void AndroidPaymentApp::OnPaymentDetailsNotUpdated() {} + +bool AndroidPaymentApp::IsPreferred() const { + // This class used only on Chrome OS, where the only Android payment app + // available is the trusted web application (TWA) that launched this instance + // of Chrome with a TWA specific payment method, so this app should be + // preferred. +#if !defined(OS_CHROMEOS) + NOTREACHED(); +#endif // OS_CHROMEOS + DCHECK_EQ(1U, GetAppMethodNames().size()); + DCHECK_EQ(methods::kGooglePlayBilling, *GetAppMethodNames().begin()); + return true; +} + +void AndroidPaymentApp::OnPaymentAppResponse( + Delegate* delegate, + const base::Optional<std::string>& error_message, + bool is_activity_result_ok, + const std::string& payment_method_identifier, + const std::string& stringified_details) { + if (error_message.has_value()) { + delegate->OnInstrumentDetailsError(error_message.value()); + return; + } + + if (!is_activity_result_ok) { + delegate->OnInstrumentDetailsError(errors::kUserClosedPaymentApp); + return; + } + + delegate->OnInstrumentDetailsReady(payment_method_identifier, + stringified_details, PayerData()); +} + +} // namespace payments diff --git a/chromium/components/payments/content/android_payment_app.h b/chromium/components/payments/content/android_payment_app.h new file mode 100644 index 00000000000..0e826e0edfa --- /dev/null +++ b/chromium/components/payments/content/android_payment_app.h @@ -0,0 +1,97 @@ +// Copyright 2020 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. + +#ifndef COMPONENTS_PAYMENTS_CONTENT_ANDROID_PAYMENT_APP_H_ +#define COMPONENTS_PAYMENTS_CONTENT_ANDROID_PAYMENT_APP_H_ + +#include <memory> +#include <set> +#include <string> + +#include "base/memory/weak_ptr.h" +#include "base/optional.h" +#include "components/payments/content/android_app_communication.h" +#include "components/payments/content/payment_app.h" +#include "components/payments/core/android_app_description.h" +#include "url/gurl.h" + +namespace payments { + +// A cross-platform way to invoke an Android payment app. +class AndroidPaymentApp : public PaymentApp { + public: + // Creates an instance of AndroidPaymentApp. + // + // The |payment_method_names| is the set of payment method identifiers + // supported by this app, e.g., ["https://example1.com", + // "https://example2.com"]. This set should not be empty. + // + // The |stringified_method_data| is a mapping from payment method identifiers + // that this app can handle to the method-specific data provided by the + // merchant. The set of keys should match exactly the |payment_method_names|. + // It is the responsibility of the creator of AndroidPaymentApp to filter out + // the data from merchant that is not in |payment_method_names|. + AndroidPaymentApp( + const std::set<std::string>& payment_method_names, + std::unique_ptr<std::map<std::string, std::set<std::string>>> + stringified_method_data, + const GURL& top_level_origin, + const GURL& payment_request_origin, + const std::string& payment_request_id, + std::unique_ptr<AndroidAppDescription> description, + base::WeakPtr<AndroidAppCommunication> communication); + ~AndroidPaymentApp() override; + + AndroidPaymentApp(const AndroidPaymentApp& other) = delete; + AndroidPaymentApp& operator=(const AndroidPaymentApp& other) = delete; + + // PaymentApp implementation. + void InvokePaymentApp(Delegate* delegate) override; + bool IsCompleteForPayment() const override; + uint32_t GetCompletenessScore() const override; + bool CanPreselect() const override; + base::string16 GetMissingInfoLabel() const override; + bool HasEnrolledInstrument() const override; + void RecordUse() override; + bool NeedsInstallation() const override; + std::string GetId() const override; + base::string16 GetLabel() const override; + base::string16 GetSublabel() const override; + const SkBitmap* icon_bitmap() const override; + bool IsValidForModifier( + const std::string& method, + bool supported_networks_specified, + const std::set<std::string>& supported_networks) const override; + base::WeakPtr<PaymentApp> AsWeakPtr() override; + bool HandlesShippingAddress() const override; + bool HandlesPayerName() const override; + bool HandlesPayerEmail() const override; + bool HandlesPayerPhone() const override; + bool IsWaitingForPaymentDetailsUpdate() const override; + void UpdateWith( + mojom::PaymentRequestDetailsUpdatePtr details_update) override; + void OnPaymentDetailsNotUpdated() override; + bool IsPreferred() const override; + + private: + void OnPaymentAppResponse(Delegate* delegate, + const base::Optional<std::string>& error_message, + bool is_activity_result_ok, + const std::string& payment_method_identifier, + const std::string& stringified_details); + + const std::unique_ptr<std::map<std::string, std::set<std::string>>> + stringified_method_data_; + const GURL top_level_origin_; + const GURL payment_request_origin_; + const std::string payment_request_id_; + const std::unique_ptr<AndroidAppDescription> description_; + base::WeakPtr<AndroidAppCommunication> communication_; + + base::WeakPtrFactory<AndroidPaymentApp> weak_ptr_factory_{this}; +}; + +} // namespace payments + +#endif // COMPONENTS_PAYMENTS_CONTENT_ANDROID_PAYMENT_APP_H_ diff --git a/chromium/components/payments/content/android_payment_app_factory.cc b/chromium/components/payments/content/android_payment_app_factory.cc new file mode 100644 index 00000000000..5611de2da5b --- /dev/null +++ b/chromium/components/payments/content/android_payment_app_factory.cc @@ -0,0 +1,231 @@ +// Copyright 2020 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. + +#include "components/payments/content/android_payment_app_factory.h" + +#include <map> +#include <memory> +#include <set> +#include <string> +#include <utility> +#include <vector> + +#include "base/bind.h" +#include "base/memory/weak_ptr.h" +#include "base/stl_util.h" +#include "base/supports_user_data.h" +#include "components/payments/content/android_app_communication.h" +#include "components/payments/content/android_payment_app.h" +#include "components/payments/content/payment_request_spec.h" +#include "components/payments/core/android_app_description.h" +#include "components/payments/core/android_app_description_tools.h" +#include "components/payments/core/method_strings.h" +#include "components/payments/core/native_error_strings.h" +#include "components/payments/core/payment_request_data_util.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/render_document_host_user_data.h" +#include "content/public/browser/web_contents.h" + +namespace payments { +namespace { + +class AppFinder : public base::SupportsUserData::Data { + public: + static base::WeakPtr<AppFinder> CreateAndSetOwnedBy( + base::SupportsUserData* owner) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + DCHECK(owner); + auto owned = std::make_unique<AppFinder>(owner); + auto weak_ptr = owned->weak_ptr_factory_.GetWeakPtr(); + const void* key = owned.get(); + owner->SetUserData(key, std::move(owned)); + return weak_ptr; + } + + explicit AppFinder(base::SupportsUserData* owner) : owner_(owner) {} + ~AppFinder() override = default; + + AppFinder(const AppFinder& other) = delete; + AppFinder& operator=(const AppFinder& other) = delete; + + void FindApps(base::WeakPtr<AndroidAppCommunication> communication, + base::WeakPtr<PaymentAppFactory::Delegate> delegate) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + DCHECK_EQ(nullptr, delegate_.get()); + DCHECK_NE(nullptr, delegate.get()); + DCHECK_EQ(0U, number_of_pending_is_ready_to_pay_queries_); + DCHECK_EQ(nullptr, communication_.get()); + DCHECK_NE(nullptr, communication.get()); + DCHECK(delegate->GetSpec()); + DCHECK(delegate->GetSpec()->details().id.has_value()); + + delegate_ = delegate; + communication_ = communication; + + std::string twa_package_name = delegate_->GetTwaPackageName(); + std::set<std::string> twa_payment_method_names = { + methods::kGooglePlayBilling, + }; + if (twa_package_name.empty() || + base::STLSetIntersection<std::set<std::string>>( + delegate_->GetSpec()->payment_method_identifiers_set(), + twa_payment_method_names) + .empty()) { + OnDoneCreatingPaymentApps(); + return; + } + + communication_->GetAppDescriptions( + twa_package_name, base::BindOnce(&AppFinder::OnGetAppDescriptions, + weak_ptr_factory_.GetWeakPtr())); + } + + private: + void OnGetAppDescriptions( + const base::Optional<std::string>& error_message, + std::vector<std::unique_ptr<AndroidAppDescription>> app_descriptions) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + // The browser could be shutting down. + if (!communication_ || !delegate_ || !delegate_->GetSpec()) + return; + + if (error_message.has_value()) { + delegate_->OnPaymentAppCreationError(error_message.value()); + OnDoneCreatingPaymentApps(); + return; + } + + std::vector<std::unique_ptr<AndroidAppDescription>> single_activity_apps; + for (size_t i = 0; i < app_descriptions.size(); ++i) { + auto app = std::move(app_descriptions[i]); + if (app->service_names.size() > 1U) { + delegate_->OnPaymentAppCreationError(errors::kMoreThanOneService); + continue; + } + + // Move each activity in the given |app| to its own AndroidAppDescription + // in |single_activity_apps|, so the code can treat each PAY intent as its + // own payment app. This allows Android apps to implement PAY intent in + // multiple activities with different names and icons for different use + // cases. + SplitPotentiallyMultipleActivities(std::move(app), &single_activity_apps); + } + + number_of_pending_is_ready_to_pay_queries_ = single_activity_apps.size(); + if (number_of_pending_is_ready_to_pay_queries_ == 0U) { + OnDoneCreatingPaymentApps(); + return; + } + + for (size_t i = 0; i < single_activity_apps.size(); ++i) { + std::unique_ptr<AndroidAppDescription> single_activity_app = + std::move(single_activity_apps[i]); + + const std::string& default_method = + single_activity_app->activities.front()->default_payment_method; + DCHECK_EQ(methods::kGooglePlayBilling, default_method); + + std::set<std::string> supported_payment_methods = {default_method}; + std::set<std::string> payment_method_names = + base::STLSetIntersection<std::set<std::string>>( + delegate_->GetSpec()->payment_method_identifiers_set(), + supported_payment_methods); + + std::unique_ptr<std::map<std::string, std::set<std::string>>> + stringified_method_data = data_util::FilterStringifiedMethodData( + delegate_->GetSpec()->stringified_method_data(), + supported_payment_methods); + + // TODO(crbug.com/1022512): Download the web app manifest for + // |default_payment_method_name| to verify Android app signature. + + // Skip querying IS_READY_TO_PAY service when Chrome is off-the-record or + // when the app does not implement the IS_READY_TO_PAY service. + if (delegate_->IsOffTheRecord() || + single_activity_app->service_names.empty()) { + OnIsReadyToPay(std::move(single_activity_app), payment_method_names, + std::move(stringified_method_data), + /*error_message=*/base::nullopt, + /*is_ready_to_pay=*/true); + continue; + } + + std::map<std::string, std::set<std::string>> + stringified_method_data_copy = *stringified_method_data; + communication_->IsReadyToPay( + single_activity_app->package, + single_activity_app->service_names.front(), + stringified_method_data_copy, delegate_->GetTopOrigin(), + delegate_->GetFrameOrigin(), + delegate_->GetSpec()->details().id.value(), + base::BindOnce(&AppFinder::OnIsReadyToPay, + weak_ptr_factory_.GetWeakPtr(), + std::move(single_activity_app), payment_method_names, + std::move(stringified_method_data))); + } + } + + void OnIsReadyToPay( + std::unique_ptr<AndroidAppDescription> app_description, + const std::set<std::string>& payment_method_names, + std::unique_ptr<std::map<std::string, std::set<std::string>>> + stringified_method_data, + const base::Optional<std::string>& error_message, + bool is_ready_to_pay) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + DCHECK_LT(0U, number_of_pending_is_ready_to_pay_queries_); + + // The browser could be shutting down. + if (!communication_ || !delegate_ || !delegate_->GetSpec()) { + OnDoneCreatingPaymentApps(); + return; + } + + if (error_message.has_value()) { + delegate_->OnPaymentAppCreationError(error_message.value()); + } else if (is_ready_to_pay) { + delegate_->OnPaymentAppCreated(std::make_unique<AndroidPaymentApp>( + payment_method_names, std::move(stringified_method_data), + delegate_->GetTopOrigin(), delegate_->GetFrameOrigin(), + delegate_->GetSpec()->details().id.value(), + std::move(app_description), communication_)); + } + + if (--number_of_pending_is_ready_to_pay_queries_ == 0) + OnDoneCreatingPaymentApps(); + } + + void OnDoneCreatingPaymentApps() { + if (delegate_) + delegate_->OnDoneCreatingPaymentApps(); + + owner_->RemoveUserData(this); + } + + base::SupportsUserData* owner_; + base::WeakPtr<PaymentAppFactory::Delegate> delegate_; + size_t number_of_pending_is_ready_to_pay_queries_ = 0; + base::WeakPtr<AndroidAppCommunication> communication_; + + base::WeakPtrFactory<AppFinder> weak_ptr_factory_{this}; +}; + +} // namespace + +AndroidPaymentAppFactory::AndroidPaymentAppFactory( + base::WeakPtr<AndroidAppCommunication> communication) + : PaymentAppFactory(PaymentApp::Type::NATIVE_MOBILE_APP), + communication_(communication) { + DCHECK(communication_); +} + +AndroidPaymentAppFactory::~AndroidPaymentAppFactory() = default; + +void AndroidPaymentAppFactory::Create(base::WeakPtr<Delegate> delegate) { + auto app_finder = AppFinder::CreateAndSetOwnedBy(delegate->GetWebContents()); + app_finder->FindApps(communication_, delegate); +} + +} // namespace payments diff --git a/chromium/components/payments/content/android_payment_app_factory.h b/chromium/components/payments/content/android_payment_app_factory.h new file mode 100644 index 00000000000..09b4df56ee7 --- /dev/null +++ b/chromium/components/payments/content/android_payment_app_factory.h @@ -0,0 +1,37 @@ +// Copyright 2020 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. + +#ifndef COMPONENTS_PAYMENTS_CONTENT_ANDROID_PAYMENT_APP_FACTORY_H_ +#define COMPONENTS_PAYMENTS_CONTENT_ANDROID_PAYMENT_APP_FACTORY_H_ + +#include "base/memory/weak_ptr.h" +#include "components/payments/content/payment_app_factory.h" + +namespace payments { + +class AndroidAppCommunication; + +// Retrieves Android payment apps. +class AndroidPaymentAppFactory : public PaymentAppFactory { + public: + // The given |communication| is used for communication with Android payment + // apps. + explicit AndroidPaymentAppFactory( + base::WeakPtr<AndroidAppCommunication> communication); + ~AndroidPaymentAppFactory() override; + + AndroidPaymentAppFactory(const AndroidPaymentAppFactory& other) = delete; + AndroidPaymentAppFactory& operator=(const AndroidPaymentAppFactory& other) = + delete; + + // PaymentAppFactory: + void Create(base::WeakPtr<Delegate> delegate) override; + + private: + base::WeakPtr<AndroidAppCommunication> communication_; +}; + +} // namespace payments + +#endif // COMPONENTS_PAYMENTS_CONTENT_ANDROID_PAYMENT_APP_FACTORY_H_ diff --git a/chromium/components/payments/content/android_payment_app_factory_unittest.cc b/chromium/components/payments/content/android_payment_app_factory_unittest.cc new file mode 100644 index 00000000000..4f53c6a5241 --- /dev/null +++ b/chromium/components/payments/content/android_payment_app_factory_unittest.cc @@ -0,0 +1,519 @@ +// Copyright 2020 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. + +#include "components/payments/content/android_payment_app_factory.h" + +#include <utility> + +#include "base/memory/weak_ptr.h" +#include "base/stl_util.h" +#include "components/autofill/core/browser/payments/internal_authenticator.h" +#include "components/payments/content/android_app_communication.h" +#include "components/payments/content/android_app_communication_test_support.h" +#include "components/payments/content/payment_app_factory.h" +#include "components/payments/content/payment_manifest_web_data_service.h" +#include "components/payments/content/payment_request_spec.h" +#include "components/payments/core/android_app_description.h" +#include "content/public/browser/web_contents.h" +#include "content/public/test/test_web_contents_factory.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" +#include "url/origin.h" + +namespace content { +class BrowserContext; +} // namespace content + +namespace payments { +namespace { + +// A mock delegate for payment app factories. +class MockPaymentAppFactoryDelegate : public PaymentAppFactory::Delegate { + public: + explicit MockPaymentAppFactoryDelegate(content::BrowserContext* context) + : web_contents_(web_contents_factory_.CreateWebContents(context)), + top_origin_("https://top-origin.test"), + frame_origin_("https://frame-origin.test") { + SetRequestedPaymentMethod("https://play.google.com/billing"); + } + + ~MockPaymentAppFactoryDelegate() override = default; + + void SetRequestedPaymentMethod(const std::string& method) { + auto details = mojom::PaymentDetails::New(); + details->id = "id"; + + std::vector<mojom::PaymentMethodDataPtr> methods; + methods.emplace_back(mojom::PaymentMethodData::New()); + methods.back()->supported_method = method; + methods.back()->stringified_data = "{}"; + + spec_ = std::make_unique<PaymentRequestSpec>( + mojom::PaymentOptions::New(), std::move(details), std::move(methods), + /*observer=*/nullptr, /*app_locale=*/"en-US"); + } + + void set_is_off_the_record() { is_off_the_record_ = true; } + + // PaymentAppFactory::Delegate implementation: + content::WebContents* GetWebContents() override { return web_contents_; } + const GURL& GetTopOrigin() override { return top_origin_; } + const GURL& GetFrameOrigin() override { return frame_origin_; } + MOCK_METHOD0(GetFrameSecurityOrigin, const url::Origin&()); + MOCK_CONST_METHOD0(GetInitiatorRenderFrameHost, content::RenderFrameHost*()); + MOCK_CONST_METHOD0(GetMethodData, + const std::vector<mojom::PaymentMethodDataPtr>&()); + MOCK_CONST_METHOD0(CreateInternalAuthenticator, + std::unique_ptr<autofill::InternalAuthenticator>()); + MOCK_CONST_METHOD0(GetPaymentManifestWebDataService, + scoped_refptr<PaymentManifestWebDataService>()); + MOCK_METHOD0(MayCrawlForInstallablePaymentApps, bool()); + bool IsOffTheRecord() const override { return is_off_the_record_; } + PaymentRequestSpec* GetSpec() const override { return spec_.get(); } + MOCK_CONST_METHOD0(GetTwaPackageName, std::string()); + MOCK_METHOD0(ShowProcessingSpinner, void()); + MOCK_METHOD0(GetBillingProfiles, + const std::vector<autofill::AutofillProfile*>&()); + MOCK_METHOD0(IsRequestedAutofillDataAvailable, bool()); + MOCK_CONST_METHOD0(GetPaymentRequestDelegate, + ContentPaymentRequestDelegate*()); + MOCK_METHOD1(OnPaymentAppCreated, void(std::unique_ptr<PaymentApp> app)); + MOCK_METHOD1(OnPaymentAppCreationError, + void(const std::string& error_message)); + MOCK_CONST_METHOD0(SkipCreatingNativePaymentApps, bool()); + MOCK_METHOD0(OnDoneCreatingPaymentApps, void()); + MOCK_METHOD0(SetCanMakePaymentEvenWithoutApps, void()); + + base::WeakPtr<PaymentAppFactory::Delegate> GetWeakPtr() { + return weak_ptr_factory_.GetWeakPtr(); + } + + private: + content::TestWebContentsFactory web_contents_factory_; + content::WebContents* web_contents_; + GURL top_origin_; + GURL frame_origin_; + std::unique_ptr<PaymentRequestSpec> spec_; + bool is_off_the_record_ = false; + base::WeakPtrFactory<PaymentAppFactory::Delegate> weak_ptr_factory_{this}; +}; + +// The scaffolding for testing the Android payment app factory. +class AndroidPaymentAppFactoryTest : public testing::Test { + public: + AndroidPaymentAppFactoryTest() + : support_(AndroidAppCommunicationTestSupport::Create()), + delegate_(support_->context()), + factory_(GetCommunication(support_->context())) {} + + std::unique_ptr<AndroidAppCommunicationTestSupport> support_; + MockPaymentAppFactoryDelegate delegate_; + AndroidPaymentAppFactory factory_; + + private: + // Returns the Android app communication that can be used in unit tests. + static base::WeakPtr<AndroidAppCommunication> GetCommunication( + content::BrowserContext* context) { + base::WeakPtr<AndroidAppCommunication> communication = + AndroidAppCommunication::GetForBrowserContext(context); + communication->SetForTesting(); + return communication; + } +}; + +// The payment app factory should return an error if it's unable to invoke +// Aneroid payment apps on a platform that supports such apps, e.g, when ARC is +// disabled on Chrome OS. +TEST_F(AndroidPaymentAppFactoryTest, FactoryReturnsErrorWithoutArc) { + EXPECT_CALL(delegate_, GetTwaPackageName()) + .WillRepeatedly(testing::Return("com.example.app")); + EXPECT_CALL(delegate_, OnDoneCreatingPaymentApps()); + + EXPECT_CALL(delegate_, + OnPaymentAppCreationError("Unable to invoke Android apps.")) + .Times(support_->AreAndroidAppsSupportedOnThisPlatform() ? 1 : 0); + EXPECT_CALL(delegate_, OnPaymentAppCreated(testing::_)).Times(0); + support_->ExpectNoListOfPaymentAppsQuery(); + support_->ExpectNoIsReadyToPayQuery(); + + factory_.Create(delegate_.GetWeakPtr()); +} + +// The payment app factory should not return any errors when there're no Android +// payment apps available. +TEST_F(AndroidPaymentAppFactoryTest, NoErrorsWhenNoApps) { + // Enable invoking Android payment apps on those platforms that support it. + auto scoped_initialization_ = support_->CreateScopedInitialization(); + + EXPECT_CALL(delegate_, GetTwaPackageName()) + .WillRepeatedly(testing::Return("com.example.app")); + EXPECT_CALL(delegate_, OnDoneCreatingPaymentApps()); + + EXPECT_CALL(delegate_, OnPaymentAppCreationError(testing::_)).Times(0); + EXPECT_CALL(delegate_, OnPaymentAppCreated(testing::_)).Times(0); + support_->ExpectQueryListOfPaymentAppsAndRespond({}); + support_->ExpectNoIsReadyToPayQuery(); + + factory_.Create(delegate_.GetWeakPtr()); +} + +// The |arg| is of type std::unique_ptr<PaymentApp>. +MATCHER_P3(PaymentAppMatches, type, package, method, "") { + return arg->type() == type && arg->GetId() == package && + base::Contains(arg->GetAppMethodNames(), method); +} + +// The payment app factory should return the TWA payment app when running in TWA +// mode, even when it does not have an IS_READY_TO_PAY service. +TEST_F(AndroidPaymentAppFactoryTest, FindAppsThatDoNotHaveReadyToPayService) { + // Enable invoking Android payment apps on those platforms that support it. + auto scoped_initialization_ = support_->CreateScopedInitialization(); + + EXPECT_CALL(delegate_, GetTwaPackageName()) + .WillRepeatedly(testing::Return("com.example.app")); + EXPECT_CALL(delegate_, OnDoneCreatingPaymentApps()); + + EXPECT_CALL(delegate_, OnPaymentAppCreationError(testing::_)).Times(0); + + EXPECT_CALL(delegate_, + OnPaymentAppCreated(PaymentAppMatches( + PaymentApp::Type::NATIVE_MOBILE_APP, "com.example.app", + "https://play.google.com/billing"))) + .Times(support_->AreAndroidAppsSupportedOnThisPlatform() ? 1 : 0); + + // This app does not have an IS_READY_TO_PAY service. + std::vector<std::unique_ptr<AndroidAppDescription>> apps; + apps.emplace_back(std::make_unique<AndroidAppDescription>()); + apps.back()->package = "com.example.app"; + apps.back()->activities.emplace_back( + std::make_unique<AndroidActivityDescription>()); + apps.back()->activities.back()->name = "com.example.app.Activity"; + apps.back()->activities.back()->default_payment_method = + "https://play.google.com/billing"; + support_->ExpectQueryListOfPaymentAppsAndRespond(std::move(apps)); + + // There is no IS_READY_TO_PAY service to invoke. + support_->ExpectNoIsReadyToPayQuery(); + + factory_.Create(delegate_.GetWeakPtr()); +} + +// The payment app factory should return one payment app and should not query +// the IS_READY_TO_PAY service, because of being off the record. +TEST_F(AndroidPaymentAppFactoryTest, + DoNotQueryReadyToPaySericeWhenOffTheRecord) { + // Enable invoking Android payment apps on those platforms that support it. + auto scoped_initialization_ = support_->CreateScopedInitialization(); + + // Simulate being off the record. + delegate_.set_is_off_the_record(); + + EXPECT_CALL(delegate_, GetTwaPackageName()) + .WillRepeatedly(testing::Return("com.example.app")); + EXPECT_CALL(delegate_, OnDoneCreatingPaymentApps()); + + EXPECT_CALL(delegate_, OnPaymentAppCreationError(testing::_)).Times(0); + + EXPECT_CALL(delegate_, + OnPaymentAppCreated(PaymentAppMatches( + PaymentApp::Type::NATIVE_MOBILE_APP, "com.example.app", + "https://play.google.com/billing"))) + .Times(support_->AreAndroidAppsSupportedOnThisPlatform() ? 1 : 0); + + std::vector<std::unique_ptr<AndroidAppDescription>> apps; + apps.emplace_back(std::make_unique<AndroidAppDescription>()); + apps.back()->package = "com.example.app"; + apps.back()->service_names.push_back("com.example.app.Service"); + apps.back()->activities.emplace_back( + std::make_unique<AndroidActivityDescription>()); + apps.back()->activities.back()->name = "com.example.app.Activity"; + apps.back()->activities.back()->default_payment_method = + "https://play.google.com/billing"; + support_->ExpectQueryListOfPaymentAppsAndRespond(std::move(apps)); + + // The IS_READY_TO_PAY service should not be invoked when off the record. + support_->ExpectNoIsReadyToPayQuery(); + + factory_.Create(delegate_.GetWeakPtr()); +} + +// The payment app factory should return the TWA payment app that returns true +// from IS_READY_TO_PAY service when running in TWA mode. +TEST_F(AndroidPaymentAppFactoryTest, + FindTheTwaPaymentAppThatIsReadyToPayInTwaMode) { + // Enable invoking Android payment apps on those platforms that support it. + auto scoped_initialization_ = support_->CreateScopedInitialization(); + + EXPECT_CALL(delegate_, GetTwaPackageName()) + .WillRepeatedly(testing::Return("com.twa.app")); + EXPECT_CALL(delegate_, OnDoneCreatingPaymentApps()); + + EXPECT_CALL(delegate_, OnPaymentAppCreationError(testing::_)).Times(0); + + EXPECT_CALL(delegate_, OnPaymentAppCreated(PaymentAppMatches( + PaymentApp::Type::NATIVE_MOBILE_APP, "com.twa.app", + "https://play.google.com/billing"))) + .Times(support_->AreAndroidAppsSupportedOnThisPlatform() ? 1 : 0); + + std::vector<std::unique_ptr<AndroidAppDescription>> apps; + apps.emplace_back(std::make_unique<AndroidAppDescription>()); + apps.back()->package = "com.twa.app"; + apps.back()->service_names.push_back("com.twa.app.Service"); + apps.back()->activities.emplace_back( + std::make_unique<AndroidActivityDescription>()); + apps.back()->activities.back()->name = "com.twa.app.Activity"; + apps.back()->activities.back()->default_payment_method = + "https://play.google.com/billing"; + support_->ExpectQueryListOfPaymentAppsAndRespond(std::move(apps)); + support_->ExpectQueryIsReadyToPayAndRespond(/*is_ready_to_pay=*/true); + + factory_.Create(delegate_.GetWeakPtr()); +} + +// The payment app factory should return no payment apps when IS_READY_TO_PAY +// service returns false. +TEST_F(AndroidPaymentAppFactoryTest, IgnoreAppsThatAreNotReadyToPay) { + // Enable invoking Android payment apps on those platforms that support it. + auto scoped_initialization_ = support_->CreateScopedInitialization(); + + EXPECT_CALL(delegate_, GetTwaPackageName()) + .WillRepeatedly(testing::Return("com.example.app")); + EXPECT_CALL(delegate_, OnDoneCreatingPaymentApps()); + EXPECT_CALL(delegate_, OnPaymentAppCreationError(testing::_)).Times(0); + EXPECT_CALL(delegate_, OnPaymentAppCreated(testing::_)).Times(0); + + std::vector<std::unique_ptr<AndroidAppDescription>> apps; + apps.emplace_back(std::make_unique<AndroidAppDescription>()); + apps.back()->package = "com.example.app"; + apps.back()->service_names.push_back("com.example.app.Service"); + apps.back()->activities.emplace_back( + std::make_unique<AndroidActivityDescription>()); + apps.back()->activities.back()->name = "com.example.app.Activity"; + apps.back()->activities.back()->default_payment_method = + "https://play.google.com/billing"; + support_->ExpectQueryListOfPaymentAppsAndRespond(std::move(apps)); + support_->ExpectQueryIsReadyToPayAndRespond(/*is_ready_to_pay=*/false); + + factory_.Create(delegate_.GetWeakPtr()); +} + +// The payment app factory should return the correct TWA payment app out of two +// installed payment apps, when running in TWA mode. +TEST_F(AndroidPaymentAppFactoryTest, FindTheCorrectTwaAppInTwaMode) { + // Enable invoking Android payment apps on those platforms that support it. + auto scoped_initialization_ = support_->CreateScopedInitialization(); + + EXPECT_CALL(delegate_, GetTwaPackageName()) + .WillRepeatedly(testing::Return("com.correct-twa.app")); + EXPECT_CALL(delegate_, OnDoneCreatingPaymentApps()); + + EXPECT_CALL(delegate_, OnPaymentAppCreationError(testing::_)).Times(0); + + EXPECT_CALL(delegate_, + OnPaymentAppCreated(PaymentAppMatches( + PaymentApp::Type::NATIVE_MOBILE_APP, "com.correct-twa.app", + "https://play.google.com/billing"))) + .Times(support_->AreAndroidAppsSupportedOnThisPlatform() ? 1 : 0); + EXPECT_CALL(delegate_, + OnPaymentAppCreated(PaymentAppMatches( + PaymentApp::Type::NATIVE_MOBILE_APP, "com.different.app", + "https://play.google.com/billing"))) + .Times(0); + + std::vector<std::unique_ptr<AndroidAppDescription>> apps; + apps.emplace_back(std::make_unique<AndroidAppDescription>()); + apps.back()->package = "com.correct-twa.app"; + apps.back()->service_names.push_back("com.correct-twa.app.Service"); + apps.back()->activities.emplace_back( + std::make_unique<AndroidActivityDescription>()); + apps.back()->activities.back()->name = "com.correct-twa.app.Activity"; + apps.back()->activities.back()->default_payment_method = + "https://play.google.com/billing"; + + apps.emplace_back(std::make_unique<AndroidAppDescription>()); + apps.back()->package = "com.different.app"; + apps.back()->service_names.push_back("com.different.app.Service"); + apps.back()->activities.emplace_back( + std::make_unique<AndroidActivityDescription>()); + apps.back()->activities.back()->name = "com.different.app.Activity"; + apps.back()->activities.back()->default_payment_method = + "https://play.google.com/billing"; + + support_->ExpectQueryListOfPaymentAppsAndRespond(std::move(apps)); + support_->ExpectQueryIsReadyToPayAndRespond(/*is_ready_to_pay=*/true); + + factory_.Create(delegate_.GetWeakPtr()); +} + +// The payment app factory does not return non-TWA payment apps when running in +// TWA mode. +TEST_F(AndroidPaymentAppFactoryTest, IgnoreNonTwaAppsInTwaMode) { + // Enable invoking Android payment apps on those platforms that support it. + auto scoped_initialization_ = support_->CreateScopedInitialization(); + + EXPECT_CALL(delegate_, GetTwaPackageName()) + .WillRepeatedly(testing::Return("com.twa.app")); + EXPECT_CALL(delegate_, OnDoneCreatingPaymentApps()); + EXPECT_CALL(delegate_, OnPaymentAppCreationError(testing::_)).Times(0); + EXPECT_CALL(delegate_, OnPaymentAppCreated(testing::_)).Times(0); + + std::vector<std::unique_ptr<AndroidAppDescription>> apps; + apps.emplace_back(std::make_unique<AndroidAppDescription>()); + apps.back()->package = "com.non-twa.app"; + apps.back()->service_names.push_back("com.non-twa.app.Service"); + apps.back()->activities.emplace_back( + std::make_unique<AndroidActivityDescription>()); + apps.back()->activities.back()->name = "com.non-twa.app.Activity"; + apps.back()->activities.back()->default_payment_method = + "https://play.google.com/billing"; + support_->ExpectQueryListOfPaymentAppsAndRespond(std::move(apps)); + support_->ExpectNoIsReadyToPayQuery(); + + factory_.Create(delegate_.GetWeakPtr()); +} + +// The payment app factory does not return any payment apps when not running +// inside of TWA. +TEST_F(AndroidPaymentAppFactoryTest, DoNotLookForAppsWhenOutsideOfTwaMode) { + // Enable invoking Android payment apps on those platforms that support it. + auto scoped_initialization_ = support_->CreateScopedInitialization(); + + EXPECT_CALL(delegate_, GetTwaPackageName()) + .WillRepeatedly(testing::Return("")); + EXPECT_CALL(delegate_, OnDoneCreatingPaymentApps()); + EXPECT_CALL(delegate_, OnPaymentAppCreationError(testing::_)).Times(0); + EXPECT_CALL(delegate_, OnPaymentAppCreated(testing::_)).Times(0); + support_->ExpectNoListOfPaymentAppsQuery(); + + factory_.Create(delegate_.GetWeakPtr()); +} + +// The Android payment app factory works only with TWA specific payment methods. +TEST_F(AndroidPaymentAppFactoryTest, DoNotLookForAppsForNonTwaMethod) { + // Enable invoking Android payment apps on those platforms that support it. + auto scoped_initialization_ = support_->CreateScopedInitialization(); + + // "https://example.test" is not a TWA specific payment method. + delegate_.SetRequestedPaymentMethod("https://example.test"); + + EXPECT_CALL(delegate_, GetTwaPackageName()) + .WillRepeatedly(testing::Return("com.example.app")); + EXPECT_CALL(delegate_, OnDoneCreatingPaymentApps()); + EXPECT_CALL(delegate_, OnPaymentAppCreationError(testing::_)).Times(0); + EXPECT_CALL(delegate_, OnPaymentAppCreated(testing::_)).Times(0); + support_->ExpectNoListOfPaymentAppsQuery(); + support_->ExpectNoIsReadyToPayQuery(); + + factory_.Create(delegate_.GetWeakPtr()); +} + +// If the TWA supports a non-TWA-specific payment method, then it should be +// ignored. +TEST_F(AndroidPaymentAppFactoryTest, IgnoreNonTwaMethodInTheTwa) { + // Enable invoking Android payment apps on those platforms that support it. + auto scoped_initialization_ = support_->CreateScopedInitialization(); + + EXPECT_CALL(delegate_, GetTwaPackageName()) + .WillRepeatedly(testing::Return("com.twa.app")); + EXPECT_CALL(delegate_, OnDoneCreatingPaymentApps()); + EXPECT_CALL(delegate_, OnPaymentAppCreationError(testing::_)).Times(0); + EXPECT_CALL(delegate_, OnPaymentAppCreated(testing::_)).Times(0); + + std::vector<std::unique_ptr<AndroidAppDescription>> apps; + apps.emplace_back(std::make_unique<AndroidAppDescription>()); + apps.back()->package = "com.twa.app"; + apps.back()->service_names.push_back("com.twa.app.Service"); + apps.back()->activities.emplace_back( + std::make_unique<AndroidActivityDescription>()); + apps.back()->activities.back()->name = "com.twa.app.Activity"; + apps.back()->activities.back()->default_payment_method = + "https://example.test"; + support_->ExpectQueryListOfPaymentAppsAndRespond(std::move(apps)); + support_->ExpectNoIsReadyToPayQuery(); + + factory_.Create(delegate_.GetWeakPtr()); +} + +// If the TWA supports both a TWA-specific and a non-TWA-specific payment +// method, then only the TWA-specific payment method activity should be +// returned. +TEST_F(AndroidPaymentAppFactoryTest, + FindOnlyActivitiesWithTwaSpecificMethodName) { + // Enable invoking Android payment apps on those platforms that support it. + auto scoped_initialization_ = support_->CreateScopedInitialization(); + + EXPECT_CALL(delegate_, GetTwaPackageName()) + .WillRepeatedly(testing::Return("com.twa.app")); + EXPECT_CALL(delegate_, OnDoneCreatingPaymentApps()); + EXPECT_CALL(delegate_, OnPaymentAppCreationError(testing::_)).Times(0); + EXPECT_CALL(delegate_, OnPaymentAppCreated(PaymentAppMatches( + PaymentApp::Type::NATIVE_MOBILE_APP, "com.twa.app", + "https://play.google.com/billing"))) + .Times(support_->AreAndroidAppsSupportedOnThisPlatform() ? 1 : 0); + EXPECT_CALL(delegate_, OnPaymentAppCreated(PaymentAppMatches( + PaymentApp::Type::NATIVE_MOBILE_APP, "com.twa.app", + "https://example.test"))) + .Times(0); + + std::vector<std::unique_ptr<AndroidAppDescription>> apps; + apps.emplace_back(std::make_unique<AndroidAppDescription>()); + apps.back()->package = "com.twa.app"; + apps.back()->service_names.push_back("com.twa.app.Service"); + + apps.back()->activities.emplace_back( + std::make_unique<AndroidActivityDescription>()); + apps.back()->activities.back()->name = "com.twa.app.ActivityOne"; + apps.back()->activities.back()->default_payment_method = + "https://play.google.com/billing"; + + apps.back()->activities.emplace_back( + std::make_unique<AndroidActivityDescription>()); + apps.back()->activities.back()->name = "com.twa.app.ActivityTwo"; + apps.back()->activities.back()->default_payment_method = + "https://example.test"; + + support_->ExpectQueryListOfPaymentAppsAndRespond(std::move(apps)); + support_->ExpectQueryIsReadyToPayAndRespond(/*is_ready_to_pay=*/true); + + factory_.Create(delegate_.GetWeakPtr()); +} + +// At most one IS_READY_TO_PAY service is allowed in an Android payment app. +TEST_F(AndroidPaymentAppFactoryTest, ReturnErrorWhenMoreThanOneServiceInApp) { + // Enable invoking Android payment apps on those platforms that support it. + auto scoped_initialization_ = support_->CreateScopedInitialization(); + + EXPECT_CALL(delegate_, GetTwaPackageName()) + .WillRepeatedly(testing::Return("com.example.app")); + EXPECT_CALL(delegate_, OnDoneCreatingPaymentApps()); + + EXPECT_CALL(delegate_, OnPaymentAppCreationError( + "Found more than one IS_READY_TO_PAY service, but " + "at most one service is supported.")) + .Times(support_->AreAndroidAppsSupportedOnThisPlatform() ? 1 : 0); + + EXPECT_CALL(delegate_, OnPaymentAppCreated(testing::_)).Times(0); + + std::vector<std::unique_ptr<AndroidAppDescription>> apps; + apps.emplace_back(std::make_unique<AndroidAppDescription>()); + apps.back()->package = "com.example.app"; + + // Two IS_READY_TO_PAY services: + apps.back()->service_names.push_back("com.example.app.ServiceOne"); + apps.back()->service_names.push_back("com.example.app.ServiceTwo"); + + apps.back()->activities.emplace_back( + std::make_unique<AndroidActivityDescription>()); + apps.back()->activities.back()->name = "com.example.app.Activity"; + apps.back()->activities.back()->default_payment_method = + "https://play.google.com/billing"; + support_->ExpectQueryListOfPaymentAppsAndRespond(std::move(apps)); + support_->ExpectNoIsReadyToPayQuery(); + + factory_.Create(delegate_.GetWeakPtr()); +} + +} // namespace +} // namespace payments diff --git a/chromium/components/payments/content/android_payment_app_unittest.cc b/chromium/components/payments/content/android_payment_app_unittest.cc new file mode 100644 index 00000000000..cb515a09d53 --- /dev/null +++ b/chromium/components/payments/content/android_payment_app_unittest.cc @@ -0,0 +1,164 @@ +// Copyright 2020 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. + +#include "components/payments/content/android_payment_app.h" + +#include <map> +#include <memory> +#include <set> +#include <string> +#include <utility> +#include <vector> + +#include "base/bind.h" +#include "base/callback.h" +#include "base/optional.h" +#include "components/payments/content/android_app_communication.h" +#include "components/payments/content/android_app_communication_test_support.h" +#include "components/payments/core/android_app_description.h" +#include "components/payments/core/method_strings.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +namespace payments { +namespace { + +class AndroidPaymentAppTest : public testing::Test, + public PaymentApp::Delegate { + public: + static std::unique_ptr<AndroidPaymentApp> CreateAndroidPaymentApp( + base::WeakPtr<AndroidAppCommunication> communication) { + std::set<std::string> payment_method_names; + payment_method_names.insert(methods::kGooglePlayBilling); + auto stringified_method_data = + std::make_unique<std::map<std::string, std::set<std::string>>>(); + stringified_method_data->insert({methods::kGooglePlayBilling, {"{}"}}); + auto description = std::make_unique<AndroidAppDescription>(); + description->package = "com.example.app"; + description->service_names.push_back("com.example.app.Service"); + description->activities.emplace_back( + std::make_unique<AndroidActivityDescription>()); + description->activities.back()->name = "com.example.app.Activity"; + description->activities.back()->default_payment_method = + methods::kGooglePlayBilling; + + return std::make_unique<AndroidPaymentApp>( + payment_method_names, std::move(stringified_method_data), + GURL("https://top-level-origin.com"), + GURL("https://payment-request-origin.com"), "payment-request-id", + std::move(description), communication); + } + + AndroidPaymentAppTest() + : support_(AndroidAppCommunicationTestSupport::Create()) {} + + ~AndroidPaymentAppTest() override = default; + + AndroidPaymentAppTest(const AndroidPaymentAppTest& other) = delete; + AndroidPaymentAppTest& operator=(const AndroidPaymentAppTest& other) = delete; + + // PaymentApp::Delegate implementation. + void OnInstrumentDetailsReady(const std::string& method_name, + const std::string& stringified_details, + const PayerData& payer_data) override { + method_name_ = method_name; + stringified_details_ = stringified_details; + } + + // PaymentApp::Delegate implementation. + void OnInstrumentDetailsError(const std::string& error_message) override { + error_message_ = error_message; + } + + std::unique_ptr<AndroidAppCommunicationTestSupport> support_; + std::unique_ptr<AndroidAppCommunicationTestSupport::ScopedInitialization> + scoped_initialization_; + base::WeakPtr<AndroidAppCommunication> communication_; + std::string method_name_; + std::string stringified_details_; + std::string error_message_; +}; + +TEST_F(AndroidPaymentAppTest, BrowserShutdown) { + // Explicitly do not initialize AndroidAppCommunication. This can happen + // during browser shutdown. + scoped_initialization_ = support_->CreateScopedInitialization(); + + support_->ExpectNoPaymentAppInvoke(); + + auto app = CreateAndroidPaymentApp(communication_); + app->InvokePaymentApp(/*delegate=*/this); + + EXPECT_TRUE(error_message_.empty()); + EXPECT_TRUE(method_name_.empty()); + EXPECT_TRUE(stringified_details_.empty()); +} + +TEST_F(AndroidPaymentAppTest, UnableToCommunicateToAndroidApps) { + communication_ = + AndroidAppCommunication::GetForBrowserContext(support_->context()); + communication_->SetForTesting(); + // Explicitly do not create ScopedInitialization. + + support_->ExpectNoPaymentAppInvoke(); + + auto app = CreateAndroidPaymentApp(communication_); + app->InvokePaymentApp(/*delegate=*/this); + + EXPECT_EQ("Unable to invoke Android apps.", error_message_); + EXPECT_TRUE(method_name_.empty()); + EXPECT_TRUE(stringified_details_.empty()); +} + +TEST_F(AndroidPaymentAppTest, OnInstrumentDetailsError) { + communication_ = + AndroidAppCommunication::GetForBrowserContext(support_->context()); + communication_->SetForTesting(); + scoped_initialization_ = support_->CreateScopedInitialization(); + + support_->ExpectInvokePaymentAppAndRespond( + /*is_activity_result_ok=*/false, + /*payment_method_identifier=*/methods::kGooglePlayBilling, + /*stringified_details=*/"{}"); + + auto app = CreateAndroidPaymentApp(communication_); + app->InvokePaymentApp(/*delegate=*/this); + + if (support_->AreAndroidAppsSupportedOnThisPlatform()) { + EXPECT_EQ("User closed the payment app.", error_message_); + } else { + EXPECT_EQ("Unable to invoke Android apps.", error_message_); + } + + EXPECT_TRUE(method_name_.empty()); + EXPECT_TRUE(stringified_details_.empty()); +} + +TEST_F(AndroidPaymentAppTest, OnInstrumentDetailsReady) { + communication_ = + AndroidAppCommunication::GetForBrowserContext(support_->context()); + communication_->SetForTesting(); + scoped_initialization_ = support_->CreateScopedInitialization(); + + support_->ExpectInvokePaymentAppAndRespond( + /*is_activity_result_ok=*/true, + /*payment_method_identifier=*/methods::kGooglePlayBilling, + /*stringified_details=*/"{\"status\": \"ok\"}"); + + auto app = CreateAndroidPaymentApp(communication_); + app->InvokePaymentApp(/*delegate=*/this); + + if (support_->AreAndroidAppsSupportedOnThisPlatform()) { + EXPECT_TRUE(error_message_.empty()); + EXPECT_EQ(methods::kGooglePlayBilling, method_name_); + EXPECT_EQ("{\"status\": \"ok\"}", stringified_details_); + } else { + EXPECT_EQ("Unable to invoke Android apps.", error_message_); + EXPECT_TRUE(method_name_.empty()); + EXPECT_TRUE(stringified_details_.empty()); + } +} + +} // namespace +} // namespace payments diff --git a/chromium/components/payments/content/autofill_payment_app_factory.cc b/chromium/components/payments/content/autofill_payment_app_factory.cc index b6eadc5e02c..dafa63818f8 100644 --- a/chromium/components/payments/content/autofill_payment_app_factory.cc +++ b/chromium/components/payments/content/autofill_payment_app_factory.cc @@ -21,6 +21,9 @@ std::unique_ptr<PaymentApp> AutofillPaymentAppFactory::ConvertCardToPaymentAppIfSupportedNetwork( const autofill::CreditCard& card, base::WeakPtr<Delegate> delegate) { + DCHECK(delegate); + DCHECK(delegate->GetSpec()); + std::string basic_card_network = autofill::data_util::GetPaymentRequestData(card.network()) .basic_card_issuer_network; @@ -46,6 +49,11 @@ AutofillPaymentAppFactory::AutofillPaymentAppFactory() AutofillPaymentAppFactory::~AutofillPaymentAppFactory() = default; void AutofillPaymentAppFactory::Create(base::WeakPtr<Delegate> delegate) { + DCHECK(delegate); + + if (!delegate->GetSpec()) + return; + // No need to create autofill payment apps if native app creation is skipped // because autofill payment apps are created completely by the Java factory. if (delegate->SkipCreatingNativePaymentApps()) { diff --git a/chromium/components/payments/content/autofill_payment_app_unittest.cc b/chromium/components/payments/content/autofill_payment_app_unittest.cc index acaf906122c..a465d7d3636 100644 --- a/chromium/components/payments/content/autofill_payment_app_unittest.cc +++ b/chromium/components/payments/content/autofill_payment_app_unittest.cc @@ -21,7 +21,6 @@ #include "components/autofill/core/browser/test_personal_data_manager.h" #include "components/payments/core/test_payment_request_delegate.h" #include "components/strings/grit/components_strings.h" -#include "net/url_request/url_request_test_util.h" #include "services/network/public/cpp/shared_url_loader_factory.h" #include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h" #include "services/network/test/test_url_loader_factory.h" diff --git a/chromium/components/payments/content/content_payment_request_delegate.cc b/chromium/components/payments/content/content_payment_request_delegate.cc new file mode 100644 index 00000000000..65b8d7c99c1 --- /dev/null +++ b/chromium/components/payments/content/content_payment_request_delegate.cc @@ -0,0 +1,18 @@ +// Copyright 2020 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. + +#include "components/payments/content/content_payment_request_delegate.h" + +namespace payments { + +ContentPaymentRequestDelegate::~ContentPaymentRequestDelegate() = default; + +base::WeakPtr<ContentPaymentRequestDelegate> +ContentPaymentRequestDelegate::GetContentWeakPtr() { + return weak_ptr_factory_.GetWeakPtr(); +} + +ContentPaymentRequestDelegate::ContentPaymentRequestDelegate() = default; + +} // namespace payments diff --git a/chromium/components/payments/content/content_payment_request_delegate.h b/chromium/components/payments/content/content_payment_request_delegate.h index c6db3f4215b..60bef446b9f 100644 --- a/chromium/components/payments/content/content_payment_request_delegate.h +++ b/chromium/components/payments/content/content_payment_request_delegate.h @@ -5,23 +5,35 @@ #ifndef COMPONENTS_PAYMENTS_CONTENT_CONTENT_PAYMENT_REQUEST_DELEGATE_H_ #define COMPONENTS_PAYMENTS_CONTENT_CONTENT_PAYMENT_REQUEST_DELEGATE_H_ +#include <memory> #include <string> +#include "base/memory/weak_ptr.h" #include "components/payments/content/payment_request_display_manager.h" #include "components/payments/core/payment_request_delegate.h" template <class T> class scoped_refptr; +namespace autofill { +class InternalAuthenticator; +} // namespace autofill + namespace payments { class PaymentManifestWebDataService; +class PaymentRequestDialog; class PaymentRequestDisplayManager; // The delegate for PaymentRequest that can use content. class ContentPaymentRequestDelegate : public PaymentRequestDelegate { public: - ~ContentPaymentRequestDelegate() override {} + ~ContentPaymentRequestDelegate() override; + + // Creates and returns an instance of the InternalAuthenticator interface for + // communication with WebAuthn. + virtual std::unique_ptr<autofill::InternalAuthenticator> + CreateInternalAuthenticator() const = 0; // Returns the web data service for caching payment method manifests. virtual scoped_refptr<PaymentManifestWebDataService> @@ -59,6 +71,17 @@ class ContentPaymentRequestDelegate : public PaymentRequestDelegate { // Returns the Android package name of the Trusted Web Activity that invoked // this browser, if any. Otherwise, an empty string. virtual std::string GetTwaPackageName() const = 0; + + virtual PaymentRequestDialog* GetDialogForTesting() = 0; + + // Returns a weak pointer to this delegate. + base::WeakPtr<ContentPaymentRequestDelegate> GetContentWeakPtr(); + + protected: + ContentPaymentRequestDelegate(); + + private: + base::WeakPtrFactory<ContentPaymentRequestDelegate> weak_ptr_factory_{this}; }; } // namespace payments diff --git a/chromium/components/payments/content/icon/BUILD.gn b/chromium/components/payments/content/icon/BUILD.gn index b031c0b8bd1..a5d3326f4c3 100644 --- a/chromium/components/payments/content/icon/BUILD.gn +++ b/chromium/components/payments/content/icon/BUILD.gn @@ -2,9 +2,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -import("//build/config/jumbo.gni") - -jumbo_static_library("icon") { +static_library("icon") { sources = [ "icon_size.cc", "icon_size.h", diff --git a/chromium/components/payments/content/manifest_verifier.cc b/chromium/components/payments/content/manifest_verifier.cc index 9529b48a2e2..5800ab987e9 100644 --- a/chromium/components/payments/content/manifest_verifier.cc +++ b/chromium/components/payments/content/manifest_verifier.cc @@ -32,7 +32,7 @@ namespace { void EnableMethodManifestUrlForSupportedApps( const GURL& method_manifest_url, const std::vector<std::string>& supported_origin_strings, - content::PaymentAppProvider::PaymentApps* apps, + content::InstalledPaymentAppsFinder::PaymentApps* apps, std::vector<int64_t> app_ids, std::map<GURL, std::set<GURL>>* prohibited_payment_methods) { for (auto app_id : app_ids) { @@ -68,9 +68,10 @@ ManifestVerifier::~ManifestVerifier() { } } -void ManifestVerifier::Verify(content::PaymentAppProvider::PaymentApps apps, - VerifyCallback finished_verification, - base::OnceClosure finished_using_resources) { +void ManifestVerifier::Verify( + content::InstalledPaymentAppsFinder::PaymentApps apps, + VerifyCallback finished_verification, + base::OnceClosure finished_using_resources) { DCHECK(apps_.empty()); DCHECK(finished_verification_callback_.is_null()); DCHECK(finished_using_resources_callback_.is_null()); diff --git a/chromium/components/payments/content/manifest_verifier.h b/chromium/components/payments/content/manifest_verifier.h index 6e1207eaca8..0974571665a 100644 --- a/chromium/components/payments/content/manifest_verifier.h +++ b/chromium/components/payments/content/manifest_verifier.h @@ -18,6 +18,7 @@ #include "components/payments/content/developer_console_logger.h" #include "components/webdata/common/web_data_service_base.h" #include "components/webdata/common/web_data_service_consumer.h" +#include "content/public/browser/installed_payment_apps_finder.h" #include "content/public/browser/payment_app_provider.h" #include "content/public/browser/web_contents_observer.h" #include "url/origin.h" @@ -59,7 +60,7 @@ class ManifestVerifier final : public WebDataServiceConsumer { // remaining. If a payment handler does not have any valid payment method // names, then it's not included in the returned set of handlers. using VerifyCallback = - base::OnceCallback<void(content::PaymentAppProvider::PaymentApps, + base::OnceCallback<void(content::InstalledPaymentAppsFinder::PaymentApps, const std::string& error_message)>; // Creates the verifier and starts up the parser utility process. @@ -81,7 +82,7 @@ class ManifestVerifier final : public WebDataServiceConsumer { // Initiates the verification. This object should be deleted after // |finished_using_resources| is invoked. - void Verify(content::PaymentAppProvider::PaymentApps apps, + void Verify(content::InstalledPaymentAppsFinder::PaymentApps apps, VerifyCallback finished_verification, base::OnceClosure finished_using_resources); @@ -121,7 +122,7 @@ class ManifestVerifier final : public WebDataServiceConsumer { PaymentManifestWebDataService* cache_; // The list of payment apps being verified. - content::PaymentAppProvider::PaymentApps apps_; + content::InstalledPaymentAppsFinder::PaymentApps apps_; // A mapping from the payment app scope to the set of the URL-based payment // methods that it claims to support, but is not allowed due to the payment diff --git a/chromium/components/payments/content/payment_app.cc b/chromium/components/payments/content/payment_app.cc index 594eba178bb..69f2c695efc 100644 --- a/chromium/components/payments/content/payment_app.cc +++ b/chromium/components/payments/content/payment_app.cc @@ -99,6 +99,10 @@ void PaymentApp::AbortPaymentApp( std::move(abort_callback).Run(/*aborted=*/false); } +bool PaymentApp::IsPreferred() const { + return false; +} + // static void PaymentApp::SortApps(std::vector<std::unique_ptr<PaymentApp>>* apps) { DCHECK(apps); diff --git a/chromium/components/payments/content/payment_app.h b/chromium/components/payments/content/payment_app.h index 570038c36b9..5bb070fba9d 100644 --- a/chromium/components/payments/content/payment_app.h +++ b/chromium/components/payments/content/payment_app.h @@ -185,6 +185,10 @@ class PaymentApp { // payment app is currently invoked. virtual void AbortPaymentApp(base::OnceCallback<void(bool)> abort_callback); + // Whether this app should be chosen over other available payment apps. For + // example, when the Play Billing payment app is available in a TWA. + virtual bool IsPreferred() const; + protected: PaymentApp(int icon_resource_id, Type type); diff --git a/chromium/components/payments/content/payment_app_factory.h b/chromium/components/payments/content/payment_app_factory.h index be8a7731ffd..0708bd1c203 100644 --- a/chromium/components/payments/content/payment_app_factory.h +++ b/chromium/components/payments/content/payment_app_factory.h @@ -20,6 +20,7 @@ class GURL; namespace autofill { class AutofillProfile; +class InternalAuthenticator; } // namespace autofill namespace content { @@ -51,12 +52,21 @@ class PaymentAppFactory { virtual content::RenderFrameHost* GetInitiatorRenderFrameHost() const = 0; virtual const std::vector<mojom::PaymentMethodDataPtr>& GetMethodData() const = 0; + virtual std::unique_ptr<autofill::InternalAuthenticator> + CreateInternalAuthenticator() const = 0; virtual scoped_refptr<PaymentManifestWebDataService> GetPaymentManifestWebDataService() const = 0; virtual bool MayCrawlForInstallablePaymentApps() = 0; virtual bool IsOffTheRecord() const = 0; + + // Returns the merchant provided information, or null if the payment is + // being aborted. virtual PaymentRequestSpec* GetSpec() const = 0; + // Returns the Android package name of the Trusted Web Activity that invoked + // this browser, if any. Otherwise, an empty string. + virtual std::string GetTwaPackageName() const = 0; + // Tells the UI to show the processing spinner. Only desktop UI needs this // notification. virtual void ShowProcessingSpinner() = 0; @@ -84,6 +94,14 @@ class PaymentAppFactory { // Called when all apps of this factory have been created. virtual void OnDoneCreatingPaymentApps() = 0; + + // Make both canMakePayment() and hasEnrolledInstrument() return true, + // regardless of presence of payment apps. This is used by secure payment + // confirmation method, which returns true for canMakePayment() and + // hasEnrolledInstrument() regardless of presence of credentials in user + // profile or the authenticator device, as long as a user-verifying platform + // authenticator device is available. + virtual void SetCanMakePaymentEvenWithoutApps() = 0; }; explicit PaymentAppFactory(PaymentApp::Type type); diff --git a/chromium/components/payments/content/payment_app_service.cc b/chromium/components/payments/content/payment_app_service.cc index 1dc68fb0b2c..45fb49ea67c 100644 --- a/chromium/components/payments/content/payment_app_service.cc +++ b/chromium/components/payments/content/payment_app_service.cc @@ -5,18 +5,40 @@ #include "components/payments/content/payment_app_service.h" #include "base/feature_list.h" +#include "components/payments/content/android_app_communication.h" +#include "components/payments/content/android_payment_app_factory.h" #include "components/payments/content/autofill_payment_app_factory.h" #include "components/payments/content/payment_app.h" +#include "components/payments/content/secure_payment_confirmation_app_factory.h" #include "components/payments/content/service_worker_payment_app_factory.h" +#include "components/payments/core/features.h" +#include "components/payments/core/payments_experimental_features.h" #include "content/public/common/content_features.h" namespace payments { -PaymentAppService::PaymentAppService() { +PaymentAppService::PaymentAppService(content::BrowserContext* context) { factories_.emplace_back(std::make_unique<AutofillPaymentAppFactory>()); - if (base::FeatureList::IsEnabled(::features::kServiceWorkerPaymentApps)) - factories_.emplace_back(std::make_unique<ServiceWorkerPaymentAppFactory>()); + if (base::FeatureList::IsEnabled(::features::kServiceWorkerPaymentApps)) { + factories_.push_back(std::make_unique<ServiceWorkerPaymentAppFactory>()); + } + + // TODO(https://crbug.com/1022512): Review the feature flag name when + // AndroidPaymentAppFactory works on Android OS with generic 3rd party payment + // apps. (Currently it works only on Chrome OS with app store billing payment + // methods.) + if (PaymentsExperimentalFeatures::IsEnabled(features::kAppStoreBilling)) { + factories_.push_back(std::make_unique<AndroidPaymentAppFactory>( + AndroidAppCommunication::GetForBrowserContext(context))); + } + + // SecurePaymentConfirmation is enabled if both the feature flag and the Blink + // runtime feature "SecurePaymentConfirmation" are enabled. + if (base::FeatureList::IsEnabled(features::kSecurePaymentConfirmation)) { + factories_.push_back( + std::make_unique<SecurePaymentConfirmationAppFactory>()); + } } PaymentAppService::~PaymentAppService() = default; diff --git a/chromium/components/payments/content/payment_app_service.h b/chromium/components/payments/content/payment_app_service.h index a26ce3ef423..40f1c18fcdd 100644 --- a/chromium/components/payments/content/payment_app_service.h +++ b/chromium/components/payments/content/payment_app_service.h @@ -14,12 +14,17 @@ #include "components/keyed_service/core/keyed_service.h" #include "components/payments/content/payment_app_factory.h" +namespace content { +class BrowserContext; +} // namespace content + namespace payments { // Retrieves payment apps of all types. class PaymentAppService : public KeyedService { public: - PaymentAppService(); + // The |context| pointer is not being saved. + explicit PaymentAppService(content::BrowserContext* context); ~PaymentAppService() override; // Returns the number of payment app factories, which is the number of times diff --git a/chromium/components/payments/content/payment_app_service_factory.cc b/chromium/components/payments/content/payment_app_service_factory.cc index 3fb5f947b37..686ac505998 100644 --- a/chromium/components/payments/content/payment_app_service_factory.cc +++ b/chromium/components/payments/content/payment_app_service_factory.cc @@ -43,7 +43,7 @@ PaymentAppServiceFactory::~PaymentAppServiceFactory() = default; KeyedService* PaymentAppServiceFactory::BuildServiceInstanceFor( content::BrowserContext* context) const { - return new PaymentAppService(); + return new PaymentAppService(context); } content::BrowserContext* PaymentAppServiceFactory::GetBrowserContextToUse( diff --git a/chromium/components/payments/content/payment_credential.cc b/chromium/components/payments/content/payment_credential.cc new file mode 100644 index 00000000000..390a402750e --- /dev/null +++ b/chromium/components/payments/content/payment_credential.cc @@ -0,0 +1,129 @@ +// Copyright 2020 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. + +#include "components/payments/content/payment_credential.h" + +#include <memory> + +#include "base/memory/ref_counted_memory.h" +#include "base/strings/utf_string_conversions.h" +#include "components/payments/content/payment_manifest_web_data_service.h" +#include "components/payments/core/secure_payment_confirmation_instrument.h" +#include "components/payments/core/url_util.h" +#include "content/public/browser/web_contents.h" +#include "ui/gfx/image/image.h" + +namespace payments { + +PaymentCredential::PaymentCredential( + content::WebContents* web_contents, + content::GlobalFrameRoutingId initiator_frame_routing_id, + scoped_refptr<PaymentManifestWebDataService> web_data_sevice, + mojo::PendingReceiver<mojom::PaymentCredential> receiver) + : WebContentsObserver(web_contents), + initiator_frame_routing_id_(initiator_frame_routing_id), + web_data_service_(web_data_sevice) { + DCHECK(web_contents); + receiver_.Bind(std::move(receiver)); +} + +PaymentCredential::~PaymentCredential() = default; + +void PaymentCredential::StorePaymentCredential( + payments::mojom::PaymentCredentialInstrumentPtr instrument, + const std::vector<uint8_t>& credential_id, + const std::string& rp_id, + StorePaymentCredentialCallback callback) { + if (!web_data_service_) { + std::move(callback).Run( + mojom::PaymentCredentialCreationStatus::FAILED_TO_STORE_INSTRUMENT); + return; + } + + if (!web_contents() || + !UrlUtil::IsOriginAllowedToUseWebPaymentApis(instrument->icon)) { + std::move(callback).Run( + mojom::PaymentCredentialCreationStatus::FAILED_TO_DOWNLOAD_ICON); + return; + } + + // If the initiator frame doesn't exist any more, e.g. the frame has + // navigated away, don't download the icon. + content::RenderFrameHost* render_frame_host = + content::RenderFrameHost::FromID(initiator_frame_routing_id_); + if (!render_frame_host || !render_frame_host->IsCurrent()) { + std::move(callback).Run( + mojom::PaymentCredentialCreationStatus::FAILED_TO_DOWNLOAD_ICON); + return; + } + + int request_id = web_contents()->DownloadImageInFrame( + initiator_frame_routing_id_, + instrument->icon, // source URL + true, // is_favicon + 0, // no preferred size + 0, // no max size + false, // normal cache policy (a.k.a. do not bypass cache) + base::BindOnce(&PaymentCredential::DidDownloadFavicon, + weak_ptr_factory_.GetWeakPtr(), std::move(instrument), + credential_id, rp_id, std::move(callback))); + pending_icon_download_request_ids_.insert(request_id); +} + +void PaymentCredential::DidDownloadFavicon( + payments::mojom::PaymentCredentialInstrumentPtr instrument, + const std::vector<uint8_t>& credential_id, + const std::string& rp_id, + StorePaymentCredentialCallback callback, + int request_id, + int unused_http_status_code, + const GURL& image_url, + const std::vector<SkBitmap>& bitmaps, + const std::vector<gfx::Size>& unused_sizes) { + auto iterator = pending_icon_download_request_ids_.find(request_id); + DCHECK(iterator != pending_icon_download_request_ids_.end()); + pending_icon_download_request_ids_.erase(iterator); + + if (bitmaps.empty()) { + std::move(callback).Run( + mojom::PaymentCredentialCreationStatus::FAILED_TO_DOWNLOAD_ICON); + return; + } + + // TODO(https://crbug.com/1110320): Get the best icon using |preferred size| + // rather than the first one if multiple downloaded. + gfx::Image downloaded_image = gfx::Image::CreateFrom1xBitmap(bitmaps[0]); + scoped_refptr<base::RefCountedMemory> raw_data = + downloaded_image.As1xPNGBytes(); + auto encoded_icon = + std::vector<uint8_t>(raw_data->front_as<uint8_t>(), + raw_data->front_as<uint8_t>() + raw_data->size()); + + WebDataServiceBase::Handle handle = + web_data_service_->AddSecurePaymentConfirmationInstrument( + std::make_unique<SecurePaymentConfirmationInstrument>( + credential_id, rp_id, base::UTF8ToUTF16(instrument->display_name), + std::move(encoded_icon)), + /*consumer=*/this); + callbacks_[handle] = std::move(callback); +} + +void PaymentCredential::OnWebDataServiceRequestDone( + WebDataServiceBase::Handle h, + std::unique_ptr<WDTypedResult> result) { + auto iterator = callbacks_.find(h); + if (iterator == callbacks_.end()) + return; + + auto callback = std::move(iterator->second); + DCHECK(callback); + callbacks_.erase(iterator); + + std::move(callback).Run( + static_cast<WDResult<bool>*>(result.get())->GetValue() + ? mojom::PaymentCredentialCreationStatus::SUCCESS + : mojom::PaymentCredentialCreationStatus::FAILED_TO_STORE_INSTRUMENT); +} + +} // namespace payments diff --git a/chromium/components/payments/content/payment_credential.h b/chromium/components/payments/content/payment_credential.h new file mode 100644 index 00000000000..9b646c9e76e --- /dev/null +++ b/chromium/components/payments/content/payment_credential.h @@ -0,0 +1,84 @@ +// Copyright 2020 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. + +#ifndef COMPONENTS_PAYMENTS_CONTENT_PAYMENT_CREDENTIAL_H_ +#define COMPONENTS_PAYMENTS_CONTENT_PAYMENT_CREDENTIAL_H_ + +#include <map> +#include <set> +#include <string> +#include <vector> + +#include "base/memory/scoped_refptr.h" +#include "base/memory/weak_ptr.h" +#include "components/webdata/common/web_data_service_base.h" +#include "components/webdata/common/web_data_service_consumer.h" +#include "content/public/browser/global_routing_id.h" +#include "content/public/browser/web_contents_observer.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "mojo/public/cpp/bindings/receiver.h" +#include "third_party/blink/public/mojom/payments/payment_credential.mojom.h" + +namespace content { +class WebContents; +} // namespace content + +namespace payments { + +class PaymentManifestWebDataService; + +// Implementation of the mojom::PaymentCredential interface for storing +// PaymentCredential instruments and their associated WebAuthn credential IDs. +// These can be retrieved later to authenticate during a PaymentRequest +// that uses Secure Payment Confirmation. +class PaymentCredential : public mojom::PaymentCredential, + public WebDataServiceConsumer, + public content::WebContentsObserver { + public: + PaymentCredential( + content::WebContents* web_contents, + content::GlobalFrameRoutingId initiator_frame_routing_id, + scoped_refptr<PaymentManifestWebDataService> web_data_sevice, + mojo::PendingReceiver<mojom::PaymentCredential> receiver); + ~PaymentCredential() override; + + PaymentCredential(const PaymentCredential&) = delete; + PaymentCredential& operator=(const PaymentCredential&) = delete; + + // mojom::PaymentCredential: + void StorePaymentCredential( + payments::mojom::PaymentCredentialInstrumentPtr instrument, + const std::vector<uint8_t>& credential_id, + const std::string& rp_id, + StorePaymentCredentialCallback callback) override; + + private: + // WebDataServiceConsumer: + void OnWebDataServiceRequestDone( + WebDataServiceBase::Handle h, + std::unique_ptr<WDTypedResult> result) override; + + void DidDownloadFavicon( + payments::mojom::PaymentCredentialInstrumentPtr instrument, + const std::vector<uint8_t>& credential_id, + const std::string& rp_id, + StorePaymentCredentialCallback callback, + int request_id, + int unused_http_status_code, + const GURL& image_url, + const std::vector<SkBitmap>& bitmaps, + const std::vector<gfx::Size>& unused_sizes); + + const content::GlobalFrameRoutingId initiator_frame_routing_id_; + scoped_refptr<PaymentManifestWebDataService> web_data_service_; + std::map<WebDataServiceBase::Handle, StorePaymentCredentialCallback> + callbacks_; + mojo::Receiver<mojom::PaymentCredential> receiver_{this}; + std::set<int> pending_icon_download_request_ids_; + base::WeakPtrFactory<PaymentCredential> weak_ptr_factory_{this}; +}; + +} // namespace payments + +#endif // COMPONENTS_PAYMENTS_CONTENT_PAYMENT_CREDENTIAL_H_ diff --git a/chromium/components/payments/content/payment_handler_host.cc b/chromium/components/payments/content/payment_handler_host.cc index 4779d9b5b11..0ef89baaf68 100644 --- a/chromium/components/payments/content/payment_handler_host.cc +++ b/chromium/components/payments/content/payment_handler_host.cc @@ -23,11 +23,15 @@ namespace payments { namespace { content::DevToolsBackgroundServicesContext* GetDevTools( - content::BrowserContext* browser_context, + content::WebContents* web_contents, const url::Origin& sw_origin) { DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + if (!web_contents) + return nullptr; + auto* storage_partition = content::BrowserContext::GetStoragePartitionForSite( - browser_context, sw_origin.GetURL(), /*can_create=*/true); + web_contents->GetBrowserContext(), sw_origin.GetURL(), + /*can_create=*/true); if (!storage_partition) return nullptr; @@ -52,8 +56,8 @@ void RunCallbackWithError(const std::string& error, PaymentHandlerHost::PaymentHandlerHost(content::WebContents* web_contents, Delegate* delegate) - : web_contents_(web_contents), delegate_(delegate) { - DCHECK(web_contents_); + : WebContentsObserver(web_contents), delegate_(delegate) { + DCHECK(web_contents); DCHECK(delegate_); } @@ -76,8 +80,7 @@ void PaymentHandlerHost::UpdateWith( if (!change_payment_request_details_callback_) return; - auto* dev_tools = - GetDevTools(web_contents_->GetBrowserContext(), sw_origin_for_logs_); + auto* dev_tools = GetDevTools(web_contents(), sw_origin_for_logs_); if (dev_tools) { std::map<std::string, std::string> data = {{"Error", response->error}}; @@ -195,8 +198,7 @@ void PaymentHandlerHost::ChangePaymentMethod( return; } - auto* dev_tools = - GetDevTools(web_contents_->GetBrowserContext(), sw_origin_for_logs_); + auto* dev_tools = GetDevTools(web_contents(), sw_origin_for_logs_); if (dev_tools) { dev_tools->LogBackgroundServiceEvent( registration_id_for_logs_, sw_origin_for_logs_, @@ -226,8 +228,7 @@ void PaymentHandlerHost::ChangeShippingOption( return; } - auto* dev_tools = - GetDevTools(web_contents_->GetBrowserContext(), sw_origin_for_logs_); + auto* dev_tools = GetDevTools(web_contents(), sw_origin_for_logs_); if (dev_tools) { dev_tools->LogBackgroundServiceEvent( registration_id_for_logs_, sw_origin_for_logs_, @@ -256,8 +257,7 @@ void PaymentHandlerHost::ChangeShippingAddress( return; } - auto* dev_tools = - GetDevTools(web_contents_->GetBrowserContext(), sw_origin_for_logs_); + auto* dev_tools = GetDevTools(web_contents(), sw_origin_for_logs_); if (dev_tools) { std::map<std::string, std::string> shipping_address_map; shipping_address_map.emplace("Country", shipping_address->country); diff --git a/chromium/components/payments/content/payment_handler_host.h b/chromium/components/payments/content/payment_handler_host.h index c3814c48489..435f33e65c2 100644 --- a/chromium/components/payments/content/payment_handler_host.h +++ b/chromium/components/payments/content/payment_handler_host.h @@ -11,6 +11,7 @@ #include "base/callback_forward.h" #include "base/macros.h" #include "base/memory/weak_ptr.h" +#include "content/public/browser/web_contents_observer.h" #include "mojo/public/cpp/bindings/pending_remote.h" #include "mojo/public/cpp/bindings/receiver.h" #include "third_party/blink/public/mojom/payments/payment_handler_host.mojom.h" @@ -27,7 +28,8 @@ using ChangePaymentRequestDetailsCallback = // Handles the communication from the payment handler renderer process to the // merchant renderer process. -class PaymentHandlerHost : public mojom::PaymentHandlerHost { +class PaymentHandlerHost : public mojom::PaymentHandlerHost, + public content::WebContentsObserver { public: // The interface to be implemented by the object that can communicate to the // merchant's renderer process. @@ -127,9 +129,6 @@ class PaymentHandlerHost : public mojom::PaymentHandlerHost { // browser process. mojo::Receiver<mojom::PaymentHandlerHost> receiver_{this}; - // The merchant page that invoked the Payment Request API. - content::WebContents* web_contents_; - // Not null and outlives this object. Either owns this object or is owned by // the owner of this object. Delegate* delegate_; diff --git a/chromium/components/payments/content/payment_manifest_web_data_service.cc b/chromium/components/payments/content/payment_manifest_web_data_service.cc index 8294b9eaac2..7842bf448f4 100644 --- a/chromium/components/payments/content/payment_manifest_web_data_service.cc +++ b/chromium/components/payments/content/payment_manifest_web_data_service.cc @@ -4,10 +4,13 @@ #include "components/payments/content/payment_manifest_web_data_service.h" +#include <utility> + #include "base/bind.h" #include "base/location.h" #include "components/payments/content/payment_method_manifest_table.h" #include "components/payments/content/web_app_manifest_section_table.h" +#include "components/payments/core/secure_payment_confirmation_instrument.h" #include "components/webdata/common/web_data_results.h" #include "components/webdata/common/web_database_service.h" @@ -108,6 +111,51 @@ PaymentManifestWebDataService::GetPaymentMethodManifestImpl( payment_method)); } +WebDataServiceBase::Handle +PaymentManifestWebDataService::AddSecurePaymentConfirmationInstrument( + std::unique_ptr<SecurePaymentConfirmationInstrument> instrument, + WebDataServiceConsumer* consumer) { + DCHECK(instrument); + return wdbs_->ScheduleDBTaskWithResult( + FROM_HERE, + base::BindOnce(&PaymentManifestWebDataService:: + AddSecurePaymentConfirmationInstrumentImpl, + this, std::move(instrument)), + consumer); +} + +std::unique_ptr<WDTypedResult> +PaymentManifestWebDataService::AddSecurePaymentConfirmationInstrumentImpl( + std::unique_ptr<SecurePaymentConfirmationInstrument> instrument, + WebDatabase* db) { + return std::make_unique<WDResult<bool>>( + BOOL_RESULT, PaymentMethodManifestTable::FromWebDatabase(db) + ->AddSecurePaymentConfirmationInstrument(*instrument)); +} + +WebDataServiceBase::Handle +PaymentManifestWebDataService::GetSecurePaymentConfirmationInstruments( + std::vector<std::vector<uint8_t>> credential_ids, + WebDataServiceConsumer* consumer) { + return wdbs_->ScheduleDBTaskWithResult( + FROM_HERE, + base::BindOnce(&PaymentManifestWebDataService:: + GetSecurePaymentConfirmationInstrumentsImpl, + this, std::move(credential_ids)), + consumer); +} + +std::unique_ptr<WDTypedResult> +PaymentManifestWebDataService::GetSecurePaymentConfirmationInstrumentsImpl( + std::vector<std::vector<uint8_t>> credential_ids, + WebDatabase* db) { + return std::make_unique<WDResult< + std::vector<std::unique_ptr<SecurePaymentConfirmationInstrument>>>>( + SECURE_PAYMENT_CONFIRMATION, + PaymentMethodManifestTable::FromWebDatabase(db) + ->GetSecurePaymentConfirmationInstruments(std::move(credential_ids))); +} + void PaymentManifestWebDataService::RemoveExpiredData(WebDatabase* db) { PaymentMethodManifestTable::FromWebDatabase(db)->RemoveExpiredData(); WebAppManifestSectionTable::FromWebDatabase(db)->RemoveExpiredData(); diff --git a/chromium/components/payments/content/payment_manifest_web_data_service.h b/chromium/components/payments/content/payment_manifest_web_data_service.h index e81845e9cb3..91ef2a4255c 100644 --- a/chromium/components/payments/content/payment_manifest_web_data_service.h +++ b/chromium/components/payments/content/payment_manifest_web_data_service.h @@ -5,6 +5,8 @@ #ifndef COMPONENTS_PAYMENTS_CONTENT_PAYMENT_MANIFEST_WEB_DATA_SERVICE_H_ #define COMPONENTS_PAYMENTS_CONTENT_PAYMENT_MANIFEST_WEB_DATA_SERVICE_H_ +#include <stdint.h> + #include <memory> #include <vector> @@ -23,6 +25,8 @@ class SingleThreadTaskRunner; namespace payments { +struct SecurePaymentConfirmationInstrument; + // Web data service to read/write data in WebAppManifestSectionTable and // PaymentMethodManifestTable. class PaymentManifestWebDataService : public WebDataServiceBase { @@ -31,23 +35,40 @@ class PaymentManifestWebDataService : public WebDataServiceBase { scoped_refptr<WebDatabaseService> wdbs, scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner); - // Adds the web app |manifest|. + // Adds the web app `manifest`. void AddPaymentWebAppManifest(std::vector<WebAppManifestSection> manifest); - // Adds the |payment_method|'s manifest. + // Adds the `payment_method`'s manifest. void AddPaymentMethodManifest(const std::string& payment_method, std::vector<std::string> app_package_names); - // Gets the |web_app|'s manifest. + // Gets the `web_app`'s manifest and returns it to the `consumer`, which must + // outlive the DB operation, because DB tasks cannot be cancelled. WebDataServiceBase::Handle GetPaymentWebAppManifest( const std::string& web_app, WebDataServiceConsumer* consumer); - // Gets the |payment_method|'s manifest. + // Gets the `payment_method`'s manifest and returns it the `consumer`, which + // must outlive the DB operation, because DB tasks cannot be cancelled. WebDataServiceBase::Handle GetPaymentMethodManifest( const std::string& payment_method, WebDataServiceConsumer* consumer); + // Adds the secure payment confirmation `instrument` and returns a boolean + // status to the `consumer`, which must outlive the DB operation, because DB + // tasks cannot be cancelled. The `instrument` should not be null. + WebDataServiceBase::Handle AddSecurePaymentConfirmationInstrument( + std::unique_ptr<SecurePaymentConfirmationInstrument> instrument, + WebDataServiceConsumer* consumer); + + // Gets the secure payment confirmation instrument information for the given + // `credential_ids` and returns it to the `consumer`, which must outlive the + // DB operation, because DB tasks cannot be cancelled. Please use + // `std::move()` for `credential_ids` parameter to avoid extra copies. + WebDataServiceBase::Handle GetSecurePaymentConfirmationInstruments( + std::vector<std::vector<uint8_t>> credential_ids, + WebDataServiceConsumer* consumer); + private: ~PaymentManifestWebDataService() override; @@ -60,6 +81,9 @@ class PaymentManifestWebDataService : public WebDataServiceBase { const std::string& payment_method, const std::vector<std::string>& app_package_names, WebDatabase* db); + std::unique_ptr<WDTypedResult> AddSecurePaymentConfirmationInstrumentImpl( + std::unique_ptr<SecurePaymentConfirmationInstrument> instrument, + WebDatabase* db); std::unique_ptr<WDTypedResult> GetPaymentWebAppManifestImpl( const std::string& web_app, @@ -67,6 +91,9 @@ class PaymentManifestWebDataService : public WebDataServiceBase { std::unique_ptr<WDTypedResult> GetPaymentMethodManifestImpl( const std::string& payment_method, WebDatabase* db); + std::unique_ptr<WDTypedResult> GetSecurePaymentConfirmationInstrumentsImpl( + std::vector<std::vector<uint8_t>> credential_ids, + WebDatabase* db); DISALLOW_COPY_AND_ASSIGN(PaymentManifestWebDataService); }; diff --git a/chromium/components/payments/content/payment_method_manifest_table.cc b/chromium/components/payments/content/payment_method_manifest_table.cc index cc0cb0f1f9a..03e9ce69fd6 100644 --- a/chromium/components/payments/content/payment_method_manifest_table.cc +++ b/chromium/components/payments/content/payment_method_manifest_table.cc @@ -8,6 +8,7 @@ #include "base/notreached.h" #include "base/time/time.h" +#include "components/payments/core/secure_payment_confirmation_instrument.h" #include "components/webdata/common/web_database.h" #include "sql/statement.h" #include "sql/transaction.h" @@ -27,9 +28,9 @@ WebDatabaseTable::TypeKey GetPaymentMethodManifestKey() { } // namespace -PaymentMethodManifestTable::PaymentMethodManifestTable() {} +PaymentMethodManifestTable::PaymentMethodManifestTable() = default; -PaymentMethodManifestTable::~PaymentMethodManifestTable() {} +PaymentMethodManifestTable::~PaymentMethodManifestTable() = default; PaymentMethodManifestTable* PaymentMethodManifestTable::FromWebDatabase( WebDatabase* db) { @@ -45,7 +46,20 @@ bool PaymentMethodManifestTable::CreateTablesIfNecessary() { if (!db_->Execute("CREATE TABLE IF NOT EXISTS payment_method_manifest ( " "expire_date INTEGER NOT NULL DEFAULT 0, " "method_name VARCHAR, " - "web_app_id VARCHAR) ")) { + "web_app_id VARCHAR)")) { + NOTREACHED(); + return false; + } + + // The `credential_id` column is 20 bytes for UbiKey on Linux, but the size + // can vary for different authenticators. The relatively small sizes make it + // OK to make `credential_id` the primary key. + if (!db_->Execute( + "CREATE TABLE IF NOT EXISTS secure_payment_confirmation_instrument ( " + "credential_id BLOB NOT NULL PRIMARY KEY, " + "relying_party_id VARCHAR NOT NULL, " + "label VARCHAR NOT NULL, " + "icon BLOB NOT NULL)")) { NOTREACHED(); return false; } @@ -66,7 +80,7 @@ bool PaymentMethodManifestTable::MigrateToVersion( void PaymentMethodManifestTable::RemoveExpiredData() { const time_t now_date_in_seconds = base::Time::NowFromSystemTime().ToTimeT(); sql::Statement s(db_->GetUniqueStatement( - "DELETE FROM payment_method_manifest WHERE expire_date < ? ")); + "DELETE FROM payment_method_manifest WHERE expire_date < ?")); s.BindInt64(0, now_date_in_seconds); s.Run(); } @@ -79,7 +93,7 @@ bool PaymentMethodManifestTable::AddManifest( return false; sql::Statement s1(db_->GetUniqueStatement( - "DELETE FROM payment_method_manifest WHERE method_name=? ")); + "DELETE FROM payment_method_manifest WHERE method_name=?")); s1.BindString(0, payment_method); if (!s1.Run()) return false; @@ -87,7 +101,7 @@ bool PaymentMethodManifestTable::AddManifest( sql::Statement s2( db_->GetUniqueStatement("INSERT INTO payment_method_manifest " "(expire_date, method_name, web_app_id) " - "VALUES (?, ?, ?) ")); + "VALUES (?, ?, ?)")); const time_t expire_date_in_seconds = base::Time::NowFromSystemTime().ToTimeT() + PAYMENT_METHOD_MANIFEST_VALID_TIME_IN_SECONDS; @@ -123,4 +137,112 @@ std::vector<std::string> PaymentMethodManifestTable::GetManifest( return web_app_ids; } +bool PaymentMethodManifestTable::AddSecurePaymentConfirmationInstrument( + const SecurePaymentConfirmationInstrument& instrument) { + if (!instrument.IsValid()) + return false; + + sql::Transaction transaction(db_); + if (!transaction.Begin()) + return false; + + { + // Check for credential identifier reuse by a different relying party. + sql::Statement s0( + db_->GetUniqueStatement("SELECT label " + "FROM secure_payment_confirmation_instrument " + "WHERE credential_id=? " + "AND relying_party_id<>?")); + int index = 0; + if (!s0.BindBlob(index++, instrument.credential_id.data(), + instrument.credential_id.size())) + return false; + + if (!s0.BindString(index++, instrument.relying_party_id)) + return false; + + if (s0.Step()) + return false; + } + + { + sql::Statement s1(db_->GetUniqueStatement( + "DELETE FROM secure_payment_confirmation_instrument " + "WHERE credential_id=?")); + if (!s1.BindBlob(0, instrument.credential_id.data(), + instrument.credential_id.size())) + return false; + + if (!s1.Run()) + return false; + } + + { + sql::Statement s2(db_->GetUniqueStatement( + "INSERT INTO secure_payment_confirmation_instrument " + "(credential_id, relying_party_id, label, icon) " + "VALUES (?, ?, ?, ?)")); + int index = 0; + if (!s2.BindBlob(index++, instrument.credential_id.data(), + instrument.credential_id.size())) + return false; + + if (!s2.BindString(index++, instrument.relying_party_id)) + return false; + + if (!s2.BindString16(index++, instrument.label)) + return false; + + if (!s2.BindBlob(index++, instrument.icon.data(), instrument.icon.size())) + return false; + + if (!s2.Run()) + return false; + } + + if (!transaction.Commit()) + return false; + + return true; +} + +std::vector<std::unique_ptr<SecurePaymentConfirmationInstrument>> +PaymentMethodManifestTable::GetSecurePaymentConfirmationInstruments( + std::vector<std::vector<uint8_t>> credential_ids) { + std::vector<std::unique_ptr<SecurePaymentConfirmationInstrument>> instruments; + sql::Statement s( + db_->GetUniqueStatement("SELECT relying_party_id, label, icon " + "FROM secure_payment_confirmation_instrument " + "WHERE credential_id=?")); + // The `credential_id` temporary variable is not `const` because of the + // `std::move()` on line 231. + for (auto& credential_id : credential_ids) { + s.Reset(true); + if (credential_id.empty()) + continue; + + if (!s.BindBlob(0, credential_id.data(), credential_id.size())) + continue; + + if (!s.Step()) + continue; + + auto instrument = std::make_unique<SecurePaymentConfirmationInstrument>(); + instrument->credential_id = std::move(credential_id); + + int index = 0; + instrument->relying_party_id = s.ColumnString(index++); + instrument->label = s.ColumnString16(index++); + if (!s.ColumnBlobAsVector(index++, &instrument->icon)) + continue; + + if (!instrument->IsValid()) + continue; + + instruments.push_back(std::move(instrument)); + } + + return instruments; +} + } // namespace payments diff --git a/chromium/components/payments/content/payment_method_manifest_table.h b/chromium/components/payments/content/payment_method_manifest_table.h index c994345495d..f2eadb73124 100644 --- a/chromium/components/payments/content/payment_method_manifest_table.h +++ b/chromium/components/payments/content/payment_method_manifest_table.h @@ -5,36 +5,51 @@ #ifndef COMPONENTS_PAYMENTS_CONTENT_PAYMENT_METHOD_MANIFEST_TABLE_H_ #define COMPONENTS_PAYMENTS_CONTENT_PAYMENT_METHOD_MANIFEST_TABLE_H_ +#include <memory> #include <string> #include <vector> -#include "base/macros.h" +#include "base/strings/string16.h" #include "components/webdata/common/web_database_table.h" class WebDatabase; namespace payments { -// This class manages payment_method_manifest table in SQLite database. It -// expects the following schema. +struct SecurePaymentConfirmationInstrument; + +// This class manages Web Payment tables in SQLite database. It expects the +// following schema. // -// payment_method_manifest The table stores WebAppManifestSection.id of the +// payment_method_manifest This table stores WebAppManifestSection.id of the // supported web app in this payment method manifest. // Note that a payment method manifest might contain // multiple supported web apps ids. // -// expire_date The expire date in seconds from 1601-01-01 00:00:00 +// expire_date The expire date in seconds from 1601-01-01 00:00:00 // UTC. -// method_name The method name. -// web_app_id The supported web app id. +// method_name The method name. +// web_app_id The supported web app id. // (WebAppManifestSection.id). // +// secure_payment_confirmation_instrument +// This table stores instrument information for secure +// payment confirmation method. +// +// credential_id The WebAuthn credential identifier blob. Primary key. +// relying_party_id The relying party identifier string. +// label The instrument human-readable label string. +// icon The serialized SkBitmap blob. class PaymentMethodManifestTable : public WebDatabaseTable { public: PaymentMethodManifestTable(); ~PaymentMethodManifestTable() override; - // Retrieves the PaymentMethodManifestTable* owned by |db|. + PaymentMethodManifestTable(const PaymentMethodManifestTable& other) = delete; + PaymentMethodManifestTable& operator=( + const PaymentMethodManifestTable& other) = delete; + + // Retrieves the PaymentMethodManifestTable* owned by `db`. static PaymentMethodManifestTable* FromWebDatabase(WebDatabase* db); // WebDatabaseTable: @@ -46,17 +61,39 @@ class PaymentMethodManifestTable : public WebDatabaseTable { // Remove expired data. void RemoveExpiredData(); - // Adds |payment_method|'s manifest. |web_app_ids| contains supported web apps + // Adds `payment_method`'s manifest. `web_app_ids` contains supported web apps // ids. bool AddManifest(const std::string& payment_method, const std::vector<std::string>& web_app_ids); - // Gets manifest for |payment_method|. Return empty vector if no manifest + // Gets manifest for `payment_method`. Return empty vector if no manifest // exists for this method. std::vector<std::string> GetManifest(const std::string& payment_method); - private: - DISALLOW_COPY_AND_ASSIGN(PaymentMethodManifestTable); + // Adds a secure payment confirmation `instrument`. All existing data for the + // instrument's (relying_party_id, credential_id) tuple is erased before the + // new data is added. + // + // Each field in the `instrument` should be non-empty and `relying_party_id` + // field should be a valid domain string. See: + // https://url.spec.whatwg.org/#valid-domain-string + // + // Returns false for invalid data, e.g., credential reuse between relying + // parties, or on failure. + bool AddSecurePaymentConfirmationInstrument( + const SecurePaymentConfirmationInstrument& instrument); + + // Gets the list of secure payment confirmation instruments for the given list + // of `credential_ids`. + // + // Returns an empty vector when no data is found or when a read error occurs. + // Does not return invalid instruments. + // + // Please use `std::move()` for `credential_ids` parameter to avoid extra + // copies. + std::vector<std::unique_ptr<SecurePaymentConfirmationInstrument>> + GetSecurePaymentConfirmationInstruments( + std::vector<std::vector<uint8_t>> credential_ids); }; } // namespace payments diff --git a/chromium/components/payments/content/payment_method_manifest_table_unittest.cc b/chromium/components/payments/content/payment_method_manifest_table_unittest.cc index a3d2c1a1907..199c6c07e48 100644 --- a/chromium/components/payments/content/payment_method_manifest_table_unittest.cc +++ b/chromium/components/payments/content/payment_method_manifest_table_unittest.cc @@ -4,21 +4,77 @@ #include "components/payments/content/payment_method_manifest_table.h" +#include <stdint.h> + #include <memory> +#include <utility> +#include <vector> #include "base/files/file_path.h" #include "base/files/scoped_temp_dir.h" #include "base/stl_util.h" +#include "base/strings/string16.h" +#include "base/strings/utf_string_conversions.h" +#include "components/payments/core/secure_payment_confirmation_instrument.h" #include "components/webdata/common/web_database.h" #include "testing/gtest/include/gtest/gtest.h" namespace payments { namespace { +// Creates an icon for testing. +std::vector<uint8_t> CreateIcon(uint8_t first_byte = 0) { + std::vector<uint8_t> icon; + icon.push_back(first_byte++); + icon.push_back(first_byte++); + icon.push_back(first_byte++); + icon.push_back(first_byte); + return icon; +} + +// Creates one credential identifier for testing. +std::vector<uint8_t> CreateCredentialId(uint8_t first_byte = 0) { + std::vector<uint8_t> credential_id; + credential_id.push_back(first_byte++); + credential_id.push_back(first_byte++); + credential_id.push_back(first_byte++); + credential_id.push_back(first_byte); + return credential_id; +} + +// Creates a list of one credential identifier for testing. +std::vector<std::vector<uint8_t>> CreateCredentialIdList( + uint8_t first_byte = 0) { + std::vector<std::vector<uint8_t>> credential_ids; + credential_ids.push_back(CreateCredentialId(first_byte)); + return credential_ids; +} + +void ExpectOneValidInstrument( + const std::vector<uint8_t>& credential_id, + const std::string& relying_party_id, + const std::string& label, + const std::vector<uint8_t>& icon, + std::vector<std::unique_ptr<SecurePaymentConfirmationInstrument>> + instruments) { + ASSERT_EQ(1U, instruments.size()); + ASSERT_NE(nullptr, instruments.back()); + ASSERT_TRUE(instruments.back()->IsValid()); + EXPECT_EQ(credential_id, instruments.back()->credential_id); + EXPECT_EQ(relying_party_id, instruments.back()->relying_party_id); + EXPECT_EQ(base::ASCIIToUTF16(label), instruments.back()->label); + EXPECT_EQ(icon, instruments.back()->icon); +} + class PaymentMethodManifestTableTest : public testing::Test { public: - PaymentMethodManifestTableTest() {} - ~PaymentMethodManifestTableTest() override {} + PaymentMethodManifestTableTest() = default; + ~PaymentMethodManifestTableTest() override = default; + + PaymentMethodManifestTableTest(const PaymentMethodManifestTableTest& other) = + delete; + PaymentMethodManifestTableTest& operator=( + const PaymentMethodManifestTableTest& other) = delete; protected: void SetUp() override { @@ -31,15 +87,10 @@ class PaymentMethodManifestTableTest : public testing::Test { ASSERT_EQ(sql::INIT_OK, db_->Init(file_)); } - void TearDown() override {} - base::FilePath file_; base::ScopedTempDir temp_dir_; std::unique_ptr<PaymentMethodManifestTable> table_; std::unique_ptr<WebDatabase> db_; - - private: - DISALLOW_COPY_AND_ASSIGN(PaymentMethodManifestTableTest); }; TEST_F(PaymentMethodManifestTableTest, GetNonExistManifest) { @@ -116,5 +167,175 @@ TEST_F(PaymentMethodManifestTableTest, AddAndGetMultipleManifest) { ASSERT_TRUE(base::Contains(alicepay_web_app_ids, web_app_ids[1])); } +TEST_F(PaymentMethodManifestTableTest, GetNonExistingInstrument) { + PaymentMethodManifestTable* table = + PaymentMethodManifestTable::FromWebDatabase(db_.get()); + EXPECT_TRUE( + table->GetSecurePaymentConfirmationInstruments(CreateCredentialIdList()) + .empty()); + + EXPECT_TRUE(table->GetSecurePaymentConfirmationInstruments({}).empty()); + + EXPECT_TRUE( + table->GetSecurePaymentConfirmationInstruments({std::vector<uint8_t>()}) + .empty()); +} + +TEST_F(PaymentMethodManifestTableTest, AddAndGetOneValidInstrument) { + PaymentMethodManifestTable* table = + PaymentMethodManifestTable::FromWebDatabase(db_.get()); + + EXPECT_TRUE(table->AddSecurePaymentConfirmationInstrument( + SecurePaymentConfirmationInstrument( + CreateCredentialId(), "relying-party.example", + base::ASCIIToUTF16("Instrument label"), CreateIcon()))); + + auto instruments = + table->GetSecurePaymentConfirmationInstruments(CreateCredentialIdList()); + + ExpectOneValidInstrument({0, 1, 2, 3}, "relying-party.example", + "Instrument label", {0, 1, 2, 3}, + std::move(instruments)); + + EXPECT_TRUE(table->GetSecurePaymentConfirmationInstruments({}).empty()); + EXPECT_TRUE( + table->GetSecurePaymentConfirmationInstruments({std::vector<uint8_t>()}) + .empty()); +} + +TEST_F(PaymentMethodManifestTableTest, AddingInvalidInstrumentReturnsFalse) { + PaymentMethodManifestTable* table = + PaymentMethodManifestTable::FromWebDatabase(db_.get()); + + // An empty credential. + EXPECT_FALSE(table->AddSecurePaymentConfirmationInstrument( + SecurePaymentConfirmationInstrument( + /*credential_id=*/{}, "relying-party.example", + base::ASCIIToUTF16("Instrument label"), CreateIcon()))); + + // Empty relying party identifier. + EXPECT_FALSE(table->AddSecurePaymentConfirmationInstrument( + SecurePaymentConfirmationInstrument( + CreateCredentialId(), /*relying_party_id=*/"", + base::ASCIIToUTF16("Instrument label"), CreateIcon()))); + + // Empty label. + EXPECT_FALSE(table->AddSecurePaymentConfirmationInstrument( + SecurePaymentConfirmationInstrument( + CreateCredentialId(), "relying-party.example", + /*label=*/base::string16(), CreateIcon()))); + + // Empty icon. + EXPECT_FALSE(table->AddSecurePaymentConfirmationInstrument( + SecurePaymentConfirmationInstrument( + CreateCredentialId(), "relying-party.example", + base::ASCIIToUTF16("Instrument label"), {}))); +} + +TEST_F(PaymentMethodManifestTableTest, UpdatingInstrumentReturnsTrue) { + PaymentMethodManifestTable* table = + PaymentMethodManifestTable::FromWebDatabase(db_.get()); + EXPECT_TRUE(table->AddSecurePaymentConfirmationInstrument( + SecurePaymentConfirmationInstrument( + CreateCredentialId(/*first_byte=*/0), "relying-party.example", + base::ASCIIToUTF16("Instrument label 1"), + CreateIcon(/*first_byte=*/0)))); + + EXPECT_TRUE(table->AddSecurePaymentConfirmationInstrument( + SecurePaymentConfirmationInstrument( + CreateCredentialId(/*first_byte=*/0), "relying-party.example", + base::ASCIIToUTF16("Instrument label 2"), + CreateIcon(/*first_byte=*/4)))); + + auto instruments = table->GetSecurePaymentConfirmationInstruments( + CreateCredentialIdList(/*first_byte=*/0)); + ExpectOneValidInstrument({0, 1, 2, 3}, "relying-party.example", + "Instrument label 2", {4, 5, 6, 7}, + std::move(instruments)); +} + +TEST_F(PaymentMethodManifestTableTest, + DifferentRelyingPartiesCannotUseSameCredentialIdentifier) { + PaymentMethodManifestTable* table = + PaymentMethodManifestTable::FromWebDatabase(db_.get()); + EXPECT_TRUE(table->AddSecurePaymentConfirmationInstrument( + SecurePaymentConfirmationInstrument( + CreateCredentialId(/*first_byte=*/0), "relying-party-1.example", + base::ASCIIToUTF16("Instrument label 1"), + CreateIcon(/*first_byte=*/0)))); + + EXPECT_FALSE(table->AddSecurePaymentConfirmationInstrument( + SecurePaymentConfirmationInstrument( + CreateCredentialId(/*first_byte=*/0), "relying-party-2.example", + base::ASCIIToUTF16("Instrument label 2"), + CreateIcon(/*first_byte=*/4)))); + + auto instruments = table->GetSecurePaymentConfirmationInstruments( + CreateCredentialIdList(/*first_byte=*/0)); + ExpectOneValidInstrument({0, 1, 2, 3}, "relying-party-1.example", + "Instrument label 1", {0, 1, 2, 3}, + std::move(instruments)); +} + +TEST_F(PaymentMethodManifestTableTest, RelyingPartyCanHaveMultipleCredentials) { + PaymentMethodManifestTable* table = + PaymentMethodManifestTable::FromWebDatabase(db_.get()); + + EXPECT_TRUE(table->AddSecurePaymentConfirmationInstrument( + SecurePaymentConfirmationInstrument( + CreateCredentialId(/*first_byte=*/0), "relying-party.example", + base::ASCIIToUTF16("Instrument label 1"), + CreateIcon(/*first_byte=*/0)))); + + EXPECT_TRUE(table->AddSecurePaymentConfirmationInstrument( + SecurePaymentConfirmationInstrument( + CreateCredentialId(/*first_byte=*/4), "relying-party.example", + base::ASCIIToUTF16("Instrument label 2"), + CreateIcon(/*first_byte=*/4)))); + + auto instruments = table->GetSecurePaymentConfirmationInstruments( + CreateCredentialIdList(/*first_byte=*/0)); + + ExpectOneValidInstrument({0, 1, 2, 3}, "relying-party.example", + "Instrument label 1", {0, 1, 2, 3}, + std::move(instruments)); + + instruments = table->GetSecurePaymentConfirmationInstruments( + CreateCredentialIdList(/*first_byte=*/4)); + + ExpectOneValidInstrument({4, 5, 6, 7}, "relying-party.example", + "Instrument label 2", {4, 5, 6, 7}, + std::move(instruments)); + + std::vector<std::vector<uint8_t>> credential_ids; + credential_ids.push_back(CreateCredentialId(/*first_byte=*/0)); + credential_ids.push_back(CreateCredentialId(/*first_byte=*/4)); + + instruments = + table->GetSecurePaymentConfirmationInstruments(std::move(credential_ids)); + + ASSERT_EQ(2U, instruments.size()); + + ASSERT_NE(nullptr, instruments.front()); + ASSERT_TRUE(instruments.front()->IsValid()); + std::vector<uint8_t> expected_credential_id = {0, 1, 2, 3}; + EXPECT_EQ(expected_credential_id, instruments.front()->credential_id); + EXPECT_EQ("relying-party.example", instruments.front()->relying_party_id); + EXPECT_EQ(base::ASCIIToUTF16("Instrument label 1"), + instruments.front()->label); + std::vector<uint8_t> expected_icon = {0, 1, 2, 3}; + EXPECT_EQ(expected_icon, instruments.front()->icon); + + ASSERT_NE(nullptr, instruments.back()); + ASSERT_TRUE(instruments.back()->IsValid()); + expected_credential_id = {4, 5, 6, 7}; + EXPECT_EQ(expected_credential_id, instruments.back()->credential_id); + EXPECT_EQ("relying-party.example", instruments.back()->relying_party_id); + EXPECT_EQ(base::ASCIIToUTF16("Instrument label 2"), + instruments.back()->label); + expected_icon = {4, 5, 6, 7}; + EXPECT_EQ(expected_icon, instruments.back()->icon); +} + } // namespace } // namespace payments diff --git a/chromium/components/payments/content/payment_request.cc b/chromium/components/payments/content/payment_request.cc index 34323322dc5..a34ae79f92c 100644 --- a/chromium/components/payments/content/payment_request.cc +++ b/chromium/components/payments/content/payment_request.cc @@ -27,6 +27,7 @@ #include "components/payments/core/payment_details.h" #include "components/payments/core/payment_details_validation.h" #include "components/payments/core/payment_prefs.h" +#include "components/payments/core/payment_request_delegate.h" #include "components/payments/core/payments_experimental_features.h" #include "components/payments/core/payments_validators.h" #include "components/payments/core/url_util.h" @@ -123,6 +124,8 @@ void PaymentRequest::Init( return; } + journey_logger_.RecordCheckoutStep( + JourneyLogger::CheckoutFunnelStep::kInitiated); is_initialized_ = true; client_.Bind(std::move(client)); @@ -208,10 +211,17 @@ void PaymentRequest::Init( base::Contains(spec_->url_payment_method_identifiers(), google_pay_url) || base::Contains(spec_->url_payment_method_identifiers(), android_pay_url), + /*requested_method_secure_payment_confirmation=*/ + spec_->IsSecurePaymentConfirmationRequested(), /*requested_method_other=*/non_google_it != spec_->url_payment_method_identifiers().end()); payment_handler_host_.set_payment_request_id_for_logs(*spec_->details().id); + + if (spec_->IsSecurePaymentConfirmationRequested()) { + delegate_->set_dialog_type( + PaymentRequestDelegate::DialogType::SECURE_PAYMENT_CONFIRMATION); + } } void PaymentRequest::Show(bool is_user_gesture, bool wait_for_updated_details) { @@ -227,6 +237,8 @@ void PaymentRequest::Show(bool is_user_gesture, bool wait_for_updated_details) { return; } + journey_logger_.RecordCheckoutStep( + JourneyLogger::CheckoutFunnelStep::kShowCalled); is_show_called_ = true; journey_logger_.SetTriggerTime(); @@ -482,7 +494,7 @@ void PaymentRequest::CanMakePayment() { } } -void PaymentRequest::HasEnrolledInstrument(bool per_method_quota) { +void PaymentRequest::HasEnrolledInstrument() { if (!IsInitialized()) { log_.Error(errors::kCannotCallHasEnrolledInstrumentWithoutInit); OnConnectionTerminated(); @@ -496,12 +508,11 @@ void PaymentRequest::HasEnrolledInstrument(bool per_method_quota) { if (!delegate_->GetPrefService()->GetBoolean(kCanMakePaymentEnabled) || !state_) { - HasEnrolledInstrumentCallback(per_method_quota, - /*has_enrolled_instrument=*/false); + HasEnrolledInstrumentCallback(/*has_enrolled_instrument=*/false); } else { state_->HasEnrolledInstrument( base::BindOnce(&PaymentRequest::HasEnrolledInstrumentCallback, - weak_ptr_factory_.GetWeakPtr(), per_method_quota)); + weak_ptr_factory_.GetWeakPtr())); } } @@ -560,7 +571,7 @@ void PaymentRequest::AreRequestedMethodsSupportedCallback( bool methods_supported, const std::string& error_message) { if (is_show_called_ && observer_for_testing_) - observer_for_testing_->OnShowAppsReady(weak_ptr_factory_.GetWeakPtr()); + observer_for_testing_->OnAppListReady(weak_ptr_factory_.GetWeakPtr()); if (methods_supported) { if (SatisfiesSkipUIConstraints()) @@ -581,6 +592,10 @@ void PaymentRequest::AreRequestedMethodsSupportedCallback( } } +base::WeakPtr<PaymentRequest> PaymentRequest::GetWeakPtr() { + return weak_ptr_factory_.GetWeakPtr(); +} + bool PaymentRequest::IsInitialized() const { return is_initialized_ && client_ && client_.is_bound() && receiver_.is_bound(); @@ -619,8 +634,9 @@ bool PaymentRequest::OnlySingleAppCanProvideAllRequiredInformation() const { } bool PaymentRequest::SatisfiesSkipUIConstraints() { - // Only allowing URL base payment apps to skip the payment sheet. + // Only allowing URL based payment apps to skip the payment sheet. skipped_payment_request_ui_ = + !spec()->IsSecurePaymentConfirmationRequested() && (spec()->url_payment_method_identifiers().size() > 0 || delegate_->SkipUiForBasicCard()) && base::FeatureList::IsEnabled(features::kWebPaymentsSingleAppUiSkip) && @@ -657,17 +673,22 @@ void PaymentRequest::OnPaymentResponseAvailable( case PaymentApp::Type::AUTOFILL: selected_event = JourneyLogger::Event::EVENT_SELECTED_CREDIT_CARD; break; - case PaymentApp::Type::SERVICE_WORKER_APP: { + case PaymentApp::Type::SERVICE_WORKER_APP: + // Intentionally fall through. + case PaymentApp::Type::NATIVE_MOBILE_APP: { selected_event = IsGooglePaymentMethod(response->method_name) ? JourneyLogger::Event::EVENT_SELECTED_GOOGLE : JourneyLogger::Event::EVENT_SELECTED_OTHER; break; } + case PaymentApp::Type::INTERNAL: { + if (response->method_name == methods::kSecurePaymentConfirmation) { + selected_event = + JourneyLogger::Event::EVENT_SELECTED_SECURE_PAYMENT_CONFIRMATION; + } + break; + } case PaymentApp::Type::UNDEFINED: - // Intentionally fall through. - case PaymentApp::Type::NATIVE_MOBILE_APP: - // Intentionally fall through. - case PaymentApp::Type::INTERNAL: NOTREACHED(); break; } @@ -738,6 +759,21 @@ void PaymentRequest::DidStartMainFrameNavigationToDifferentDocument( : JourneyLogger::ABORT_REASON_MERCHANT_NAVIGATION); } +void PaymentRequest::RenderFrameDeleted( + content::RenderFrameHost* render_frame_host) { + DCHECK(render_frame_host == + content::RenderFrameHost::FromID(initiator_frame_routing_id_)); + // RenderFrameHost is usually deleted explicitly before PaymentRequest + // destruction if the user closes the tab or browser window without closing + // the payment request dialog. + RecordFirstAbortReason(JourneyLogger::ABORT_REASON_ABORTED_BY_USER); + // But don't bother sending errors to |client_| because the mojo pipe will be + // torn down anyways when RenderFrameHost is destroyed. It's not safe to call + // UserCancelled() here because it is not re-entrant. + // TODO(crbug.com/1121841) Make UserCancelled re-entrant. + OnConnectionTerminated(); +} + void PaymentRequest::OnConnectionTerminated() { // We are here because of a browser-side error, or likely as a result of the // disconnect_handler on |receiver_|, which can mean that the renderer @@ -757,6 +793,8 @@ void PaymentRequest::OnConnectionTerminated() { void PaymentRequest::Pay() { journey_logger_.SetEventOccurred(JourneyLogger::EVENT_PAY_CLICKED); + journey_logger_.RecordCheckoutStep( + JourneyLogger::CheckoutFunnelStep::kPaymentHandlerInvoked); DCHECK(state_->selected_app()); state_->selected_app()->SetPaymentHandlerHost( payment_handler_host_.AsWeakPtr()); @@ -799,12 +837,11 @@ void PaymentRequest::CanMakePaymentCallback(bool can_make_payment) { } void PaymentRequest::HasEnrolledInstrumentCallback( - bool per_method_quota, bool has_enrolled_instrument) { if (!spec_ || CanMakePaymentQueryFactory::GetInstance() ->GetForContext(web_contents_->GetBrowserContext()) ->CanQuery(top_level_origin_, frame_origin_, - spec_->query_for_quota(), per_method_quota)) { + spec_->query_for_quota())) { RespondToHasEnrolledInstrumentQuery(has_enrolled_instrument, /*warn_local_development=*/false); } else if (UrlUtil::IsLocalDevelopmentUrl(frame_origin_)) { diff --git a/chromium/components/payments/content/payment_request.h b/chromium/components/payments/content/payment_request.h index 24c759882ba..da18347c5aa 100644 --- a/chromium/components/payments/content/payment_request.h +++ b/chromium/components/payments/content/payment_request.h @@ -51,8 +51,8 @@ class PaymentRequest : public mojom::PaymentRequest, virtual void OnCanMakePaymentReturned() = 0; virtual void OnHasEnrolledInstrumentCalled() = 0; virtual void OnHasEnrolledInstrumentReturned() = 0; - virtual void OnShowAppsReady( - base::WeakPtr<PaymentRequest> payment_request) {} + virtual void OnAppListReady(base::WeakPtr<PaymentRequest> payment_request) { + } virtual void OnNotSupportedError() = 0; virtual void OnConnectionTerminated() = 0; virtual void OnAbortCalled() = 0; @@ -83,7 +83,7 @@ class PaymentRequest : public mojom::PaymentRequest, void Abort() override; void Complete(mojom::PaymentComplete result) override; void CanMakePayment() override; - void HasEnrolledInstrument(bool per_method_quota) override; + void HasEnrolledInstrument() override; // PaymentHandlerHost::Delegate bool ChangePaymentMethod(const std::string& method_name, @@ -111,6 +111,12 @@ class PaymentRequest : public mojom::PaymentRequest, // another document, but before the PaymentRequest is destroyed. void DidStartMainFrameNavigationToDifferentDocument(bool is_user_initiated); + // Called when the frame attached to this PaymentRequest is about to be + // destroyed. This is used to clean up before the RenderFrameHost is actually + // destroyed because some objects held by the PaymentRequest (e.g. + // InternalAuthenticator) must be out-lived by the RenderFrameHost. + void RenderFrameDeleted(content::RenderFrameHost* render_frame_host); + // As a result of a browser-side error or renderer-initiated mojo channel // closure (e.g. there was an error on the renderer side, or payment was // successful), this method is called. It is responsible for cleaning up, @@ -130,6 +136,10 @@ class PaymentRequest : public mojom::PaymentRequest, content::WebContents* web_contents() { return web_contents_; } + const content::GlobalFrameRoutingId& initiator_frame_routing_id() { + return initiator_frame_routing_id_; + } + bool skipped_payment_request_ui() { return skipped_payment_request_ui_; } bool is_show_user_gesture() const { return is_show_user_gesture_; } @@ -139,6 +149,8 @@ class PaymentRequest : public mojom::PaymentRequest, PaymentRequestSpec* spec() const { return spec_.get(); } PaymentRequestState* state() const { return state_.get(); } + base::WeakPtr<PaymentRequest> GetWeakPtr(); + private: // Returns true after init() has been called and the mojo connection has been // established. If the mojo connection gets later disconnected, this will @@ -169,8 +181,7 @@ class PaymentRequest : public mojom::PaymentRequest, // The callback for PaymentRequestState::HasEnrolledInstrument. Checks for // query quota and may send QUERY_QUOTA_EXCEEDED. - void HasEnrolledInstrumentCallback(bool per_method_quota, - bool has_enrolled_instrument); + void HasEnrolledInstrumentCallback(bool has_enrolled_instrument); // The callback for PaymentRequestState::AreRequestedMethodsSupported. void AreRequestedMethodsSupportedCallback(bool methods_supported, diff --git a/chromium/components/payments/content/payment_request_dialog.h b/chromium/components/payments/content/payment_request_dialog.h index 0f3c186323e..4a6d02bcf5b 100644 --- a/chromium/components/payments/content/payment_request_dialog.h +++ b/chromium/components/payments/content/payment_request_dialog.h @@ -47,6 +47,9 @@ class PaymentRequestDialog { virtual void ShowPaymentHandlerScreen( const GURL& url, PaymentHandlerOpenWindowCallback callback) = 0; + + // Confirms payment. Used only in tests. + virtual void ConfirmPaymentForTesting() = 0; }; } // namespace payments diff --git a/chromium/components/payments/content/payment_request_spec.cc b/chromium/components/payments/content/payment_request_spec.cc index bbb7c61a19a..984656779bb 100644 --- a/chromium/components/payments/content/payment_request_spec.cc +++ b/chromium/components/payments/content/payment_request_spec.cc @@ -358,6 +358,18 @@ PaymentRequestSpec::GetShippingOptions() const { return *details_->shipping_options; } +bool PaymentRequestSpec::IsSecurePaymentConfirmationRequested() const { + // No other payment method will be requested together with secure payment + // confirmation. + return payment_method_identifiers_set_.size() == 1 && + *payment_method_identifiers_set_.begin() == + methods::kSecurePaymentConfirmation; +} + +base::WeakPtr<PaymentRequestSpec> PaymentRequestSpec::GetWeakPtr() { + return weak_ptr_factory_.GetWeakPtr(); +} + const mojom::PaymentDetailsModifierPtr* PaymentRequestSpec::GetApplicableModifier(PaymentApp* selected_app) const { if (!selected_app || diff --git a/chromium/components/payments/content/payment_request_spec.h b/chromium/components/payments/content/payment_request_spec.h index 87af537dadd..caa20936bba 100644 --- a/chromium/components/payments/content/payment_request_spec.h +++ b/chromium/components/payments/content/payment_request_spec.h @@ -11,6 +11,7 @@ #include <vector> #include "base/macros.h" +#include "base/memory/weak_ptr.h" #include "base/observer_list.h" #include "base/strings/string16.h" #include "components/autofill/core/browser/data_model/credit_card.h" @@ -119,6 +120,8 @@ class PaymentRequestSpec : public PaymentOptionsProvider, bool request_payer_email() const override; PaymentShippingType shipping_type() const override; + const mojom::PaymentOptionsPtr& payment_options() const { return options_; } + // Returns the query to be used for the quota on hasEnrolledInstrument() // calls. Generally this returns the payment method identifiers and their // corresponding data. However, in the case of basic-card with @@ -200,6 +203,10 @@ class PaymentRequestSpec : public PaymentOptionsProvider, return method_data_; } + bool IsSecurePaymentConfirmationRequested() const; + + base::WeakPtr<PaymentRequestSpec> GetWeakPtr(); + private: // Returns the first applicable modifier in the Payment Request for the // |selected_app|. @@ -280,6 +287,8 @@ class PaymentRequestSpec : public PaymentOptionsProvider, base::string16 retry_error_message_; mojom::PayerErrorsPtr payer_errors_; + base::WeakPtrFactory<PaymentRequestSpec> weak_ptr_factory_{this}; + DISALLOW_COPY_AND_ASSIGN(PaymentRequestSpec); }; diff --git a/chromium/components/payments/content/payment_request_state.cc b/chromium/components/payments/content/payment_request_state.cc index 8b7640c1937..4c5dda70ac2 100644 --- a/chromium/components/payments/content/payment_request_state.cc +++ b/chromium/components/payments/content/payment_request_state.cc @@ -73,7 +73,7 @@ PaymentRequestState::PaymentRequestState( frame_origin_(frame_origin), frame_security_origin_(frame_security_origin), app_locale_(app_locale), - spec_(spec), + spec_(spec->GetWeakPtr()), delegate_(delegate), personal_data_manager_(personal_data_manager), journey_logger_(journey_logger), @@ -108,7 +108,11 @@ void PaymentRequestState::ShowProcessingSpinner() { } PaymentRequestSpec* PaymentRequestState::GetSpec() const { - return spec_; + return spec_.get(); +} + +std::string PaymentRequestState::GetTwaPackageName() const { + return GetPaymentRequestDelegate()->GetTwaPackageName(); } const GURL& PaymentRequestState::GetTopOrigin() { @@ -130,9 +134,15 @@ content::RenderFrameHost* PaymentRequestState::GetInitiatorRenderFrameHost() const std::vector<mojom::PaymentMethodDataPtr>& PaymentRequestState::GetMethodData() const { + DCHECK(GetSpec()); return GetSpec()->method_data(); } +std::unique_ptr<autofill::InternalAuthenticator> +PaymentRequestState::CreateInternalAuthenticator() const { + return GetPaymentRequestDelegate()->CreateInternalAuthenticator(); +} + scoped_refptr<PaymentManifestWebDataService> PaymentRequestState::GetPaymentManifestWebDataService() const { return GetPaymentRequestDelegate()->GetPaymentManifestWebDataService(); @@ -150,7 +160,7 @@ bool PaymentRequestState::IsRequestedAutofillDataAvailable() { bool PaymentRequestState::MayCrawlForInstallablePaymentApps() { return PaymentsExperimentalFeatures::IsEnabled( features::kAlwaysAllowJustInTimePaymentApp) || - !spec_->supports_basic_card(); + !spec_ || !spec_->supports_basic_card(); } bool PaymentRequestState::IsOffTheRecord() const { @@ -186,6 +196,25 @@ void PaymentRequestState::OnDoneCreatingPaymentApps() { if (--number_of_payment_app_factories_ > 0U) return; + if (IsInTwa()) { + // If a preferred payment app is present (e.g. Play Billing within a TWA), + // all other payment apps are ignored. + bool has_preferred_app = + std::any_of(available_apps_.begin(), available_apps_.end(), + [](const auto& app) { return app->IsPreferred(); }); + if (has_preferred_app) { + available_apps_.erase( + std::remove_if(available_apps_.begin(), available_apps_.end(), + [](const auto& app) { return !app->IsPreferred(); }), + available_apps_.end()); + + // By design, only one payment app can be preferred. + DCHECK_EQ(available_apps_.size(), 1u); + if (available_apps_.size() > 1) + available_apps_.resize(1); + } + } + SetDefaultProfileSelections(); get_all_apps_finished_ = true; @@ -198,11 +227,12 @@ void PaymentRequestState::OnDoneCreatingPaymentApps() { // Fulfill the pending CanMakePayment call. if (can_make_payment_callback_) - std::move(can_make_payment_callback_).Run(are_requested_methods_supported_); + std::move(can_make_payment_callback_).Run(GetCanMakePaymentValue()); // Fulfill the pending HasEnrolledInstrument call. if (has_enrolled_instrument_callback_) - std::move(has_enrolled_instrument_callback_).Run(has_enrolled_instrument_); + std::move(has_enrolled_instrument_callback_) + .Run(GetHasEnrolledInstrumentValue()); // Fulfill the pending AreRequestedMethodsSupported call. if (are_requested_methods_supported_callback_) @@ -210,6 +240,10 @@ void PaymentRequestState::OnDoneCreatingPaymentApps() { std::move(are_requested_methods_supported_callback_)); } +void PaymentRequestState::SetCanMakePaymentEvenWithoutApps() { + can_make_payment_even_without_apps_ = true; +} + void PaymentRequestState::OnPaymentResponseReady( mojom::PaymentResponsePtr payment_response) { delegate_->OnPaymentResponseAvailable(std::move(payment_response)); @@ -221,6 +255,9 @@ void PaymentRequestState::OnPaymentResponseError( } void PaymentRequestState::OnSpecUpdated() { + if (!spec_) + return; + autofill::AutofillProfile* selected_shipping_profile = selected_shipping_profile_; autofill::AutofillProfile* selected_contact_profile = @@ -260,7 +297,7 @@ void PaymentRequestState::CanMakePayment(StatusCallback callback) { return; } - PostStatusCallback(std::move(callback), are_requested_methods_supported_); + PostStatusCallback(std::move(callback), GetCanMakePaymentValue()); } void PaymentRequestState::HasEnrolledInstrument(StatusCallback callback) { @@ -270,7 +307,7 @@ void PaymentRequestState::HasEnrolledInstrument(StatusCallback callback) { return; } - PostStatusCallback(std::move(callback), has_enrolled_instrument_); + PostStatusCallback(std::move(callback), GetHasEnrolledInstrumentValue()); } void PaymentRequestState::AreRequestedMethodsSupported( @@ -297,6 +334,9 @@ void PaymentRequestState::CheckRequestedMethodsSupported( MethodsSupportedCallback callback) { DCHECK(get_all_apps_finished_); + if (!spec_) + return; + // Don't modify the value of |are_requested_methods_supported_|, because it's // used for canMakePayment(). bool supported = are_requested_methods_supported_; @@ -310,11 +350,10 @@ void PaymentRequestState::CheckRequestedMethodsSupported( get_all_payment_apps_error_ = errors::kStrictBasicCardShowReject; } - bool is_in_twa = !payment_request_delegate_->GetTwaPackageName().empty(); if (!supported && get_all_payment_apps_error_.empty() && base::Contains(spec_->payment_method_identifiers_set(), methods::kGooglePlayBilling) && - !is_in_twa) { + !IsInTwa()) { get_all_payment_apps_error_ = errors::kAppStoreMethodOnlySupportedInTwa; } @@ -337,9 +376,12 @@ void PaymentRequestState::RemoveObserver(Observer* observer) { void PaymentRequestState::GeneratePaymentResponse() { DCHECK(is_ready_to_pay()); + if (!spec_) + return; + // Once the response is ready, will call back into OnPaymentResponseReady. response_helper_ = std::make_unique<PaymentResponseHelper>( - app_locale_, spec_, selected_app_, payment_request_delegate_, + app_locale_, spec_.get(), selected_app_, payment_request_delegate_, selected_shipping_profile_, selected_contact_profile_, this); } @@ -429,6 +471,9 @@ void PaymentRequestState::AddAutofillContactProfile( void PaymentRequestState::SetSelectedShippingOption( const std::string& shipping_option_id) { + if (!spec_) + return; + spec_->StartWaitingForUpdateWith( PaymentRequestSpec::UpdateReason::SHIPPING_OPTION); // This will inform the merchant and will lead to them calling updateWith with @@ -439,6 +484,9 @@ void PaymentRequestState::SetSelectedShippingOption( void PaymentRequestState::SetSelectedShippingProfile( autofill::AutofillProfile* profile, SectionSelectionStatus selection_status) { + if (!spec_) + return; + spec_->StartWaitingForUpdateWith( PaymentRequestSpec::UpdateReason::SHIPPING_ADDRESS); selected_shipping_profile_ = profile; @@ -534,7 +582,8 @@ void PaymentRequestState::SelectDefaultShippingAddressAndNotifyObservers() { // Only pre-select an address if the merchant provided at least one selected // shipping option, and the top profile is complete. Assumes that profiles // have already been sorted for completeness and frecency. - if (!shipping_profiles().empty() && spec_->selected_shipping_option() && + if (!shipping_profiles().empty() && spec_ && + spec_->selected_shipping_option() && profile_comparator()->IsShippingComplete(shipping_profiles_[0])) { selected_shipping_profile_ = shipping_profiles()[0]; } @@ -546,13 +595,16 @@ void PaymentRequestState::SelectDefaultShippingAddressAndNotifyObservers() { } bool PaymentRequestState::ShouldShowShippingSection() const { - if (!spec_->request_shipping()) + if (!spec_ || !spec_->request_shipping()) return false; return selected_app_ ? !selected_app_->HandlesShippingAddress() : true; } bool PaymentRequestState::ShouldShowContactSection() const { + if (!spec_) + return false; + if (spec_->request_payer_name() && (!selected_app_ || !selected_app_->HandlesPayerName())) { return true; @@ -642,7 +694,7 @@ void PaymentRequestState::SetDefaultProfileSelections() { // Record the missing required payment fields when no complete payment // info exists. if (available_apps_.empty()) { - if (spec_->supports_basic_card()) { + if (spec_ && spec_->supports_basic_card()) { // All fields are missing when basic-card is requested but no card exits. base::UmaHistogramSparse("PaymentRequest.MissingPaymentFields", CREDIT_CARD_EXPIRED | CREDIT_CARD_NO_CARDHOLDER | @@ -686,7 +738,7 @@ bool PaymentRequestState::ArePaymentDetailsSatisfied() { } bool PaymentRequestState::ArePaymentOptionsSatisfied() { - if (is_waiting_for_merchant_validation_) + if (is_waiting_for_merchant_validation_ || !spec_) return false; if (ShouldShowShippingSection() && @@ -711,4 +763,17 @@ void PaymentRequestState::OnAddressNormalized( app_locale_)); } +bool PaymentRequestState::IsInTwa() const { + return !payment_request_delegate_->GetTwaPackageName().empty(); +} + +bool PaymentRequestState::GetCanMakePaymentValue() const { + return are_requested_methods_supported_ || + can_make_payment_even_without_apps_; +} + +bool PaymentRequestState::GetHasEnrolledInstrumentValue() const { + return has_enrolled_instrument_ || can_make_payment_even_without_apps_; +} + } // namespace payments diff --git a/chromium/components/payments/content/payment_request_state.h b/chromium/components/payments/content/payment_request_state.h index 469b3104402..6b0cd174b31 100644 --- a/chromium/components/payments/content/payment_request_state.h +++ b/chromium/components/payments/content/payment_request_state.h @@ -114,6 +114,7 @@ class PaymentRequestState : public PaymentAppFactory::Delegate, base::OnceCallback<void(bool methods_supported, const std::string& error_message)>; + // The `spec` parameter should not be null. PaymentRequestState(content::WebContents* web_contents, content::RenderFrameHost* initiator_render_frame_host, const GURL& top_level_origin, @@ -132,12 +133,15 @@ class PaymentRequestState : public PaymentAppFactory::Delegate, ContentPaymentRequestDelegate* GetPaymentRequestDelegate() const override; void ShowProcessingSpinner() override; PaymentRequestSpec* GetSpec() const override; + std::string GetTwaPackageName() const override; const GURL& GetTopOrigin() override; const GURL& GetFrameOrigin() override; const url::Origin& GetFrameSecurityOrigin() override; content::RenderFrameHost* GetInitiatorRenderFrameHost() const override; const std::vector<mojom::PaymentMethodDataPtr>& GetMethodData() const override; + std::unique_ptr<autofill::InternalAuthenticator> CreateInternalAuthenticator() + const override; scoped_refptr<PaymentManifestWebDataService> GetPaymentManifestWebDataService() const override; const std::vector<autofill::AutofillProfile*>& GetBillingProfiles() override; @@ -148,6 +152,7 @@ class PaymentRequestState : public PaymentAppFactory::Delegate, void OnPaymentAppCreationError(const std::string& error_message) override; bool SkipCreatingNativePaymentApps() const override; void OnDoneCreatingPaymentApps() override; + void SetCanMakePaymentEvenWithoutApps() override; // PaymentResponseHelper::Delegate void OnPaymentResponseReady( @@ -344,6 +349,12 @@ class PaymentRequestState : public PaymentAppFactory::Delegate, void IncrementSelectionStatus(JourneyLogger::Section section, SectionSelectionStatus selection_status); + // Returns whether the browser is currently in a TWA. + bool IsInTwa() const; + + bool GetCanMakePaymentValue() const; + bool GetHasEnrolledInstrumentValue() const; + content::WebContents* web_contents_; content::RenderFrameHost* initiator_render_frame_host_; const GURL top_origin_; @@ -380,8 +391,9 @@ class PaymentRequestState : public PaymentAppFactory::Delegate, const std::string app_locale_; + base::WeakPtr<PaymentRequestSpec> spec_; + // Not owned. Never null. Will outlive this object. - PaymentRequestSpec* spec_; Delegate* delegate_; autofill::PersonalDataManager* personal_data_manager_; JourneyLogger* journey_logger_; @@ -420,6 +432,15 @@ class PaymentRequestState : public PaymentAppFactory::Delegate, // Whether PaymentRequest.show() was invoked with a user gesture. bool is_show_user_gesture_ = false; + // If set to true, then both GetCanMakePaymentValue() and + // GetHasEnrolledInstrumentValue() will return true, regardless of presence of + // payment apps. This is used by secure payment confirmation, where + // PaymentRequest.canMakePayment() and PaymentRequesthasEnrolledInstrument() + // calls in JavaScript both return true without querying the SQLite database + // for instrument information and without querying the authenticator for + // credentials. + bool can_make_payment_even_without_apps_ = false; + base::WeakPtrFactory<PaymentRequestState> weak_ptr_factory_{this}; DISALLOW_COPY_AND_ASSIGN(PaymentRequestState); diff --git a/chromium/components/payments/content/payment_request_state_unittest.cc b/chromium/components/payments/content/payment_request_state_unittest.cc index 6cfc72c6dbe..1f910a9e741 100644 --- a/chromium/components/payments/content/payment_request_state_unittest.cc +++ b/chromium/components/payments/content/payment_request_state_unittest.cc @@ -78,7 +78,7 @@ class PaymentRequestStateTest : public testing::Test, std::move(options), std::move(details), std::move(method_data), /*observer=*/nullptr, "en-US"); PaymentAppServiceFactory::SetForTesting( - std::make_unique<PaymentAppService>()); + std::make_unique<PaymentAppService>(/*context=*/nullptr)); state_ = std::make_unique<PaymentRequestState>( /*web_contents=*/nullptr, /*render_frame_host=*/nullptr, GURL("https://example.com"), diff --git a/chromium/components/payments/content/payment_request_web_contents_manager.cc b/chromium/components/payments/content/payment_request_web_contents_manager.cc index 3bcda4e278c..898f5e8cbc4 100644 --- a/chromium/components/payments/content/payment_request_web_contents_manager.cc +++ b/chromium/components/payments/content/payment_request_web_contents_manager.cc @@ -8,8 +8,10 @@ #include "base/check.h" #include "components/payments/content/content_payment_request_delegate.h" +#include "components/payments/content/payment_manifest_web_data_service.h" #include "components/payments/content/payment_request.h" #include "components/payments/content/payment_request_display_manager.h" +#include "components/payments/core/features.h" #include "content/public/browser/navigation_handle.h" #include "content/public/browser/web_contents.h" @@ -33,7 +35,7 @@ void PaymentRequestWebContentsManager::CreatePaymentRequest( mojo::PendingReceiver<payments::mojom::PaymentRequest> receiver, PaymentRequest::ObserverForTest* observer_for_testing) { auto new_request = std::make_unique<PaymentRequest>( - render_frame_host, web_contents, std::move(delegate), this, + render_frame_host, web_contents, std::move(delegate), /*manager=*/this, delegate->GetDisplayManager(), std::move(receiver), observer_for_testing); PaymentRequest* request_ptr = new_request.get(); payment_requests_.insert(std::make_pair(request_ptr, std::move(new_request))); @@ -54,6 +56,23 @@ void PaymentRequestWebContentsManager::DidStartNavigation( it.second->DidStartMainFrameNavigationToDifferentDocument( !navigation_handle->IsRendererInitiated()); } + payment_credential_ = nullptr; +} + +void PaymentRequestWebContentsManager::RenderFrameDeleted( + content::RenderFrameHost* render_frame_host) { + // Two passes to avoid modifying the |payment_requests_| map while iterating + // over it. + std::vector<PaymentRequest*> obsolete; + for (auto& it : payment_requests_) { + if (content::RenderFrameHost::FromID( + it.second->initiator_frame_routing_id()) == render_frame_host) { + obsolete.push_back(it.first); + } + } + for (auto* request : obsolete) { + request->RenderFrameDeleted(render_frame_host); + } } void PaymentRequestWebContentsManager::DestroyRequest(PaymentRequest* request) { @@ -61,6 +80,15 @@ void PaymentRequestWebContentsManager::DestroyRequest(PaymentRequest* request) { payment_requests_.erase(request); } +void PaymentRequestWebContentsManager::CreatePaymentCredential( + content::GlobalFrameRoutingId initiator_frame_routing_id, + scoped_refptr<PaymentManifestWebDataService> web_data_sevice, + mojo::PendingReceiver<payments::mojom::PaymentCredential> receiver) { + payment_credential_ = std::make_unique<PaymentCredential>( + web_contents(), initiator_frame_routing_id, web_data_sevice, + std::move(receiver)); +} + PaymentRequestWebContentsManager::PaymentRequestWebContentsManager( content::WebContents* web_contents) : content::WebContentsObserver(web_contents) {} diff --git a/chromium/components/payments/content/payment_request_web_contents_manager.h b/chromium/components/payments/content/payment_request_web_contents_manager.h index 3f9e72b4adc..cee9dbdf7c1 100644 --- a/chromium/components/payments/content/payment_request_web_contents_manager.h +++ b/chromium/components/payments/content/payment_request_web_contents_manager.h @@ -9,10 +9,14 @@ #include <memory> #include "base/macros.h" +#include "base/memory/scoped_refptr.h" +#include "components/payments/content/payment_credential.h" #include "components/payments/content/payment_request.h" +#include "content/public/browser/global_routing_id.h" #include "content/public/browser/web_contents_observer.h" #include "content/public/browser/web_contents_user_data.h" #include "mojo/public/cpp/bindings/pending_receiver.h" +#include "third_party/blink/public/mojom/payments/payment_credential.mojom.h" #include "third_party/blink/public/mojom/payments/payment_request.mojom.h" namespace content { @@ -24,6 +28,7 @@ class WebContents; namespace payments { class ContentPaymentRequestDelegate; +class PaymentManifestWebDataService; // This class owns the PaymentRequest associated with a given WebContents. // @@ -44,8 +49,8 @@ class PaymentRequestWebContentsManager static PaymentRequestWebContentsManager* GetOrCreateForWebContents( content::WebContents* web_contents); - // Creates the PaymentRequest that will interact with this |render_frame_host| - // and the associated |web_contents|. + // Creates the PaymentRequest that will interact with this `render_frame_host` + // and the associated `web_contents`. void CreatePaymentRequest( content::RenderFrameHost* render_frame_host, content::WebContents* web_contents, @@ -53,12 +58,20 @@ class PaymentRequestWebContentsManager mojo::PendingReceiver<payments::mojom::PaymentRequest> receiver, PaymentRequest::ObserverForTest* observer_for_testing); - // Destroys the given |request|. + // Destroys the given `request`. void DestroyRequest(PaymentRequest* request); + // Creates the mojo IPC endpoint that will receive requests from the renderer + // to store payment credential in user's profile. + void CreatePaymentCredential( + content::GlobalFrameRoutingId initiator_frame_routing_id, + scoped_refptr<PaymentManifestWebDataService> web_data_sevice, + mojo::PendingReceiver<payments::mojom::PaymentCredential> receiver); + // WebContentsObserver:: void DidStartNavigation( content::NavigationHandle* navigation_handle) override; + void RenderFrameDeleted(content::RenderFrameHost* render_frame_host) override; private: explicit PaymentRequestWebContentsManager(content::WebContents* web_contents); @@ -71,6 +84,8 @@ class PaymentRequestWebContentsManager // the requests themselves call DestroyRequest(). std::map<PaymentRequest*, std::unique_ptr<PaymentRequest>> payment_requests_; + std::unique_ptr<PaymentCredential> payment_credential_; + WEB_CONTENTS_USER_DATA_KEY_DECL(); DISALLOW_COPY_AND_ASSIGN(PaymentRequestWebContentsManager); diff --git a/chromium/components/payments/content/payment_response_helper.cc b/chromium/components/payments/content/payment_response_helper.cc index 9f3c676fd4b..09e19de9971 100644 --- a/chromium/components/payments/content/payment_response_helper.cc +++ b/chromium/components/payments/content/payment_response_helper.cc @@ -33,12 +33,11 @@ PaymentResponseHelper::PaymentResponseHelper( : app_locale_(app_locale), is_waiting_for_shipping_address_normalization_(false), is_waiting_for_instrument_details_(false), - spec_(spec), + spec_(spec->GetWeakPtr()), delegate_(delegate), selected_app_(selected_app), payment_request_delegate_(payment_request_delegate), selected_contact_profile_(selected_contact_profile) { - DCHECK(spec_); DCHECK(selected_app_); DCHECK(delegate_); @@ -113,6 +112,9 @@ mojom::PayerDetailPtr PaymentResponseHelper::GeneratePayerDetail( const autofill::AutofillProfile* selected_contact_profile) const { mojom::PayerDetailPtr payer = mojom::PayerDetail::New(); + if (!spec_) + return payer; + if (spec_->request_payer_name()) { if (selected_app_->HandlesPayerName()) { payer->name = payer_data_from_app_.payer_name; @@ -159,6 +161,9 @@ void PaymentResponseHelper::GeneratePaymentResponse() { DCHECK(!is_waiting_for_instrument_details_); DCHECK(!is_waiting_for_shipping_address_normalization_); + if (!spec_) + return; + mojom::PaymentResponsePtr payment_response = mojom::PaymentResponse::New(); // Make sure that we return the method name that the merchant specified for diff --git a/chromium/components/payments/content/payment_response_helper.h b/chromium/components/payments/content/payment_response_helper.h index e3268660821..a761d655638 100644 --- a/chromium/components/payments/content/payment_response_helper.h +++ b/chromium/components/payments/content/payment_response_helper.h @@ -65,8 +65,9 @@ class PaymentResponseHelper bool is_waiting_for_shipping_address_normalization_; bool is_waiting_for_instrument_details_; + base::WeakPtr<PaymentRequestSpec> spec_; + // Not owned, cannot be null. - PaymentRequestSpec* spec_; Delegate* delegate_; PaymentApp* selected_app_; PaymentRequestDelegate* payment_request_delegate_; diff --git a/chromium/components/payments/content/secure_payment_confirmation_app.cc b/chromium/components/payments/content/secure_payment_confirmation_app.cc new file mode 100644 index 00000000000..654c3c35307 --- /dev/null +++ b/chromium/components/payments/content/secure_payment_confirmation_app.cc @@ -0,0 +1,318 @@ +// Copyright 2020 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. + +#include "components/payments/content/secure_payment_confirmation_app.h" + +#include <sstream> +#include <utility> + +#include "base/base64.h" +#include "base/check.h" +#include "base/containers/flat_tree.h" +#include "base/feature_list.h" +#include "base/json/json_writer.h" +#include "base/notreached.h" +#include "base/strings/strcat.h" +#include "base/strings/stringprintf.h" +#include "base/time/time.h" +#include "base/values.h" +#include "components/autofill/core/browser/payments/internal_authenticator.h" +#include "components/payments/core/method_strings.h" +#include "components/payments/core/payer_data.h" +#include "content/public/browser/web_contents.h" +#include "content/public/common/content_features.h" +#include "crypto/sha2.h" +#include "device/fido/fido_transport_protocol.h" +#include "device/fido/fido_types.h" +#include "device/fido/public_key_credential_descriptor.h" +#include "url/url_constants.h" + +namespace payments { +namespace { + +static constexpr int kDefaultTimeoutMinutes = 3; + +// Creates a SHA-256 hash over the Secure Payment Confirmation bundle, which is +// a JSON string (without whitespace) with the following structure: +// { +// "merchantData" { +// "merchantOrigin": "https://merchant.example", +// "total": { +// "currency": "CAD", +// "value": "1.25", +// }, +// }, +// "networkData": "YW=", +// } +// where "networkData" is the base64 encoding of the |networkData| specified in +// the SecurePaymentConfirmationRequest. +std::vector<uint8_t> GetSecurePaymentConfirmationChallenge( + const std::vector<uint8_t>& network_data, + const url::Origin& merchant_origin, + const mojom::PaymentCurrencyAmountPtr& amount) { + base::Value total(base::Value::Type::DICTIONARY); + total.SetKey("currency", base::Value(amount->currency)); + total.SetKey("value", base::Value(amount->value)); + + base::Value merchant_data(base::Value::Type::DICTIONARY); + merchant_data.SetKey("merchantOrigin", + base::Value(merchant_origin.Serialize())); + merchant_data.SetKey("total", std::move(total)); + + base::Value transaction_data(base::Value::Type::DICTIONARY); + transaction_data.SetKey("networkData", + base::Value(base::Base64Encode(network_data))); + transaction_data.SetKey("merchantData", std::move(merchant_data)); + + // TODO(crbug.com/1123054): change to a more robust alternative that does not + // depend on the exact whitespace, escaping and ordering of the JSON + // serialization. + std::string json; + bool success = base::JSONWriter::Write(transaction_data, &json); + DCHECK(success) << "Failed to write JSON for " << transaction_data; + + std::string sha256_hash = crypto::SHA256HashString(json); + std::vector<uint8_t> output_bytes(sha256_hash.begin(), sha256_hash.end()); + return output_bytes; +} + +} // namespace + +SecurePaymentConfirmationApp::SecurePaymentConfirmationApp( + content::WebContents* web_contents_to_observe, + const std::string& effective_relying_party_identity, + std::unique_ptr<SkBitmap> icon, + const base::string16& label, + std::vector<uint8_t> credential_id, + const url::Origin& merchant_origin, + const mojom::PaymentCurrencyAmountPtr& total, + mojom::SecurePaymentConfirmationRequestPtr request, + std::unique_ptr<autofill::InternalAuthenticator> authenticator) + : PaymentApp(/*icon_resource_id=*/0, PaymentApp::Type::INTERNAL), + content::WebContentsObserver(web_contents_to_observe), + authenticator_render_frame_host_pointer_do_not_dereference_( + authenticator->GetRenderFrameHost()), + effective_relying_party_identity_(effective_relying_party_identity), + icon_(std::move(icon)), + label_(label), + credential_id_(std::move(credential_id)), + encoded_credential_id_(base::Base64Encode(credential_id_)), + merchant_origin_(merchant_origin), + total_(total.Clone()), + request_(std::move(request)), + authenticator_(std::move(authenticator)) { + DCHECK_EQ(web_contents_to_observe->GetMainFrame(), + authenticator_render_frame_host_pointer_do_not_dereference_); + DCHECK(!credential_id_.empty()); + + app_method_names_.insert(methods::kSecurePaymentConfirmation); +} + +SecurePaymentConfirmationApp::~SecurePaymentConfirmationApp() = default; + +void SecurePaymentConfirmationApp::InvokePaymentApp(Delegate* delegate) { + if (!authenticator_) + return; + + auto options = blink::mojom::PublicKeyCredentialRequestOptions::New(); + options->relying_party_id = effective_relying_party_identity_; + options->timeout = request_->timeout.has_value() + ? request_->timeout.value() + : base::TimeDelta::FromMinutes(kDefaultTimeoutMinutes); + options->user_verification = device::UserVerificationRequirement::kRequired; + std::vector<device::PublicKeyCredentialDescriptor> credentials; + + if (base::FeatureList::IsEnabled(features::kSecurePaymentConfirmationDebug)) { + options->user_verification = + device::UserVerificationRequirement::kPreferred; + // The `device::PublicKeyCredentialDescriptor` constructor with 2 parameters + // enables authentication through all protocols. + credentials.emplace_back(device::CredentialType::kPublicKey, + credential_id_); + } else { + // Enable authentication only through internal authenticators by default. + credentials.emplace_back(device::CredentialType::kPublicKey, credential_id_, + base::flat_set<device::FidoTransportProtocol>{ + device::FidoTransportProtocol::kInternal}); + } + + options->allow_credentials = std::move(credentials); + + // Create a new challenge that is a hash of the transaction data. + options->challenge = GetSecurePaymentConfirmationChallenge( + request_->network_data, merchant_origin_, total_); + + // We are nullifying the security check by design, and the origin that created + // the credential isn't saved anywhere. + authenticator_->SetEffectiveOrigin(url::Origin::Create( + GURL(base::StrCat({url::kHttpsScheme, url::kStandardSchemeSeparator, + effective_relying_party_identity_})))); + + authenticator_->GetAssertion( + std::move(options), + base::BindOnce(&SecurePaymentConfirmationApp::OnGetAssertion, + weak_ptr_factory_.GetWeakPtr(), delegate)); +} + +bool SecurePaymentConfirmationApp::IsCompleteForPayment() const { + return true; +} + +uint32_t SecurePaymentConfirmationApp::GetCompletenessScore() const { + // This value is used for sorting multiple apps, but this app always appears + // on its own. + return 0; +} + +bool SecurePaymentConfirmationApp::CanPreselect() const { + return true; +} + +base::string16 SecurePaymentConfirmationApp::GetMissingInfoLabel() const { + NOTREACHED(); + return base::string16(); +} + +bool SecurePaymentConfirmationApp::HasEnrolledInstrument() const { + // If there's no platform authenticator, then the factory should not create + // this app. Therefore, this function can always return true. + return true; +} + +void SecurePaymentConfirmationApp::RecordUse() { + NOTIMPLEMENTED(); +} + +bool SecurePaymentConfirmationApp::NeedsInstallation() const { + return false; +} + +std::string SecurePaymentConfirmationApp::GetId() const { + return encoded_credential_id_; +} + +base::string16 SecurePaymentConfirmationApp::GetLabel() const { + return label_; +} + +base::string16 SecurePaymentConfirmationApp::GetSublabel() const { + return base::string16(); +} + +const SkBitmap* SecurePaymentConfirmationApp::icon_bitmap() const { + return icon_.get(); +} + +bool SecurePaymentConfirmationApp::IsValidForModifier( + const std::string& method, + bool supported_networks_specified, + const std::set<std::string>& supported_networks) const { + bool is_valid = false; + IsValidForPaymentMethodIdentifier(method, &is_valid); + return is_valid; +} + +base::WeakPtr<PaymentApp> SecurePaymentConfirmationApp::AsWeakPtr() { + return weak_ptr_factory_.GetWeakPtr(); +} + +bool SecurePaymentConfirmationApp::HandlesShippingAddress() const { + return false; +} + +bool SecurePaymentConfirmationApp::HandlesPayerName() const { + return false; +} + +bool SecurePaymentConfirmationApp::HandlesPayerEmail() const { + return false; +} + +bool SecurePaymentConfirmationApp::HandlesPayerPhone() const { + return false; +} + +bool SecurePaymentConfirmationApp::IsWaitingForPaymentDetailsUpdate() const { + return false; +} + +void SecurePaymentConfirmationApp::UpdateWith( + mojom::PaymentRequestDetailsUpdatePtr details_update) { + NOTREACHED(); +} + +void SecurePaymentConfirmationApp::OnPaymentDetailsNotUpdated() { + NOTREACHED(); +} + +void SecurePaymentConfirmationApp::AbortPaymentApp( + base::OnceCallback<void(bool)> abort_callback) { + std::move(abort_callback).Run(/*abort_success=*/false); +} + +void SecurePaymentConfirmationApp::RenderFrameDeleted( + content::RenderFrameHost* render_frame_host) { + if (authenticator_render_frame_host_pointer_do_not_dereference_ == + render_frame_host) { + // The authenticator requires to be deleted before the render frame. + authenticator_.reset(); + } +} + +void SecurePaymentConfirmationApp::OnGetAssertion( + Delegate* delegate, + blink::mojom::AuthenticatorStatus status, + blink::mojom::GetAssertionAuthenticatorResponsePtr response) { + if (status != blink::mojom::AuthenticatorStatus::SUCCESS || !response) { + std::stringstream status_string_stream; + status_string_stream << status; + delegate->OnInstrumentDetailsError(base::StringPrintf( + "Authenticator returned %s.", status_string_stream.str().c_str())); + return; + } + + // Serialize response into a JSON string. Browser will pass this string over + // Mojo IPC into Blink, which will parse it into a JavaScript object for the + // merchant. + auto info_json = std::make_unique<base::DictionaryValue>(); + if (response->info) { + info_json->SetString("id", response->info->id); + info_json->SetString("client_data_json", + base::Base64Encode(response->info->client_data_json)); + info_json->SetString( + "authenticator_data", + base::Base64Encode(response->info->authenticator_data)); + } + + auto prf_results_json = std::make_unique<base::DictionaryValue>(); + if (response->prf_results) { + DCHECK(!response->prf_results->id.has_value()); + prf_results_json->SetString( + "first", base::Base64Encode(response->prf_results->first)); + if (response->prf_results->second) { + prf_results_json->SetString( + "second", base::Base64Encode(*response->prf_results->second)); + } + } + + base::DictionaryValue json; + json.Set("info", std::move(info_json)); + json.SetString("signature", base::Base64Encode(response->signature)); + if (response->user_handle.has_value()) { + json.SetString("user_handle", + base::Base64Encode(response->user_handle.value())); + } + json.SetBoolean("echo_appid_extension", response->echo_appid_extension); + json.SetBoolean("appid_extension", response->appid_extension); + json.SetBoolean("echo_prf", response->echo_prf); + json.Set("prf_results", std::move(prf_results_json)); + json.SetBoolean("prf_not_evaluated", response->echo_prf); + + std::string json_serialized_response; + base::JSONWriter::Write(json, &json_serialized_response); + delegate->OnInstrumentDetailsReady(methods::kSecurePaymentConfirmation, + json_serialized_response, PayerData()); +} + +} // namespace payments diff --git a/chromium/components/payments/content/secure_payment_confirmation_app.h b/chromium/components/payments/content/secure_payment_confirmation_app.h new file mode 100644 index 00000000000..bb7b89d101c --- /dev/null +++ b/chromium/components/payments/content/secure_payment_confirmation_app.h @@ -0,0 +1,115 @@ +// Copyright 2020 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. + +#ifndef COMPONENTS_PAYMENTS_CONTENT_SECURE_PAYMENT_CONFIRMATION_APP_H_ +#define COMPONENTS_PAYMENTS_CONTENT_SECURE_PAYMENT_CONFIRMATION_APP_H_ + +#include "components/payments/content/payment_app.h" + +#include <stdint.h> +#include <memory> +#include <string> +#include <vector> + +#include "base/memory/weak_ptr.h" +#include "base/strings/string16.h" +#include "components/payments/content/secure_payment_confirmation_controller.h" +#include "content/public/browser/web_contents_observer.h" +#include "third_party/blink/public/mojom/payments/payment_request.mojom.h" +#include "third_party/blink/public/mojom/webauthn/authenticator.mojom.h" +#include "url/origin.h" + +class SkBitmap; + +namespace autofill { +class InternalAuthenticator; +} // namespace autofill + +namespace content { +class RenderFrameHost; +class WebContents; +} // namespace content + +namespace payments { + +class SecurePaymentConfirmationApp : public PaymentApp, + public content::WebContentsObserver { + public: + // Please use `std::move()` for the `credential_id` parameter to avoid extra + // copies. + SecurePaymentConfirmationApp( + content::WebContents* web_contents_to_observe, + const std::string& effective_relying_party_identity, + std::unique_ptr<SkBitmap> icon, + const base::string16& label, + std::vector<uint8_t> credential_id, + const url::Origin& merchant_origin, + const mojom::PaymentCurrencyAmountPtr& total, + mojom::SecurePaymentConfirmationRequestPtr request, + std::unique_ptr<autofill::InternalAuthenticator> authenticator); + ~SecurePaymentConfirmationApp() override; + + SecurePaymentConfirmationApp(const SecurePaymentConfirmationApp& other) = + delete; + SecurePaymentConfirmationApp& operator=( + const SecurePaymentConfirmationApp& other) = delete; + + // PaymentApp implementation. + void InvokePaymentApp(Delegate* delegate) override; + bool IsCompleteForPayment() const override; + uint32_t GetCompletenessScore() const override; + bool CanPreselect() const override; + base::string16 GetMissingInfoLabel() const override; + bool HasEnrolledInstrument() const override; + void RecordUse() override; + bool NeedsInstallation() const override; + std::string GetId() const override; + base::string16 GetLabel() const override; + base::string16 GetSublabel() const override; + const SkBitmap* icon_bitmap() const override; + bool IsValidForModifier( + const std::string& method, + bool supported_networks_specified, + const std::set<std::string>& supported_networks) const override; + base::WeakPtr<PaymentApp> AsWeakPtr() override; + bool HandlesShippingAddress() const override; + bool HandlesPayerName() const override; + bool HandlesPayerEmail() const override; + bool HandlesPayerPhone() const override; + bool IsWaitingForPaymentDetailsUpdate() const override; + void UpdateWith( + mojom::PaymentRequestDetailsUpdatePtr details_update) override; + void OnPaymentDetailsNotUpdated() override; + void AbortPaymentApp(base::OnceCallback<void(bool)> abort_callback) override; + + // WebContentsObserver implementation. + void RenderFrameDeleted(content::RenderFrameHost* render_frame_host) override; + + private: + void OnGetAssertion( + Delegate* delegate, + blink::mojom::AuthenticatorStatus status, + blink::mojom::GetAssertionAuthenticatorResponsePtr response); + + // Used only for comparison with the RenderFrameHost pointer in + // RenderFrameDeleted() method. + const content::RenderFrameHost* const + authenticator_render_frame_host_pointer_do_not_dereference_; + + const std::string effective_relying_party_identity_; + const std::unique_ptr<SkBitmap> icon_; + const base::string16 label_; + const std::vector<uint8_t> credential_id_; + const std::string encoded_credential_id_; + const url::Origin merchant_origin_; + const mojom::PaymentCurrencyAmountPtr total_; + const mojom::SecurePaymentConfirmationRequestPtr request_; + std::unique_ptr<autofill::InternalAuthenticator> authenticator_; + + base::WeakPtrFactory<SecurePaymentConfirmationApp> weak_ptr_factory_{this}; +}; + +} // namespace payments + +#endif // COMPONENTS_PAYMENTS_CONTENT_SECURE_PAYMENT_CONFIRMATION_APP_H_ diff --git a/chromium/components/payments/content/secure_payment_confirmation_app_factory.cc b/chromium/components/payments/content/secure_payment_confirmation_app_factory.cc new file mode 100644 index 00000000000..4ca453903ff --- /dev/null +++ b/chromium/components/payments/content/secure_payment_confirmation_app_factory.cc @@ -0,0 +1,239 @@ +// Copyright 2020 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. + +#include "components/payments/content/secure_payment_confirmation_app_factory.h" + +#include <stdint.h> +#include <memory> +#include <string> +#include <utility> +#include <vector> + +#include "base/check.h" +#include "base/feature_list.h" +#include "base/stl_util.h" +#include "base/strings/utf_string_conversions.h" +#include "base/time/time.h" +#include "components/autofill/core/browser/payments/internal_authenticator.h" +#include "components/payments/content/payment_manifest_web_data_service.h" +#include "components/payments/content/payment_request_spec.h" +#include "components/payments/content/secure_payment_confirmation_app.h" +#include "components/payments/core/method_strings.h" +#include "components/payments/core/native_error_strings.h" +#include "components/payments/core/secure_payment_confirmation_instrument.h" +#include "components/webdata/common/web_data_results.h" +#include "components/webdata/common/web_data_service_base.h" +#include "content/public/browser/web_contents.h" +#include "content/public/browser/web_contents_observer.h" +#include "content/public/common/content_features.h" +#include "services/data_decoder/public/cpp/decode_image.h" +#include "third_party/blink/public/mojom/payments/payment_request.mojom.h" +#include "url/origin.h" + +namespace payments { +namespace { + +// Arbitrarily chosen limit of 1 hour. Keep in sync with +// secure_payment_confirmation_helper.cc. +constexpr int64_t kMaxTimeoutInMilliseconds = 1000 * 60 * 60; + +bool IsValid(const mojom::SecurePaymentConfirmationRequestPtr& request, + std::string* error_message) { + // `request` can be null when the feature is disabled in Blink. + if (!request) + return false; + + if (request->credential_ids.empty()) { + *error_message = errors::kCredentialIdsRequired; + return false; + } + + for (const auto& credential_id : request->credential_ids) { + if (credential_id.empty()) { + *error_message = errors::kCredentialIdsRequired; + return false; + } + } + + if (request->timeout.has_value() && + request->timeout.value().InMilliseconds() > kMaxTimeoutInMilliseconds) { + *error_message = errors::kTimeoutTooLong; + return false; + } + + return true; +} + +} // namespace + +void SecurePaymentConfirmationAppFactory:: + OnIsUserVerifyingPlatformAuthenticatorAvailable( + base::WeakPtr<PaymentAppFactory::Delegate> delegate, + mojom::SecurePaymentConfirmationRequestPtr request, + std::unique_ptr<autofill::InternalAuthenticator> authenticator, + bool is_available) { + if (!delegate) + return; + + if (!is_available && !base::FeatureList::IsEnabled( + features::kSecurePaymentConfirmationDebug)) { + delegate->OnDoneCreatingPaymentApps(); + return; + } + + // Regardless of whether `web_data_service` has any apps, canMakePayment() and + // hasEnrolledInstrument() should return true when a user-verifying platform + // authenticator device is available. + delegate->SetCanMakePaymentEvenWithoutApps(); + + scoped_refptr<payments::PaymentManifestWebDataService> web_data_service = + delegate->GetPaymentManifestWebDataService(); + if (!web_data_service) { + delegate->OnDoneCreatingPaymentApps(); + return; + } + + WebDataServiceBase::Handle handle = + web_data_service->GetSecurePaymentConfirmationInstruments( + std::move(request->credential_ids), this); + requests_[handle] = std::make_unique<Request>(delegate, std::move(request), + std::move(authenticator)); +} + +SecurePaymentConfirmationAppFactory::SecurePaymentConfirmationAppFactory() + : PaymentAppFactory(PaymentApp::Type::INTERNAL) {} + +SecurePaymentConfirmationAppFactory::~SecurePaymentConfirmationAppFactory() = + default; + +void SecurePaymentConfirmationAppFactory::Create( + base::WeakPtr<Delegate> delegate) { + DCHECK(delegate); + + PaymentRequestSpec* spec = delegate->GetSpec(); + if (!spec || !base::Contains(spec->payment_method_identifiers_set(), + methods::kSecurePaymentConfirmation)) { + delegate->OnDoneCreatingPaymentApps(); + return; + } + + for (const mojom::PaymentMethodDataPtr& method_data : spec->method_data()) { + if (method_data->supported_method == methods::kSecurePaymentConfirmation) { + std::string error_message; + if (!IsValid(method_data->secure_payment_confirmation, &error_message)) { + if (!error_message.empty()) + delegate->OnPaymentAppCreationError(error_message); + delegate->OnDoneCreatingPaymentApps(); + return; + } + + std::unique_ptr<autofill::InternalAuthenticator> authenticator = + delegate->CreateInternalAuthenticator(); + + authenticator->IsUserVerifyingPlatformAuthenticatorAvailable( + base::BindOnce(&SecurePaymentConfirmationAppFactory:: + OnIsUserVerifyingPlatformAuthenticatorAvailable, + weak_ptr_factory_.GetWeakPtr(), delegate, + method_data->secure_payment_confirmation.Clone(), + std::move(authenticator))); + return; + } + } + + delegate->OnDoneCreatingPaymentApps(); +} + +struct SecurePaymentConfirmationAppFactory::Request + : public content::WebContentsObserver { + Request(base::WeakPtr<PaymentAppFactory::Delegate> delegate, + mojom::SecurePaymentConfirmationRequestPtr mojo_request, + std::unique_ptr<autofill::InternalAuthenticator> authenticator) + : content::WebContentsObserver(delegate->GetWebContents()), + delegate(delegate), + mojo_request(std::move(mojo_request)), + authenticator(std::move(authenticator)) {} + + ~Request() override = default; + + Request(const Request& other) = delete; + Request& operator=(const Request& other) = delete; + + base::WeakPtr<PaymentAppFactory::Delegate> delegate; + mojom::SecurePaymentConfirmationRequestPtr mojo_request; + std::unique_ptr<autofill::InternalAuthenticator> authenticator; +}; + +void SecurePaymentConfirmationAppFactory::OnWebDataServiceRequestDone( + WebDataServiceBase::Handle handle, + std::unique_ptr<WDTypedResult> result) { + auto iterator = requests_.find(handle); + if (iterator == requests_.end()) + return; + + std::unique_ptr<Request> request = std::move(iterator->second); + requests_.erase(iterator); + DCHECK(request.get()); + if (!request->delegate || !request->web_contents()) + return; + + if (!result || result->GetType() != SECURE_PAYMENT_CONFIRMATION) { + request->delegate->OnDoneCreatingPaymentApps(); + return; + } + + std::vector<std::unique_ptr<SecurePaymentConfirmationInstrument>> + instruments = static_cast<WDResult< + std::vector<std::unique_ptr<SecurePaymentConfirmationInstrument>>>*>( + result.get()) + ->GetValue(); + if (instruments.empty()) { + request->delegate->OnDoneCreatingPaymentApps(); + return; + } + + // For the pilot phase, arbitrarily use the first matching instrument. + // TODO(https://crbug.com/1110320): Handle multiple instruments. + std::unique_ptr<SecurePaymentConfirmationInstrument> instrument = + std::move(instruments.front()); + + auto* instrument_ptr = instrument.get(); + // Decode the icon in a sandboxed process off the main thread. + data_decoder::DecodeImageIsolated( + instrument_ptr->icon, data_decoder::mojom::ImageCodec::DEFAULT, + /*shrink_to_fit=*/false, data_decoder::kDefaultMaxSizeInBytes, + /*desired_image_frame_size=*/gfx::Size(), + base::BindOnce(&SecurePaymentConfirmationAppFactory::OnAppIconDecoded, + weak_ptr_factory_.GetWeakPtr(), std::move(instrument), + std::move(request))); +} + +void SecurePaymentConfirmationAppFactory::OnAppIconDecoded( + std::unique_ptr<SecurePaymentConfirmationInstrument> instrument, + std::unique_ptr<Request> request, + const SkBitmap& decoded_icon) { + DCHECK(request); + if (!request->delegate || !request->web_contents() || + !request->delegate->GetSpec() || + request->authenticator->GetRenderFrameHost() != + request->web_contents()->GetMainFrame()) { + request->delegate->OnDoneCreatingPaymentApps(); + return; + } + + DCHECK(!decoded_icon.drawsNothing()); + auto icon = std::make_unique<SkBitmap>(decoded_icon); + + request->delegate->OnPaymentAppCreated( + std::make_unique<SecurePaymentConfirmationApp>( + request->web_contents(), instrument->relying_party_id, + std::move(icon), instrument->label, + std::move(instrument->credential_id), + url::Origin::Create(request->delegate->GetTopOrigin()), + request->delegate->GetSpec()->details().total->amount, + std::move(request->mojo_request), std::move(request->authenticator))); + + request->delegate->OnDoneCreatingPaymentApps(); +} + +} // namespace payments diff --git a/chromium/components/payments/content/secure_payment_confirmation_app_factory.h b/chromium/components/payments/content/secure_payment_confirmation_app_factory.h new file mode 100644 index 00000000000..3eab505598b --- /dev/null +++ b/chromium/components/payments/content/secure_payment_confirmation_app_factory.h @@ -0,0 +1,59 @@ +// Copyright 2020 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. + +#ifndef COMPONENTS_PAYMENTS_CONTENT_SECURE_PAYMENT_CONFIRMATION_APP_FACTORY_H_ +#define COMPONENTS_PAYMENTS_CONTENT_SECURE_PAYMENT_CONFIRMATION_APP_FACTORY_H_ + +#include <map> +#include <memory> + +#include "base/memory/weak_ptr.h" +#include "components/payments/content/payment_app_factory.h" +#include "components/webdata/common/web_data_service_consumer.h" + +namespace payments { + +struct SecurePaymentConfirmationInstrument; + +class SecurePaymentConfirmationAppFactory : public PaymentAppFactory, + public WebDataServiceConsumer { + public: + SecurePaymentConfirmationAppFactory(); + ~SecurePaymentConfirmationAppFactory() override; + + SecurePaymentConfirmationAppFactory( + const SecurePaymentConfirmationAppFactory& other) = delete; + SecurePaymentConfirmationAppFactory& operator=( + const SecurePaymentConfirmationAppFactory& other) = delete; + + // PaymentAppFactory: + void Create(base::WeakPtr<Delegate> delegate) override; + + private: + struct Request; + + // WebDataServiceConsumer: + void OnWebDataServiceRequestDone( + WebDataServiceBase::Handle handle, + std::unique_ptr<WDTypedResult> result) override; + + void OnIsUserVerifyingPlatformAuthenticatorAvailable( + base::WeakPtr<PaymentAppFactory::Delegate> delegate, + mojom::SecurePaymentConfirmationRequestPtr request, + std::unique_ptr<autofill::InternalAuthenticator> authenticator, + bool is_available); + + void OnAppIconDecoded( + std::unique_ptr<SecurePaymentConfirmationInstrument> instrument, + std::unique_ptr<Request> request, + const SkBitmap& decoded_image); + + std::map<WebDataServiceBase::Handle, std::unique_ptr<Request>> requests_; + base::WeakPtrFactory<SecurePaymentConfirmationAppFactory> weak_ptr_factory_{ + this}; +}; + +} // namespace payments + +#endif // COMPONENTS_PAYMENTS_CONTENT_SECURE_PAYMENT_CONFIRMATION_APP_FACTORY_H_ diff --git a/chromium/components/payments/content/secure_payment_confirmation_app_unittest.cc b/chromium/components/payments/content/secure_payment_confirmation_app_unittest.cc new file mode 100644 index 00000000000..1a00229a37b --- /dev/null +++ b/chromium/components/payments/content/secure_payment_confirmation_app_unittest.cc @@ -0,0 +1,129 @@ +// Copyright 2020 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. + +#include "components/payments/content/secure_payment_confirmation_app.h" + +#include <memory> + +#include "base/base64.h" +#include "base/strings/string_piece.h" +#include "base/strings/utf_string_conversions.h" +#include "components/autofill/core/browser/payments/internal_authenticator.h" +#include "content/public/browser/web_contents.h" +#include "content/public/test/browser_task_environment.h" +#include "content/public/test/test_browser_context.h" +#include "content/public/test/test_web_contents_factory.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/blink/public/mojom/webauthn/authenticator.mojom.h" +#include "url/origin.h" + +namespace payments { +namespace { + +using ::testing::_; +using ::testing::Eq; + +static constexpr char kNetworkDataBase64[] = "aaaa"; +static constexpr char kCredentialIdBase64[] = "cccc"; + +class MockAuthenticator : public autofill::InternalAuthenticator { + public: + MockAuthenticator() + : web_contents_(web_contents_factory_.CreateWebContents(&context_)) {} + ~MockAuthenticator() override = default; + + MOCK_METHOD1(SetEffectiveOrigin, void(const url::Origin&)); + MOCK_METHOD2( + MakeCredential, + void(blink::mojom::PublicKeyCredentialCreationOptionsPtr options, + blink::mojom::Authenticator::MakeCredentialCallback callback)); + MOCK_METHOD1(IsUserVerifyingPlatformAuthenticatorAvailable, + void(blink::mojom::Authenticator:: + IsUserVerifyingPlatformAuthenticatorAvailableCallback)); + MOCK_METHOD0(Cancel, void()); + MOCK_METHOD1(VerifyChallenge, void(const std::vector<uint8_t>&)); + + content::RenderFrameHost* GetRenderFrameHost() override { + return web_contents_->GetMainFrame(); + } + + // Implements an autofill::InternalAuthenticator method to delegate fields of + // |options| to gmock methods for easier verification. + void GetAssertion( + blink::mojom::PublicKeyCredentialRequestOptionsPtr options, + blink::mojom::Authenticator::GetAssertionCallback callback) override { + VerifyChallenge(options->challenge); + } + + content::WebContents* web_contents() { return web_contents_; } + + private: + content::BrowserTaskEnvironment task_environment_; + content::TestBrowserContext context_; + content::TestWebContentsFactory web_contents_factory_; + content::WebContents* web_contents_; // Owned by `web_contents_factory_`. +}; + +class SecurePaymentConfirmationAppTest : public testing::Test { + protected: + SecurePaymentConfirmationAppTest() + : label_(base::ASCIIToUTF16("test instrument")), + total_(mojom::PaymentCurrencyAmount::New()) { + total_->currency = "USD"; + total_->value = "1.25"; + } + + void SetUp() override { + ASSERT_TRUE(base::Base64Decode(kNetworkDataBase64, &network_data_bytes_)); + ASSERT_TRUE(base::Base64Decode(kCredentialIdBase64, &credential_id_bytes_)); + } + + mojom::SecurePaymentConfirmationRequestPtr MakeRequest() { + auto request = mojom::SecurePaymentConfirmationRequest::New(); + request->network_data = std::vector<uint8_t>(network_data_bytes_.begin(), + network_data_bytes_.end()); + return request; + } + + base::string16 label_; + mojom::PaymentCurrencyAmountPtr total_; + std::string network_data_bytes_; + std::string credential_id_bytes_; +}; + +TEST_F(SecurePaymentConfirmationAppTest, Smoke) { + std::vector<uint8_t> credential_id(credential_id_bytes_.begin(), + credential_id_bytes_.end()); + + auto authenticator = std::make_unique<MockAuthenticator>(); + MockAuthenticator* mock_authenticator = authenticator.get(); + content::WebContents* web_contents = authenticator->web_contents(); + + SecurePaymentConfirmationApp app( + web_contents, "effective_rp.example", + /*icon=*/std::make_unique<SkBitmap>(), label_, std::move(credential_id), + url::Origin::Create(GURL("https://merchant.example")), total_, + MakeRequest(), std::move(authenticator)); + + EXPECT_CALL(*mock_authenticator, SetEffectiveOrigin(Eq(url::Origin::Create( + GURL("https://effective_rp.example"))))); + + // This is the SHA-256 hash of the serialized JSON string: + // {"merchantData":{"merchantOrigin":"https://merchant.example","total": + // {"currency":"USD","value":"1.25"}},"networkData":"aaaa"} + // + // To update the test expectation, open + // //components/test/data/payments/secure_payment_confirmation_debut.html in a + // browser and follow the instructions. + std::vector<uint8_t> expected_bytes = { + 240, 123, 37, 51, 16, 34, 244, 220, 166, 179, 139, + 85, 229, 152, 242, 133, 88, 44, 222, 133, 49, 97, + 146, 20, 207, 119, 43, 142, 171, 239, 125, 250}; + EXPECT_CALL(*mock_authenticator, VerifyChallenge(Eq(expected_bytes))); + app.InvokePaymentApp(/*delegate=*/nullptr); +} + +} // namespace +} // namespace payments diff --git a/chromium/components/payments/content/secure_payment_confirmation_controller.cc b/chromium/components/payments/content/secure_payment_confirmation_controller.cc new file mode 100644 index 00000000000..2b96947ad68 --- /dev/null +++ b/chromium/components/payments/content/secure_payment_confirmation_controller.cc @@ -0,0 +1,207 @@ +// Copyright 2020 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. + +#include "components/payments/content/secure_payment_confirmation_controller.h" + +#include "base/bind.h" +#include "base/check.h" +#include "base/location.h" +#include "base/strings/strcat.h" +#include "base/strings/utf_string_conversions.h" +#include "base/threading/thread_task_runner_handle.h" +#include "build/build_config.h" +#include "components/payments/content/payment_request.h" +#include "components/payments/core/currency_formatter.h" +#include "components/payments/core/method_strings.h" +#include "components/strings/grit/components_strings.h" +#include "components/url_formatter/elide_url.h" +#include "ui/base/l10n/l10n_util.h" + +namespace payments { + +SecurePaymentConfirmationController::SecurePaymentConfirmationController( + base::WeakPtr<PaymentRequest> request) + : request_(request) { + DCHECK(request_); +} + +SecurePaymentConfirmationController::~SecurePaymentConfirmationController() = + default; + +void SecurePaymentConfirmationController::ShowDialog() { +#if defined(OS_ANDROID) + NOTREACHED(); +#endif // OS_ANDROID + + if (!request_ || !request_->spec()) + return; + + if (!request_->state()->IsInitialized()) { + number_of_initialization_tasks_++; + request_->state()->AddInitializationObserver(this); + } + + if (!request_->spec()->IsInitialized()) { + number_of_initialization_tasks_++; + request_->spec()->AddInitializationObserver(this); + } + + if (number_of_initialization_tasks_ == 0) + SetupModelAndShowDialogIfApplicable(); +} + +void SecurePaymentConfirmationController:: + SetupModelAndShowDialogIfApplicable() { + DCHECK(!view_); + // If no apps are available then don't show any UI. The payment_request.cc + // code will reject the PaymentRequest.show() call with appropriate error + // message on its own. + if (!request_ || !request_->state() || !request_->spec() || + request_->state()->available_apps().empty()) { + return; + } + + if (!request_->web_contents() || !request_->state()->selected_app() || + request_->state()->selected_app()->type() != PaymentApp::Type::INTERNAL || + request_->state()->selected_app()->GetAppMethodNames().size() != 1 || + *request_->state()->selected_app()->GetAppMethodNames().begin() != + methods::kSecurePaymentConfirmation || + request_->state()->available_apps().size() != 1 || !request_->spec() || + !request_->spec()->IsSecurePaymentConfirmationRequested() || + request_->spec()->request_shipping() || + request_->spec()->request_payer_name() || + request_->spec()->request_payer_email() || + request_->spec()->request_payer_phone()) { + OnCancel(); + return; + } + + model_.set_verify_button_label(l10n_util::GetStringUTF16( + IDS_SECURE_PAYMENT_CONFIRMATION_VERIFY_BUTTON_LABEL)); + model_.set_cancel_button_label(l10n_util::GetStringUTF16(IDS_CANCEL)); + model_.set_progress_bar_visible(false); + + model_.set_title(l10n_util::GetStringUTF16( + IDS_SECURE_PAYMENT_CONFIRMATION_VERIFY_PURCHASE)); + + model_.set_merchant_label( + l10n_util::GetStringUTF16(IDS_SECURE_PAYMENT_CONFIRMATION_STORE_LABEL)); + model_.set_merchant_value(url_formatter::FormatUrlForSecurityDisplay( + request_->state()->GetTopOrigin(), + url_formatter::SchemeDisplay::OMIT_CRYPTOGRAPHIC)); + + model_.set_instrument_label(l10n_util::GetStringUTF16( + IDS_PAYMENT_REQUEST_PAYMENT_METHOD_SECTION_NAME)); + PaymentApp* app = request_->state()->selected_app(); + model_.set_instrument_value(app->GetLabel()); + model_.set_instrument_icon(app->icon_bitmap()); + + model_.set_total_label( + l10n_util::GetStringUTF16(IDS_SECURE_PAYMENT_CONFIRMATION_TOTAL_LABEL)); + const mojom::PaymentItemPtr& total = request_->spec()->GetTotal(app); + base::string16 total_value = base::UTF8ToUTF16(total->amount->currency); + model_.set_total_value(base::StrCat( + {base::UTF8ToUTF16(total->amount->currency), base::ASCIIToUTF16(" "), + CurrencyFormatter(total->amount->currency, + request_->state()->GetApplicationLocale()) + .Format(total->amount->value)})); + + view_ = SecurePaymentConfirmationView::Create(); + view_->ShowDialog( + request_->web_contents(), model_.GetWeakPtr(), + base::BindOnce(&SecurePaymentConfirmationController::OnConfirm, + weak_ptr_factory_.GetWeakPtr()), + base::BindOnce(&SecurePaymentConfirmationController::OnCancel, + weak_ptr_factory_.GetWeakPtr())); +} + +void SecurePaymentConfirmationController::RetryDialog() { + // Retry is not supported. + OnCancel(); +} + +void SecurePaymentConfirmationController::CloseDialog() { + if (view_) + view_->HideDialog(); +} + +void SecurePaymentConfirmationController::ShowErrorMessage() { + // Error message is not supported. + OnCancel(); +} + +void SecurePaymentConfirmationController::ShowProcessingSpinner() { + if (!view_) + return; + + model_.set_progress_bar_visible(true); + model_.set_verify_button_enabled(false); + model_.set_cancel_button_enabled(false); + view_->OnModelUpdated(); +} + +bool SecurePaymentConfirmationController::IsInteractive() const { + return view_ && !model_.progress_bar_visible(); +} + +void SecurePaymentConfirmationController::ShowCvcUnmaskPrompt( + const autofill::CreditCard& credit_card, + base::WeakPtr<autofill::payments::FullCardRequest::ResultDelegate> + result_delegate, + content::WebContents* web_contents) { + // CVC unmasking is nut supported. + NOTREACHED(); +} + +void SecurePaymentConfirmationController::ShowPaymentHandlerScreen( + const GURL& url, + PaymentHandlerOpenWindowCallback callback) { + // Payment handler screen is not supported. + NOTREACHED(); +} + +void SecurePaymentConfirmationController::ConfirmPaymentForTesting() { + OnConfirm(); +} + +void SecurePaymentConfirmationController::OnInitialized( + InitializationTask* initialization_task) { + if (--number_of_initialization_tasks_ == 0) + SetupModelAndShowDialogIfApplicable(); +} + +void SecurePaymentConfirmationController::OnDismiss() { + OnCancel(); +} + +void SecurePaymentConfirmationController::OnCancel() { + CloseDialog(); + + if (!request_) + return; + + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(&PaymentRequest::UserCancelled, request_)); +} + +void SecurePaymentConfirmationController::OnConfirm() { + if (!request_) + return; + + ShowProcessingSpinner(); + + // This will trigger WebAuthn with OS-level UI (if any) on top of the |view_| + // with its animated processing spinner. For example, on Linux, there's no + // OS-level UI, while on MacOS, there's an OS-level prompt for the Touch ID + // that shows on top of Chrome. + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(&PaymentRequest::Pay, request_)); +} + +base::WeakPtr<SecurePaymentConfirmationController> +SecurePaymentConfirmationController::GetWeakPtr() { + return weak_ptr_factory_.GetWeakPtr(); +} + +} // namespace payments diff --git a/chromium/components/payments/content/secure_payment_confirmation_controller.h b/chromium/components/payments/content/secure_payment_confirmation_controller.h new file mode 100644 index 00000000000..4917544d606 --- /dev/null +++ b/chromium/components/payments/content/secure_payment_confirmation_controller.h @@ -0,0 +1,80 @@ +// Copyright 2020 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. + +#ifndef COMPONENTS_PAYMENTS_CONTENT_SECURE_PAYMENT_CONFIRMATION_CONTROLLER_H_ +#define COMPONENTS_PAYMENTS_CONTENT_SECURE_PAYMENT_CONFIRMATION_CONTROLLER_H_ + +#include "base/memory/weak_ptr.h" +#include "components/payments/content/initialization_task.h" +#include "components/payments/content/payment_request_dialog.h" +#include "components/payments/content/secure_payment_confirmation_model.h" +#include "components/payments/content/secure_payment_confirmation_view.h" + +namespace payments { + +class PaymentRequest; + +// Controls the user interface in the secure payment confirmation flow. +class SecurePaymentConfirmationController + : public PaymentRequestDialog, + public InitializationTask::Observer { + public: + explicit SecurePaymentConfirmationController( + base::WeakPtr<PaymentRequest> request); + ~SecurePaymentConfirmationController() override; + + SecurePaymentConfirmationController( + const SecurePaymentConfirmationController& other) = delete; + SecurePaymentConfirmationController& operator=( + const SecurePaymentConfirmationController& other) = delete; + + // PaymentRequestDialog: + void ShowDialog() override; + void RetryDialog() override; + void CloseDialog() override; + void ShowErrorMessage() override; + void ShowProcessingSpinner() override; + bool IsInteractive() const override; + void ShowCvcUnmaskPrompt( + const autofill::CreditCard& credit_card, + base::WeakPtr<autofill::payments::FullCardRequest::ResultDelegate> + result_delegate, + content::WebContents* web_contents) override; + void ShowPaymentHandlerScreen( + const GURL& url, + PaymentHandlerOpenWindowCallback callback) override; + void ConfirmPaymentForTesting() override; + + // InitializationTask::Observer: + void OnInitialized(InitializationTask* initialization_task) override; + + // Callbacks for user interaction. + void OnDismiss(); + void OnCancel(); + void OnConfirm(); + + base::WeakPtr<SecurePaymentConfirmationController> GetWeakPtr(); + + private: + void SetupModelAndShowDialogIfApplicable(); + + base::WeakPtr<PaymentRequest> request_; + + SecurePaymentConfirmationModel model_; + + // On desktop, the SecurePaymentConfirmationView object is memory managed by + // the views:: machinery. It is deleted when the window is closed and + // views::DialogDelegateView::DeleteDelegate() is called by its corresponding + // views::Widget. + base::WeakPtr<SecurePaymentConfirmationView> view_; + + int number_of_initialization_tasks_ = 0; + + base::WeakPtrFactory<SecurePaymentConfirmationController> weak_ptr_factory_{ + this}; +}; + +} // namespace payments + +#endif // COMPONENTS_PAYMENTS_CONTENT_SECURE_PAYMENT_CONFIRMATION_CONTROLLER_H_ diff --git a/chromium/components/payments/content/secure_payment_confirmation_model.cc b/chromium/components/payments/content/secure_payment_confirmation_model.cc new file mode 100644 index 00000000000..416252ac059 --- /dev/null +++ b/chromium/components/payments/content/secure_payment_confirmation_model.cc @@ -0,0 +1,18 @@ +// Copyright 2020 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. + +#include "components/payments/content/secure_payment_confirmation_model.h" + +namespace payments { + +SecurePaymentConfirmationModel::SecurePaymentConfirmationModel() = default; + +SecurePaymentConfirmationModel::~SecurePaymentConfirmationModel() = default; + +base::WeakPtr<SecurePaymentConfirmationModel> +SecurePaymentConfirmationModel::GetWeakPtr() { + return weak_ptr_factory_.GetWeakPtr(); +} + +} // namespace payments diff --git a/chromium/components/payments/content/secure_payment_confirmation_model.h b/chromium/components/payments/content/secure_payment_confirmation_model.h new file mode 100644 index 00000000000..ab1cba303b2 --- /dev/null +++ b/chromium/components/payments/content/secure_payment_confirmation_model.h @@ -0,0 +1,150 @@ +// Copyright 2020 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. + +#ifndef COMPONENTS_PAYMENTS_CONTENT_SECURE_PAYMENT_CONFIRMATION_MODEL_H_ +#define COMPONENTS_PAYMENTS_CONTENT_SECURE_PAYMENT_CONFIRMATION_MODEL_H_ + +#include "base/memory/weak_ptr.h" +#include "base/strings/string16.h" +#include "third_party/skia/include/core/SkBitmap.h" + +namespace payments { + +// The data model for the secure payment confirmation flow. Owned by the +// SecurePaymentConfirmationController. +class SecurePaymentConfirmationModel { + public: + SecurePaymentConfirmationModel(); + ~SecurePaymentConfirmationModel(); + + // Disallow copy and assign. + SecurePaymentConfirmationModel(const SecurePaymentConfirmationModel& other) = + delete; + SecurePaymentConfirmationModel& operator=( + const SecurePaymentConfirmationModel& other) = delete; + + // Title, e.g. "Use TouchID to verify and complete your purchase?" + const base::string16& title() const { return title_; } + void set_title(const base::string16& title) { title_ = title; } + + // Label for the merchant row, e.g. "Store". + const base::string16& merchant_label() const { return merchant_label_; } + void set_merchant_label(const base::string16& merchant_label) { + merchant_label_ = merchant_label; + } + + // Label for the merchant row value, e.g. "merchant.com" + const base::string16& merchant_value() const { return merchant_value_; } + void set_merchant_value(const base::string16& merchant_value) { + merchant_value_ = merchant_value; + } + + // Label for the instrument row, e.g. "Payment". + const base::string16& instrument_label() const { return instrument_label_; } + void set_instrument_label(const base::string16& instrument_label) { + instrument_label_ = instrument_label; + } + + // Label for the instrument row value, e.g. "Mastercard ****4444" + const base::string16& instrument_value() const { return instrument_value_; } + void set_instrument_value(const base::string16& instrument_value) { + instrument_value_ = instrument_value; + } + + // Instrument icon. + const SkBitmap* instrument_icon() const { return instrument_icon_; } + void set_instrument_icon(const SkBitmap* instrument_icon) { + instrument_icon_ = instrument_icon; + } + + // Label for the total row, e.g. "Total". + const base::string16& total_label() const { return total_label_; } + void set_total_label(const base::string16& total_label) { + total_label_ = total_label; + } + + // Label for the total row value, e.g. "$20.00 USD" + const base::string16& total_value() const { return total_value_; } + void set_total_value(const base::string16& total_value) { + total_value_ = total_value; + } + + // Label for the verify button, e.g. "Verify". + const base::string16& verify_button_label() const { + return verify_button_label_; + } + void set_verify_button_label(const base::string16& verify_button_label) { + verify_button_label_ = verify_button_label; + } + + // Label for the cancel button, e.g. "Cancel". + const base::string16& cancel_button_label() const { + return cancel_button_label_; + } + void set_cancel_button_label(const base::string16& cancel_button_label) { + cancel_button_label_ = cancel_button_label; + } + + // Progress bar visibility. + bool progress_bar_visible() const { return progress_bar_visible_; } + void set_progress_bar_visible(bool progress_bar_visible) { + progress_bar_visible_ = progress_bar_visible; + } + + // Verify button enabled state. + bool verify_button_enabled() const { return verify_button_enabled_; } + void set_verify_button_enabled(bool verify_button_enabled) { + verify_button_enabled_ = verify_button_enabled; + } + + // Verify button visibility. + bool verify_button_visible() const { return verify_button_visible_; } + void set_verify_button_visible(bool verify_button_visible) { + verify_button_visible_ = verify_button_visible; + } + + // Cancel button enabled state. + bool cancel_button_enabled() const { return cancel_button_enabled_; } + void set_cancel_button_enabled(bool cancel_button_enabled) { + cancel_button_enabled_ = cancel_button_enabled; + } + + // Cancel button visibility. + bool cancel_button_visible() const { return cancel_button_visible_; } + void set_cancel_button_visible(bool cancel_button_visible) { + cancel_button_visible_ = cancel_button_visible; + } + + base::WeakPtr<SecurePaymentConfirmationModel> GetWeakPtr(); + + private: + base::string16 title_; + + base::string16 merchant_label_; + base::string16 merchant_value_; + + base::string16 instrument_label_; + base::string16 instrument_value_; + const SkBitmap* instrument_icon_ = nullptr; + + base::string16 total_label_; + base::string16 total_value_; + + base::string16 verify_button_label_; + base::string16 cancel_button_label_; + + bool progress_bar_visible_ = false; + + bool verify_button_enabled_ = true; + bool verify_button_visible_ = true; + + bool cancel_button_enabled_ = true; + bool cancel_button_visible_ = true; + + base::WeakPtrFactory<SecurePaymentConfirmationModel> weak_ptr_factory_{this}; +}; + +} // namespace payments + +#endif // COMPONENTS_PAYMENTS_CONTENT_SECURE_PAYMENT_CONFIRMATION_MODEL_H_ diff --git a/chromium/components/payments/content/secure_payment_confirmation_model_unittest.cc b/chromium/components/payments/content/secure_payment_confirmation_model_unittest.cc new file mode 100644 index 00000000000..0d4eef1433e --- /dev/null +++ b/chromium/components/payments/content/secure_payment_confirmation_model_unittest.cc @@ -0,0 +1,82 @@ +// Copyright 2020 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. + +#include "components/payments/content/secure_payment_confirmation_model.h" + +#include "base/strings/string16.h" +#include "base/strings/utf_string_conversions.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/skia/include/core/SkBitmap.h" +#include "ui/gfx/vector_icon_types.h" + +namespace payments { + +class SecurePaymentConfirmationModelTest : public testing::Test {}; + +TEST_F(SecurePaymentConfirmationModelTest, SmokeTest) { + SecurePaymentConfirmationModel model; + + base::string16 title( + base::UTF8ToUTF16("Use Touch ID to verify and complete your purchase?")); + base::string16 merchant_label(base::UTF8ToUTF16("Store")); + base::string16 merchant_value(base::UTF8ToUTF16("merchant.com")); + base::string16 instrument_label(base::UTF8ToUTF16("Payment")); + base::string16 instrument_value(base::UTF8ToUTF16("Mastercard ****4444")); + SkBitmap instrument_icon; + base::string16 total_label(base::UTF8ToUTF16("Total")); + base::string16 total_value(base::UTF8ToUTF16("$20.00 USD")); + base::string16 verify_button_label(base::UTF8ToUTF16("Verify")); + base::string16 cancel_button_label(base::UTF8ToUTF16("Cancel")); + + model.set_title(title); + EXPECT_EQ(title, model.title()); + + model.set_merchant_label(merchant_label); + EXPECT_EQ(merchant_label, model.merchant_label()); + + model.set_merchant_value(merchant_value); + EXPECT_EQ(merchant_value, model.merchant_value()); + + model.set_instrument_label(instrument_label); + EXPECT_EQ(instrument_label, model.instrument_label()); + + model.set_instrument_value(instrument_value); + EXPECT_EQ(instrument_value, model.instrument_value()); + + model.set_instrument_icon(&instrument_icon); + EXPECT_EQ(&instrument_icon, model.instrument_icon()); + + model.set_total_label(total_label); + EXPECT_EQ(total_label, model.total_label()); + + model.set_total_value(total_value); + EXPECT_EQ(total_value, model.total_value()); + + model.set_verify_button_label(verify_button_label); + EXPECT_EQ(verify_button_label, model.verify_button_label()); + + model.set_cancel_button_label(cancel_button_label); + EXPECT_EQ(cancel_button_label, model.cancel_button_label()); + + // Default values for visibility and enabled states + EXPECT_FALSE(model.progress_bar_visible()); + EXPECT_TRUE(model.verify_button_enabled()); + EXPECT_TRUE(model.verify_button_visible()); + EXPECT_TRUE(model.cancel_button_enabled()); + EXPECT_TRUE(model.cancel_button_visible()); + + model.set_progress_bar_visible(true); + model.set_verify_button_enabled(false); + model.set_verify_button_visible(false); + model.set_cancel_button_enabled(false); + model.set_cancel_button_visible(false); + + EXPECT_TRUE(model.progress_bar_visible()); + EXPECT_FALSE(model.verify_button_enabled()); + EXPECT_FALSE(model.verify_button_visible()); + EXPECT_FALSE(model.cancel_button_enabled()); + EXPECT_FALSE(model.cancel_button_visible()); +} + +} // namespace payments diff --git a/chromium/components/payments/content/secure_payment_confirmation_view.h b/chromium/components/payments/content/secure_payment_confirmation_view.h new file mode 100644 index 00000000000..ea80e2b0466 --- /dev/null +++ b/chromium/components/payments/content/secure_payment_confirmation_view.h @@ -0,0 +1,45 @@ +// Copyright 2020 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. + +#ifndef COMPONENTS_PAYMENTS_CONTENT_SECURE_PAYMENT_CONFIRMATION_VIEW_H_ +#define COMPONENTS_PAYMENTS_CONTENT_SECURE_PAYMENT_CONFIRMATION_VIEW_H_ + +#include "base/callback_forward.h" +#include "base/memory/weak_ptr.h" + +namespace content { +class WebContents; +} // namespace content + +namespace payments { + +class SecurePaymentConfirmationModel; + +// Draws the user interface in the secure payment confirmation flow. Owned by +// the SecurePaymentConfirmationController. +class SecurePaymentConfirmationView { + public: + using VerifyCallback = base::OnceCallback<void()>; + using CancelCallback = base::OnceCallback<void()>; + + static base::WeakPtr<SecurePaymentConfirmationView> Create(); + + virtual ~SecurePaymentConfirmationView() = 0; + + virtual void ShowDialog(content::WebContents* web_contents, + base::WeakPtr<SecurePaymentConfirmationModel> model, + VerifyCallback verify_callback, + CancelCallback cancel_callback) = 0; + virtual void OnModelUpdated() = 0; + virtual void HideDialog() = 0; + + protected: + SecurePaymentConfirmationView(); + + base::WeakPtr<SecurePaymentConfirmationModel> model_; +}; + +} // namespace payments + +#endif // COMPONENTS_PAYMENTS_CONTENT_SECURE_PAYMENT_CONFIRMATION_VIEW_H_ diff --git a/chromium/components/payments/content/secure_payment_confirmation_view_stub.cc b/chromium/components/payments/content/secure_payment_confirmation_view_stub.cc new file mode 100644 index 00000000000..6241d62f0b0 --- /dev/null +++ b/chromium/components/payments/content/secure_payment_confirmation_view_stub.cc @@ -0,0 +1,15 @@ +// Copyright 2020 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. + +#include "components/payments/content/secure_payment_confirmation_view.h" + +namespace payments { + +// static +base::WeakPtr<SecurePaymentConfirmationView> +SecurePaymentConfirmationView::Create() { + return nullptr; +} + +} // namespace payments diff --git a/chromium/components/payments/content/service_worker_payment_app.cc b/chromium/components/payments/content/service_worker_payment_app.cc index 6d51dcaca5d..1848cdae159 100644 --- a/chromium/components/payments/content/service_worker_payment_app.cc +++ b/chromium/components/payments/content/service_worker_payment_app.cc @@ -32,7 +32,7 @@ ServiceWorkerPaymentApp::ServiceWorkerPaymentApp( content::WebContents* web_contents, const GURL& top_origin, const GURL& frame_origin, - const PaymentRequestSpec* spec, + PaymentRequestSpec* spec, std::unique_ptr<content::StoredPaymentApp> stored_payment_app_info, bool is_incognito, const base::RepeatingClosure& show_processing_spinner) @@ -40,7 +40,7 @@ ServiceWorkerPaymentApp::ServiceWorkerPaymentApp( content::WebContentsObserver(web_contents), top_origin_(top_origin), frame_origin_(frame_origin), - spec_(spec), + spec_(spec->GetWeakPtr()), stored_payment_app_info_(std::move(stored_payment_app_info)), delegate_(nullptr), is_incognito_(is_incognito), @@ -51,7 +51,6 @@ ServiceWorkerPaymentApp::ServiceWorkerPaymentApp( DCHECK(web_contents); DCHECK(top_origin_.is_valid()); DCHECK(frame_origin_.is_valid()); - DCHECK(spec_); app_method_names_.insert(stored_payment_app_info_->enabled_methods.begin(), stored_payment_app_info_->enabled_methods.end()); @@ -63,7 +62,7 @@ ServiceWorkerPaymentApp::ServiceWorkerPaymentApp( content::WebContents* web_contents, const GURL& top_origin, const GURL& frame_origin, - const PaymentRequestSpec* spec, + PaymentRequestSpec* spec, std::unique_ptr<WebAppInstallationInfo> installable_payment_app_info, const std::string& enabled_method, bool is_incognito, @@ -72,7 +71,7 @@ ServiceWorkerPaymentApp::ServiceWorkerPaymentApp( content::WebContentsObserver(web_contents), top_origin_(top_origin), frame_origin_(frame_origin), - spec_(spec), + spec_(spec->GetWeakPtr()), delegate_(nullptr), is_incognito_(is_incognito), show_processing_spinner_(show_processing_spinner), @@ -84,7 +83,6 @@ ServiceWorkerPaymentApp::ServiceWorkerPaymentApp( DCHECK(web_contents); DCHECK(top_origin_.is_valid()); DCHECK(frame_origin_.is_valid()); - DCHECK(spec_); app_method_names_.insert(installable_enabled_method_); } @@ -100,6 +98,9 @@ ServiceWorkerPaymentApp::~ServiceWorkerPaymentApp() { void ServiceWorkerPaymentApp::ValidateCanMakePayment( ValidateCanMakePaymentCallback callback) { + if (!spec_) + return; + // Returns true for payment app that needs installation. if (needs_installation_) { OnCanMakePaymentEventSkipped(std::move(callback)); @@ -140,6 +141,9 @@ void ServiceWorkerPaymentApp::ValidateCanMakePayment( mojom::CanMakePaymentEventDataPtr ServiceWorkerPaymentApp::CreateCanMakePaymentEventData() { + if (!spec_) + return nullptr; + std::set<std::string> requested_url_methods; for (const auto& method : spec_->payment_method_identifiers_set()) { GURL url_method(method); @@ -250,6 +254,9 @@ ServiceWorkerPaymentApp::CreatePaymentRequestEventData() { mojom::PaymentRequestEventDataPtr event_data = mojom::PaymentRequestEventData::New(); + if (!spec_) + return event_data; + event_data->top_origin = top_origin_; event_data->payment_request_origin = frame_origin_; @@ -509,7 +516,7 @@ void ServiceWorkerPaymentApp::DisableShowingOwnUI() { } bool ServiceWorkerPaymentApp::HandlesShippingAddress() const { - if (!spec_->request_shipping()) + if (!spec_ || !spec_->request_shipping()) return false; return needs_installation_ @@ -518,7 +525,7 @@ bool ServiceWorkerPaymentApp::HandlesShippingAddress() const { } bool ServiceWorkerPaymentApp::HandlesPayerName() const { - if (!spec_->request_payer_name()) + if (!spec_ || !spec_->request_payer_name()) return false; return needs_installation_ @@ -527,7 +534,7 @@ bool ServiceWorkerPaymentApp::HandlesPayerName() const { } bool ServiceWorkerPaymentApp::HandlesPayerEmail() const { - if (!spec_->request_payer_email()) + if (!spec_ || !spec_->request_payer_email()) return false; return needs_installation_ @@ -536,7 +543,7 @@ bool ServiceWorkerPaymentApp::HandlesPayerEmail() const { } bool ServiceWorkerPaymentApp::HandlesPayerPhone() const { - if (!spec_->request_payer_phone()) + if (!spec_ || !spec_->request_payer_phone()) return false; return needs_installation_ @@ -592,6 +599,9 @@ void ServiceWorkerPaymentApp::OnPaymentDetailsNotUpdated() { void ServiceWorkerPaymentApp::AbortPaymentApp( base::OnceCallback<void(bool)> abort_callback) { + if (!spec_) + return; + content::PaymentAppProvider::GetInstance()->AbortPayment( web_contents(), registration_id_, stored_payment_app_info_ diff --git a/chromium/components/payments/content/service_worker_payment_app.h b/chromium/components/payments/content/service_worker_payment_app.h index a7ab79d38bf..69ada2abb92 100644 --- a/chromium/components/payments/content/service_worker_payment_app.h +++ b/chromium/components/payments/content/service_worker_payment_app.h @@ -36,23 +36,24 @@ class ServiceWorkerPaymentApp : public PaymentApp, public content::WebContentsObserver { public: // This constructor is used for a payment app that has been installed in - // Chrome. + // Chrome. The `spec` parameter should not be null. ServiceWorkerPaymentApp( content::WebContents* web_contents, const GURL& top_origin, const GURL& frame_origin, - const PaymentRequestSpec* spec, + PaymentRequestSpec* spec, std::unique_ptr<content::StoredPaymentApp> stored_payment_app_info, bool is_incognito, const base::RepeatingClosure& show_processing_spinner); // This constructor is used for a payment app that has not been installed in - // Chrome but can be installed when paying with it. + // Chrome but can be installed when paying with it. The `spec` parameter + // should not be null. ServiceWorkerPaymentApp( content::WebContents* web_contents, const GURL& top_origin, const GURL& frame_origin, - const PaymentRequestSpec* spec, + PaymentRequestSpec* spec, std::unique_ptr<WebAppInstallationInfo> installable_payment_app_info, const std::string& enabled_method, bool is_incognito, @@ -131,7 +132,7 @@ class ServiceWorkerPaymentApp : public PaymentApp, GURL top_origin_; GURL frame_origin_; - const PaymentRequestSpec* spec_; + base::WeakPtr<PaymentRequestSpec> spec_; std::unique_ptr<content::StoredPaymentApp> stored_payment_app_info_; // Weak pointer is fine here since the owner of this object is diff --git a/chromium/components/payments/content/service_worker_payment_app_factory.cc b/chromium/components/payments/content/service_worker_payment_app_factory.cc index 5b710d33c7f..963a064fb99 100644 --- a/chromium/components/payments/content/service_worker_payment_app_factory.cc +++ b/chromium/components/payments/content/service_worker_payment_app_factory.cc @@ -10,9 +10,15 @@ #include "base/bind.h" #include "base/check_op.h" #include "base/memory/weak_ptr.h" +#include "components/payments/content/developer_console_logger.h" #include "components/payments/content/payment_manifest_web_data_service.h" #include "components/payments/content/service_worker_payment_app.h" #include "components/payments/content/service_worker_payment_app_finder.h" +#include "components/payments/core/error_message_util.h" +#include "components/payments/core/features.h" +#include "components/payments/core/method_strings.h" +#include "content/public/browser/stored_payment_app.h" +#include "content/public/browser/supported_delegations.h" #include "content/public/browser/web_contents.h" namespace payments { @@ -34,15 +40,15 @@ class ServiceWorkerPaymentAppCreator { ServiceWorkerPaymentAppCreator( ServiceWorkerPaymentAppFactory* owner, base::WeakPtr<PaymentAppFactory::Delegate> delegate) - : owner_(owner), delegate_(delegate) {} + : owner_(owner), delegate_(delegate), log_(delegate->GetWebContents()) {} ~ServiceWorkerPaymentAppCreator() {} void CreatePaymentApps( - content::PaymentAppProvider::PaymentApps apps, + content::InstalledPaymentAppsFinder::PaymentApps apps, ServiceWorkerPaymentAppFinder::InstallablePaymentApps installable_apps, const std::string& error_message) { - if (!delegate_) { + if (!delegate_ || !delegate_->GetSpec()) { FinishAndCleanup(); return; } @@ -50,15 +56,22 @@ class ServiceWorkerPaymentAppCreator { if (!error_message.empty()) delegate_->OnPaymentAppCreationError(error_message); - number_of_pending_sw_payment_apps_ = apps.size() + installable_apps.size(); - if (number_of_pending_sw_payment_apps_ == 0U) { - FinishAndCleanup(); - return; - } base::RepeatingClosure show_processing_spinner = base::BindRepeating( &PaymentAppFactory::Delegate::ShowProcessingSpinner, delegate_); - + std::vector<std::string> skipped_app_names; for (auto& installed_app : apps) { + std::vector<std::string> enabled_methods = + installed_app.second->enabled_methods; + bool has_app_store_billing_method = + enabled_methods.end() != std::find(enabled_methods.begin(), + enabled_methods.end(), + methods::kGooglePlayBilling); + if (ShouldSkipAppForPartialDelegation( + installed_app.second->supported_delegations, delegate_, + has_app_store_billing_method)) { + skipped_app_names.emplace_back(installed_app.second->name); + continue; + } auto app = std::make_unique<ServiceWorkerPaymentApp>( delegate_->GetWebContents(), delegate_->GetTopOrigin(), delegate_->GetFrameOrigin(), delegate_->GetSpec(), @@ -69,9 +82,18 @@ class ServiceWorkerPaymentAppCreator { weak_ptr_factory_.GetWeakPtr())); PaymentApp* raw_payment_app_pointer = app.get(); available_apps_[raw_payment_app_pointer] = std::move(app); + number_of_pending_sw_payment_apps_++; } for (auto& installable_app : installable_apps) { + bool is_app_store_billing_method = + installable_app.first.spec() == methods::kGooglePlayBilling; + if (ShouldSkipAppForPartialDelegation( + installable_app.second->supported_delegations, delegate_, + is_app_store_billing_method)) { + skipped_app_names.emplace_back(installable_app.second->name); + continue; + } auto app = std::make_unique<ServiceWorkerPaymentApp>( delegate_->GetWebContents(), delegate_->GetTopOrigin(), delegate_->GetFrameOrigin(), delegate_->GetSpec(), @@ -82,7 +104,35 @@ class ServiceWorkerPaymentAppCreator { weak_ptr_factory_.GetWeakPtr())); PaymentApp* raw_payment_app_pointer = app.get(); available_apps_[raw_payment_app_pointer] = std::move(app); + number_of_pending_sw_payment_apps_++; + } + + if (!skipped_app_names.empty()) { + std::string warning_message = + GetAppsSkippedForPartialDelegationErrorMessage(skipped_app_names); + log_.Warn(warning_message); } + + if (number_of_pending_sw_payment_apps_ == 0U) { + if (error_message.empty() && !skipped_app_names.empty()) { + std::string new_error_message = + GetAppsSkippedForPartialDelegationErrorMessage(skipped_app_names); + delegate_->OnPaymentAppCreationError(new_error_message); + } + FinishAndCleanup(); + } + } + + bool ShouldSkipAppForPartialDelegation( + const content::SupportedDelegations& supported_delegations, + const base::WeakPtr<PaymentAppFactory::Delegate>& delegate, + bool has_app_store_billing_method) const { + DCHECK(delegate); + DCHECK(delegate->GetSpec()); + return (base::FeatureList::IsEnabled(features::kEnforceFullDelegation) || + has_app_store_billing_method) && + !supported_delegations.ProvidesAll( + delegate->GetSpec()->payment_options()); } base::WeakPtr<ServiceWorkerPaymentAppCreator> GetWeakPtr() { @@ -116,6 +166,7 @@ class ServiceWorkerPaymentAppCreator { ServiceWorkerPaymentAppFactory* owner_; base::WeakPtr<PaymentAppFactory::Delegate> delegate_; std::map<PaymentApp*, std::unique_ptr<PaymentApp>> available_apps_; + DeveloperConsoleLogger log_; int number_of_pending_sw_payment_apps_ = 0; base::WeakPtrFactory<ServiceWorkerPaymentAppCreator> weak_ptr_factory_{this}; diff --git a/chromium/components/payments/content/service_worker_payment_app_finder.cc b/chromium/components/payments/content/service_worker_payment_app_finder.cc index 87b33d0f5af..5dc754e8c97 100644 --- a/chromium/components/payments/content/service_worker_payment_app_finder.cc +++ b/chromium/components/payments/content/service_worker_payment_app_finder.cc @@ -31,7 +31,6 @@ #include "content/public/browser/storage_partition.h" #include "content/public/browser/stored_payment_app.h" #include "content/public/browser/web_contents.h" -#include "net/url_request/url_request_context_getter.h" #include "ui/gfx/image/image.h" #include "url/url_canon.h" @@ -84,7 +83,7 @@ bool AppSupportsAtLeastOneRequestedMethodData( } void RemovePortNumbersFromScopesForTest( - content::PaymentAppProvider::PaymentApps* apps) { + content::InstalledPaymentAppsFinder::PaymentApps* apps) { GURL::Replacements replacements; replacements.ClearPort(); for (auto& app : *apps) { @@ -160,9 +159,9 @@ class SelfDeletingServiceWorkerPaymentAppFinder finished_using_resources_callback_ = std::move(finished_using_resources_callback); - content::PaymentAppProvider::GetInstance()->GetAllPaymentApps( - web_contents->GetBrowserContext(), - base::BindOnce( + content::InstalledPaymentAppsFinder::GetInstance( + web_contents->GetBrowserContext()) + ->GetAllPaymentApps(base::BindOnce( &SelfDeletingServiceWorkerPaymentAppFinder::OnGotAllPaymentApps, weak_ptr_factory_.GetWeakPtr())); } @@ -179,7 +178,7 @@ class SelfDeletingServiceWorkerPaymentAppFinder static void RemoveUnrequestedMethods( const std::vector<mojom::PaymentMethodDataPtr>& requested_method_data, - content::PaymentAppProvider::PaymentApps* apps) { + content::InstalledPaymentAppsFinder::PaymentApps* apps) { std::set<std::string> requested_methods; for (const auto& requested_method_datum : requested_method_data) { requested_methods.insert(requested_method_datum->supported_method); @@ -193,7 +192,8 @@ class SelfDeletingServiceWorkerPaymentAppFinder } } - void OnGotAllPaymentApps(content::PaymentAppProvider::PaymentApps apps) { + void OnGotAllPaymentApps( + content::InstalledPaymentAppsFinder::PaymentApps apps) { if (ignore_port_in_origin_comparison_for_testing_) RemovePortNumbersFromScopesForTest(&apps); @@ -220,8 +220,9 @@ class SelfDeletingServiceWorkerPaymentAppFinder weak_ptr_factory_.GetWeakPtr())); } - void OnPaymentAppsVerified(content::PaymentAppProvider::PaymentApps apps, - const std::string& error_message) { + void OnPaymentAppsVerified( + content::InstalledPaymentAppsFinder::PaymentApps apps, + const std::string& error_message) { if (first_error_message_.empty()) first_error_message_ = error_message; @@ -382,7 +383,7 @@ class SelfDeletingServiceWorkerPaymentAppFinder bool ignore_port_in_origin_comparison_for_testing_ = false; - content::PaymentAppProvider::PaymentApps installed_apps_; + content::InstalledPaymentAppsFinder::PaymentApps installed_apps_; size_t number_of_app_icons_to_update_ = 0; @@ -410,7 +411,7 @@ void ServiceWorkerPaymentAppFinder::GetAllPaymentApps( }); if (requested_method_data.empty()) { std::move(callback).Run( - content::PaymentAppProvider::PaymentApps(), + content::InstalledPaymentAppsFinder::PaymentApps(), std::map<GURL, std::unique_ptr<WebAppInstallationInfo>>(), /*error_message=*/""); return; @@ -444,7 +445,7 @@ void ServiceWorkerPaymentAppFinder::GetAllPaymentApps( // static void ServiceWorkerPaymentAppFinder::RemoveAppsWithoutMatchingMethodData( const std::vector<mojom::PaymentMethodDataPtr>& requested_method_data, - content::PaymentAppProvider::PaymentApps* apps) { + content::InstalledPaymentAppsFinder::PaymentApps* apps) { for (auto it = apps->begin(); it != apps->end();) { if (AppSupportsAtLeastOneRequestedMethodData(*it->second, requested_method_data)) { diff --git a/chromium/components/payments/content/service_worker_payment_app_finder.h b/chromium/components/payments/content/service_worker_payment_app_finder.h index 980e4f044ed..0f8b632f866 100644 --- a/chromium/components/payments/content/service_worker_payment_app_finder.h +++ b/chromium/components/payments/content/service_worker_payment_app_finder.h @@ -13,6 +13,7 @@ #include "base/callback.h" #include "base/macros.h" #include "components/payments/content/web_app_manifest.h" +#include "content/public/browser/installed_payment_apps_finder.h" #include "content/public/browser/payment_app_provider.h" #include "content/public/browser/render_document_host_user_data.h" #include "third_party/blink/public/mojom/payments/payment_request.mojom.h" @@ -43,7 +44,7 @@ class ServiceWorkerPaymentAppFinder using InstallablePaymentApps = std::map<GURL, std::unique_ptr<WebAppInstallationInfo>>; using GetAllPaymentAppsCallback = - base::OnceCallback<void(content::PaymentAppProvider::PaymentApps, + base::OnceCallback<void(content::InstalledPaymentAppsFinder::PaymentApps, InstallablePaymentApps, const std::string& error_message)>; @@ -78,7 +79,7 @@ class ServiceWorkerPaymentAppFinder // the method names and method-specific capabilities. static void RemoveAppsWithoutMatchingMethodData( const std::vector<mojom::PaymentMethodDataPtr>& requested_method_data, - content::PaymentAppProvider::PaymentApps* apps); + content::InstalledPaymentAppsFinder::PaymentApps* apps); // Ignore the given |method|, so that no installed or installable service // workers would ever be looked up in GetAllPaymentApps(). Calling this @@ -93,6 +94,7 @@ class ServiceWorkerPaymentAppFinder friend class ServiceWorkerPaymentAppFinderBrowserTest; friend class PaymentRequestPlatformBrowserTestBase; friend class PaymentMethodViewControllerTest; + friend class PaymentHandlerIconRefetchTest; explicit ServiceWorkerPaymentAppFinder(content::RenderFrameHost* rfh); diff --git a/chromium/components/payments/content/service_worker_payment_app_finder_unittest.cc b/chromium/components/payments/content/service_worker_payment_app_finder_unittest.cc index 32ab27500ca..feb18459886 100644 --- a/chromium/components/payments/content/service_worker_payment_app_finder_unittest.cc +++ b/chromium/components/payments/content/service_worker_payment_app_finder_unittest.cc @@ -13,7 +13,7 @@ class ServiceWorkerPaymentAppFinderTest : public testing::Test { protected: void RemoveAppsWithoutMatchingMethodData( const std::vector<mojom::PaymentMethodDataPtr>& requested_method_data, - content::PaymentAppProvider::PaymentApps* apps) { + content::InstalledPaymentAppsFinder::PaymentApps* apps) { ServiceWorkerPaymentAppFinder::RemoveAppsWithoutMatchingMethodData( requested_method_data, apps); } @@ -24,7 +24,7 @@ TEST_F(ServiceWorkerPaymentAppFinderTest, std::vector<mojom::PaymentMethodDataPtr> requested_methods; requested_methods.emplace_back(mojom::PaymentMethodData::New()); requested_methods.back()->supported_method = "method"; - content::PaymentAppProvider::PaymentApps no_apps; + content::InstalledPaymentAppsFinder::PaymentApps no_apps; RemoveAppsWithoutMatchingMethodData(requested_methods, &no_apps); @@ -34,7 +34,7 @@ TEST_F(ServiceWorkerPaymentAppFinderTest, TEST_F(ServiceWorkerPaymentAppFinderTest, RemoveAppsWithoutMatchingMethodData_NoMethods) { std::vector<mojom::PaymentMethodDataPtr> no_requested_methods; - content::PaymentAppProvider::PaymentApps apps; + content::InstalledPaymentAppsFinder::PaymentApps apps; apps[0] = std::make_unique<content::StoredPaymentApp>(); apps[0]->enabled_methods = {"method1", "method2"}; @@ -52,7 +52,7 @@ TEST_F(ServiceWorkerPaymentAppFinderTest, requested_methods.back()->supported_method = "method2"; requested_methods.emplace_back(mojom::PaymentMethodData::New()); requested_methods.back()->supported_method = "method3"; - content::PaymentAppProvider::PaymentApps apps; + content::InstalledPaymentAppsFinder::PaymentApps apps; apps[0] = std::make_unique<content::StoredPaymentApp>(); apps[0]->enabled_methods = {"method2"}; apps[1] = std::make_unique<content::StoredPaymentApp>(); @@ -76,7 +76,7 @@ TEST_F(ServiceWorkerPaymentAppFinderTest, std::vector<mojom::PaymentMethodDataPtr> requested_methods; requested_methods.emplace_back(mojom::PaymentMethodData::New()); requested_methods.back()->supported_method = "basic-card"; - content::PaymentAppProvider::PaymentApps apps; + content::InstalledPaymentAppsFinder::PaymentApps apps; apps[0] = std::make_unique<content::StoredPaymentApp>(); apps[0]->enabled_methods = {"basic-card"}; @@ -95,7 +95,7 @@ TEST_F(ServiceWorkerPaymentAppFinderTest, requested_methods.back()->supported_method = "basic-card"; requested_methods.back()->supported_networks = { mojom::BasicCardNetwork::AMEX}; - content::PaymentAppProvider::PaymentApps apps; + content::InstalledPaymentAppsFinder::PaymentApps apps; apps[0] = std::make_unique<content::StoredPaymentApp>(); apps[0]->enabled_methods = {"basic-card"}; @@ -111,7 +111,7 @@ TEST_F(ServiceWorkerPaymentAppFinderTest, requested_methods.back()->supported_method = "basic-card"; requested_methods.back()->supported_networks = { mojom::BasicCardNetwork::AMEX}; - content::PaymentAppProvider::PaymentApps apps; + content::InstalledPaymentAppsFinder::PaymentApps apps; apps[0] = std::make_unique<content::StoredPaymentApp>(); apps[0]->enabled_methods = {"basic-card"}; apps[0]->capabilities.emplace_back(); @@ -128,7 +128,7 @@ TEST_F(ServiceWorkerPaymentAppFinderTest, std::vector<mojom::PaymentMethodDataPtr> requested_methods; requested_methods.emplace_back(mojom::PaymentMethodData::New()); requested_methods.back()->supported_method = "basic-card"; - content::PaymentAppProvider::PaymentApps apps; + content::InstalledPaymentAppsFinder::PaymentApps apps; apps[0] = std::make_unique<content::StoredPaymentApp>(); apps[0]->enabled_methods = {"basic-card"}; apps[0]->capabilities.emplace_back(); @@ -155,7 +155,7 @@ TEST_F(ServiceWorkerPaymentAppFinderTest, requested_methods.back()->supported_method = "basic-card"; requested_methods.back()->supported_networks = { mojom::BasicCardNetwork::AMEX, mojom::BasicCardNetwork::DINERS}; - content::PaymentAppProvider::PaymentApps apps; + content::InstalledPaymentAppsFinder::PaymentApps apps; apps[0] = std::make_unique<content::StoredPaymentApp>(); apps[0]->enabled_methods = {"basic-card"}; apps[0]->capabilities.emplace_back(); @@ -182,7 +182,7 @@ TEST_F(ServiceWorkerPaymentAppFinderTest, std::vector<mojom::PaymentMethodDataPtr> requested_methods; requested_methods.emplace_back(mojom::PaymentMethodData::New()); requested_methods.back()->supported_method = "unknown-method"; - content::PaymentAppProvider::PaymentApps apps; + content::InstalledPaymentAppsFinder::PaymentApps apps; apps[0] = std::make_unique<content::StoredPaymentApp>(); apps[0]->enabled_methods = {"unknown-method"}; apps[0]->capabilities.emplace_back(); diff --git a/chromium/components/payments/content/test_content_payment_request_delegate.cc b/chromium/components/payments/content/test_content_payment_request_delegate.cc index 281f95c2a40..beac6fd3800 100644 --- a/chromium/components/payments/content/test_content_payment_request_delegate.cc +++ b/chromium/components/payments/content/test_content_payment_request_delegate.cc @@ -15,6 +15,11 @@ TestContentPaymentRequestDelegate::TestContentPaymentRequestDelegate( TestContentPaymentRequestDelegate::~TestContentPaymentRequestDelegate() {} +std::unique_ptr<autofill::InternalAuthenticator> +TestContentPaymentRequestDelegate::CreateInternalAuthenticator() const { + return nullptr; +} + scoped_refptr<PaymentManifestWebDataService> TestContentPaymentRequestDelegate::GetPaymentManifestWebDataService() const { return nullptr; @@ -57,6 +62,10 @@ std::string TestContentPaymentRequestDelegate::GetTwaPackageName() const { return ""; } +PaymentRequestDialog* TestContentPaymentRequestDelegate::GetDialogForTesting() { + return nullptr; +} + autofill::PersonalDataManager* TestContentPaymentRequestDelegate::GetPersonalDataManager() { return core_delegate_.GetPersonalDataManager(); diff --git a/chromium/components/payments/content/test_content_payment_request_delegate.h b/chromium/components/payments/content/test_content_payment_request_delegate.h index 4cf7dceda3f..56101135d6b 100644 --- a/chromium/components/payments/content/test_content_payment_request_delegate.h +++ b/chromium/components/payments/content/test_content_payment_request_delegate.h @@ -22,6 +22,8 @@ class TestContentPaymentRequestDelegate : public ContentPaymentRequestDelegate { ~TestContentPaymentRequestDelegate() override; // ContentPaymentRequestDelegate: + std::unique_ptr<autofill::InternalAuthenticator> CreateInternalAuthenticator() + const override; scoped_refptr<PaymentManifestWebDataService> GetPaymentManifestWebDataService() const override; PaymentRequestDisplayManager* GetDisplayManager() override; @@ -33,6 +35,7 @@ class TestContentPaymentRequestDelegate : public ContentPaymentRequestDelegate { bool IsBrowserWindowActive() const override; bool SkipUiForBasicCard() const override; std::string GetTwaPackageName() const override; + PaymentRequestDialog* GetDialogForTesting() override; autofill::PersonalDataManager* GetPersonalDataManager() override; const std::string& GetApplicationLocale() const override; bool IsOffTheRecord() const override; diff --git a/chromium/components/payments/core/BUILD.gn b/chromium/components/payments/core/BUILD.gn index 5e5e871fe3b..1f6d6e3976d 100644 --- a/chromium/components/payments/core/BUILD.gn +++ b/chromium/components/payments/core/BUILD.gn @@ -2,10 +2,12 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -import("//build/config/jumbo.gni") - -jumbo_static_library("core") { +static_library("core") { sources = [ + "android_app_description.cc", + "android_app_description.h", + "android_app_description_tools.cc", + "android_app_description_tools.h", "autofill_card_validation.cc", "autofill_card_validation.h", "basic_card_response.cc", @@ -53,6 +55,8 @@ jumbo_static_library("core") { "payments_experimental_features.h", "payments_validators.cc", "payments_validators.h", + "secure_payment_confirmation_instrument.cc", + "secure_payment_confirmation_instrument.h", "url_util.cc", "url_util.h", ] @@ -99,23 +103,30 @@ jumbo_static_library("core") { ] } -jumbo_static_library("error_strings") { +static_library("error_strings") { sources = [ "error_strings.cc", "error_strings.h", "native_error_strings.cc", "native_error_strings.h", ] + + if (is_chromeos) { + sources += [ + "chrome_os_error_strings.cc", + "chrome_os_error_strings.h", + ] + } } -jumbo_static_library("method_strings") { +static_library("method_strings") { sources = [ "method_strings.cc", "method_strings.h", ] } -jumbo_static_library("test_support") { +static_library("test_support") { testonly = true sources = [ "payments_test_util.cc", @@ -141,9 +152,10 @@ jumbo_static_library("test_support") { ] } -jumbo_source_set("unit_tests") { +source_set("unit_tests") { testonly = true sources = [ + "android_app_description_tools_unittest.cc", "can_make_payment_query_unittest.cc", "currency_formatter_unittest.cc", "journey_logger_unittest.cc", diff --git a/chromium/components/payments/core/android_app_description.cc b/chromium/components/payments/core/android_app_description.cc new file mode 100644 index 00000000000..abe6ba3cb6e --- /dev/null +++ b/chromium/components/payments/core/android_app_description.cc @@ -0,0 +1,17 @@ +// Copyright 2020 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. + +#include "components/payments/core/android_app_description.h" + +namespace payments { + +AndroidActivityDescription::AndroidActivityDescription() = default; + +AndroidActivityDescription::~AndroidActivityDescription() = default; + +AndroidAppDescription::AndroidAppDescription() = default; + +AndroidAppDescription::~AndroidAppDescription() = default; + +} // namespace payments diff --git a/chromium/components/payments/core/android_app_description.h b/chromium/components/payments/core/android_app_description.h new file mode 100644 index 00000000000..4274dba9a77 --- /dev/null +++ b/chromium/components/payments/core/android_app_description.h @@ -0,0 +1,65 @@ +// Copyright 2020 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. + +#ifndef COMPONENTS_PAYMENTS_CORE_ANDROID_APP_DESCRIPTION_H_ +#define COMPONENTS_PAYMENTS_CORE_ANDROID_APP_DESCRIPTION_H_ + +#include <memory> +#include <string> +#include <vector> + +namespace payments { + +// Describes an Android activity with org.chromium.intent.action.PAY intent +// filter. Documentation: +// https://web.dev/android-payment-apps-overview/ +struct AndroidActivityDescription { + AndroidActivityDescription(); + ~AndroidActivityDescription(); + + // Disallow copy and assign. + AndroidActivityDescription(const AndroidActivityDescription& other) = delete; + AndroidActivityDescription& operator=( + const AndroidActivityDescription& other) = delete; + + // The name of the activity, e.g., "com.example.app.PaymentActivity". + std::string name; + + // The payment method identifier from the + // "org.chromium.default_payment_method_name" metadata value of this activity. + // For example, "https://example.com/web-pay". + // + // The metadata value of "org.chromium.payment_method_names" is not yet used + // here, so it's omitted from the struct at this time. + std::string default_payment_method; +}; + +// Describes an Android app that can handle payments. +struct AndroidAppDescription { + AndroidAppDescription(); + ~AndroidAppDescription(); + + // Disallow copy and assign. + AndroidAppDescription(const AndroidAppDescription& other) = delete; + AndroidAppDescription& operator=(const AndroidAppDescription& other) = delete; + + // The name of the Android package of this app, e.g., "com.example.app". + std::string package; + + // The list of activities with org.chromium.intent.action.PAY intent filters + // in this app. + std::vector<std::unique_ptr<AndroidActivityDescription>> activities; + + // The list of service names with org.chromium.intent.action.IS_READY_TO_PAY + // intent filters in this app. For example, + // ["com.example.app.IsReadyToPayService"]. + // + // Note that it's a mistake to declare multiple IS_READY_TO_PAY services in an + // app. This mistake would be reported to the developer. + std::vector<std::string> service_names; +}; + +} // namespace payments + +#endif // COMPONENTS_PAYMENTS_CORE_ANDROID_APP_DESCRIPTION_H_ diff --git a/chromium/components/payments/core/android_app_description_tools.cc b/chromium/components/payments/core/android_app_description_tools.cc new file mode 100644 index 00000000000..7183601d490 --- /dev/null +++ b/chromium/components/payments/core/android_app_description_tools.cc @@ -0,0 +1,32 @@ +// Copyright 2020 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. + +#include "components/payments/core/android_app_description_tools.h" + +#include <utility> + +#include "base/check.h" +#include "components/payments/core/android_app_description.h" + +namespace payments { + +void SplitPotentiallyMultipleActivities( + std::unique_ptr<AndroidAppDescription> app, + std::vector<std::unique_ptr<AndroidAppDescription>>* destination) { + DCHECK(destination); + if (app->activities.empty()) + return; + destination->emplace_back(std::move(app)); + for (size_t i = 1; i < destination->front()->activities.size(); ++i) { + auto single_activity_app = std::make_unique<AndroidAppDescription>(); + single_activity_app->package = destination->front()->package; + single_activity_app->service_names = destination->front()->service_names; + single_activity_app->activities.emplace_back( + std::move(destination->front()->activities[i])); + destination->emplace_back(std::move(single_activity_app)); + } + destination->front()->activities.resize(1); +} + +} // namespace payments diff --git a/chromium/components/payments/core/android_app_description_tools.h b/chromium/components/payments/core/android_app_description_tools.h new file mode 100644 index 00000000000..53036905aac --- /dev/null +++ b/chromium/components/payments/core/android_app_description_tools.h @@ -0,0 +1,29 @@ +// Copyright 2020 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. + +#ifndef COMPONENTS_PAYMENTS_CORE_ANDROID_APP_DESCRIPTION_TOOLS_H_ +#define COMPONENTS_PAYMENTS_CORE_ANDROID_APP_DESCRIPTION_TOOLS_H_ + +#include <memory> +#include <vector> + +namespace payments { + +struct AndroidAppDescription; + +// Moves each activity in the given |app| into its own AndroidAppDescription in +// |destination|, so the code can treat each PAY intent target as its own +// payment app. +// +// The function does not clear |destination|, so the results of multiple calls +// can be appended to the same |destination|, e.g., in a loop. +// +// The |destination| should not be null. +void SplitPotentiallyMultipleActivities( + std::unique_ptr<AndroidAppDescription> app, + std::vector<std::unique_ptr<AndroidAppDescription>>* destination); + +} // namespace payments + +#endif // COMPONENTS_PAYMENTS_CORE_ANDROID_APP_DESCRIPTION_TOOLS_H_ diff --git a/chromium/components/payments/core/android_app_description_tools_unittest.cc b/chromium/components/payments/core/android_app_description_tools_unittest.cc new file mode 100644 index 00000000000..e7697733d70 --- /dev/null +++ b/chromium/components/payments/core/android_app_description_tools_unittest.cc @@ -0,0 +1,85 @@ +// Copyright 2020 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. + +#include "components/payments/core/android_app_description_tools.h" + +#include <memory> +#include <utility> +#include <vector> + +#include "components/payments/core/android_app_description.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace payments { +namespace { + +// Absence of activities should result in zero apps. +TEST(AndroidAppDescriptionToolsTest, SplitNoActivities) { + std::vector<std::unique_ptr<AndroidAppDescription>> destination; + SplitPotentiallyMultipleActivities(std::make_unique<AndroidAppDescription>(), + &destination); + EXPECT_TRUE(destination.empty()); +} + +// One activity should result in one app. +TEST(AndroidAppDescriptionToolsTest, SplitOneActivity) { + std::vector<std::unique_ptr<AndroidAppDescription>> destination; + auto app = std::make_unique<AndroidAppDescription>(); + app->package = "com.example.app"; + app->service_names = {"com.example.app.Service"}; + app->activities.emplace_back(std::make_unique<AndroidActivityDescription>()); + app->activities.back()->name = "com.example.app.Activity"; + app->activities.back()->default_payment_method = "https://example.test"; + + SplitPotentiallyMultipleActivities(std::move(app), &destination); + + ASSERT_EQ(1U, destination.size()); + EXPECT_EQ("com.example.app", destination.back()->package); + EXPECT_EQ(std::vector<std::string>{"com.example.app.Service"}, + destination.back()->service_names); + ASSERT_EQ(1U, destination.back()->activities.size()); + EXPECT_EQ("com.example.app.Activity", + destination.back()->activities.back()->name); + EXPECT_EQ("https://example.test", + destination.back()->activities.back()->default_payment_method); +} + +// Two activities should result in two apps. +TEST(AndroidAppDescriptionToolsTest, SplitTwoActivities) { + std::vector<std::unique_ptr<AndroidAppDescription>> destination; + auto app = std::make_unique<AndroidAppDescription>(); + app->package = "com.example.app"; + app->service_names = {"com.example.app.Service"}; + app->activities.emplace_back(std::make_unique<AndroidActivityDescription>()); + app->activities.back()->name = "com.example.app.ActivityOne"; + app->activities.back()->default_payment_method = "https://one.test"; + app->activities.emplace_back(std::make_unique<AndroidActivityDescription>()); + app->activities.back()->name = "com.example.app.ActivityTwo"; + app->activities.back()->default_payment_method = "https://two.test"; + + SplitPotentiallyMultipleActivities(std::move(app), &destination); + + ASSERT_EQ(2U, destination.size()); + + EXPECT_EQ("com.example.app", destination.front()->package); + EXPECT_EQ(std::vector<std::string>{"com.example.app.Service"}, + destination.front()->service_names); + ASSERT_EQ(1U, destination.front()->activities.size()); + EXPECT_EQ("com.example.app.ActivityOne", + destination.front()->activities.back()->name); + EXPECT_EQ("https://one.test", + destination.front()->activities.back()->default_payment_method); + + EXPECT_EQ("com.example.app", destination.back()->package); + EXPECT_EQ(std::vector<std::string>{"com.example.app.Service"}, + destination.back()->service_names); + ASSERT_EQ(1U, destination.back()->activities.size()); + EXPECT_EQ("com.example.app.ActivityTwo", + destination.back()->activities.back()->name); + EXPECT_EQ("https://two.test", + destination.back()->activities.back()->default_payment_method); +} + +} // namespace +} // namespace payments diff --git a/chromium/components/payments/core/can_make_payment_query.cc b/chromium/components/payments/core/can_make_payment_query.cc index 17c4c6ea78a..1f955334ae7 100644 --- a/chromium/components/payments/core/can_make_payment_query.cc +++ b/chromium/components/payments/core/can_make_payment_query.cc @@ -24,64 +24,6 @@ CanMakePaymentQuery::~CanMakePaymentQuery() {} bool CanMakePaymentQuery::CanQuery( const GURL& top_level_origin, const GURL& frame_origin, - const std::map<std::string, std::set<std::string>>& query, - bool per_method_quota) { - // Check both with and without per-method quota, so that both queries are - // recorded in case if two different tabs of the same website run with and - // without the origin trial. - bool can_query_with_per_method_quota = - CanQueryWithPerMethodQuota(top_level_origin, frame_origin, query); - - bool can_query_without_per_method_quota = - CanQueryWithoutPerMethodQuota(top_level_origin, frame_origin, query); - - if (per_method_quota || - base::FeatureList::IsEnabled( - features::kWebPaymentsPerMethodCanMakePaymentQuota)) { - return can_query_with_per_method_quota; - } - - return can_query_without_per_method_quota; -} - -void CanMakePaymentQuery::Shutdown() { - // OneShotTimer cancels the timer when it is destroyed. - timers_.clear(); -} - -bool CanMakePaymentQuery::CanQueryWithPerMethodQuota( - const GURL& top_level_origin, - const GURL& frame_origin, - const std::map<std::string, std::set<std::string>>& query) { - bool can_query = true; - for (const auto& method_and_params : query) { - std::string method = method_and_params.first; - std::set<std::string> params = method_and_params.second; - - const std::string id = - frame_origin.spec() + ":" + top_level_origin.spec() + ":" + method; - - auto it = per_method_queries_.find(id); - if (it == per_method_queries_.end()) { - auto timer = std::make_unique<base::OneShotTimer>(); - timer->Start(FROM_HERE, base::TimeDelta::FromMinutes(30), - base::BindOnce( - &CanMakePaymentQuery::ExpireQuotaForFrameOriginAndMethod, - weak_ptr_factory_.GetWeakPtr(), id)); - timers_.insert(std::make_pair(id, std::move(timer))); - per_method_queries_.insert(std::make_pair(id, params)); - continue; - } - - can_query &= it->second == params; - } - - return can_query; -} - -bool CanMakePaymentQuery::CanQueryWithoutPerMethodQuota( - const GURL& top_level_origin, - const GURL& frame_origin, const std::map<std::string, std::set<std::string>>& query) { const std::string id = frame_origin.spec() + ":" + top_level_origin.spec(); @@ -99,15 +41,14 @@ bool CanMakePaymentQuery::CanQueryWithoutPerMethodQuota( return it->second == query; } -void CanMakePaymentQuery::ExpireQuotaForFrameOrigin(const std::string& id) { - timers_.erase(id); - queries_.erase(id); +void CanMakePaymentQuery::Shutdown() { + // OneShotTimer cancels the timer when it is destroyed. + timers_.clear(); } -void CanMakePaymentQuery::ExpireQuotaForFrameOriginAndMethod( - const std::string& id) { +void CanMakePaymentQuery::ExpireQuotaForFrameOrigin(const std::string& id) { timers_.erase(id); - per_method_queries_.erase(id); + queries_.erase(id); } } // namespace payments diff --git a/chromium/components/payments/core/can_make_payment_query.h b/chromium/components/payments/core/can_make_payment_query.h index 519baedf820..05171abb72d 100644 --- a/chromium/components/payments/core/can_make_payment_query.h +++ b/chromium/components/payments/core/can_make_payment_query.h @@ -43,23 +43,13 @@ class CanMakePaymentQuery : public KeyedService { // also allowed. bool CanQuery(const GURL& top_level_origin, const GURL& frame_origin, - const std::map<std::string, std::set<std::string>>& query, - bool per_method_quota); + const std::map<std::string, std::set<std::string>>& query); // KeyedService implementation. void Shutdown() override; private: - bool CanQueryWithPerMethodQuota( - const GURL& top_level_origin, - const GURL& frame_origin, - const std::map<std::string, std::set<std::string>>& query); - bool CanQueryWithoutPerMethodQuota( - const GURL& top_level_origin, - const GURL& frame_origin, - const std::map<std::string, std::set<std::string>>& query); void ExpireQuotaForFrameOrigin(const std::string& id); - void ExpireQuotaForFrameOriginAndMethod(const std::string& id); // A mapping of an identififer to the timer that, when fired, allows the frame // to invoke canMakePayment() with the same identifier again. @@ -70,10 +60,6 @@ class CanMakePaymentQuery : public KeyedService { // JSON-stringified payment method data. std::map<std::string, std::map<std::string, std::set<std::string>>> queries_; - // A mapping of frame origin, top level origin, and payment method identifier - // to the last query in the form of JSON-stringified payment method data. - std::map<std::string, std::set<std::string>> per_method_queries_; - base::WeakPtrFactory<CanMakePaymentQuery> weak_ptr_factory_{this}; DISALLOW_COPY_AND_ASSIGN(CanMakePaymentQuery); diff --git a/chromium/components/payments/core/can_make_payment_query_unittest.cc b/chromium/components/payments/core/can_make_payment_query_unittest.cc index 87a72b90b62..73765e46e0f 100644 --- a/chromium/components/payments/core/can_make_payment_query_unittest.cc +++ b/chromium/components/payments/core/can_make_payment_query_unittest.cc @@ -25,12 +25,10 @@ TEST_F(CanMakePaymentQueryTest, SameHttpsOriginCannotQueryTwoDifferentCardNetworks) { EXPECT_TRUE( guard_.CanQuery(GURL("https://example.com"), GURL("https://example.com"), - {{"basic-card", {"{supportedNetworks: ['amex']}"}}}, - /*per_method_quota=*/true)); + {{"basic-card", {"{supportedNetworks: ['amex']}"}}})); EXPECT_FALSE( guard_.CanQuery(GURL("https://example.com"), GURL("https://example.com"), - {{"basic-card", {"{supportedNetworks: ['visa']}"}}}, - /*per_method_quota=*/true)); + {{"basic-card", {"{supportedNetworks: ['visa']}"}}})); } // A localhost website is not allowed to query all of the networks of the cards @@ -39,12 +37,10 @@ TEST_F(CanMakePaymentQueryTest, SameLocalhostOriginCannotQueryTwoDifferentCardNetworks) { EXPECT_TRUE(guard_.CanQuery( GURL("http://localhost:8080"), GURL("http://localhost:8080"), - {{"basic-card", {"{supportedNetworks: ['amex']}"}}}, - /*per_method_quota=*/true)); + {{"basic-card", {"{supportedNetworks: ['amex']}"}}})); EXPECT_FALSE(guard_.CanQuery( GURL("http://localhost:8080"), GURL("http://localhost:8080"), - {{"basic-card", {"{supportedNetworks: ['visa']}"}}}, - /*per_method_quota=*/true)); + {{"basic-card", {"{supportedNetworks: ['visa']}"}}})); } // A file website is not allowed to query all of the networks of the cards in @@ -53,12 +49,10 @@ TEST_F(CanMakePaymentQueryTest, SameFileOriginCannotQueryTwoDifferentCardNetworks) { EXPECT_TRUE(guard_.CanQuery( GURL("file:///tmp/test.html"), GURL("file:///tmp/test.html"), - {{"basic-card", {"{supportedNetworks: ['amex']}"}}}, - /*per_method_quota=*/true)); + {{"basic-card", {"{supportedNetworks: ['amex']}"}}})); EXPECT_FALSE(guard_.CanQuery( GURL("file:///tmp/test.html"), GURL("file:///tmp/test.html"), - {{"basic-card", {"{supportedNetworks: ['visa']}"}}}, - /*per_method_quota=*/true)); + {{"basic-card", {"{supportedNetworks: ['visa']}"}}})); } // Different HTTPS websites are allowed to query different card networks in @@ -67,12 +61,10 @@ TEST_F(CanMakePaymentQueryTest, DifferentHttpsOriginsCanQueryTwoDifferentCardNetworks) { EXPECT_TRUE( guard_.CanQuery(GURL("https://example.com"), GURL("https://example.com"), - {{"basic-card", {"{supportedNetworks: ['amex']}"}}}, - /*per_method_quota=*/true)); + {{"basic-card", {"{supportedNetworks: ['amex']}"}}})); EXPECT_TRUE(guard_.CanQuery( GURL("https://not-example.com"), GURL("https://not-example.com"), - {{"basic-card", {"{supportedNetworks: ['visa']}"}}}, - /*per_method_quota=*/true)); + {{"basic-card", {"{supportedNetworks: ['visa']}"}}})); } // Different localhost websites are allowed to query different card networks in @@ -81,12 +73,10 @@ TEST_F(CanMakePaymentQueryTest, DifferentLocalhostOriginsCanQueryTwoDifferentCardNetworks) { EXPECT_TRUE(guard_.CanQuery( GURL("http://localhost:8080"), GURL("http://localhost:8080"), - {{"basic-card", {"{supportedNetworks: ['amex']}"}}}, - /*per_method_quota=*/true)); + {{"basic-card", {"{supportedNetworks: ['amex']}"}}})); EXPECT_TRUE(guard_.CanQuery( GURL("http://localhost:9090"), GURL("http://localhost:9090"), - {{"basic-card", {"{supportedNetworks: ['visa']}"}}}, - /*per_method_quota=*/true)); + {{"basic-card", {"{supportedNetworks: ['visa']}"}}})); } // Different file websites are allowed to query different card networks in @@ -95,12 +85,10 @@ TEST_F(CanMakePaymentQueryTest, DifferentFileOriginsCanQueryTwoDifferentCardNetworks) { EXPECT_TRUE(guard_.CanQuery( GURL("file:///tmp/test.html"), GURL("file:///tmp/test.html"), - {{"basic-card", {"{supportedNetworks: ['amex']}"}}}, - /*per_method_quota=*/true)); + {{"basic-card", {"{supportedNetworks: ['amex']}"}}})); EXPECT_TRUE(guard_.CanQuery( GURL("file:///tmp/not-test.html"), GURL("file:///tmp/not-test.html"), - {{"basic-card", {"{supportedNetworks: ['visa']}"}}}, - /*per_method_quota=*/true)); + {{"basic-card", {"{supportedNetworks: ['visa']}"}}})); } // The same website is not allowed to query the same payment method with @@ -110,18 +98,15 @@ TEST_F(CanMakePaymentQueryTest, EXPECT_TRUE( guard_.CanQuery(GURL("https://example.com"), GURL("https://example.com"), {{"basic-card", {"{supportedNetworks: ['visa']}"}}, - {"https://alicepay.com", {"{alicePayParameter: 1}"}}}, - /*per_method_quota=*/true)); - EXPECT_TRUE( + {"https://alicepay.com", {"{alicePayParameter: 1}"}}})); + EXPECT_FALSE( guard_.CanQuery(GURL("https://example.com"), GURL("https://example.com"), {{"basic-card", {"{supportedNetworks: ['visa']}"}}, - {"https://bobpay.com", {"{bobPayParameter: 2}"}}}, - /*per_method_quota=*/true)); + {"https://bobpay.com", {"{bobPayParameter: 2}"}}})); EXPECT_FALSE( guard_.CanQuery(GURL("https://example.com"), GURL("https://example.com"), {{"basic-card", {"{supportedNetworks: ['amex']}"}}, - {"https://bobpay.com", {"{bobPayParameter: 2}"}}}, - /*per_method_quota=*/true)); + {"https://bobpay.com", {"{bobPayParameter: 2}"}}})); } // Two different websites are allowed to query the same payment method with @@ -130,12 +115,10 @@ TEST_F(CanMakePaymentQueryTest, DifferentOriginsCanQueryBasicCardWithTwoDifferentCardNetworks) { EXPECT_TRUE( guard_.CanQuery(GURL("https://example.com"), GURL("https://example.com"), - {{"basic-card", {"{supportedNetworks: ['visa']}"}}}, - /*per_method_quota=*/true)); + {{"basic-card", {"{supportedNetworks: ['visa']}"}}})); EXPECT_TRUE(guard_.CanQuery( GURL("https://not-example.com"), GURL("https://not-example.com"), - {{"basic-card", {"{supportedNetworks: ['amex']}"}}}, - /*per_method_quota=*/true)); + {{"basic-card", {"{supportedNetworks: ['amex']}"}}})); } // A website can query several different payment methods, as long as each @@ -145,18 +128,15 @@ TEST_F(CanMakePaymentQueryTest, EXPECT_TRUE( guard_.CanQuery(GURL("https://example.com"), GURL("https://example.com"), {{"basic-card", {"{supportedNetworks: ['visa']}"}}, - {"https://alicepay.com", {"{alicePayParameter: 1}"}}}, - /*per_method_quota=*/true)); - EXPECT_TRUE( + {"https://alicepay.com", {"{alicePayParameter: 1}"}}})); + EXPECT_FALSE( guard_.CanQuery(GURL("https://example.com"), GURL("https://example.com"), {{"https://alicepay.com", {"{alicePayParameter: 1}"}}, - {"https://bobpay.com", {"{bobPayParameter: 2}"}}}, - /*per_method_quota=*/true)); - EXPECT_TRUE( + {"https://bobpay.com", {"{bobPayParameter: 2}"}}})); + EXPECT_FALSE( guard_.CanQuery(GURL("https://example.com"), GURL("https://example.com"), {{"https://bobpay.com", {"{bobPayParameter: 2}"}}, - {"basic-card", {"{supportedNetworks: ['visa']}"}}}, - /*per_method_quota=*/true)); + {"basic-card", {"{supportedNetworks: ['visa']}"}}})); } // A website cannot query several different payment methods without the @@ -166,18 +146,15 @@ TEST_F(CanMakePaymentQueryTest, EXPECT_TRUE( guard_.CanQuery(GURL("https://example.com"), GURL("https://example.com"), {{"basic-card", {"{supportedNetworks: ['visa']}"}}, - {"https://alicepay.com", {"{alicePayParameter: 1}"}}}, - /*per_method_quota=*/false)); + {"https://alicepay.com", {"{alicePayParameter: 1}"}}})); EXPECT_FALSE( guard_.CanQuery(GURL("https://example.com"), GURL("https://example.com"), {{"https://alicepay.com", {"{alicePayParameter: 1}"}}, - {"https://bobpay.com", {"{bobPayParameter: 2}"}}}, - /*per_method_quota=*/false)); + {"https://bobpay.com", {"{bobPayParameter: 2}"}}})); EXPECT_FALSE( guard_.CanQuery(GURL("https://example.com"), GURL("https://example.com"), {{"https://bobpay.com", {"{bobPayParameter: 2}"}}, - {"basic-card", {"{supportedNetworks: ['visa']}"}}}, - /*per_method_quota=*/false)); + {"basic-card", {"{supportedNetworks: ['visa']}"}}})); } // An instance of a website with per-method quota enabled (e.g., through an @@ -192,69 +169,57 @@ TEST_F(CanMakePaymentQueryTest, SameWebsiteDifferentQuotaPolicy) { EXPECT_TRUE( guard_.CanQuery(GURL("https://example.com"), GURL("https://example.com"), {{"basic-card", {"{supportedNetworks: ['visa']}"}}, - {"https://alicepay.com", {"{alicePayParameter: 1}"}}}, - /*per_method_quota=*/true)); - EXPECT_TRUE( + {"https://alicepay.com", {"{alicePayParameter: 1}"}}})); + EXPECT_FALSE( guard_.CanQuery(GURL("https://example.com"), GURL("https://example.com"), {{"https://alicepay.com", {"{alicePayParameter: 1}"}}, - {"https://bobpay.com", {"{bobPayParameter: 2}"}}}, - /*per_method_quota=*/true)); - EXPECT_TRUE( + {"https://bobpay.com", {"{bobPayParameter: 2}"}}})); + EXPECT_FALSE( guard_.CanQuery(GURL("https://example.com"), GURL("https://example.com"), {{"https://bobpay.com", {"{bobPayParameter: 2}"}}, - {"basic-card", {"{supportedNetworks: ['visa']}"}}}, - /*per_method_quota=*/true)); + {"basic-card", {"{supportedNetworks: ['visa']}"}}})); // Second instance of https://example.com has per-method quota feature // disabled and so can only repeat the first query. EXPECT_FALSE( guard_.CanQuery(GURL("https://example.com"), GURL("https://example.com"), {{"https://alicepay.com", {"{alicePayParameter: 1}"}}, - {"https://bobpay.com", {"{bobPayParameter: 2}"}}}, - /*per_method_quota=*/false)); + {"https://bobpay.com", {"{bobPayParameter: 2}"}}})); EXPECT_TRUE( guard_.CanQuery(GURL("https://example.com"), GURL("https://example.com"), {{"basic-card", {"{supportedNetworks: ['visa']}"}}, - {"https://alicepay.com", {"{alicePayParameter: 1}"}}}, - /*per_method_quota=*/false)); + {"https://alicepay.com", {"{alicePayParameter: 1}"}}})); EXPECT_FALSE( guard_.CanQuery(GURL("https://example.com"), GURL("https://example.com"), {{"https://bobpay.com", {"{bobPayParameter: 2}"}}, - {"basic-card", {"{supportedNetworks: ['visa']}"}}}, - /*per_method_quota=*/false)); + {"basic-card", {"{supportedNetworks: ['visa']}"}}})); // The two website queries can be interleaved any number of times in any order // with the same results. EXPECT_TRUE( guard_.CanQuery(GURL("https://example.com"), GURL("https://example.com"), {{"basic-card", {"{supportedNetworks: ['visa']}"}}, - {"https://alicepay.com", {"{alicePayParameter: 1}"}}}, - /*per_method_quota=*/true)); - EXPECT_TRUE( + {"https://alicepay.com", {"{alicePayParameter: 1}"}}})); + EXPECT_FALSE( guard_.CanQuery(GURL("https://example.com"), GURL("https://example.com"), {{"https://bobpay.com", {"{bobPayParameter: 2}"}}, - {"basic-card", {"{supportedNetworks: ['visa']}"}}}, - /*per_method_quota=*/true)); + {"basic-card", {"{supportedNetworks: ['visa']}"}}})); EXPECT_FALSE( guard_.CanQuery(GURL("https://example.com"), GURL("https://example.com"), {{"https://alicepay.com", {"{alicePayParameter: 1}"}}, - {"https://bobpay.com", {"{bobPayParameter: 2}"}}}, - /*per_method_quota=*/false)); + {"https://bobpay.com", {"{bobPayParameter: 2}"}}})); EXPECT_TRUE( guard_.CanQuery(GURL("https://example.com"), GURL("https://example.com"), {{"basic-card", {"{supportedNetworks: ['visa']}"}}, - {"https://alicepay.com", {"{alicePayParameter: 1}"}}}, - /*per_method_quota=*/false)); - EXPECT_TRUE( + {"https://alicepay.com", {"{alicePayParameter: 1}"}}})); + EXPECT_FALSE( guard_.CanQuery(GURL("https://example.com"), GURL("https://example.com"), {{"https://alicepay.com", {"{alicePayParameter: 1}"}}, - {"https://bobpay.com", {"{bobPayParameter: 2}"}}}, - /*per_method_quota=*/true)); + {"https://bobpay.com", {"{bobPayParameter: 2}"}}})); EXPECT_FALSE( guard_.CanQuery(GURL("https://example.com"), GURL("https://example.com"), {{"https://bobpay.com", {"{bobPayParameter: 2}"}}, - {"basic-card", {"{supportedNetworks: ['visa']}"}}}, - /*per_method_quota=*/false)); + {"basic-card", {"{supportedNetworks: ['visa']}"}}})); } } // namespace diff --git a/chromium/components/payments/core/chrome_os_error_strings.cc b/chromium/components/payments/core/chrome_os_error_strings.cc new file mode 100644 index 00000000000..27bdd8c3370 --- /dev/null +++ b/chromium/components/payments/core/chrome_os_error_strings.cc @@ -0,0 +1,22 @@ +// Copyright 2020 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. + +#include "components/payments/core/chrome_os_error_strings.h" + +namespace payments { +namespace errors { + +const char kEmptyResponse[] = "Android app response is empty."; + +const char kInvalidResponse[] = "Android app response is not valid."; + +const char kMoreThanOneActivity[] = + "Found more than one PAY activity in the Trusted Web Activity, but at most " + "one activity is supported."; + +const char kMoreThanOneMethodData[] = + "At most one payment method specific data is supported."; + +} // namespace errors +} // namespace payments diff --git a/chromium/components/payments/core/chrome_os_error_strings.h b/chromium/components/payments/core/chrome_os_error_strings.h new file mode 100644 index 00000000000..13c4be4b35e --- /dev/null +++ b/chromium/components/payments/core/chrome_os_error_strings.h @@ -0,0 +1,30 @@ +// Copyright 2020 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. + +#ifndef COMPONENTS_PAYMENTS_CORE_CHROME_OS_ERROR_STRINGS_H_ +#define COMPONENTS_PAYMENTS_CORE_CHROME_OS_ERROR_STRINGS_H_ + +namespace payments { +namespace errors { + +// Developer facing error messages that are used only on Chrome OS. + +// Used if ARC sends a null object to the browser. +extern const char kEmptyResponse[]; + +// Used if ARC sends an object ot the browser that has neither an error message +// nor a valid response. +extern const char kInvalidResponse[]; + +// Used when the TWA declares more than one PAY activity. +extern const char kMoreThanOneActivity[]; + +// Used when the merchant invokes the Trusted Web Activity with more than set of +// payment method specific data. +extern const char kMoreThanOneMethodData[]; + +} // namespace errors +} // namespace payments + +#endif // COMPONENTS_PAYMENTS_CORE_CHROME_OS_ERROR_STRINGS_H_ diff --git a/chromium/components/payments/core/error_message_util.cc b/chromium/components/payments/core/error_message_util.cc index 1fdf75f77c2..dd96cbba18f 100644 --- a/chromium/components/payments/core/error_message_util.cc +++ b/chromium/components/payments/core/error_message_util.cc @@ -9,27 +9,46 @@ #include "base/check.h" #include "base/strings/string_util.h" +#include "components/payments/core/error_strings.h" #include "components/payments/core/native_error_strings.h" namespace payments { -std::string GetNotSupportedErrorMessage(const std::set<std::string>& methods) { - if (methods.empty()) - return errors::kGenericPaymentMethodNotSupportedMessage; +namespace { - std::vector<std::string> with_quotes(methods.size()); +template <class Collection> +std::string concatNamesWithQuotesAndCommma(const Collection& names) { + std::vector<std::string> with_quotes(names.size()); std::transform( - methods.begin(), methods.end(), with_quotes.begin(), + names.begin(), names.end(), with_quotes.begin(), [](const std::string& method_name) { return "\"" + method_name + "\""; }); + std::string result = base::JoinString(with_quotes, ", "); + return result; +} + +} // namespace + +std::string GetNotSupportedErrorMessage(const std::set<std::string>& methods) { + if (methods.empty()) + return errors::kGenericPaymentMethodNotSupportedMessage; std::string output; bool replaced = base::ReplaceChars( - with_quotes.size() == 1 - ? errors::kSinglePaymentMethodNotSupportedFormat - : errors::kMultiplePaymentMethodsNotSupportedFormat, - "$", base::JoinString(with_quotes, ", "), &output); + methods.size() == 1 ? errors::kSinglePaymentMethodNotSupportedFormat + : errors::kMultiplePaymentMethodsNotSupportedFormat, + "$", concatNamesWithQuotesAndCommma(methods), &output); DCHECK(replaced); return output; } +std::string GetAppsSkippedForPartialDelegationErrorMessage( + const std::vector<std::string>& skipped_app_names) { + std::string output; + bool replaced = base::ReplaceChars( + errors::kSkipAppForPartialDelegation, "$", + concatNamesWithQuotesAndCommma(skipped_app_names), &output); + + DCHECK(replaced); + return output; +} } // namespace payments diff --git a/chromium/components/payments/core/error_message_util.h b/chromium/components/payments/core/error_message_util.h index a99870480a9..5a09197d370 100644 --- a/chromium/components/payments/core/error_message_util.h +++ b/chromium/components/payments/core/error_message_util.h @@ -7,6 +7,7 @@ #include <set> #include <string> +#include <vector> namespace payments { @@ -14,6 +15,11 @@ namespace payments { // not supported. std::string GetNotSupportedErrorMessage(const std::set<std::string>& methods); +// Returns a developer-facing error message that the apps are skipped because +// they do not support full delegation. +std::string GetAppsSkippedForPartialDelegationErrorMessage( + const std::vector<std::string>& skipped_apps); + } // namespace payments #endif // COMPONENTS_PAYMENTS_CORE_ERROR_MESSAGE_UTIL_H_ diff --git a/chromium/components/payments/core/error_strings.cc b/chromium/components/payments/core/error_strings.cc index 992734553e9..87c83302a6f 100644 --- a/chromium/components/payments/core/error_strings.cc +++ b/chromium/components/payments/core/error_strings.cc @@ -24,6 +24,7 @@ const char kMethodNameRequired[] = "Method name required."; const char kMissingDetailsFromPaymentApp[] = "Payment app returned invalid response. Missing field \"details\"."; const char kMissingMethodNameFromPaymentApp[] = "Payment app returned invalid response. Missing field \"methodName\"."; const char kNotInASecureOrigin[] = "Not in a secure origin."; +const char kNoWebContents[] = "The frame that initiated payment is not associated with any web page."; const char kPayerEmailEmpty[] = "Payment app returned invalid response. Missing field \"payerEmail\"."; const char kPayerNameEmpty[] = "Payment app returned invalid response. Missing field \"payerName\"."; const char kPayerPhoneEmpty[] = "Payment app returned invalid response. Missing field \"payerPhone\"."; @@ -32,6 +33,7 @@ const char kProhibitedOriginOrInvalidSslExplanation[] = "No UI will be shown. Ca const char kShippingAddressInvalid[] = "Payment app returned invalid shipping address in response."; const char kShippingOptionEmpty[] = "Payment app returned invalid response. Missing field \"shipping option\"."; const char kShippingOptionIdRequired[] = "Shipping option identifier required."; +const char kSkipAppForPartialDelegation[] = "Skipping $ for not providing all of the requested PaymentOptions."; const char kStrictBasicCardShowReject[] = "User does not have valid information on file."; const char kTotalRequired[] = "Total required."; const char kUserCancelled[] = "User closed the Payment Request UI."; diff --git a/chromium/components/payments/core/error_strings.h b/chromium/components/payments/core/error_strings.h index bfd208d0c11..063a401132e 100644 --- a/chromium/components/payments/core/error_strings.h +++ b/chromium/components/payments/core/error_strings.h @@ -58,6 +58,9 @@ extern const char kMissingMethodNameFromPaymentApp[]; // The PaymentRequest API is available only on secure origins. extern const char kNotInASecureOrigin[]; +// WebContents is not available from RenderFrameHost. +extern const char kNoWebContents[]; + // The payment handler responded with an empty "payer name" field. extern const char kPayerNameEmpty[]; @@ -83,6 +86,10 @@ extern const char kShippingOptionEmpty[]; // Used when non-empty "shippingOptionId": "" is required, but not provided. extern const char kShippingOptionIdRequired[]; +// Used when an app is skipped for supporting only part of the requested payment +// options. +extern const char kSkipAppForPartialDelegation[]; + // Used when rejecting show() with NotSupportedError, because the user did not // have all valid autofill data. extern const char kStrictBasicCardShowReject[]; diff --git a/chromium/components/payments/core/features.cc b/chromium/components/payments/core/features.cc index bc4b1a5e6de..89f088bc722 100644 --- a/chromium/components/payments/core/features.cc +++ b/chromium/components/payments/core/features.cc @@ -4,6 +4,8 @@ #include "components/payments/core/features.h" +#include "build/build_config.h" + namespace payments { namespace features { @@ -37,13 +39,18 @@ const base::Feature kWebPaymentsJustInTimePaymentApp{ const base::Feature kAlwaysAllowJustInTimePaymentApp{ "AlwaysAllowJustInTimePaymentApp", base::FEATURE_DISABLED_BY_DEFAULT}; -const base::Feature kWebPaymentsPerMethodCanMakePaymentQuota{ - "WebPaymentsPerMethodCanMakePaymentQuota", - base::FEATURE_DISABLED_BY_DEFAULT}; - const base::Feature kWebPaymentsRedactShippingAddress{ "WebPaymentsRedactShippingAddress", base::FEATURE_ENABLED_BY_DEFAULT}; +const base::Feature kAppStoreBilling { + "AppStoreBilling", +#if defined(OS_ANDROID) + base::FEATURE_ENABLED_BY_DEFAULT +#else + base::FEATURE_DISABLED_BY_DEFAULT +#endif // OS_ANDROID +}; + const base::Feature kAppStoreBillingDebug{"AppStoreBillingDebug", base::FEATURE_DISABLED_BY_DEFAULT}; @@ -66,5 +73,17 @@ const base::Feature kAllowJITInstallationWhenAppIconIsMissing{ "AllowJITInstallationWhenAppIconIsMissing", base::FEATURE_DISABLED_BY_DEFAULT}; +const base::Feature kEnforceFullDelegation{"EnforceFullDelegation", + base::FEATURE_DISABLED_BY_DEFAULT}; + +const base::Feature kSecurePaymentConfirmation { + "SecurePaymentConfirmation", +#if defined(OS_MAC) + base::FEATURE_ENABLED_BY_DEFAULT +#else + base::FEATURE_DISABLED_BY_DEFAULT +#endif // OS_MAC +}; + } // namespace features } // namespace payments diff --git a/chromium/components/payments/core/features.h b/chromium/components/payments/core/features.h index 273a692ca27..3b8c7547928 100644 --- a/chromium/components/payments/core/features.h +++ b/chromium/components/payments/core/features.h @@ -35,6 +35,10 @@ extern const base::Feature kWebPaymentsModifiers; // with a single URL based payment app and no other info requested. extern const base::Feature kWebPaymentsSingleAppUiSkip; +// Used to control whether the invoking TWA can handle payments for app store +// payment method identifiers. +extern const base::Feature kAppStoreBilling; + // Used to control whether to remove the restriction that TWA has to be // installed from specific app stores. extern const base::Feature kAppStoreBillingDebug; @@ -46,9 +50,6 @@ extern const base::Feature kWebPaymentsJustInTimePaymentApp; // basic-card is also requested. extern const base::Feature kAlwaysAllowJustInTimePaymentApp; -// Used to control whether canMakePayment() quota is per-method. -extern const base::Feature kWebPaymentsPerMethodCanMakePaymentQuota; - // Used to control whether the shipping address returned for the // ShippingAddressChangeEvent is redacted of fine-grained details. extern const base::Feature kWebPaymentsRedactShippingAddress; @@ -75,6 +76,15 @@ extern const base::Feature kPaymentHandlerPopUpSizeWindow; // Used to test icon refetch for JIT installed apps with missing icons. extern const base::Feature kAllowJITInstallationWhenAppIconIsMissing; +// Used to reject the apps with partial delegation. +extern const base::Feature kEnforceFullDelegation; + +// Browser-side feature flag for SecurePaymentConfirmation, which can be used to +// disable the feature. The feature is also controlled by the Blink runtime +// feature "SecurePaymentConfirmation". Both have to be enabled for +// SecurePaymentConfirmation to be available. +extern const base::Feature kSecurePaymentConfirmation; + } // namespace features } // namespace payments diff --git a/chromium/components/payments/core/journey_logger.cc b/chromium/components/payments/core/journey_logger.cc index 92e7b958cff..06769aa57e0 100644 --- a/chromium/components/payments/core/journey_logger.cc +++ b/chromium/components/payments/core/journey_logger.cc @@ -179,6 +179,7 @@ void JourneyLogger::SetRequestedInformation(bool requested_shipping, void JourneyLogger::SetRequestedPaymentMethodTypes( bool requested_basic_card, bool requested_method_google, + bool requested_method_secure_payment_confirmation, bool requested_method_other) { if (requested_basic_card) SetEventOccurred(EVENT_REQUEST_METHOD_BASIC_CARD); @@ -186,6 +187,9 @@ void JourneyLogger::SetRequestedPaymentMethodTypes( if (requested_method_google) SetEventOccurred(EVENT_REQUEST_METHOD_GOOGLE); + if (requested_method_secure_payment_confirmation) + SetEventOccurred(EVENT_REQUEST_METHOD_SECURE_PAYMENT_CONFIRMATION); + if (requested_method_other) SetEventOccurred(EVENT_REQUEST_METHOD_OTHER); } @@ -264,6 +268,10 @@ void JourneyLogger::RecordTransactionAmount(std::string currency, .Record(ukm::UkmRecorder::Get()); } +void JourneyLogger::RecordCheckoutStep(CheckoutFunnelStep step) { + base::UmaHistogramEnumeration("PaymentRequest.CheckoutFunnel", step); +} + void JourneyLogger::RecordJourneyStatsHistograms( CompletionStatus completion_status) { if (has_recorded_) { @@ -275,6 +283,23 @@ void JourneyLogger::RecordJourneyStatsHistograms( RecordEventsMetric(completion_status); RecordTimeToCheckout(completion_status); + // Depending on the completion status record kPaymentRequestTriggered and/or + // kCompleted checkout steps. + switch (completion_status) { + case COMPLETION_STATUS_COMPLETED: + RecordCheckoutStep(CheckoutFunnelStep::kPaymentRequestTriggered); + RecordCheckoutStep(CheckoutFunnelStep::kCompleted); + break; + case COMPLETION_STATUS_USER_ABORTED: + case COMPLETION_STATUS_OTHER_ABORTED: + RecordCheckoutStep(CheckoutFunnelStep::kPaymentRequestTriggered); + break; + case COMPLETION_STATUS_COULD_NOT_SHOW: + break; + default: + NOTREACHED(); + } + // These following metrics only make sense if the Payment Request was // triggered. if (WasPaymentRequestTriggered()) { @@ -409,6 +434,8 @@ void JourneyLogger::RecordTimeToCheckout( selected_method_suffix = ".BasicCard"; } else if (events_ & EVENT_SELECTED_GOOGLE) { selected_method_suffix = ".Google"; + } else if (events_ & EVENT_SELECTED_SECURE_PAYMENT_CONFIRMATION) { + selected_method_suffix = ".SecurePaymentConfirmation"; } else { DCHECK(events_ & EVENT_SELECTED_OTHER); selected_method_suffix = ".Other"; @@ -460,6 +487,7 @@ void JourneyLogger::ValidateEventBits() const { bit_vector.push_back(events_ & EVENT_SELECTED_CREDIT_CARD); bit_vector.push_back(events_ & EVENT_SELECTED_GOOGLE); bit_vector.push_back(events_ & EVENT_SELECTED_OTHER); + bit_vector.push_back(events_ & EVENT_SELECTED_SECURE_PAYMENT_CONFIRMATION); DCHECK(ValidateExclusiveBitVector(bit_vector)); bit_vector.clear(); } @@ -469,6 +497,8 @@ void JourneyLogger::ValidateEventBits() const { DCHECK(events_ & EVENT_REQUEST_METHOD_BASIC_CARD); } else if (events_ & EVENT_SELECTED_GOOGLE) { DCHECK(events_ & EVENT_REQUEST_METHOD_GOOGLE); + } else if (events_ & EVENT_SELECTED_SECURE_PAYMENT_CONFIRMATION) { + DCHECK(events_ & EVENT_REQUEST_METHOD_SECURE_PAYMENT_CONFIRMATION); } else if (events_ & EVENT_SELECTED_OTHER) { // It is possible that a service worker based app responds to "basic-card" // request. @@ -488,6 +518,9 @@ void JourneyLogger::ValidateEventBits() const { if (events_ & EVENT_SKIPPED_SHOW) { // Built in autofill payment handler for basic card should not skip UI show. DCHECK(!(events_ & EVENT_SELECTED_CREDIT_CARD)); + // Internal secure payment confirmation payment handler should not skip UI + // show. + DCHECK(!(events_ & EVENT_SELECTED_SECURE_PAYMENT_CONFIRMATION)); } // Check that the two bits are not set at the same time. diff --git a/chromium/components/payments/core/journey_logger.h b/chromium/components/payments/core/journey_logger.h index 4a3d7df3e12..fdf7c1c83af 100644 --- a/chromium/components/payments/core/journey_logger.h +++ b/chromium/components/payments/core/journey_logger.h @@ -62,7 +62,7 @@ class JourneyLogger { EVENT_SKIPPED_SHOW = 1 << 3, // .complete() was called by the merchant, completing the flow. EVENT_COMPLETED = 1 << 4, - // The user aborted the flow by either dismissing it explicitely, or + // The user aborted the flow by either dismissing it explicitly, or // navigating away (if possible). EVENT_USER_ABORTED = 1 << 5, // Other reasons for aborting include the merchant calling .abort(), the @@ -108,7 +108,12 @@ class JourneyLogger { EVENT_AVAILABLE_METHOD_BASIC_CARD = 1 << 27, EVENT_AVAILABLE_METHOD_GOOGLE = 1 << 28, EVENT_AVAILABLE_METHOD_OTHER = 1 << 29, - EVENT_ENUM_MAX = 1 << 30, + + // Bits for secure-payment-confirmation method. + EVENT_REQUEST_METHOD_SECURE_PAYMENT_CONFIRMATION = 1 << 30, + EVENT_SELECTED_SECURE_PAYMENT_CONFIRMATION = 1 << 31, + + EVENT_ENUM_MAX = EVENT_SELECTED_SECURE_PAYMENT_CONFIRMATION, }; // The reason why the Payment Request was aborted. @@ -152,6 +157,30 @@ class JourneyLogger { kMaxValue = kRegularTransaction, }; + // Records different checkout steps for payment requests. The difference + // between number of requests recorded for each step and its successor shows + // the drop-off that happened during that step. + // GENERATED_JAVA_ENUM_PACKAGE: org.chromium.components.payments + // GENERATED_JAVA_CLASS_NAME_OVERRIDE: CheckoutFunnelStep + enum class CheckoutFunnelStep { + // Payment request has been initiated. + kInitiated = 0, + // .show() has been called. (a.k.a. the user has clicked on the checkout/buy + // button on merchant's site.) + kShowCalled = 1, + // Payment request UI has been shown or skipped in favor of the payment + // handler UI. Drop-off before this step means that the browser could not + // proceed with the payment request. (e.g. because of no payment app being + // available for requested payment method(s) or an unsecured origin.) + kPaymentRequestTriggered = 2, + // Payment handler UI has been invoked either by skipping to it directly or + // the user clicking on the "Continue" button in payment sheet UI. + kPaymentHandlerInvoked = 3, + // Payment request has been completed with 'success' status. + kCompleted = 4, + kMaxValue = kCompleted, + }; + JourneyLogger(bool is_incognito, ukm::SourceId payment_request_source_id); ~JourneyLogger(); @@ -187,12 +216,15 @@ class JourneyLogger { bool requested_name); // Records the requested payment method types. A value should be true if at - // least one payment method in the category (basic-card, google payment method - // or other url-based payment method, respectively) is requested. + // least one payment method in the category (basic-card, google payment + // method, secure payment confirmation method or other url-based payment + // method, respectively) is requested. // TODO(crbug.com/754811): Add support for non-basic-card, non-URL methods. - void SetRequestedPaymentMethodTypes(bool requested_basic_card, - bool requested_method_google, - bool requested_method_other); + void SetRequestedPaymentMethodTypes( + bool requested_basic_card, + bool requested_method_google, + bool requested_method_secure_payment_confirmation, + bool requested_method_other); // Records that the Payment Request was completed successfully, and starts the // logging of all the journey metrics. @@ -206,16 +238,19 @@ class JourneyLogger { // reason. void SetNotShown(NotShownReason reason); - // Records the transcation amount after converting to USD separated by + // Records the transaction amount after converting to USD separated by // completion status (complete vs triggered). void RecordTransactionAmount(std::string currency, const std::string& value, bool completed); + // Increments the bucket count for the given checkout step. + void RecordCheckoutStep(CheckoutFunnelStep step); + // Records when Payment Request .show is called. void SetTriggerTime(); - // Sets the ukm source id of the selected app when it gets invoked. + // Sets the UKM source id of the selected app when it gets invoked. void SetPaymentAppUkmSourceId(ukm::SourceId payment_app_source_id); private: diff --git a/chromium/components/payments/core/journey_logger_unittest.cc b/chromium/components/payments/core/journey_logger_unittest.cc index c430e07138c..468e4e71750 100644 --- a/chromium/components/payments/core/journey_logger_unittest.cc +++ b/chromium/components/payments/core/journey_logger_unittest.cc @@ -97,6 +97,7 @@ TEST(JourneyLoggerTest, logger.SetRequestedInformation(true, false, false, false); logger.SetRequestedPaymentMethodTypes( /*requested_basic_card=*/true, /*requested_method_google=*/false, + /*requested_method_secure_payment_confirmation=*/false, /*requested_method_other=*/false); logger.SetEventOccurred(JourneyLogger::EVENT_PAY_CLICKED); logger.SetEventOccurred(JourneyLogger::EVENT_SELECTED_CREDIT_CARD); @@ -214,6 +215,7 @@ TEST(JourneyLoggerTest, logger.SetRequestedInformation(true, false, false, false); logger.SetRequestedPaymentMethodTypes( /*requested_basic_card=*/true, /*requested_method_google=*/false, + /*requested_method_secure_payment_confirmation=*/false, /*requested_method_other=*/false); logger.SetCanMakePaymentValue(false); logger.SetEventOccurred(JourneyLogger::EVENT_PAY_CLICKED); @@ -291,6 +293,7 @@ TEST(JourneyLoggerTest, logger.SetRequestedInformation(true, false, false, false); logger.SetRequestedPaymentMethodTypes( /*requested_basic_card=*/true, /*requested_method_google=*/false, + /*requested_method_secure_payment_confirmation=*/false, /*requested_method_other=*/false); logger.SetCanMakePaymentValue(true); logger.SetEventOccurred(JourneyLogger::EVENT_PAY_CLICKED); @@ -320,6 +323,7 @@ TEST(JourneyLoggerTest, logger.SetRequestedInformation(true, false, false, false); logger.SetRequestedPaymentMethodTypes( /*requested_basic_card=*/true, /*requested_method_google=*/false, + /*requested_method_secure_payment_confirmation=*/false, /*requested_method_other=*/false); logger.SetCanMakePaymentValue(true); logger.SetEventOccurred(JourneyLogger::EVENT_PAY_CLICKED); @@ -349,6 +353,7 @@ TEST(JourneyLoggerTest, /*requested_phone=*/false, /*requested_name=*/false); logger.SetRequestedPaymentMethodTypes( /*requested_basic_card=*/true, /*requested_method_google=*/true, + /*requested_method_secure_payment_confirmation=*/false, /*requested_method_other=*/false); // Simulate that the user had suggestions for all the requested sections. @@ -396,6 +401,7 @@ TEST(JourneyLoggerTest, /*requested_phone=*/false, /*requested_name=*/false); logger.SetRequestedPaymentMethodTypes( /*requested_basic_card=*/true, /*requested_method_google=*/true, + /*requested_method_secure_payment_confirmation=*/false, /*requested_method_other=*/false); // Simulate that the user had suggestions for all the requested sections. @@ -441,6 +447,7 @@ TEST(JourneyLoggerTest, /*requested_phone=*/false, /*requested_name=*/false); logger.SetRequestedPaymentMethodTypes( /*requested_basic_card=*/true, /*requested_method_google=*/true, + /*requested_method_secure_payment_confirmation=*/false, /*requested_method_other=*/false); // Simulate that the user had suggestions for all the requested sections. @@ -487,6 +494,7 @@ TEST(JourneyLoggerTest, /*requested_phone=*/false, /*requested_name=*/false); logger.SetRequestedPaymentMethodTypes( /*requested_basic_card=*/true, /*requested_method_google=*/true, + /*requested_method_secure_payment_confirmation=*/false, /*requested_method_other=*/false); // Simulate that the user had suggestions for all the requested sections. @@ -534,6 +542,7 @@ TEST(JourneyLoggerTest, /*requested_phone=*/false, /*requested_name=*/false); logger.SetRequestedPaymentMethodTypes( /*requested_basic_card=*/true, /*requested_method_google=*/true, + /*requested_method_secure_payment_confirmation=*/false, /*requested_method_other=*/false); // Simulate that the user had suggestions for none of the requested sections. @@ -581,6 +590,7 @@ TEST(JourneyLoggerTest, /*requested_phone=*/false, /*requested_name=*/false); logger.SetRequestedPaymentMethodTypes( /*requested_basic_card=*/true, /*requested_method_google=*/true, + /*requested_method_secure_payment_confirmation=*/false, /*requested_method_other=*/false); // Simulate that the user had suggestions for none of the requested sections. @@ -626,6 +636,7 @@ TEST(JourneyLoggerTest, /*requested_phone=*/false, /*requested_name=*/false); logger.SetRequestedPaymentMethodTypes( /*requested_basic_card=*/true, /*requested_method_google=*/true, + /*requested_method_secure_payment_confirmation=*/false, /*requested_method_other=*/false); // Simulate that the user had suggestions for none of the requested sections. @@ -672,6 +683,7 @@ TEST(JourneyLoggerTest, /*requested_phone=*/false, /*requested_name=*/false); logger.SetRequestedPaymentMethodTypes( /*requested_basic_card=*/true, /*requested_method_google=*/true, + /*requested_method_secure_payment_confirmation=*/false, /*requested_method_other=*/false); // Simulate that the user had suggestions for none of the requested sections. @@ -718,6 +730,7 @@ TEST( /*requested_phone=*/false, /*requested_name=*/false); logger.SetRequestedPaymentMethodTypes( /*requested_basic_card=*/true, /*requested_method_google=*/true, + /*requested_method_secure_payment_confirmation=*/false, /*requested_method_other=*/false); // Simulate that the user had incomplete suggestions for the requested @@ -765,6 +778,7 @@ TEST( /*requested_phone=*/false, /*requested_name=*/false); logger.SetRequestedPaymentMethodTypes( /*requested_basic_card=*/true, /*requested_method_google=*/true, + /*requested_method_secure_payment_confirmation=*/false, /*requested_method_other=*/false); // Simulate that the user had incomplete suggestions for one of the requested @@ -814,6 +828,7 @@ TEST( /*requested_phone=*/false, /*requested_name=*/false); logger.SetRequestedPaymentMethodTypes( /*requested_basic_card=*/true, /*requested_method_google=*/true, + /*requested_method_secure_payment_confirmation=*/false, /*requested_method_other=*/false); // Simulate that the user had incomplete suggestions for one of the requested @@ -863,6 +878,7 @@ TEST(JourneyLoggerTest, RecordJourneyStatsHistograms_TwoPaymentRequests) { /*requested_phone=*/false, /*requested_name=*/false); logger1.SetRequestedPaymentMethodTypes( /*requested_basic_card=*/true, /*requested_method_google=*/true, + /*requested_method_secure_payment_confirmation=*/false, /*requested_method_other=*/true); logger2.SetEventOccurred(JourneyLogger::EVENT_SHOWN); logger2.SetRequestedInformation( @@ -870,6 +886,7 @@ TEST(JourneyLoggerTest, RecordJourneyStatsHistograms_TwoPaymentRequests) { /*requested_phone=*/false, /*requested_name=*/false); logger2.SetRequestedPaymentMethodTypes( /*requested_basic_card=*/false, /*requested_method_google=*/false, + /*requested_method_secure_payment_confirmation=*/false, /*requested_method_other=*/true); logger1.SetCanMakePaymentValue(true); @@ -945,6 +962,7 @@ TEST(JourneyLoggerTest, /*requested_phone=*/false, /*requested_name=*/false); logger.SetRequestedPaymentMethodTypes( /*requested_basic_card=*/true, /*requested_method_google=*/false, + /*requested_method_secure_payment_confirmation=*/false, /*requested_method_other=*/false); // Simulate that the user aborts after being shown the Payment Request and @@ -996,6 +1014,7 @@ TEST(JourneyLoggerTest, /*requested_phone=*/false, /*requested_name=*/false); logger.SetRequestedPaymentMethodTypes( /*requested_basic_card=*/true, /*requested_method_google=*/false, + /*requested_method_secure_payment_confirmation=*/false, /*requested_method_other=*/false); // Simulate that the user aborts after being shown the Payment Request. diff --git a/chromium/components/payments/core/method_strings.cc b/chromium/components/payments/core/method_strings.cc index 177d810616c..bcd0248dbcd 100644 --- a/chromium/components/payments/core/method_strings.cc +++ b/chromium/components/payments/core/method_strings.cc @@ -18,6 +18,7 @@ const char kGooglePlayBilling[] = "https://play.google.com/billing"; const char kInterledger[] = "interledger"; const char kPayeeCreditTransfer[] = "payee-credit-transfer"; const char kPayerCreditTransfer[] = "payer-credit-transfer"; +const char kSecurePaymentConfirmation[] = "secure-payment-confirmation"; const char kTokenizedCard[] = "tokenized-card"; } // namespace methods diff --git a/chromium/components/payments/core/method_strings.h b/chromium/components/payments/core/method_strings.h index 25f97e8b6cb..90a199f5bc5 100644 --- a/chromium/components/payments/core/method_strings.h +++ b/chromium/components/payments/core/method_strings.h @@ -40,6 +40,10 @@ extern const char kPayeeCreditTransfer[]; // https://w3c.github.io/payment-method-credit-transfer/ extern const char kPayerCreditTransfer[]; +// Secure Payment Confirmation method name. +// https://github.com/rsolomakhin/secure-payment-confirmation/ +extern const char kSecurePaymentConfirmation[]; + // Tokenized Card method name. // https://w3c.github.io/webpayments-methods-tokenization/ extern const char kTokenizedCard[]; diff --git a/chromium/components/payments/core/native_error_strings.cc b/chromium/components/payments/core/native_error_strings.cc index 1d2a70c8e33..477f11da3da 100644 --- a/chromium/components/payments/core/native_error_strings.cc +++ b/chromium/components/payments/core/native_error_strings.cc @@ -201,5 +201,22 @@ const char kNoContentAndNoLinkHeader[] = const char kNoContentInPaymentManifest[] = "No content found in payment manifest \"$1\"."; +const char kUnableToInvokeAndroidPaymentApps[] = + "Unable to invoke Android apps."; + +const char kUserClosedPaymentApp[] = "User closed the payment app."; + +const char kMoreThanOneService[] = + "Found more than one IS_READY_TO_PAY service, but at most one service is " + "supported."; + +const char kCredentialIdsRequired[] = + "The \"secure-payment-confirmation\" method requires a non-empty " + "\"credentialIds\" array of non-empty arrays."; + +const char kTimeoutTooLong[] = + "The \"secure-payment-confirmation\" method requires at most 1 hour " + "\"timeout\" field."; + } // namespace errors } // namespace payments diff --git a/chromium/components/payments/core/native_error_strings.h b/chromium/components/payments/core/native_error_strings.h index d70468ab0e3..e1f6a0465a2 100644 --- a/chromium/components/payments/core/native_error_strings.h +++ b/chromium/components/payments/core/native_error_strings.h @@ -222,10 +222,29 @@ extern const char kGenericPaymentMethodNotSupportedMessage[]; // be used with base::ReplaceStringPlaceholders(fmt, {A}, nullptr). extern const char kNoContentAndNoLinkHeader[]; -// User when the downloaded payment manifest A is empty. This format should be +// Used when the downloaded payment manifest A is empty. This format should be // used with base::ReplaceStringPlaceholders(fmt, {A}, nullptr). extern const char kNoContentInPaymentManifest[]; +// Used when it's impossible to invoke Android payment apps, e.g., when ARC is +// disabled on Chrome OS. +extern const char kUnableToInvokeAndroidPaymentApps[]; + +// Used when the user has closed the payment app. For example, An Android app +// indicates this by returning Activity.RESULT_CANCELED. +extern const char kUserClosedPaymentApp[]; + +// Used when an Android app declares more than one IS_READY_TO_PAY service. +extern const char kMoreThanOneService[]; + +// Used when no credential IDs are specified for the +// "secure-payment-confirmation" method. +extern const char kCredentialIdsRequired[]; + +// Used when the timeout specified for the "secure-payment-confirmation" method +// is too long. +extern const char kTimeoutTooLong[]; + } // namespace errors } // namespace payments diff --git a/chromium/components/payments/core/payment_manifest_downloader_unittest.cc b/chromium/components/payments/core/payment_manifest_downloader_unittest.cc index 9d72ec2bd5d..4cf6aabde2a 100644 --- a/chromium/components/payments/core/payment_manifest_downloader_unittest.cc +++ b/chromium/components/payments/core/payment_manifest_downloader_unittest.cc @@ -11,7 +11,6 @@ #include "base/threading/thread_task_runner_handle.h" #include "components/payments/core/error_logger.h" #include "net/http/http_response_headers.h" -#include "net/url_request/url_request_test_util.h" #include "services/network/public/cpp/simple_url_loader.h" #include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h" #include "services/network/test/test_url_loader_factory.h" diff --git a/chromium/components/payments/core/payment_request_data_util.cc b/chromium/components/payments/core/payment_request_data_util.cc index 9d7766dc959..b3f87858338 100644 --- a/chromium/components/payments/core/payment_request_data_util.cc +++ b/chromium/components/payments/core/payment_request_data_util.cc @@ -4,7 +4,7 @@ #include "components/payments/core/payment_request_data_util.h" -#include <memory> +#include <utility> #include "base/stl_util.h" #include "base/strings/string_split.h" @@ -166,5 +166,19 @@ base::string16 FormatCardNumberForDisplay(const base::string16& card_number) { return number; } +std::unique_ptr<std::map<std::string, std::set<std::string>>> +FilterStringifiedMethodData( + const std::map<std::string, std::set<std::string>>& stringified_method_data, + const std::set<std::string>& supported_payment_method_names) { + auto result = + std::make_unique<std::map<std::string, std::set<std::string>>>(); + for (const auto& pair : stringified_method_data) { + if (base::Contains(supported_payment_method_names, pair.first)) { + result->insert({pair.first, pair.second}); + } + } + return result; +} + } // namespace data_util } // namespace payments diff --git a/chromium/components/payments/core/payment_request_data_util.h b/chromium/components/payments/core/payment_request_data_util.h index 633d5173260..bdaaaae2719 100644 --- a/chromium/components/payments/core/payment_request_data_util.h +++ b/chromium/components/payments/core/payment_request_data_util.h @@ -5,6 +5,7 @@ #ifndef COMPONENTS_PAYMENTS_CORE_PAYMENT_REQUEST_DATA_UTIL_H_ #define COMPONENTS_PAYMENTS_CORE_PAYMENT_REQUEST_DATA_UTIL_H_ +#include <memory> #include <set> #include <string> #include <vector> @@ -64,6 +65,24 @@ void ParseSupportedMethods( // numbers, which start with a letter. base::string16 FormatCardNumberForDisplay(const base::string16& card_number); +// Returns the subset of |stringified_method_data| map where the keys are in the +// |supported_payment_method_names| set. Used for ensuring that a payment app +// will not be queried about payment method names that it does not support. +// +// FilterStringifiedMethodData({"a": {"b"}: "c": {"d"}}, {"a"}) -> {"a": {"b"}} +// +// Both the return value and the first parameter to the function have the +// following format: +// Key: Payment method identifier, such as "example-test" or +// "https://example.test". +// Value: The set of all payment method specific parameters for the given +// payment method identifier, each one serialized into a JSON string, +// e.g., '{"key": "value"}'. +std::unique_ptr<std::map<std::string, std::set<std::string>>> +FilterStringifiedMethodData( + const std::map<std::string, std::set<std::string>>& stringified_method_data, + const std::set<std::string>& supported_payment_method_names); + } // namespace data_util } // namespace payments diff --git a/chromium/components/payments/core/payment_request_data_util_unittest.cc b/chromium/components/payments/core/payment_request_data_util_unittest.cc index b034fca5f37..19f2578b3a5 100644 --- a/chromium/components/payments/core/payment_request_data_util_unittest.cc +++ b/chromium/components/payments/core/payment_request_data_util_unittest.cc @@ -4,7 +4,10 @@ #include "components/payments/core/payment_request_data_util.h" +#include <map> #include <memory> +#include <set> +#include <string> #include "base/json/json_writer.h" #include "base/macros.h" @@ -274,5 +277,25 @@ TEST(PaymentRequestDataUtil, ParseSupportedMethods_MultipleEntries) { UnorderedElementsAre(kBasicCardMethodName, kBobPayMethod)); } +TEST(PaymentRequestDataUtil, FilterStringifiedMethodData) { + std::map<std::string, std::set<std::string>> requested; + std::set<std::string> supported; + EXPECT_TRUE(FilterStringifiedMethodData(requested, supported)->empty()); + + requested["a"].insert("{\"b\": \"c\"}"); + EXPECT_TRUE(FilterStringifiedMethodData(requested, supported)->empty()); + + requested["x"].insert("{\"y\": \"z\"}"); + EXPECT_TRUE(FilterStringifiedMethodData(requested, supported)->empty()); + + supported.insert("x"); + std::map<std::string, std::set<std::string>> expected; + expected["x"].insert("{\"y\": \"z\"}"); + EXPECT_EQ(expected, *FilterStringifiedMethodData(requested, supported)); + + supported.insert("g"); + EXPECT_EQ(expected, *FilterStringifiedMethodData(requested, supported)); +} + } // namespace data_util } // namespace payments diff --git a/chromium/components/payments/core/payment_request_delegate.h b/chromium/components/payments/core/payment_request_delegate.h index dadb21e5018..e0356aa10bd 100644 --- a/chromium/components/payments/core/payment_request_delegate.h +++ b/chromium/components/payments/core/payment_request_delegate.h @@ -14,9 +14,16 @@ class PaymentRequest; class PaymentRequestDelegate : public PaymentRequestBaseDelegate { public: + enum class DialogType { + PAYMENT_REQUEST, + SECURE_PAYMENT_CONFIRMATION, + }; + PaymentRequestDelegate(); ~PaymentRequestDelegate() override; + void set_dialog_type(DialogType dialog_type) { dialog_type_ = dialog_type; } + // Shows the Payment Request dialog for the given |request|. virtual void ShowDialog(PaymentRequest* request) = 0; @@ -39,6 +46,9 @@ class PaymentRequestDelegate : public PaymentRequestBaseDelegate { // Returns a weak pointer to this delegate. base::WeakPtr<PaymentRequestDelegate> GetWeakPtr(); + protected: + DialogType dialog_type_ = DialogType::PAYMENT_REQUEST; + private: base::WeakPtrFactory<PaymentRequestDelegate> weak_ptr_factory_{this}; }; diff --git a/chromium/components/payments/core/secure_payment_confirmation_instrument.cc b/chromium/components/payments/core/secure_payment_confirmation_instrument.cc new file mode 100644 index 00000000000..1674561bc05 --- /dev/null +++ b/chromium/components/payments/core/secure_payment_confirmation_instrument.cc @@ -0,0 +1,32 @@ +// Copyright 2020 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. + +#include "components/payments/core/secure_payment_confirmation_instrument.h" + +#include <utility> + +namespace payments { + +SecurePaymentConfirmationInstrument::SecurePaymentConfirmationInstrument() = + default; + +SecurePaymentConfirmationInstrument::SecurePaymentConfirmationInstrument( + std::vector<uint8_t> credential_id, + const std::string& relying_party_id, + const base::string16& label, + std::vector<uint8_t> icon) + : credential_id(std::move(credential_id)), + relying_party_id(relying_party_id), + label(label), + icon(std::move(icon)) {} + +SecurePaymentConfirmationInstrument::~SecurePaymentConfirmationInstrument() = + default; + +bool SecurePaymentConfirmationInstrument::IsValid() const { + return !credential_id.empty() && !relying_party_id.empty() && + !label.empty() && !icon.empty(); +} + +} // namespace payments
\ No newline at end of file diff --git a/chromium/components/payments/core/secure_payment_confirmation_instrument.h b/chromium/components/payments/core/secure_payment_confirmation_instrument.h new file mode 100644 index 00000000000..8540b760a8e --- /dev/null +++ b/chromium/components/payments/core/secure_payment_confirmation_instrument.h @@ -0,0 +1,51 @@ +// Copyright 2020 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. + +#ifndef COMPONENTS_PAYMENTS_CORE_SECURE_PAYMENT_CONFIRMATION_INSTRUMENT_H_ +#define COMPONENTS_PAYMENTS_CORE_SECURE_PAYMENT_CONFIRMATION_INSTRUMENT_H_ + +#include <stdint.h> + +#include <string> +#include <vector> + +#include "base/strings/string16.h" + +namespace payments { + +// Secure payment information instrument information that can be stored in +// SQLite database. +struct SecurePaymentConfirmationInstrument { + // Constructs an empty instrument. This instrument is not valid until all + // fields are populated. + SecurePaymentConfirmationInstrument(); + + // Constructs an instrument with the given fields. Please use `std::move()` + // when passing the `credential_id` and the `icon` byte arrays to avoid + // excessive copying. + SecurePaymentConfirmationInstrument(std::vector<uint8_t> credential_id, + const std::string& relying_party_id, + const base::string16& label, + std::vector<uint8_t> icon); + + ~SecurePaymentConfirmationInstrument(); + + // An instrument should not be copied or assigned. + SecurePaymentConfirmationInstrument( + const SecurePaymentConfirmationInstrument& other) = delete; + SecurePaymentConfirmationInstrument& operator=( + const SecurePaymentConfirmationInstrument& other) = delete; + + // Checks instrument validity. + bool IsValid() const; + + std::vector<uint8_t> credential_id; + std::string relying_party_id; + base::string16 label; + std::vector<uint8_t> icon; +}; + +} // namespace payments + +#endif // COMPONENTS_PAYMENTS_CORE_SECURE_PAYMENT_CONFIRMATION_INSTRUMENT_H_
\ No newline at end of file diff --git a/chromium/components/payments/core/test_payment_request_delegate.h b/chromium/components/payments/core/test_payment_request_delegate.h index df0b9dca01b..ff638f5f3aa 100644 --- a/chromium/components/payments/core/test_payment_request_delegate.h +++ b/chromium/components/payments/core/test_payment_request_delegate.h @@ -14,7 +14,6 @@ #include "components/autofill/core/browser/test_address_normalizer.h" #include "components/autofill/core/browser/test_autofill_client.h" #include "components/payments/core/payment_request_delegate.h" -#include "net/url_request/url_request_test_util.h" #include "services/network/public/cpp/shared_url_loader_factory.h" #include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h" #include "services/network/test/test_url_loader_factory.h" |