Commit ef61a21f authored by Danyao Wang's avatar Danyao Wang Committed by Commit Bot

[Payment Request] Blink-side changes for skip-to-GPay flow.

Skip-to-GPay is an experimental flow that skips the payment sheet for
{basic-card, GPay} hybrid requests. Today requests for shipping and
contact information is fulfilled using autofill data, this requires the
payment sheet to be shown to the user to confirm the information. Since
the Payment Handler API does not yet support a general mechanism for
delegating shipping and contact information to payment handlers, this
patch is part of a bridge solution customized for GPay.

With this patch, the renderer process will detect {basic-card, GPay}
hybrid requests that are eligible for the skip-to-GPay flow. Then it
creates a copy of the merchant-provided GPay-specific-data, and sets
additional parameters to request shipping and contact information from
GPay based on the merchant-provided PaymentOptions. The copy is passed
to the browser process to make a final decision (based on a Feature flag)
whether to invoke the skip-to-GPay flow.

The browser process changes will be landed in a separate patch.

Design Doc: https://docs.google.com/document/d/1rCA4tk1xS3nwiRqmAk1eCAh64K55wHOE1JWYNTqm9LQ/edit#

Bug: 877284
Change-Id: I7822fe0157fa6ee77e48035a2c1c0fae41168c8d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1769853
Commit-Queue: Danyao Wang <danyao@chromium.org>
Reviewed-by: default avatarDaniel Cheng <dcheng@chromium.org>
Reviewed-by: default avatarRouslan Solomakhin <rouslan@chromium.org>
Cr-Commit-Position: refs/heads/master@{#692072}
parent b88d8012
......@@ -47,7 +47,8 @@ public class PaymentRequestFactory implements InterfaceFactory<PaymentRequest> {
@Override
public void init(PaymentRequestClient client, PaymentMethodData[] methodData,
PaymentDetails details, PaymentOptions options) {
PaymentDetails details, PaymentOptions options,
boolean unusedGooglePayBridgeEligible) {
mClient = client;
}
......
......@@ -477,7 +477,7 @@ public class PaymentRequestImpl
*/
@Override
public void init(PaymentRequestClient client, PaymentMethodData[] methodData,
PaymentDetails details, PaymentOptions options) {
PaymentDetails details, PaymentOptions options, boolean googlePayBridgeEligible) {
if (mClient != null) {
mJourneyLogger.setAborted(AbortReason.INVALID_DATA_FROM_RENDERER);
disconnectFromClientWithDebugMessage(ErrorStrings.ATTEMPTED_INITIALIZATION_TWICE);
......
......@@ -105,6 +105,27 @@ enum BasicCardType {
PREPAID
};
struct GooglePaymentMethodData {
// A JSON string built by the renderer from the same merchant-provided
// JavaScript object used to build |stringified_data| in the containing
// PaymentMethodData. The renderer interprets the JavaScript object as a GPay
// API object and sets additional parameters to request contact and shipping
// information from the GPay payment app. The renderer uses
// blink::JSONObject::toJSONString() to generate this string from the modified
// JavaScript object. The browser does not parse the string and passes it
// as-is directly to the GPay payment app.
string stringified_data;
// Each of the following flags is true if the corresponding piece of data was
// not requested in |stringified_data| in the containing PaymentMethodDatabut
// is now set in |stringified_data| above. The Android browser process uses
// these flags to redact the response from the native GPay SDK before
// returning it to the renderer.
bool phone_requested;
bool name_requested;
bool email_requested;
bool shipping_requested;
};
struct PaymentMethodData {
string supported_method;
......@@ -118,6 +139,12 @@ struct PaymentMethodData {
// {"gateway": "stripe"}
string stringified_data;
// Data specific to the skip-to-GPay experimental flow. This field is only set
// if |supported_method| is Google Pay and if the current request is eligible
// for the skip-to-GPay experimental flow.
[EnableIf=is_android]
GooglePaymentMethodData? gpay_bridge_data;
// Android Pay specific method data is parsed in the renderer.
// https://developers.google.com/web/fundamentals/getting-started/primers/payment-request/android-pay
// TODO(rouslan): Stop parsing Android Pay data. http://crbug.com/620173
......@@ -188,10 +215,14 @@ enum PaymentComplete {
interface PaymentRequest {
// Instantiates the renderer-browser connection with the information from the
// JavaScript constructor of PaymentRequest.
// |google_pay_bridge_eligible| is 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.
Init(pending_remote<PaymentRequestClient> client,
array<PaymentMethodData> method_data,
PaymentDetails details,
PaymentOptions options);
PaymentOptions options,
[EnableIf=is_android] bool google_pay_bridge_eligible);
// Shows the user interface with the payment details.
Show(bool is_user_gesture, bool wait_for_updated_details);
......
......@@ -437,6 +437,10 @@ jumbo_source_set("unit_tests") {
"xr/xr_view_test.cc",
]
if (is_android) {
sources += [ "payments/skip_to_gpay_utils_test.cc" ]
}
if (is_android && notouch_build) {
sources += [
"media_controls/touchless/media_controls_touchless_impl_test.cc",
......
......@@ -53,6 +53,13 @@ blink_modules_sources("payments") {
"update_payment_details_function.h",
]
if (is_android) {
sources += [
"skip_to_gpay_utils.cc",
"skip_to_gpay_utils.h",
]
}
deps = [
"//components/payments/mojom:mojom_blink",
]
......
......@@ -5,10 +5,12 @@
#include "third_party/blink/renderer/modules/payments/payment_request.h"
#include <stddef.h>
#include <utility>
#include "base/location.h"
#include "base/stl_util.h"
#include "build/build_config.h"
#include "mojo/public/cpp/bindings/interface_request.h"
#include "services/service_manager/public/cpp/interface_provider.h"
#include "third_party/blink/public/mojom/devtools/console_message.mojom-blink.h"
......@@ -51,6 +53,9 @@
#include "third_party/blink/renderer/modules/payments/payment_shipping_option.h"
#include "third_party/blink/renderer/modules/payments/payment_validation_errors.h"
#include "third_party/blink/renderer/modules/payments/payments_validators.h"
#if defined(OS_ANDROID)
#include "third_party/blink/renderer/modules/payments/skip_to_gpay_utils.h"
#endif // defined(OS_ANDROID)
#include "third_party/blink/renderer/modules/payments/update_payment_details_function.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h"
......@@ -88,6 +93,8 @@ using ::payments::mojom::blink::PaymentValidationErrors;
using ::payments::mojom::blink::PaymentValidationErrorsPtr;
const char kHasEnrolledInstrumentDebugName[] = "hasEnrolledInstrument";
const char kGooglePayMethod[] = "https://google.com/pay";
const char kAndroidPayMethod[] = "https://android.com/pay";
} // namespace
......@@ -130,7 +137,7 @@ struct TypeConverter<PaymentShippingOptionPtr, blink::PaymentShippingOption*> {
};
template <>
struct TypeConverter<PaymentOptionsPtr, blink::PaymentOptions*> {
struct TypeConverter<PaymentOptionsPtr, const blink::PaymentOptions*> {
static PaymentOptionsPtr Convert(const blink::PaymentOptions* input) {
PaymentOptionsPtr output = payments::mojom::blink::PaymentOptions::New();
output->request_payer_name = input->requestPayerName();
......@@ -407,8 +414,8 @@ void StringifyAndParseMethodSpecificData(v8::Isolate* isolate,
// Serialize payment method specific data to be sent to the payment apps. The
// payment apps are responsible for validating and processing their method
// data asynchronously. Do not throw exceptions here.
if (supported_method == "https://android.com/pay" ||
supported_method == "https://google.com/pay") {
if (supported_method == kGooglePayMethod ||
supported_method == kAndroidPayMethod) {
SetAndroidPayMethodData(input, output, exception_state);
if (exception_state.HadException())
exception_state.ClearException();
......@@ -573,6 +580,8 @@ void ValidateAndConvertPaymentDetailsUpdate(const PaymentDetailsUpdate* input,
void ValidateAndConvertPaymentMethodData(
const HeapVector<Member<PaymentMethodData>>& input,
const PaymentOptions* options,
bool& skip_to_gpay_ready,
Vector<payments::mojom::blink::PaymentMethodDataPtr>& output,
HashSet<String>& method_names,
ExecutionContext& execution_context,
......@@ -588,6 +597,12 @@ void ValidateAndConvertPaymentMethodData(
return;
}
#if defined(OS_ANDROID)
// TODO(crbug.com/984694): Remove this special hack for GPay after general
// delegation for shipping and contact information is available.
bool skip_to_gpay_eligible = SkipToGPayUtils::IsEligible(input);
#endif
for (const PaymentMethodData* payment_method_data : input) {
if (!PaymentsValidators::IsValidMethodFormat(
payment_method_data->supportedMethod())) {
......@@ -598,7 +613,6 @@ void ValidateAndConvertPaymentMethodData(
method_names.insert(payment_method_data->supportedMethod());
output.push_back(payments::mojom::blink::PaymentMethodData::New());
output.back()->supported_method = payment_method_data->supportedMethod();
if (payment_method_data->hasData() &&
......@@ -607,6 +621,17 @@ void ValidateAndConvertPaymentMethodData(
execution_context.GetIsolate(),
payment_method_data->supportedMethod(), payment_method_data->data(),
output.back(), exception_state);
if (exception_state.HadException())
continue;
#if defined(OS_ANDROID)
if (skip_to_gpay_eligible &&
payment_method_data->supportedMethod() == kGooglePayMethod &&
SkipToGPayUtils::PatchPaymentMethodData(*options, output.back())) {
skip_to_gpay_ready = true;
}
#endif // defined(OS_ANDROID)
} else {
output.back()->stringified_data = "";
}
......@@ -1084,10 +1109,16 @@ PaymentRequest::PaymentRequest(
validated_details->id = id_ =
details->hasId() ? details->id() : WTF::CreateCanonicalUUIDString();
// This flag is set to true by ValidateAndConvertPaymentMethodData() if this
// request is eligible for the Skip-to-GPay experimental flow and the GPay
// payment method data has been patched to delegate shipping and contact
// information collection to the GPay payment app.
bool skip_to_gpay_ready = false;
Vector<payments::mojom::blink::PaymentMethodDataPtr> validated_method_data;
ValidateAndConvertPaymentMethodData(method_data, validated_method_data,
method_names_, *GetExecutionContext(),
exception_state);
ValidateAndConvertPaymentMethodData(method_data, options_, skip_to_gpay_ready,
validated_method_data, method_names_,
*GetExecutionContext(), exception_state);
if (exception_state.HadException())
return;
......@@ -1097,10 +1128,22 @@ PaymentRequest::PaymentRequest(
if (exception_state.HadException())
return;
if (options_->requestShipping())
if (options_->requestShipping()) {
shipping_type_ = options_->shippingType();
else
// Skip-to-GPay flow does not support changing shipping address or shipping
// options, so disable it if the merchant provides more than one shipping
// option for the user to choose from or if no payment option is selected up
// front, as this may indicate an intent to change it based on shipping
// address change.
if (!validated_details->shipping_options ||
!(validated_details->shipping_options->size() == 1 &&
validated_details->shipping_options->front()->selected)) {
skip_to_gpay_ready = false;
}
} else {
validated_details->shipping_options = base::nullopt;
}
DCHECK(shipping_type_.IsNull() || shipping_type_ == "shipping" ||
shipping_type_ == "delivery" || shipping_type_ == "pickup");
......@@ -1116,10 +1159,18 @@ PaymentRequest::PaymentRequest(
UseCounter::Count(execution_context, WebFeature::kPaymentRequestInitialized);
mojo::PendingRemote<payments::mojom::blink::PaymentRequestClient> client;
client_receiver_.Bind(client.InitWithNewPipeAndPassReceiver(), task_runner);
payment_provider_->Init(std::move(client), std::move(validated_method_data),
std::move(validated_details),
payments::mojom::blink::PaymentOptions::From(
const_cast<PaymentOptions*>(options_.Get())));
#if defined(OS_ANDROID)
payment_provider_->Init(
std::move(client), std::move(validated_method_data),
std::move(validated_details),
payments::mojom::blink::PaymentOptions::From(options_.Get()),
skip_to_gpay_ready);
#else
payment_provider_->Init(
std::move(client), std::move(validated_method_data),
std::move(validated_details),
payments::mojom::blink::PaymentOptions::From(options_.Get()));
#endif
}
void PaymentRequest::ContextDestroyed(ExecutionContext*) {
......
// 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.
#include "third_party/blink/renderer/modules/payments/skip_to_gpay_utils.h"
#include "base/logging.h"
#include "third_party/blink/renderer/modules/payments/payment_method_data.h"
#include "third_party/blink/renderer/modules/payments/payment_options.h"
#include "third_party/blink/renderer/platform/heap/handle.h"
#include "third_party/blink/renderer/platform/json/json_parser.h"
#include "third_party/blink/renderer/platform/json/json_values.h"
namespace blink {
namespace {
using ::payments::mojom::blink::GooglePaymentMethodDataPtr;
using ::payments::mojom::blink::PaymentMethodDataPtr;
// Convenience function to return an object-type value for |name| in |map|, and
// inserts if |name| does not exist in |map|.
JSONObject* GetJSONObjectOrInsert(JSONObject* map, const String& name) {
if (!map->GetJSONObject(name)) {
map->SetObject(name, std::make_unique<JSONObject>());
}
return map->GetJSONObject(name);
}
// Update |output| to request payer name and payer phone number, if they are
// requested in |options|. |output| is expected to conform to GPay API v1.
// Returns true if |output| is successfully modified. The output arguments
// |phone_requested| and |name_requested| are set to true if the corresponding
// parameters are not already set before the update.
bool PatchGooglePayContactRequestV1(const PaymentOptions& options,
JSONObject* output,
bool* phone_requested,
bool* name_requested) {
if (options.requestPayerName()) {
const JSONObject* card_requirements =
output->GetJSONObject("cardRequirements");
if (!card_requirements ||
!card_requirements->BooleanProperty("billingAddressRequired", false)) {
GetJSONObjectOrInsert(output, "cardRequirements")
->SetBoolean("billingAddressRequired", true);
*name_requested = true;
}
}
if (options.requestPayerPhone() &&
!output->BooleanProperty("phoneNumberRequired", false)) {
output->SetBoolean("phoneNumberRequired", true);
*phone_requested = true;
}
return true;
}
// Update |output| to request payer name and payer phone number, if they are
// requested in |options|. |output| is expected to conform to GPay API v2.
// Returns true if |output| is successfully modified. The output arguments
// |phone_requested| and |name_requested| are set to true if the corresponding
// parameters are not already set before the update.
// See
// https://developers.google.com/pay/api/web/reference/object#PaymentDataRequest
bool PatchGooglePayContactRequestV2(const PaymentOptions& options,
JSONObject* output,
bool* phone_requested,
bool* name_requested) {
JSONObject* card_method = nullptr;
if (options.requestPayerName() || options.requestPayerPhone()) {
JSONArray* payment_methods = output->GetArray("allowedPaymentMethods");
if (!payment_methods)
return false;
for (wtf_size_t i = 0; i < payment_methods->size(); i++) {
JSONObject* method = JSONObject::Cast(payment_methods->at(i));
String method_type;
if (method && method->GetString("type", &method_type) &&
method_type == "CARD") {
card_method = method;
break;
}
}
if (!card_method)
return false;
const JSONObject* parameters = card_method->GetJSONObject("parameters");
if (!parameters ||
!parameters->BooleanProperty("billingAddressRequired", false)) {
GetJSONObjectOrInsert(card_method, "parameters")
->SetBoolean("billingAddressRequired", true);
*name_requested = true;
}
}
if (options.requestPayerPhone()) {
const JSONObject* parameters = card_method->GetJSONObject("parameters");
const JSONObject* billing_parameters =
parameters ? parameters->GetJSONObject("billingAddressParameters")
: nullptr;
if (!billing_parameters ||
!billing_parameters->BooleanProperty("phoneNumberRequired", false)) {
GetJSONObjectOrInsert(GetJSONObjectOrInsert(card_method, "parameters"),
"billingAddressParameters")
->SetBoolean("phoneNumberRequired", true);
*phone_requested = true;
}
}
return true;
}
} // namespace
bool SkipToGPayUtils::IsEligible(
const HeapVector<Member<PaymentMethodData>>& method_data) {
bool has_basic_card = false;
bool has_gpay = false;
bool has_other = false;
for (const PaymentMethodData* payment_method_data : method_data) {
if (payment_method_data->supportedMethod() == "basic-card") {
has_basic_card = true;
} else if (payment_method_data->supportedMethod() ==
"https://google.com/pay") {
has_gpay = true;
} else if (payment_method_data->supportedMethod() !=
"https://android.com/pay") {
has_other = true;
}
}
return has_basic_card && has_gpay && !has_other;
}
bool SkipToGPayUtils::PatchPaymentMethodData(
const PaymentOptions& options,
PaymentMethodDataPtr& payment_method_data) {
DCHECK_EQ("https://google.com/pay", payment_method_data->supported_method);
GooglePaymentMethodDataPtr gpay =
payments::mojom::blink::GooglePaymentMethodData::New();
const String& input = payment_method_data->stringified_data;
String& output = gpay->stringified_data;
gpay->phone_requested = false;
gpay->name_requested = false;
gpay->email_requested = false;
gpay->shipping_requested = false;
// |input| has just been serialized from a ScriptValue. We are taking the
// performance hit of parsing it again here because blink::JSONObject provides
// a nicer interface to work with the underlying object than v8::Object. Using
// an IDLDictionary is not feasible either because the conversion from
// v8::Object to IDLDictionary is lossy without fully duplicating the entire
// GPay request schema in IDL. It should be OK for now as this code is only
// exercised on the experimental skip-to-GPay flow.
JSONParseError error;
std::unique_ptr<JSONValue> value = ParseJSON(input, &error);
if (error.type != JSONParseErrorType::kNoError) {
return false;
}
std::unique_ptr<JSONObject> object = JSONObject::From(std::move(value));
if (!object)
return false;
int api_version;
if (!object->GetInteger("apiVersion", &api_version))
return false;
bool success = true;
if (api_version == 1) {
success &= PatchGooglePayContactRequestV1(options, object.get(),
&(gpay->phone_requested),
&(gpay->name_requested));
} else if (api_version == 2) {
success &= PatchGooglePayContactRequestV2(options, object.get(),
&(gpay->phone_requested),
&(gpay->name_requested));
} else {
return false;
}
if (options.requestPayerEmail() &&
!object->BooleanProperty("emailRequired", false)) {
object->SetBoolean("emailRequired", true);
gpay->email_requested = true;
}
if (options.requestShipping() &&
!object->BooleanProperty("shippingAddressRequired", false)) {
object->SetBoolean("shippingAddressRequired", true);
gpay->shipping_requested = true;
}
if (success) {
output = object->ToJSONString();
payment_method_data->gpay_bridge_data = std::move(gpay);
}
return success;
}
} // namespace blink
// 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.
#ifndef THIRD_PARTY_BLINK_RENDERER_MODULES_PAYMENTS_SKIP_TO_GPAY_UTILS_H_
#define THIRD_PARTY_BLINK_RENDERER_MODULES_PAYMENTS_SKIP_TO_GPAY_UTILS_H_
#include "third_party/blink/public/mojom/payments/payment_request.mojom-blink.h"
#include "third_party/blink/renderer/modules/modules_export.h"
#include "third_party/blink/renderer/platform/heap/handle.h"
namespace blink {
class PaymentOptions;
class PaymentMethodData;
class MODULES_EXPORT SkipToGPayUtils final {
STATIC_ONLY(SkipToGPayUtils);
public:
// Skip-to-Google-Pay is eligible if the request specifies basic-card and
// https://google.com/pay, with no other URL-based payment methods that is
// not https://android.com/pay.
static bool IsEligible(
const HeapVector<Member<blink::PaymentMethodData>>& method_data);
// Given a |payment_method_data| that contains the merchant-specified
// GPay-specific data, patch it with the following additional parameters so
// shipping and contact information requested in |options| can be requested
// from GPay, if the experiment is enabled in the browser process:
// - |gpay_bridge_data.stringified_data|: encodes a JSON object that is an
// extension of the JSON object in |stringified_data| with parameters
// for shipping and contact information set.
// - |gpay_bridge_data.{phone, name, email, shipping}_requested|: each flag is
// set to true if the corresponding piece of information was not in the
// merchant-specified GPay data, but is now set in
// |gpay_bridge_data.stringified_data|.
// Returns true if |gpay_bridge_data| is generated successfully.
// |payment_method_data| is guaranteed to be unchanged if this function
// returns false.
static bool PatchPaymentMethodData(
const PaymentOptions& options,
::payments::mojom::blink::PaymentMethodDataPtr& payment_method_data);
};
} // namespace blink
#endif // THIRD_PARTY_BLINK_RENDERER_MODULES_PAYMENTS_SKIP_TO_GPAY_UTILS_H_
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment