Commit f8f56a44 authored by Liquan (Max) Gu's avatar Liquan (Max) Gu Committed by Commit Bot

[PlayBilling] Total optional on only app-store billing requested

Context:
Total is a field in the "new PaymentRequest()" API. It specifies the
amount and currency of the payment request. However, when the merchant
requests for the app-store billing (e.g., Google Play Store billing -
by specifying "https://play.google.com/billing" as the method), the
total field becomes unnecessary. This is because app-stores takes
the total from elsewhere.

Before:
The total field is mandatory for PaymentRequest.

After:
The total field is optional if only app-store methods are requested.
When total field is optional and left out, Chrome would add a total of
amount 0, currency "ZZZ" and label "AppStoreBillingTotalPlaceHolder".

Change:
* Added a RuntimeEnabledFeature: PaymentRequestTotalOptional
* Added an about flag: payment-request-optional-total
* change the optionality of the total field and details field of
PaymentRequest API.

Related Doc:
* Chrome Status: https://www.chromestatus.com/feature/5226111782879232
* Intent to Prototype: https://groups.google.com/a/chromium.org/forum/#!msg/blink-dev/TJVn0Ps9ugA/3unr2Vo8AgAJ
* W3C explainer: https://github.com/w3c/payment-request/issues/912

Bug: 1066531

Change-Id: Id5ad87b9fc452fd41a1ebef066d981737545a235
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2150974Reviewed-by: default avatarRouslan Solomakhin <rouslan@chromium.org>
Reviewed-by: default avatarYaron Friedman <yfriedman@chromium.org>
Reviewed-by: default avatarDavid Bokan <bokan@chromium.org>
Commit-Queue: Liquan (Max) Gu <maxlg@chromium.org>
Cr-Commit-Position: refs/heads/master@{#769914}
parent 596bbbfe
...@@ -50,6 +50,9 @@ const base::Feature kStrictHasEnrolledAutofillInstrument{ ...@@ -50,6 +50,9 @@ const base::Feature kStrictHasEnrolledAutofillInstrument{
const base::Feature kPaymentRequestSkipToGPay{ const base::Feature kPaymentRequestSkipToGPay{
"PaymentRequestSkipToGPay", base::FEATURE_DISABLED_BY_DEFAULT}; "PaymentRequestSkipToGPay", base::FEATURE_DISABLED_BY_DEFAULT};
const base::Feature kPaymentRequestOptionalTotal{
"PaymentRequestOptionalTotal", base::FEATURE_DISABLED_BY_DEFAULT};
const base::Feature kPaymentRequestSkipToGPayIfNoCard{ const base::Feature kPaymentRequestSkipToGPayIfNoCard{
"PaymentRequestSkipToGPayIfNoCard", base::FEATURE_DISABLED_BY_DEFAULT}; "PaymentRequestSkipToGPayIfNoCard", base::FEATURE_DISABLED_BY_DEFAULT};
......
...@@ -60,6 +60,9 @@ extern const base::Feature kPaymentRequestSkipToGPay; ...@@ -60,6 +60,9 @@ extern const base::Feature kPaymentRequestSkipToGPay;
// eligible credit card. // eligible credit card.
extern const base::Feature kPaymentRequestSkipToGPayIfNoCard; extern const base::Feature kPaymentRequestSkipToGPayIfNoCard;
// Enables the total field of PaymentRequest API to be optional.
extern const base::Feature kPaymentRequestOptionalTotal;
// If enabled, just-in-time installable payment handlers are ranked lower than // If enabled, just-in-time installable payment handlers are ranked lower than
// complete autofill instruments in payment sheet's method selection section. // complete autofill instruments in payment sheet's method selection section.
extern const base::Feature kDownRankJustInTimePaymentApp; extern const base::Feature kDownRankJustInTimePaymentApp;
......
...@@ -225,6 +225,8 @@ void SetRuntimeFeaturesFromChromiumFeatures() { ...@@ -225,6 +225,8 @@ void SetRuntimeFeaturesFromChromiumFeatures() {
{wf::EnablePaymentRequest, features::kWebPayments, kUseFeatureState}, {wf::EnablePaymentRequest, features::kWebPayments, kUseFeatureState},
{wf::EnablePaymentHandlerMinimalUI, features::kWebPaymentsMinimalUI, {wf::EnablePaymentHandlerMinimalUI, features::kWebPaymentsMinimalUI,
kEnableOnly}, kEnableOnly},
{wf::EnablePaymentRequestOptionalTotal,
features::kPaymentRequestOptionalTotal, kEnableOnly},
{wf::EnablePaymentApp, features::kServiceWorkerPaymentApps, kEnableOnly}, {wf::EnablePaymentApp, features::kServiceWorkerPaymentApps, kEnableOnly},
{wf::EnableGenericSensorExtraClasses, features::kGenericSensorExtraClasses, {wf::EnableGenericSensorExtraClasses, features::kGenericSensorExtraClasses,
kEnableOnly}, kEnableOnly},
......
...@@ -809,6 +809,10 @@ const base::Feature kWebPayments{"WebPayments", ...@@ -809,6 +809,10 @@ const base::Feature kWebPayments{"WebPayments",
const base::Feature kWebPaymentsMinimalUI{"WebPaymentsMinimalUI", const base::Feature kWebPaymentsMinimalUI{"WebPaymentsMinimalUI",
base::FEATURE_DISABLED_BY_DEFAULT}; base::FEATURE_DISABLED_BY_DEFAULT};
// Allows the total field of PaymentRequest API to be optional.
const base::Feature kPaymentRequestOptionalTotal{
"PaymentRequestOptionalTotal", base::FEATURE_DISABLED_BY_DEFAULT};
// Makes WebRTC use ECDSA certs by default (i.e., when no cert type was // Makes WebRTC use ECDSA certs by default (i.e., when no cert type was
// specified in JS). // specified in JS).
const base::Feature kWebRtcEcdsaDefault{"WebRTC-EnableWebRtcEcdsa", const base::Feature kWebRtcEcdsaDefault{"WebRTC-EnableWebRtcEcdsa",
......
...@@ -94,6 +94,7 @@ CONTENT_EXPORT extern const base::Feature kOriginIsolationHeader; ...@@ -94,6 +94,7 @@ CONTENT_EXPORT extern const base::Feature kOriginIsolationHeader;
CONTENT_EXPORT extern const base::Feature kOriginPolicy; CONTENT_EXPORT extern const base::Feature kOriginPolicy;
CONTENT_EXPORT extern const base::Feature kOverscrollHistoryNavigation; CONTENT_EXPORT extern const base::Feature kOverscrollHistoryNavigation;
CONTENT_EXPORT extern const base::Feature kParkableStringsToDisk; CONTENT_EXPORT extern const base::Feature kParkableStringsToDisk;
CONTENT_EXPORT extern const base::Feature kPaymentRequestOptionalTotal;
CONTENT_EXPORT extern const base::Feature kPeriodicBackgroundSync; CONTENT_EXPORT extern const base::Feature kPeriodicBackgroundSync;
CONTENT_EXPORT extern const base::Feature kPepper3DImageChromium; CONTENT_EXPORT extern const base::Feature kPepper3DImageChromium;
CONTENT_EXPORT extern const base::Feature kPepperCrossOriginRedirectRestriction; CONTENT_EXPORT extern const base::Feature kPepperCrossOriginRedirectRestriction;
......
...@@ -136,6 +136,7 @@ class WebRuntimeFeatures { ...@@ -136,6 +136,7 @@ class WebRuntimeFeatures {
BLINK_PLATFORM_EXPORT static void EnablePagePopup(bool); BLINK_PLATFORM_EXPORT static void EnablePagePopup(bool);
BLINK_PLATFORM_EXPORT static void EnablePaymentApp(bool); BLINK_PLATFORM_EXPORT static void EnablePaymentApp(bool);
BLINK_PLATFORM_EXPORT static void EnablePaymentHandlerMinimalUI(bool); BLINK_PLATFORM_EXPORT static void EnablePaymentHandlerMinimalUI(bool);
BLINK_PLATFORM_EXPORT static void EnablePaymentRequestOptionalTotal(bool);
BLINK_PLATFORM_EXPORT static void EnablePaymentRequest(bool); BLINK_PLATFORM_EXPORT static void EnablePaymentRequest(bool);
BLINK_PLATFORM_EXPORT static void EnablePercentBasedScrolling(bool); BLINK_PLATFORM_EXPORT static void EnablePercentBasedScrolling(bool);
BLINK_PLATFORM_EXPORT static void EnablePerformanceManagerInstrumentation( BLINK_PLATFORM_EXPORT static void EnablePerformanceManagerInstrumentation(
......
...@@ -367,6 +367,7 @@ jumbo_source_set("unit_tests") { ...@@ -367,6 +367,7 @@ jumbo_source_set("unit_tests") {
"payments/payment_address_test.cc", "payments/payment_address_test.cc",
"payments/payment_event_data_conversion_test.cc", "payments/payment_event_data_conversion_test.cc",
"payments/payment_request_details_test.cc", "payments/payment_request_details_test.cc",
"payments/payment_request_optional_total_test.cc",
"payments/payment_request_test.cc", "payments/payment_request_test.cc",
"payments/payment_request_update_event_test.cc", "payments/payment_request_update_event_test.cc",
"payments/payment_response_test.cc", "payments/payment_response_test.cc",
......
...@@ -2,9 +2,9 @@ ...@@ -2,9 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
// https://w3c.github.io/browser-payment-api/#paymentdetailsinit-dictionary // https://w3c.github.io/payment-request/#paymentdetailsinit-dictionary
dictionary PaymentDetailsInit : PaymentDetailsBase { dictionary PaymentDetailsInit : PaymentDetailsBase {
DOMString id; DOMString id;
required PaymentItem total; PaymentItem total;
}; };
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
#include <utility> #include <utility>
#include "base/bind.h"
#include "base/location.h" #include "base/location.h"
#include "base/stl_util.h" #include "base/stl_util.h"
#include "build/build_config.h" #include "build/build_config.h"
...@@ -93,6 +94,9 @@ using ::payments::mojom::blink::PaymentValidationErrorsPtr; ...@@ -93,6 +94,9 @@ using ::payments::mojom::blink::PaymentValidationErrorsPtr;
const char kHasEnrolledInstrumentDebugName[] = "hasEnrolledInstrument"; const char kHasEnrolledInstrumentDebugName[] = "hasEnrolledInstrument";
const char kGooglePayMethod[] = "https://google.com/pay"; const char kGooglePayMethod[] = "https://google.com/pay";
const char kAndroidPayMethod[] = "https://android.com/pay"; const char kAndroidPayMethod[] = "https://android.com/pay";
const char kGooglePlayBillingMethod[] = "https://play.google.com/billing";
const char kUnknownCurrency[] = "ZZZ";
const char kAppStoreBillingLabelPlaceHolder[] = "AppStoreBillingPlaceHolder";
} // namespace } // namespace
...@@ -266,6 +270,18 @@ void ValidateShippingOptionOrPaymentItem(const T* item, ...@@ -266,6 +270,18 @@ void ValidateShippingOptionOrPaymentItem(const T* item,
} }
} }
bool RequestingOnlyAppStoreBillingMethods(
const Vector<payments::mojom::blink::PaymentMethodDataPtr>& method_data) {
DCHECK(!method_data.IsEmpty());
static const WTF::HashSet<String> app_store_billing_methods = {
kGooglePlayBillingMethod};
for (const auto& method : method_data) {
if (!app_store_billing_methods.Contains(method->supported_method))
return false;
}
return true;
}
void ValidateAndConvertDisplayItems( void ValidateAndConvertDisplayItems(
const HeapVector<Member<PaymentItem>>& input, const HeapVector<Member<PaymentItem>>& input,
const String& item_names, const String& item_names,
...@@ -504,17 +520,50 @@ void ValidateAndConvertPaymentDetailsBase(const PaymentDetailsBase* input, ...@@ -504,17 +520,50 @@ void ValidateAndConvertPaymentDetailsBase(const PaymentDetailsBase* input,
} }
} }
PaymentItemPtr CreateTotalPlaceHolderForAppStoreBilling(
ExecutionContext& execution_context) {
PaymentItemPtr total = payments::mojom::blink::PaymentItem::New();
total->label = kAppStoreBillingLabelPlaceHolder;
total->amount = payments::mojom::blink::PaymentCurrencyAmount::New();
total->amount->currency = kUnknownCurrency;
total->amount->value = "0";
return total;
}
void ValidateAndConvertPaymentDetailsInit(const PaymentDetailsInit* input, void ValidateAndConvertPaymentDetailsInit(const PaymentDetailsInit* input,
const PaymentOptions* options, const PaymentOptions* options,
PaymentDetailsPtr& output, PaymentDetailsPtr& output,
String& shipping_option_output, String& shipping_option_output,
bool ignore_total,
ExecutionContext& execution_context, ExecutionContext& execution_context,
ExceptionState& exception_state) { ExceptionState& exception_state) {
DCHECK(input->hasTotal()); if (ignore_total) {
ValidateAndConvertTotal(input->total(), "total", output->total, output->total = CreateTotalPlaceHolderForAppStoreBilling(execution_context);
execution_context, exception_state); if (input->hasTotal()) {
if (exception_state.HadException()) execution_context.AddConsoleMessage(MakeGarbageCollected<ConsoleMessage>(
return; mojom::blink::ConsoleMessageSource::kJavaScript,
mojom::blink::ConsoleMessageLevel::kWarning,
"Specified total is ignored for in-app purchases with app stores. "
"User will be shown the total derived from the product identifier."));
}
} else {
// Whether details (i.e., input) being omitted, null, defined or {} is
// indistinguishable, so we check all of its attributes to decide whether it
// has been provided.
if (!input->hasTotal() && !input->hasId()) {
exception_state.ThrowTypeError("required member details is undefined.");
return;
}
if (!input->hasTotal()) {
exception_state.ThrowTypeError("required member total is undefined.");
return;
}
ValidateAndConvertTotal(input->total(), "total", output->total,
execution_context, exception_state);
if (exception_state.HadException())
return;
}
ValidateAndConvertPaymentDetailsBase(input, options, output, ValidateAndConvertPaymentDetailsBase(input, options, output,
shipping_option_output, shipping_option_output,
...@@ -525,6 +574,7 @@ void ValidateAndConvertPaymentDetailsUpdate(const PaymentDetailsUpdate* input, ...@@ -525,6 +574,7 @@ void ValidateAndConvertPaymentDetailsUpdate(const PaymentDetailsUpdate* input,
const PaymentOptions* options, const PaymentOptions* options,
PaymentDetailsPtr& output, PaymentDetailsPtr& output,
String& shipping_option_output, String& shipping_option_output,
bool ignore_total,
ExecutionContext& execution_context, ExecutionContext& execution_context,
ExceptionState& exception_state) { ExceptionState& exception_state) {
ValidateAndConvertPaymentDetailsBase(input, options, output, ValidateAndConvertPaymentDetailsBase(input, options, output,
...@@ -532,12 +582,18 @@ void ValidateAndConvertPaymentDetailsUpdate(const PaymentDetailsUpdate* input, ...@@ -532,12 +582,18 @@ void ValidateAndConvertPaymentDetailsUpdate(const PaymentDetailsUpdate* input,
execution_context, exception_state); execution_context, exception_state);
if (exception_state.HadException()) if (exception_state.HadException())
return; return;
if (input->hasTotal()) { if (input->hasTotal()) {
ValidateAndConvertTotal(input->total(), "total", output->total, DCHECK(!RuntimeEnabledFeatures::PaymentRequestOptionalTotalEnabled() ||
execution_context, exception_state); !ignore_total);
if (exception_state.HadException()) if (ignore_total) {
return; output->total =
CreateTotalPlaceHolderForAppStoreBilling(execution_context);
} else {
ValidateAndConvertTotal(input->total(), "total", output->total,
execution_context, exception_state);
if (exception_state.HadException())
return;
}
} }
if (input->hasError()) { if (input->hasError()) {
...@@ -665,9 +721,9 @@ PaymentRequest* PaymentRequest::Create( ...@@ -665,9 +721,9 @@ PaymentRequest* PaymentRequest::Create(
const HeapVector<Member<PaymentMethodData>>& method_data, const HeapVector<Member<PaymentMethodData>>& method_data,
const PaymentDetailsInit* details, const PaymentDetailsInit* details,
ExceptionState& exception_state) { ExceptionState& exception_state) {
return MakeGarbageCollected<PaymentRequest>(execution_context, method_data, return MakeGarbageCollected<PaymentRequest>(
details, PaymentOptions::Create(), execution_context, method_data, details, PaymentOptions::Create(),
exception_state); mojo::NullRemote(), exception_state);
} }
PaymentRequest* PaymentRequest::Create( PaymentRequest* PaymentRequest::Create(
...@@ -677,7 +733,8 @@ PaymentRequest* PaymentRequest::Create( ...@@ -677,7 +733,8 @@ PaymentRequest* PaymentRequest::Create(
const PaymentOptions* options, const PaymentOptions* options,
ExceptionState& exception_state) { ExceptionState& exception_state) {
return MakeGarbageCollected<PaymentRequest>( return MakeGarbageCollected<PaymentRequest>(
execution_context, method_data, details, options, exception_state); execution_context, method_data, details, options, mojo::NullRemote(),
exception_state);
} }
PaymentRequest::~PaymentRequest() = default; PaymentRequest::~PaymentRequest() = default;
...@@ -982,7 +1039,7 @@ void PaymentRequest::OnUpdatePaymentDetails( ...@@ -982,7 +1039,7 @@ void PaymentRequest::OnUpdatePaymentDetails(
PaymentDetailsPtr validated_details = PaymentDetailsPtr validated_details =
payments::mojom::blink::PaymentDetails::New(); payments::mojom::blink::PaymentDetails::New();
ValidateAndConvertPaymentDetailsUpdate( ValidateAndConvertPaymentDetailsUpdate(
details, options_, validated_details, shipping_option_, details, options_, validated_details, shipping_option_, ignore_total_,
*GetExecutionContext(), exception_state); *GetExecutionContext(), exception_state);
if (exception_state.HadException()) { if (exception_state.HadException()) {
resolver->Reject(exception_state.GetException()); resolver->Reject(exception_state.GetException());
...@@ -1067,6 +1124,8 @@ PaymentRequest::PaymentRequest( ...@@ -1067,6 +1124,8 @@ PaymentRequest::PaymentRequest(
const HeapVector<Member<PaymentMethodData>>& method_data, const HeapVector<Member<PaymentMethodData>>& method_data,
const PaymentDetailsInit* details, const PaymentDetailsInit* details,
const PaymentOptions* options, const PaymentOptions* options,
mojo::PendingRemote<payments::mojom::blink::PaymentRequest>
mock_payment_provider,
ExceptionState& exception_state) ExceptionState& exception_state)
: ExecutionContextLifecycleObserver(execution_context), : ExecutionContextLifecycleObserver(execution_context),
options_(options), options_(options),
...@@ -1081,8 +1140,8 @@ PaymentRequest::PaymentRequest( ...@@ -1081,8 +1140,8 @@ PaymentRequest::PaymentRequest(
this, this,
&PaymentRequest::OnUpdatePaymentDetailsTimeout), &PaymentRequest::OnUpdatePaymentDetailsTimeout),
is_waiting_for_show_promise_to_resolve_(false) { is_waiting_for_show_promise_to_resolve_(false) {
DCHECK(details);
DCHECK(GetExecutionContext()->IsSecureContext()); DCHECK(GetExecutionContext()->IsSecureContext());
if (!AllowedToUsePaymentRequest(execution_context)) { if (!AllowedToUsePaymentRequest(execution_context)) {
exception_state.ThrowSecurityError( exception_state.ThrowSecurityError(
"Must be in a top-level browsing context or an iframe needs to specify " "Must be in a top-level browsing context or an iframe needs to specify "
...@@ -1114,9 +1173,12 @@ PaymentRequest::PaymentRequest( ...@@ -1114,9 +1173,12 @@ PaymentRequest::PaymentRequest(
if (exception_state.HadException()) if (exception_state.HadException())
return; return;
ignore_total_ =
RuntimeEnabledFeatures::PaymentRequestOptionalTotalEnabled() &&
RequestingOnlyAppStoreBillingMethods(validated_method_data);
ValidateAndConvertPaymentDetailsInit(details, options_, validated_details, ValidateAndConvertPaymentDetailsInit(details, options_, validated_details,
shipping_option_, *GetExecutionContext(), shipping_option_, ignore_total_,
exception_state); *GetExecutionContext(), exception_state);
if (exception_state.HadException()) if (exception_state.HadException())
return; return;
...@@ -1143,8 +1205,14 @@ PaymentRequest::PaymentRequest( ...@@ -1143,8 +1205,14 @@ PaymentRequest::PaymentRequest(
scoped_refptr<base::SingleThreadTaskRunner> task_runner = scoped_refptr<base::SingleThreadTaskRunner> task_runner =
execution_context->GetTaskRunner(TaskType::kUserInteraction); execution_context->GetTaskRunner(TaskType::kUserInteraction);
GetFrame()->GetBrowserInterfaceBroker().GetInterface( if (mock_payment_provider) {
payment_provider_.BindNewPipeAndPassReceiver(task_runner)); payment_provider_.Bind(
std::move(mock_payment_provider),
execution_context->GetTaskRunner(TaskType::kMiscPlatformAPI));
} else {
GetFrame()->GetBrowserInterfaceBroker().GetInterface(
payment_provider_.BindNewPipeAndPassReceiver(task_runner));
}
payment_provider_.set_disconnect_handler( payment_provider_.set_disconnect_handler(
WTF::Bind(&PaymentRequest::OnConnectionError, WrapWeakPersistent(this))); WTF::Bind(&PaymentRequest::OnConnectionError, WrapWeakPersistent(this)));
......
...@@ -65,6 +65,8 @@ class MODULES_EXPORT PaymentRequest final ...@@ -65,6 +65,8 @@ class MODULES_EXPORT PaymentRequest final
const HeapVector<Member<PaymentMethodData>>&, const HeapVector<Member<PaymentMethodData>>&,
const PaymentDetailsInit*, const PaymentDetailsInit*,
const PaymentOptions*, const PaymentOptions*,
mojo::PendingRemote<payments::mojom::blink::PaymentRequest>
mock_payment_provider,
ExceptionState&); ExceptionState&);
~PaymentRequest() override; ~PaymentRequest() override;
...@@ -179,6 +181,7 @@ class MODULES_EXPORT PaymentRequest final ...@@ -179,6 +181,7 @@ class MODULES_EXPORT PaymentRequest final
TaskRunnerTimer<PaymentRequest> complete_timer_; TaskRunnerTimer<PaymentRequest> complete_timer_;
TaskRunnerTimer<PaymentRequest> update_payment_details_timer_; TaskRunnerTimer<PaymentRequest> update_payment_details_timer_;
bool is_waiting_for_show_promise_to_resolve_; bool is_waiting_for_show_promise_to_resolve_;
bool ignore_total_;
DISALLOW_COPY_AND_ASSIGN(PaymentRequest); DISALLOW_COPY_AND_ASSIGN(PaymentRequest);
}; };
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
Exposed=Window, Exposed=Window,
ActiveScriptWrappable ActiveScriptWrappable
] interface PaymentRequest : EventTarget { ] interface PaymentRequest : EventTarget {
[CallWith=ExecutionContext, RaisesException] constructor(sequence<PaymentMethodData> methodData, PaymentDetailsInit details, optional PaymentOptions options = {}); [CallWith=ExecutionContext, RaisesException] constructor(sequence<PaymentMethodData> methodData, optional PaymentDetailsInit details = {}, optional PaymentOptions options = {});
[CallWith=ScriptState, RaisesException, NewObject] Promise<PaymentResponse> show(optional Promise<PaymentDetailsUpdate> detailsPromise); [CallWith=ScriptState, RaisesException, NewObject] Promise<PaymentResponse> show(optional Promise<PaymentDetailsUpdate> detailsPromise);
[CallWith=ScriptState, RaisesException, NewObject] Promise<void> abort(); [CallWith=ScriptState, RaisesException, NewObject] Promise<void> abort();
[CallWith=ScriptState, RaisesException, HighEntropy, Measure, NewObject] Promise<boolean> canMakePayment(); [CallWith=ScriptState, RaisesException, HighEntropy, Measure, NewObject] Promise<boolean> canMakePayment();
......
// 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 "third_party/blink/renderer/modules/payments/payment_request.h"
#include "build/build_config.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/payments/payment_request.mojom-blink.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/event_type_names.h"
#include "third_party/blink/renderer/modules/payments/payment_test_helper.h"
#include "third_party/blink/renderer/platform/bindings/exception_code.h"
#include "third_party/blink/renderer/platform/heap/heap_allocator.h"
#include "third_party/blink/renderer/platform/testing/testing_platform_support.h"
namespace blink {
namespace {
class MockPaymentProvider : public payments::mojom::blink::PaymentRequest {
public:
// mojom::PaymentRequest
#if defined(OS_ANDROID)
void Init(
mojo::PendingRemote<payments::mojom::blink::PaymentRequestClient> client,
WTF::Vector<payments::mojom::blink::PaymentMethodDataPtr> method_data,
payments::mojom::blink::PaymentDetailsPtr details,
payments::mojom::blink::PaymentOptionsPtr options,
bool google_pay_bridge_eligible) override {
details_ = std::move(details);
}
#else
void Init(
mojo::PendingRemote<payments::mojom::blink::PaymentRequestClient> client,
WTF::Vector<payments::mojom::blink::PaymentMethodDataPtr> method_data,
payments::mojom::blink::PaymentDetailsPtr details,
payments::mojom::blink::PaymentOptionsPtr options) override {
details_ = std::move(details);
}
#endif
void Show(bool is_user_gesture, bool wait_for_updated_details) override {
NOTREACHED();
}
void Retry(
payments::mojom::blink::PaymentValidationErrorsPtr errors) override {
NOTREACHED();
}
void UpdateWith(
payments::mojom::blink::PaymentDetailsPtr update_with_details) override {
NOTREACHED();
}
void OnPaymentDetailsNotUpdated() override { NOTREACHED(); }
void Abort() override { NOTREACHED(); }
void Complete(payments::mojom::PaymentComplete result) override {
NOTREACHED();
}
void CanMakePayment() override { NOTREACHED(); }
void HasEnrolledInstrument(bool per_method_quota) override { NOTREACHED(); }
mojo::PendingRemote<payments::mojom::blink::PaymentRequest>
CreatePendingRemoteAndBind() {
mojo::PendingRemote<payments::mojom::blink::PaymentRequest> remote;
receiver_.Bind(remote.InitWithNewPipeAndPassReceiver());
return remote;
}
payments::mojom::blink::PaymentDetailsPtr& GetDetails() { return details_; }
private:
mojo::Receiver<payments::mojom::blink::PaymentRequest> receiver_{this};
payments::mojom::blink::PaymentDetailsPtr details_;
};
class PaymentRequestOptionalTotalTest : public testing::Test {
public:
void SetUp() override {
payment_provider_ = std::make_unique<MockPaymentProvider>();
}
std::unique_ptr<MockPaymentProvider> payment_provider_;
ScopedTestingPlatformSupport<TestingPlatformSupport> platform_;
};
// This test requests a mix of app-store billing methods and normal payment
// methods. Total is required in this scenario.
TEST_F(PaymentRequestOptionalTotalTest,
AppStoreBillingFlagEnabledTotalIsRequiredWhenMixMethods) {
RuntimeEnabledFeatures::SetPaymentRequestOptionalTotalEnabled(true);
PaymentRequestV8TestingScope scope;
// Intentionally leaves the total of details unset.
PaymentDetailsInit* details = PaymentDetailsInit::Create();
HeapVector<Member<PaymentMethodData>> method_data(2);
method_data[0] = PaymentMethodData::Create();
method_data[0]->setSupportedMethod("foo");
method_data[1] = PaymentMethodData::Create();
method_data[1]->setSupportedMethod("https://play.google.com/billing");
PaymentRequest::Create(scope.GetExecutionContext(), method_data, details,
scope.GetExceptionState());
EXPECT_TRUE(scope.GetExceptionState().HadException());
EXPECT_EQ("required member details is undefined.",
scope.GetExceptionState().Message());
EXPECT_FALSE(payment_provider_->GetDetails());
}
// When the OptionalTotal is disabled: although this test requests a app-store
// billing methods, total is required.
TEST_F(PaymentRequestOptionalTotalTest,
AppStoreBillingFlagDisabledTotalIsRequiredWhenMixMethods) {
RuntimeEnabledFeatures::SetPaymentRequestOptionalTotalEnabled(false);
PaymentRequestV8TestingScope scope;
// Intentionally leaves the total of details unset.
PaymentDetailsInit* details = PaymentDetailsInit::Create();
HeapVector<Member<PaymentMethodData>> method_data(1);
method_data[0] = PaymentMethodData::Create();
method_data[0]->setSupportedMethod("https://play.google.com/billing");
PaymentRequest::Create(scope.GetExecutionContext(), method_data, details,
scope.GetExceptionState());
EXPECT_TRUE(scope.GetExceptionState().HadException());
EXPECT_EQ("required member details is undefined.",
scope.GetExceptionState().Message());
EXPECT_FALSE(payment_provider_->GetDetails());
}
// When the OptionalTotal is enabled: undefined total gets a place holder when
// only requesting app-store billing methods.
TEST_F(PaymentRequestOptionalTotalTest,
AppStoreBillingFlagEnabledTotalGetPlaceHolder) {
RuntimeEnabledFeatures::SetPaymentRequestOptionalTotalEnabled(true);
PaymentRequestV8TestingScope scope;
// Intentionally leaves the total of details unset.
PaymentDetailsInit* details = PaymentDetailsInit::Create();
HeapVector<Member<PaymentMethodData>> method_data(
1, PaymentMethodData::Create());
method_data[0]->setSupportedMethod("https://play.google.com/billing");
MakeGarbageCollected<PaymentRequest>(
scope.GetExecutionContext(), method_data, details,
PaymentOptions::Create(), payment_provider_->CreatePendingRemoteAndBind(),
ASSERT_NO_EXCEPTION);
platform_->RunUntilIdle();
EXPECT_FALSE(scope.GetExceptionState().HadException());
EXPECT_EQ("0", payment_provider_->GetDetails()->total->amount->value);
EXPECT_EQ("ZZZ", payment_provider_->GetDetails()->total->amount->currency);
}
// When the OptionalTotal is disabled: undefined total is rejected.
TEST_F(PaymentRequestOptionalTotalTest,
AppStoreBillingFlagDisabledTotalGetRejected) {
RuntimeEnabledFeatures::SetPaymentRequestOptionalTotalEnabled(false);
PaymentRequestV8TestingScope scope;
// Intentionally leaves the total of details unset.
PaymentDetailsInit* details = PaymentDetailsInit::Create();
HeapVector<Member<PaymentMethodData>> method_data(
1, PaymentMethodData::Create());
method_data[0]->setSupportedMethod("https://play.google.com/billing");
MakeGarbageCollected<PaymentRequest>(
scope.GetExecutionContext(), method_data, details,
PaymentOptions::Create(), payment_provider_->CreatePendingRemoteAndBind(),
scope.GetExceptionState());
platform_->RunUntilIdle();
// Verify that total is required.
EXPECT_TRUE(scope.GetExceptionState().HadException());
EXPECT_EQ("required member details is undefined.",
scope.GetExceptionState().Message());
EXPECT_FALSE(payment_provider_->GetDetails());
}
// When the OptionalTotal is enabled: total get overridden when only requesting
// app-store billing methods.
TEST_F(PaymentRequestOptionalTotalTest,
AppStoreBillingFlagEnabledTotalGetOverridden) {
RuntimeEnabledFeatures::SetPaymentRequestOptionalTotalEnabled(true);
PaymentRequestV8TestingScope scope;
PaymentDetailsInit* details = PaymentDetailsInit::Create();
// Set a non-empty total.
details->setTotal((BuildPaymentItemForTest()));
HeapVector<Member<PaymentMethodData>> method_data(
1, PaymentMethodData::Create());
method_data[0]->setSupportedMethod("https://play.google.com/billing");
MakeGarbageCollected<PaymentRequest>(
scope.GetExecutionContext(), method_data, details,
PaymentOptions::Create(), payment_provider_->CreatePendingRemoteAndBind(),
ASSERT_NO_EXCEPTION);
platform_->RunUntilIdle();
EXPECT_FALSE(scope.GetExceptionState().HadException());
// Verify that the total get overridden.
EXPECT_EQ("0", payment_provider_->GetDetails()->total->amount->value);
EXPECT_EQ("ZZZ", payment_provider_->GetDetails()->total->amount->currency);
}
// When the OptionalTotal is disabled: total does not get overridden when only
// requesting app-store billing methods.
TEST_F(PaymentRequestOptionalTotalTest,
AppStoreBillingFlagDisabledTotalNotGetOverridden) {
RuntimeEnabledFeatures::SetPaymentRequestOptionalTotalEnabled(false);
PaymentRequestV8TestingScope scope;
PaymentDetailsInit* details = PaymentDetailsInit::Create();
// Set a non-empty total.
details->setTotal(BuildPaymentItemForTest());
HeapVector<Member<PaymentMethodData>> method_data(
1, PaymentMethodData::Create());
method_data[0]->setSupportedMethod("https://play.google.com/billing");
MakeGarbageCollected<PaymentRequest>(
scope.GetExecutionContext(), method_data, details,
PaymentOptions::Create(), payment_provider_->CreatePendingRemoteAndBind(),
ASSERT_NO_EXCEPTION);
platform_->RunUntilIdle();
// Verify that the total is set.
EXPECT_FALSE(scope.GetExceptionState().HadException());
EXPECT_TRUE(payment_provider_->GetDetails()->total);
}
} // namespace
} // namespace blink
...@@ -315,12 +315,17 @@ void WebRuntimeFeatures::EnablePaymentHandlerMinimalUI(bool enable) { ...@@ -315,12 +315,17 @@ void WebRuntimeFeatures::EnablePaymentHandlerMinimalUI(bool enable) {
RuntimeEnabledFeatures::SetPaymentHandlerMinimalUIEnabled(enable); RuntimeEnabledFeatures::SetPaymentHandlerMinimalUIEnabled(enable);
} }
void WebRuntimeFeatures::EnablePaymentRequestOptionalTotal(bool enable) {
RuntimeEnabledFeatures::SetPaymentRequestOptionalTotalEnabled(enable);
}
void WebRuntimeFeatures::EnablePaymentRequest(bool enable) { void WebRuntimeFeatures::EnablePaymentRequest(bool enable) {
RuntimeEnabledFeatures::SetPaymentRequestEnabled(enable); RuntimeEnabledFeatures::SetPaymentRequestEnabled(enable);
if (!enable) { if (!enable) {
// Disable features that depend on Payment Request API. // Disable features that depend on Payment Request API.
RuntimeEnabledFeatures::SetPaymentAppEnabled(false); RuntimeEnabledFeatures::SetPaymentAppEnabled(false);
RuntimeEnabledFeatures::SetPaymentHandlerMinimalUIEnabled(false); RuntimeEnabledFeatures::SetPaymentHandlerMinimalUIEnabled(false);
RuntimeEnabledFeatures::SetPaymentRequestOptionalTotalEnabled(false);
RuntimeEnabledFeatures::SetPaymentMethodChangeEventEnabled(false); RuntimeEnabledFeatures::SetPaymentMethodChangeEventEnabled(false);
} }
} }
......
...@@ -1376,6 +1376,10 @@ ...@@ -1376,6 +1376,10 @@
name: "PaymentRequestMerchantValidationEvent", name: "PaymentRequestMerchantValidationEvent",
status: "experimental", status: "experimental",
}, },
{
name: "PaymentRequestOptionalTotal",
status: "experimental",
},
{ {
name: "PaymentRetry", name: "PaymentRetry",
status: "stable", status: "stable",
......
...@@ -4118,6 +4118,9 @@ crbug.com/852645 gamepad/full-screen-gamepad.html [ Timeout ] ...@@ -4118,6 +4118,9 @@ crbug.com/852645 gamepad/full-screen-gamepad.html [ Timeout ]
crbug.com/718155 payments/payment-request-in-iframe.html [ Failure ] crbug.com/718155 payments/payment-request-in-iframe.html [ Failure ]
crbug.com/718155 payments/payment-request-in-iframe-nested-not-allowed.html [ Failure ] crbug.com/718155 payments/payment-request-in-iframe-nested-not-allowed.html [ Failure ]
# Expect to fail. The test is applicable only when PaymentRequestOptionalTotal flag is disabled.
crbug.com/1080870 http/tests/payments/payment-request-app-store-billing-mandatory-total.html [ Failure ]
# Layout Tests on Swarming (Windows) - https://crbug.com/717347 # Layout Tests on Swarming (Windows) - https://crbug.com/717347
crbug.com/713094 [ Win ] fast/css/fontfaceset-check-platform-fonts.html [ Failure Pass ] crbug.com/713094 [ Win ] fast/css/fontfaceset-check-platform-fonts.html [ Failure Pass ]
......
...@@ -623,6 +623,11 @@ ...@@ -623,6 +623,11 @@
"args": ["--force-high-contrast", "args": ["--force-high-contrast",
"--enable-blink-features=CSSCascade,ForcedColors"] "--enable-blink-features=CSSCascade,ForcedColors"]
}, },
{
"prefix": "payment-request-mandatory-total",
"bases": ["http/tests/payments/payment-request-app-store-billing-mandatory-total.html"],
"args": ["--disable-blink-features=PaymentRequestTotalOptional"]
},
{ {
"prefix": "cache-storage-sequence", "prefix": "cache-storage-sequence",
"bases": ["external/wpt/service-workers"], "bases": ["external/wpt/service-workers"],
......
This is a testharness.js-based test. This is a testharness.js-based test.
Found 110 tests; 108 PASS, 2 FAIL, 0 TIMEOUT, 0 NOTRUN. Found 110 tests; 107 PASS, 3 FAIL, 0 TIMEOUT, 0 NOTRUN.
PASS idl_test setup PASS idl_test setup
PASS idl_test validation PASS idl_test validation
PASS PaymentRequest interface: existence and properties of interface object PASS PaymentRequest interface: existence and properties of interface object
PASS PaymentRequest interface object length FAIL PaymentRequest interface object length assert_equals: wrong value for PaymentRequest.length expected 2 but got 1
PASS PaymentRequest interface object name PASS PaymentRequest interface object name
PASS PaymentRequest interface: existence and properties of interface prototype object PASS PaymentRequest interface: existence and properties of interface prototype object
PASS PaymentRequest interface: existence and properties of interface prototype object's "constructor" property PASS PaymentRequest interface: existence and properties of interface prototype object's "constructor" property
......
<!doctype html>
<title>PaymentRequest: Tests for app-store billing when total is mandatory</title>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script src="../serviceworker/resources/test-helpers.js"></script>
<script>
test(() => {
assert_false(internals.runtimeFlags.paymentRequestOptionalTotalEnabled);
}, "This test suite assumes that the runtime-enabled-flag PaymentRequestOptionalTotal is disabled.");
const onlySupportAppStoreBillingMethod = [{supportedMethods: "https://play.google.com/billing"}];
const supportBothAppStoreBillingMethodAndNormalMethod = [{supportedMethods: "https://play.google.com/billing"},
{supportedMethods: "https://maxpay.com/payment-request"}];
[null, {}, "omitted", undefined].forEach(total => {
test(() => {
let details = {id: "foo"};
if (total !== "omitted") Object.assign(details, { total });
try {
new PaymentRequest(onlySupportAppStoreBillingMethod, details);
} catch (err) {
let expectedMessage;
if (total === null) {
expectedMessage = "Failed to construct 'PaymentRequest': Missing required member(s): amount, label.";
} else if (["omitted", undefined].includes(total)) {
expectedMessage = "Failed to construct 'PaymentRequest': required member total is undefined.";
} else {
expectedMessage = "Failed to construct 'PaymentRequest': required member amount is undefined.";
}
assert_equals(err.message, expectedMessage, `Error messages mismatch.`);
return;
}
assert_unreached(`Expect an exception.`);
}, `The total field is mandatory (not allowed to be ${JSON.stringify(total)}) when PaymentRequestOptionalTotal is disabled.`)
});
[null, {}, "omitted", undefined].forEach(details => {
test(() => {
let details = {id: "foo"};
try {
if (details === "omitted") {
new PaymentRequest(onlySupportAppStoreBillingMethod);
} else {
new PaymentRequest(onlySupportAppStoreBillingMethod, details);
}
} catch (err) {
let expectedMessage = "Failed to construct 'PaymentRequest': required member total is undefined.";
assert_equals(err.message, expectedMessage, `Error messages mismatch.`);
return;
}
assert_unreached(`Expect an exception.`);
}, `The details field is mandatory (not allowed to be ${JSON.stringify(details)}) when PaymentRequestOptionalTotal is disabled.`)
});
</script>
<!doctype html>
<title>PaymentRequest: Tests for app-store billing when total is optional</title>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script src="../serviceworker/resources/test-helpers.js"></script>
<script>
test(() => {
assert_true(internals.runtimeFlags.paymentRequestOptionalTotalEnabled);
}, "This test suite assumes that the runtime-enabled-flag PaymentRequestOptionalTotal is enabled.");
const onlySupportAppStoreBillingMethod = [{supportedMethods: "https://play.google.com/billing"}];
const supportBothAppStoreBillingMethodAndNormalMethod = [{supportedMethods: "https://play.google.com/billing"},
{supportedMethods: "https://maxpay.com/payment-request"}];
["omitted", undefined].forEach(total => {
test(() => {
let details = {id: "foo"};
if (total !== "omitted") Object.assign(details, { total });
try {
new PaymentRequest(onlySupportAppStoreBillingMethod, details)
} catch (err) {
assert_unreached(
`Unexpected error: ${err.message}.`
);
}
}, `The total field is optional (allowed to be ${JSON.stringify(total)}) when the merchant requests only app-store billing methods.`);
});
[null, {}].forEach(total => {
test(() => {
try {
new PaymentRequest(onlySupportAppStoreBillingMethod, {id: "foo", total});
} catch (err) {
let expectedMessage;
if (total === null) {
expectedMessage = "Failed to construct 'PaymentRequest': Missing required member(s): amount, label.";
} else {
expectedMessage = "Failed to construct 'PaymentRequest': required member amount is undefined.";
}
assert_equals(err.message, expectedMessage, `Error messages mismatch.`);
return;
}
assert_unreached(`Expect an exception.`);
}, `The total field is optional (though not allowed to be ${JSON.stringify(total)}) when the merchant requests only app-store billing methods.`)
});
[null, {}, "omitted", undefined].forEach(total => {
test(() => {
let details = {id: "foo"};
if (total !== "omitted") Object.assign(details, { total });
try {
new PaymentRequest(supportBothAppStoreBillingMethodAndNormalMethod, details);
} catch (err) {
let expectedMessage;
if (total === null) {
expectedMessage = "Failed to construct 'PaymentRequest': Missing required member(s): amount, label.";
} else if (["omitted", undefined].includes(total)) {
expectedMessage = "Failed to construct 'PaymentRequest': required member total is undefined.";
} else {
expectedMessage = "Failed to construct 'PaymentRequest': required member amount is undefined.";
}
assert_equals(err.message, expectedMessage,
`Error messages mismatch.`);
return;
}
assert_unreached(`Expect an exception.`);
}, `The total field is manadatory (not allowed to be ${JSON.stringify(total)}) when the merchant requests non-app-store billing methods.`)
});
["omitted", undefined, null, {}].forEach(details => {
test(() => {
try {
if (details === "omitted") {
new PaymentRequest(onlySupportAppStoreBillingMethod);
} else {
new PaymentRequest(onlySupportAppStoreBillingMethod, details);
}
} catch (err) {
assert_unreached(
`Unexpected error constructing PaymentReuqest with details being ${JSON.stringify(details)}: ${err.message}`
);
}
}, `The details field is optional (allowed to be ${JSON.stringify(details)}) when the merchant requests only app-store billing methods.`)
});
["omitted", undefined, null, {}].forEach(details => {
test(() => {
try {
if (details === "omitted") {
new PaymentRequest(supportBothAppStoreBillingMethodAndNormalMethod);
} else {
new PaymentRequest(supportBothAppStoreBillingMethodAndNormalMethod, details);
}
} catch (err) {
// Since the details pointer is always not null in the C++ code, total is checked as a fallback.
assert_equals(err.message, "Failed to construct 'PaymentRequest': required member details is undefined.",
`Error messages mismatch.`);
return;
}
assert_unreached(`Expect an exception.`);
}, `The details field is manadatory (not allowed to be ${JSON.stringify(details)}) when the merchant requests non-app-store billing methods.`)
});
</script>
Since the flag is default to be disabled in production but enabled in waterfall builders, we create this virtual test suite to test the cases where the flag disabled in waterfall builders.
Since PaymentRequestOptioanalTotal is default to be enabled in waterfall builder, we create this test suite to ensure the cases where PaymentRequestOptioanalTotal is disabled is covered.
...@@ -41426,6 +41426,7 @@ from previous Chrome versions. ...@@ -41426,6 +41426,7 @@ from previous Chrome versions.
<int value="1418054870" label="SpecialLocale:enabled"/> <int value="1418054870" label="SpecialLocale:enabled"/>
<int value="1421620678" label="simple-clear-browsing-data-support-string"/> <int value="1421620678" label="simple-clear-browsing-data-support-string"/>
<int value="1427905064" label="CreditCardAutofillTouchBar:enabled"/> <int value="1427905064" label="CreditCardAutofillTouchBar:enabled"/>
<int value="1428004502" label="PaymentRequestOptionalTotal:enabled"/>
<int value="1431050645" label="PayWithGoogleV1:disabled"/> <int value="1431050645" label="PayWithGoogleV1:disabled"/>
<int value="1431934725" label="OmniboxAutocompleteTitles:disabled"/> <int value="1431934725" label="OmniboxAutocompleteTitles:disabled"/>
<int value="1434515920" label="ReaderModeInCCT:enabled"/> <int value="1434515920" label="ReaderModeInCCT:enabled"/>
...@@ -41658,6 +41659,7 @@ from previous Chrome versions. ...@@ -41658,6 +41659,7 @@ from previous Chrome versions.
<int value="1694854500" label="disable-save-password-bubble"/> <int value="1694854500" label="disable-save-password-bubble"/>
<int value="1696139514" label="enable-ble-advertising-in-apps"/> <int value="1696139514" label="enable-ble-advertising-in-apps"/>
<int value="1697189972" label="WebPaymentsSingleAppUiSkip:disabled"/> <int value="1697189972" label="WebPaymentsSingleAppUiSkip:disabled"/>
<int value="1699180023" label="PaymentRequestOptionalTotal:disabled"/>
<int value="1699182601" label="DockedMagnifier:disabled"/> <int value="1699182601" label="DockedMagnifier:disabled"/>
<int value="1700394127" label="OverlayScrollbar:disabled"/> <int value="1700394127" label="OverlayScrollbar:disabled"/>
<int value="1701972870" label="NTPSnippetsIncreasedVisibility:enabled"/> <int value="1701972870" label="NTPSnippetsIncreasedVisibility:enabled"/>
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