Commit 9d98a508 authored by Sahel Sharify's avatar Sahel Sharify Committed by Commit Bot

Reland "[Payments] Enable shipping and contact info delegation [4/5]"

This is a reland of 3dd95e21
The payment_method_change_response is not renamed in the reland since clank
repository is using PaymentMethodChangeResponse.
The rename will be landed in a separate cl.

Original change's description:
> [Payments] Enable shipping and contact info delegation [4/5]
>
> This cl implements shipping address/option change events for PH. With
> this change payment handlers can notify the merchant when the user
> changes the selected shipping address/option, and wait for updated
> details (e.g. new shipping cost, etc) from merchant.
>
> For overall flow please check
> https://chromium-review.googlesource.com/c/chromium/src/+/1779003
>
> Bug: 984694
> Change-Id: Id881ba22bf4c846a4570801bacc49e5d4e89a72b
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1804557
> Reviewed-by: Mike West <mkwst@chromium.org>
> Reviewed-by: Rouslan Solomakhin <rouslan@chromium.org>
> Commit-Queue: Sahel Sharify <sahel@chromium.org>
> Cr-Commit-Position: refs/heads/master@{#700238}

Bug: 984694
Change-Id: I9a1a84e56701287eb3625b6a681a3f346c47a6e9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1828101Reviewed-by: default avatarMike West <mkwst@chromium.org>
Reviewed-by: default avatarRouslan Solomakhin <rouslan@chromium.org>
Commit-Queue: Sahel Sharify <sahel@chromium.org>
Cr-Commit-Position: refs/heads/master@{#701255}
parent 176fa53b
......@@ -8,6 +8,7 @@ import org.chromium.payments.mojom.PaymentDetails;
import org.chromium.payments.mojom.PaymentHandlerMethodData;
import org.chromium.payments.mojom.PaymentHandlerModifier;
import org.chromium.payments.mojom.PaymentMethodChangeResponse;
import org.chromium.payments.mojom.PaymentShippingOption;
import java.util.ArrayList;
......@@ -44,7 +45,7 @@ public class PaymentDetailsConverter {
* valid for the given payment method identifier. Should not be null.
* @return The data structure that can be sent to the invoked payment handler.
*/
static public PaymentMethodChangeResponse convertToPaymentMethodChangeResponse(
public static PaymentMethodChangeResponse convertToPaymentMethodChangeResponse(
PaymentDetails details, MethodChecker methodChecker) {
// Keep in sync with components/payments/content/payment_details_converter.cc.
assert details != null;
......@@ -53,30 +54,49 @@ public class PaymentDetailsConverter {
PaymentMethodChangeResponse response = new PaymentMethodChangeResponse();
response.error = details.error;
response.stringifiedPaymentMethodErrors = details.stringifiedPaymentMethodErrors;
response.shippingAddressErrors = details.shippingAddressErrors;
if (details.total != null) response.total = details.total.amount;
ArrayList<PaymentHandlerModifier> modifiers = new ArrayList<>();
if (details.modifiers != null) {
ArrayList<PaymentHandlerModifier> modifiers = new ArrayList<>();
for (int i = 0; i < details.modifiers.length; i++) {
if (!methodChecker.isInvokedInstrumentValidForPaymentMethodIdentifier(
details.modifiers[i].methodData.supportedMethod)) {
continue;
}
for (int i = 0; i < details.modifiers.length; i++) {
if (!methodChecker.isInvokedInstrumentValidForPaymentMethodIdentifier(
details.modifiers[i].methodData.supportedMethod)) {
continue;
}
PaymentHandlerModifier modifier = new PaymentHandlerModifier();
modifier.methodData = new PaymentHandlerMethodData();
modifier.methodData.methodName = details.modifiers[i].methodData.supportedMethod;
modifier.methodData.stringifiedData =
details.modifiers[i].methodData.stringifiedData;
PaymentHandlerModifier modifier = new PaymentHandlerModifier();
modifier.methodData = new PaymentHandlerMethodData();
modifier.methodData.methodName = details.modifiers[i].methodData.supportedMethod;
modifier.methodData.stringifiedData = details.modifiers[i].methodData.stringifiedData;
if (details.modifiers[i].total != null) {
modifier.total = details.modifiers[i].total.amount;
}
if (details.modifiers[i].total != null) {
modifier.total = details.modifiers[i].total.amount;
modifiers.add(modifier);
}
modifiers.add(modifier);
response.modifiers = modifiers.toArray(new PaymentHandlerModifier[modifiers.size()]);
}
response.modifiers = modifiers.toArray(new PaymentHandlerModifier[modifiers.size()]);
if (details.shippingOptions != null) {
ArrayList<PaymentShippingOption> options = new ArrayList<>();
for (int i = 0; i < details.shippingOptions.length; i++) {
PaymentShippingOption option = new PaymentShippingOption();
option.amount = details.shippingOptions[i].amount;
option.id = details.shippingOptions[i].id;
option.label = details.shippingOptions[i].label;
option.selected = details.shippingOptions[i].selected;
options.add(option);
}
response.shippingOptions = options.toArray(new PaymentShippingOption[options.size()]);
}
return response;
}
......
......@@ -34,7 +34,7 @@ PaymentHandlerHost::PaymentHandlerHost(
PaymentHandlerHost::~PaymentHandlerHost() {}
jboolean PaymentHandlerHost::IsChangingPaymentMethod(JNIEnv* env) const {
return payment_handler_host_.is_changing_payment_method();
return payment_handler_host_.is_changing();
}
jlong PaymentHandlerHost::GetNativePaymentHandlerHost(JNIEnv* env) {
......@@ -69,5 +69,21 @@ bool PaymentHandlerHost::ChangePaymentMethod(
base::android::ConvertUTF8ToJavaString(env, stringified_data));
}
bool PaymentHandlerHost::ChangeShippingOption(
const std::string& shipping_option_id) {
// Shipping and contact info delegation is not implemented on Android yet.
// TODO(sahel): crbug.com/984694
NOTREACHED();
return false;
}
bool PaymentHandlerHost::ChangeShippingAddress(
mojom::PaymentAddressPtr shipping_address) {
// Shipping and contact info delegation is not implemented on Android yet.
// TODO(sahel): crbug.com/984694
NOTREACHED();
return false;
}
} // namespace android
} // namespace payments
......@@ -66,6 +66,9 @@ class PaymentHandlerHost : public payments::PaymentHandlerHost::Delegate {
// PaymentHandlerHost::Delegate implementation:
bool ChangePaymentMethod(const std::string& method_name,
const std::string& stringified_data) override;
bool ChangeShippingOption(const std::string& shipping_option_id) override;
bool ChangeShippingAddress(
mojom::PaymentAddressPtr shipping_address) override;
base::android::ScopedJavaGlobalRef<jobject> delegate_;
payments::PaymentHandlerHost payment_handler_host_;
......
......@@ -23,31 +23,41 @@ PaymentDetailsConverter::ConvertToPaymentMethodChangeResponse(
response->error = details->error;
response->stringified_payment_method_errors =
details->stringified_payment_method_errors;
if (details->shipping_address_errors) {
response->shipping_address_errors =
details->shipping_address_errors.Clone();
}
if (details->total)
response->total = details->total->amount.Clone();
if (!details->modifiers)
return response;
if (details->modifiers) {
response->modifiers = std::vector<mojom::PaymentHandlerModifierPtr>();
response->modifiers = std::vector<mojom::PaymentHandlerModifierPtr>();
for (const auto& merchant : *details->modifiers) {
bool is_valid = false;
method_checker.Run(merchant->method_data->supported_method, &is_valid);
if (!is_valid)
continue;
for (const auto& merchant : *details->modifiers) {
bool is_valid = false;
method_checker.Run(merchant->method_data->supported_method, &is_valid);
if (!is_valid)
continue;
mojom::PaymentHandlerModifierPtr mod =
mojom::PaymentHandlerModifier::New();
mod->method_data = mojom::PaymentHandlerMethodData::New();
mod->method_data->method_name = merchant->method_data->supported_method;
mod->method_data->stringified_data =
merchant->method_data->stringified_data;
mojom::PaymentHandlerModifierPtr mod = mojom::PaymentHandlerModifier::New();
mod->method_data = mojom::PaymentHandlerMethodData::New();
mod->method_data->method_name = merchant->method_data->supported_method;
mod->method_data->stringified_data =
merchant->method_data->stringified_data;
if (merchant->total)
mod->total = merchant->total->amount.Clone();
if (merchant->total)
mod->total = merchant->total->amount.Clone();
response->modifiers->emplace_back(std::move(mod));
}
}
response->modifiers->emplace_back(std::move(mod));
if (details->shipping_options) {
response->shipping_options = std::vector<mojom::PaymentShippingOptionPtr>();
for (const auto& option : *details->shipping_options)
response->shipping_options->emplace_back(option.Clone());
}
return response;
......
......@@ -10,6 +10,8 @@
#include "base/strings/string_number_conversions.h"
#include "components/payments/core/error_strings.h"
#include "components/payments/core/native_error_strings.h"
#include "components/payments/core/payment_address.h"
#include "components/payments/core/payments_validators.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/devtools_background_services_context.h"
......@@ -36,6 +38,16 @@ content::DevToolsBackgroundServicesContext* GetDevTools(
: nullptr;
}
// Generates a PaymentResponse with the given error, then runs the provided
// callback.
void RunCallbackWithError(const std::string& error,
ChangePaymentRequestDetailsCallback callback) {
mojom::PaymentMethodChangeResponsePtr response =
mojom::PaymentMethodChangeResponse::New();
response->error = error;
std::move(callback).Run(std::move(response));
}
} // namespace
PaymentHandlerHost::PaymentHandlerHost(content::WebContents* web_contents,
......@@ -61,7 +73,7 @@ mojo::PendingRemote<mojom::PaymentHandlerHost> PaymentHandlerHost::Bind() {
void PaymentHandlerHost::UpdateWith(
mojom::PaymentMethodChangeResponsePtr response) {
if (!change_payment_method_callback_)
if (!change_payment_request_details_callback_)
return;
auto* dev_tools =
......@@ -79,6 +91,29 @@ void PaymentHandlerHost::UpdateWith(
*response->stringified_payment_method_errors;
}
if (response->shipping_address_errors) {
data["Shipping Address Address Line Error"] =
response->shipping_address_errors->address_line;
data["Shipping Address City Error"] =
response->shipping_address_errors->city;
data["Shipping Address Country Error"] =
response->shipping_address_errors->country;
data["Shipping Address Dependent Locality Error"] =
response->shipping_address_errors->dependent_locality;
data["Shipping Address Organization Error"] =
response->shipping_address_errors->organization;
data["Shipping Address Phone Error"] =
response->shipping_address_errors->phone;
data["Shipping Address Postal Code Error"] =
response->shipping_address_errors->postal_code;
data["Shipping Address Recipient Error"] =
response->shipping_address_errors->recipient;
data["Shipping Address Region Error"] =
response->shipping_address_errors->region;
data["Shipping Address Sorting Code Error"] =
response->shipping_address_errors->sorting_code;
}
if (response->modifiers) {
for (size_t i = 0; i < response->modifiers->size(); ++i) {
std::string prefix =
......@@ -97,20 +132,35 @@ void PaymentHandlerHost::UpdateWith(
}
}
if (response->shipping_options) {
for (size_t i = 0; i < response->shipping_options->size(); ++i) {
std::string prefix =
"Shipping Option" + (response->shipping_options->size() == 1
? ""
: " #" + base::NumberToString(i));
const auto& option = response->shipping_options->at(i);
data.emplace(prefix + " Id", option->id);
data.emplace(prefix + " Label", option->label);
data.emplace(prefix + " Amount Currency", option->amount->currency);
data.emplace(prefix + " Amount Value", option->amount->value);
data.emplace(prefix + " Selected", option->selected ? "true" : "false");
}
}
dev_tools->LogBackgroundServiceEvent(
registration_id_for_logs_, sw_origin_for_logs_,
content::DevToolsBackgroundService::kPaymentHandler, "Update with",
/*instance_id=*/payment_request_id_for_logs_, data);
}
std::move(change_payment_method_callback_).Run(std::move(response));
std::move(change_payment_request_details_callback_).Run(std::move(response));
}
void PaymentHandlerHost::NoUpdatedPaymentDetails() {
if (!change_payment_method_callback_)
if (!change_payment_request_details_callback_)
return;
std::move(change_payment_method_callback_)
std::move(change_payment_request_details_callback_)
.Run(mojom::PaymentMethodChangeResponse::New());
}
......@@ -124,31 +174,22 @@ base::WeakPtr<PaymentHandlerHost> PaymentHandlerHost::AsWeakPtr() {
void PaymentHandlerHost::ChangePaymentMethod(
mojom::PaymentHandlerMethodDataPtr method_data,
mojom::PaymentHandlerHost::ChangePaymentMethodCallback callback) {
ChangePaymentRequestDetailsCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!method_data) {
mojom::PaymentMethodChangeResponsePtr response =
mojom::PaymentMethodChangeResponse::New();
response->error = errors::kMethodDataRequired;
std::move(callback).Run(std::move(response));
RunCallbackWithError(errors::kMethodDataRequired, std::move(callback));
return;
}
if (method_data->method_name.empty()) {
mojom::PaymentMethodChangeResponsePtr response =
mojom::PaymentMethodChangeResponse::New();
response->error = errors::kMethodNameRequired;
std::move(callback).Run(std::move(response));
RunCallbackWithError(errors::kMethodNameRequired, std::move(callback));
return;
}
if (!delegate_->ChangePaymentMethod(method_data->method_name,
method_data->stringified_data)) {
mojom::PaymentMethodChangeResponsePtr response =
mojom::PaymentMethodChangeResponse::New();
response->error = errors::kInvalidState;
std::move(callback).Run(std::move(response));
RunCallbackWithError(errors::kInvalidState, std::move(callback));
return;
}
......@@ -164,7 +205,88 @@ void PaymentHandlerHost::ChangePaymentMethod(
{"Method Data", method_data->stringified_data}});
}
change_payment_method_callback_ = std::move(callback);
change_payment_request_details_callback_ = std::move(callback);
}
void PaymentHandlerHost::ChangeShippingOption(
const std::string& shipping_option_id,
ChangePaymentRequestDetailsCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (shipping_option_id.empty()) {
RunCallbackWithError(errors::kShippingOptionIdRequired,
std::move(callback));
return;
}
if (!delegate_->ChangeShippingOption(shipping_option_id)) {
RunCallbackWithError(errors::kInvalidState, std::move(callback));
return;
}
auto* dev_tools =
GetDevTools(web_contents_->GetBrowserContext(), sw_origin_for_logs_);
if (dev_tools) {
dev_tools->LogBackgroundServiceEvent(
registration_id_for_logs_, sw_origin_for_logs_,
content::DevToolsBackgroundService::kPaymentHandler,
"Change shipping option",
/*instance_id=*/payment_request_id_for_logs_,
{{"Shipping Option Id", shipping_option_id}});
}
change_payment_request_details_callback_ = std::move(callback);
}
void PaymentHandlerHost::ChangeShippingAddress(
mojom::PaymentAddressPtr shipping_address,
ChangePaymentRequestDetailsCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!shipping_address || !PaymentsValidators::IsValidCountryCodeFormat(
shipping_address->country, nullptr)) {
RunCallbackWithError(errors::kShippingAddressInvalid, std::move(callback));
return;
}
if (!delegate_->ChangeShippingAddress(shipping_address.Clone())) {
RunCallbackWithError(errors::kInvalidState, std::move(callback));
return;
}
auto* dev_tools =
GetDevTools(web_contents_->GetBrowserContext(), sw_origin_for_logs_);
if (dev_tools) {
std::map<std::string, std::string> shipping_address_map;
shipping_address_map.emplace("Country", shipping_address->country);
for (size_t i = 0; i < shipping_address->address_line.size(); ++i) {
std::string key =
"Address Line" + (shipping_address->address_line.size() == 1
? ""
: " #" + base::NumberToString(i));
shipping_address_map.emplace(key, shipping_address->address_line[i]);
}
shipping_address_map.emplace("Region", shipping_address->region);
shipping_address_map.emplace("City", shipping_address->city);
shipping_address_map.emplace("Dependent Locality",
shipping_address->dependent_locality);
shipping_address_map.emplace("Postal Code", shipping_address->postal_code);
shipping_address_map.emplace("Sorting Code",
shipping_address->sorting_code);
shipping_address_map.emplace("Organization",
shipping_address->organization);
shipping_address_map.emplace("Recipient", shipping_address->recipient);
shipping_address_map.emplace("Phone", shipping_address->phone);
dev_tools->LogBackgroundServiceEvent(
registration_id_for_logs_, sw_origin_for_logs_,
content::DevToolsBackgroundService::kPaymentHandler,
"Change shipping address",
/*instance_id=*/payment_request_id_for_logs_, shipping_address_map);
}
change_payment_request_details_callback_ = std::move(callback);
}
} // namespace payments
......@@ -22,6 +22,9 @@ class WebContents;
namespace payments {
using ChangePaymentRequestDetailsCallback =
base::OnceCallback<void(mojom::PaymentMethodChangeResponsePtr)>;
// Handles the communication from the payment handler renderer process to the
// merchant renderer process.
class PaymentHandlerHost : public mojom::PaymentHandlerHost {
......@@ -36,6 +39,17 @@ class PaymentHandlerHost : public mojom::PaymentHandlerHost {
// "false" if the state is invalid.
virtual bool ChangePaymentMethod(const std::string& method_name,
const std::string& stringified_data) = 0;
// Notifies the merchant that the shipping option has changed. Returns
// "false" if the state is invalid.
virtual bool ChangeShippingOption(
const std::string& shipping_option_id) = 0;
// Notifies the merchant that the shipping address has changed after
// redacting the address whenever needed. Returns "false" if the state is
// invalid.
virtual bool ChangeShippingAddress(
mojom::PaymentAddressPtr shipping_address) = 0;
};
// The |delegate| cannot be null and must outlive this object. Typically this
......@@ -63,21 +77,24 @@ class PaymentHandlerHost : public mojom::PaymentHandlerHost {
payment_request_id_for_logs_ = id;
}
// Returns "true" when the payment handler has changed the payment method, but
// has not received the response from the merchant yet.
bool is_changing_payment_method() const {
return !!change_payment_method_callback_;
// Returns "true" when the payment handler has changed any of the payment
// method, shipping address or shipping option, but has not received the
// response from the merchant yet.
bool is_changing() const {
return !!change_payment_request_details_callback_;
}
// Binds to an IPC endpoint and returns it.
mojo::PendingRemote<mojom::PaymentHandlerHost> Bind();
// Notifies the payment handler of the updated details, such as updated total,
// in response to the change of the payment method.
// in response to the change of any of the following: payment method, shipping
// address, or shipping option.
void UpdateWith(mojom::PaymentMethodChangeResponsePtr response);
// Notifies the payment handler that the merchant did not handle the payment
// method change event, so the payment details are unchanged.
// method, shipping option, or shipping address change events, so the payment
// details are unchanged.
void NoUpdatedPaymentDetails();
// Disconnects from the payment handler.
......@@ -89,12 +106,22 @@ class PaymentHandlerHost : public mojom::PaymentHandlerHost {
// mojom::PaymentHandlerHost
void ChangePaymentMethod(
mojom::PaymentHandlerMethodDataPtr method_data,
mojom::PaymentHandlerHost::ChangePaymentMethodCallback callback) override;
ChangePaymentRequestDetailsCallback callback) override;
// Payment handler's callback to invoke after merchant responds to the
// "payment method change" event.
mojom::PaymentHandlerHost::ChangePaymentMethodCallback
change_payment_method_callback_;
// mojom::PaymentHandlerHost
void ChangeShippingOption(
const std::string& shipping_option_id,
ChangePaymentRequestDetailsCallback callback) override;
// mojom::PaymentHandlerHost
void ChangeShippingAddress(
mojom::PaymentAddressPtr shipping_address,
ChangePaymentRequestDetailsCallback callback) override;
// Payment handler's callback to invoke after merchant responds to any of the
// "payment method change", "shipping option change", or "shipping address
// change" events.
ChangePaymentRequestDetailsCallback change_payment_request_details_callback_;
// The end-point for the payment handler renderer process to call into the
// browser process.
......
......@@ -70,6 +70,22 @@ std::string GetNotSupportedErrorMessage(PaymentRequestSpec* spec) {
return output;
}
// Redact shipping address before exposing it in ShippingAddressChangeEvent.
// https://w3c.github.io/payment-request/#shipping-address-changed-algorithm
mojom::PaymentAddressPtr RedactShippingAddress(
mojom::PaymentAddressPtr address) {
DCHECK(address);
if (!PaymentsExperimentalFeatures::IsEnabled(
features::kWebPaymentsRedactShippingAddress)) {
return address;
}
address->organization.clear();
address->phone.clear();
address->recipient.clear();
address->address_line.clear();
return address;
}
} // namespace
PaymentRequest::PaymentRequest(
......@@ -326,7 +342,7 @@ void PaymentRequest::UpdateWith(mojom::PaymentDetailsPtr details) {
}
if (state()->selected_instrument() && state()->IsPaymentAppInvoked() &&
payment_handler_host_.is_changing_payment_method()) {
payment_handler_host_.is_changing()) {
payment_handler_host_.UpdateWith(
PaymentDetailsConverter::ConvertToPaymentMethodChangeResponse(
details, base::BindRepeating(
......@@ -369,8 +385,7 @@ void PaymentRequest::NoUpdatedPaymentDetails() {
spec_->RecomputeSpecForDetails();
if (state()->IsPaymentAppInvoked() &&
payment_handler_host_.is_changing_payment_method()) {
if (state()->IsPaymentAppInvoked() && payment_handler_host_.is_changing()) {
payment_handler_host_.NoUpdatedPaymentDetails();
}
}
......@@ -498,6 +513,45 @@ bool PaymentRequest::ChangePaymentMethod(const std::string& method_name,
return true;
}
bool PaymentRequest::ChangeShippingOption(
const std::string& shipping_option_id) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(!shipping_option_id.empty());
bool is_valid_id = false;
if (spec_->details().shipping_options) {
for (const auto& option : spec_->GetShippingOptions()) {
if (option->id == shipping_option_id) {
is_valid_id = true;
break;
}
}
}
if (!state_ || !state_->IsPaymentAppInvoked() || !client_ || !spec_ ||
!spec_->request_shipping() || !is_valid_id) {
return false;
}
client_->OnShippingOptionChange(shipping_option_id);
return true;
}
bool PaymentRequest::ChangeShippingAddress(
mojom::PaymentAddressPtr shipping_address) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(shipping_address);
if (!state_ || !state_->IsPaymentAppInvoked() || !client_ || !spec_ ||
!spec_->request_shipping()) {
return false;
}
client_->OnShippingAddressChange(
RedactShippingAddress(std::move(shipping_address)));
return true;
}
void PaymentRequest::AreRequestedMethodsSupportedCallback(
bool methods_supported,
const std::string& error_message) {
......@@ -613,16 +667,7 @@ void PaymentRequest::OnShippingOptionIdSelected(
void PaymentRequest::OnShippingAddressSelected(
mojom::PaymentAddressPtr address) {
// Redact shipping address before exposing it in ShippingAddressChangeEvent.
// https://w3c.github.io/payment-request/#shipping-address-changed-algorithm
if (PaymentsExperimentalFeatures::IsEnabled(
features::kWebPaymentsRedactShippingAddress)) {
address->organization.clear();
address->phone.clear();
address->recipient.clear();
address->address_line.clear();
}
client_->OnShippingAddressChange(std::move(address));
client_->OnShippingAddressChange(RedactShippingAddress(std::move(address)));
}
void PaymentRequest::OnPayerInfoSelected(mojom::PayerDetailPtr payer_info) {
......
......@@ -84,6 +84,9 @@ class PaymentRequest : public mojom::PaymentRequest,
// PaymentHandlerHost::Delegate
bool ChangePaymentMethod(const std::string& method_name,
const std::string& stringified_data) override;
bool ChangeShippingOption(const std::string& shipping_option_id) override;
bool ChangeShippingAddress(
mojom::PaymentAddressPtr shipping_address) override;
// PaymentRequestSpec::Observer:
void OnSpecUpdated() override {}
......
......@@ -16,6 +16,8 @@ jumbo_static_library("core") {
"features.h",
"journey_logger.cc",
"journey_logger.h",
"payment_address.cc",
"payment_address.h",
"payment_currency_amount.cc",
"payment_currency_amount.h",
"payment_details.cc",
......@@ -50,8 +52,6 @@ jumbo_static_library("core") {
"basic_card_response.h",
"payer_data.cc",
"payer_data.h",
"payment_address.cc",
"payment_address.h",
"payment_instrument.cc",
"payment_instrument.h",
"payment_options.cc",
......
......@@ -155,6 +155,8 @@ const char kPaymentHandlerInsecureNavigation[] =
const char kSinglePaymentMethodNotSupportedFormat[] =
"The payment method $ is not supported.";
const char kShippingOptionIdRequired[] = "Shipping option identifier required.";
const char kShippingAddressInvalid[] =
"Payment app returned invalid shipping address in response.";
......
......@@ -187,6 +187,9 @@ extern const char kReachedMaximumNumberOfRedirects[];
// where "$" is the character to replace.
extern const char kSinglePaymentMethodNotSupportedFormat[];
// Used when non-empty "shippingOptionId": "" is required, but not provided.
extern const char kShippingOptionIdRequired[];
// The payment handler responded with an invalid shipping address.
extern const char kShippingAddressInvalid[];
......
......@@ -6,6 +6,7 @@
module payments.mojom;
import "components/payments/mojom/payment_request_data.mojom";
import "third_party/blink/public/mojom/payments/payment_request.mojom";
struct PaymentHandlerMethodData {
// Either a short string (e.g., "basic-string") or URL (e.g.,
......@@ -31,9 +32,11 @@ struct PaymentHandlerModifier {
// renderer. The browser sends this to the Payment Handler renderer.
struct PaymentMethodChangeResponse {
PaymentCurrencyAmount? total;
array<PaymentShippingOption>? shipping_options;
array<PaymentHandlerModifier>? modifiers;
string error = "";
string? stringified_payment_method_errors;
AddressErrors? shipping_address_errors;
};
// The interface for a PaymentRequest object in the browser, so a PaymentHandler
......@@ -44,22 +47,35 @@ struct PaymentMethodChangeResponse {
// components/payments/content/payment_request.h
interface PaymentHandlerHost {
// Called by the Payment Handler renderer to let the browser know that the
// user has selected a different payment instrument.
// user has selected a different payment instrument or shipping option, or
// when the user has changed the shipping address.
//
// The browser validates the |method_data| and passes it on to the Merchant
// renderer via
// The browser validates the |method_data|, |shipping address|, or
// |shipping option_id| and passes it on to the Merchant renderer via one of
// the following methods which are all defined in
// third_party/blink/public/mojom/payments/payment_request.mojom:
// PaymentRequestClient.OnPaymentMethodChange(method_name,stringified_data),
// defined in third_party/blink/public/mojom/payments/payment_request.mojom.
// PaymentRequestClient.OnShippingAddressChange(PaymentAddress address),
// PaymentRequestClient.OnShippingOptionChange(string shipping_option_id).
//
// The Merchant renderer than responds to the browser via
// PaymentRequest.UpdateWith(details) to update the total based on the
// selected instrument or PaymentRequest.NoUpdatedPaymentDetails() if the
// total is unchanged. The total can change, for example, based on the
// billing address of the selected instrument.
// The Merchant renderer then responds to the browser via
// PaymentRequest.UpdateWith(details) to update the total or other details of
// the payment request based on the selected instrument/shipping address/
// shipping option or PaymentRequest.NoUpdatedPaymentDetails() if the
// total and other details are unchanged. The total can change, for example,
// based on the billing address of the selected instrument or the selected
// shipping address/option. The list of available shipping options can change
// based on the selected shipping address.
//
// The browser validates the |details| from the Merchant renderer and sends
// their subset to the Payment Handler renderer as
// |PaymentMethodChangeResponse|, so it can show the updated total.
// |PaymentMethodChangeResponse|, so it can show the updated details.
// Todo(sahel): Rename PaymentMethodChangeResponse to
// PaymentRequestDetailsUpdate which is a more generic name. crbug.com/984694
ChangePaymentMethod(PaymentHandlerMethodData method_data) =>
(PaymentMethodChangeResponse response_data);
ChangeShippingOption(string shipping_option_id) =>
(PaymentMethodChangeResponse response_data);
ChangeShippingAddress(PaymentAddress shipping_address) =>
(PaymentMethodChangeResponse response_data);
};
......@@ -3,10 +3,15 @@
// found in the LICENSE file.
// https://w3c.github.io/payment-handler/#dom-paymentmethodchangeresponse
// Todo(sahel): Change the link to the updated spec. crbug.com/984694
// Todo(sahel): Rename PaymentMethodChangeResponse to
// PaymentRequestDetailsUpdate which is a more generic name. crbug.com/984694
dictionary PaymentMethodChangeResponse {
DOMString error;
PaymentCurrencyAmount total;
FrozenArray<PaymentDetailsModifier> modifiers;
FrozenArray<PaymentShippingOption> shippingOptions;
object paymentMethodErrors;
AddressErrors shippingAddressErrors;
};
......@@ -11,6 +11,7 @@
#include "third_party/blink/public/mojom/payments/payment_handler_host.mojom-blink.h"
#include "third_party/blink/renderer/bindings/core/v8/script_value.h"
#include "third_party/blink/renderer/modules/event_modules.h"
#include "third_party/blink/renderer/modules/payments/payment_address_init.h"
#include "third_party/blink/renderer/modules/payments/payment_request_event_init.h"
#include "third_party/blink/renderer/modules/service_worker/extendable_event.h"
#include "third_party/blink/renderer/platform/heap/handle.h"
......@@ -68,12 +69,16 @@ class MODULES_EXPORT PaymentRequestEvent final : public ExtendableEvent {
const String& method_name,
const ScriptValue& method_details,
ExceptionState& exception_state);
ScriptPromise changeShippingAddress(ScriptState*,
PaymentAddressInit* shippingAddress);
ScriptPromise changeShippingOption(ScriptState*,
const String& shipping_option_id);
void respondWith(ScriptState*, ScriptPromise, ExceptionState&);
void Trace(blink::Visitor*) override;
private:
void OnChangePaymentMethodResponse(
void OnChangePaymentRequestDetailsResponse(
payments::mojom::blink::PaymentMethodChangeResponsePtr);
void OnHostConnectionError();
......@@ -87,7 +92,7 @@ class MODULES_EXPORT PaymentRequestEvent final : public ExtendableEvent {
Member<PaymentOptions> payment_options_;
HeapVector<Member<PaymentShippingOption>> shipping_options_;
Member<ScriptPromiseResolver> change_payment_method_resolver_;
Member<ScriptPromiseResolver> change_payment_request_details_resolver_;
Member<RespondWithObserver> observer_;
mojo::Remote<payments::mojom::blink::PaymentHandlerHost>
payment_handler_host_;
......
......@@ -22,4 +22,6 @@
[CallWith=ScriptState] Promise<WindowClient?> openWindow(USVString url);
[CallWith=ScriptState, RaisesException, RuntimeEnabled=PaymentHandlerChangePaymentMethod] Promise<PaymentMethodChangeResponse?> changePaymentMethod(DOMString methodName, optional object? methodDetails = null);
[CallWith=ScriptState, RaisesException] void respondWith(Promise<PaymentResponse> response);
[CallWith=ScriptState, RuntimeEnabled=PaymentHandlerHandlesShippingAndContact] Promise<PaymentMethodChangeResponse?> changeShippingAddress(PaymentAddressInit shippingAddress);
[CallWith=ScriptState, RuntimeEnabled=PaymentHandlerHandlesShippingAndContact] Promise<PaymentMethodChangeResponse?> changeShippingOption(DOMString shippingOption);
};
self.addEventListener('canmakepayment', (event) => {
event.respondWith(true);
});
async function responder(event) {
const methodName = event.methodData[0].supportedMethods;
const shippingOption = event.shippingOptions[0].id;
const shippingAddress = {
addressLine: [
'1875 Explorer St #1000',
],
city: 'Reston',
country: 'US',
dependentLocality: '',
organization: 'Google',
phone: '+15555555555',
postalCode: '20190',
recipient: 'John Smith',
region: 'VA',
sortingCode: '',
};
if (!event.changeShippingAddress) {
return {
methodName,
details: {
changeShippingAddressReturned:
'The changeShippingAddress() method is not implemented.',
},
};
}
let changeShippingAddressReturned;
try {
const response = await event.changeShippingAddress(shippingAddress);
changeShippingAddressReturned = response;
} catch (err) {
changeShippingAddressReturned = err.message;
}
return {methodName, details: {changeShippingAddressReturned}, shippingAddress,
shippingOption};
}
self.addEventListener('paymentrequest', (event) => {
event.respondWith(responder(event));
});
self.addEventListener('canmakepayment', (event) => {
event.respondWith(true);
});
async function responder(event) {
const methodName = event.methodData[0].supportedMethods;
const shippingOption = event.shippingOptions[0].id;
const shippingAddress = {
addressLine: [
'1875 Explorer St #1000',
],
city: 'Reston',
country: 'US',
dependentLocality: '',
organization: 'Google',
phone: '+15555555555',
postalCode: '20190',
recipient: 'John Smith',
region: 'VA',
sortingCode: '',
};
if (!event.changeShippingOption) {
return {
methodName,
details: {
changeShippingOptionReturned:
'The changeShippingOption() method is not implemented.',
},
};
}
let changeShippingOptionReturned;
try {
const response = await event.changeShippingOption(shippingOption);
changeShippingOptionReturned = response;
} catch (err) {
changeShippingOptionReturned = err.message;
}
return {methodName, details: {changeShippingOptionReturned}, shippingAddress,
shippingOption};
}
self.addEventListener('paymentrequest', (event) => {
event.respondWith(responder(event));
});
This is a testharness.js-based test.
FAIL Tests for PaymentRequestEvent.changeShippingAddress() Unhandled rejection: Not allowed to install this payment handler
Harness: the test ran to completion.
<!DOCTYPE html>
<meta charset="utf-8" />
<title>Tests for PaymentRequestEvent.changeShippingAddress()</title>
<link rel="manifest" href="/payment-handler/basic-card.json" />
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<script src="register-and-activate-service-worker.js"></script>
<p>If the payment sheet is shown, please authorize the mock payment.</p>
<script>
const methodName = window.location.origin + '/payment-handler/payment-app/';
function createRequest() {
return new PaymentRequest([{supportedMethods: methodName}], {
total: {label: 'Total', amount: {currency: 'USD', value: '0.01'}},
shippingOptions: [{
id: 'freeShippingOption',
label: 'Free global shipping',
amount: {
currency: 'USD',
value: '0',
},
selected: false,
}],
}, {requestShipping: true});
}
async function completeAppSetUp(registration) {
await registration.paymentManager.instruments.clear();
await registration.paymentManager.instruments.set('instrument-key', {
name: 'Instrument Name',
method: methodName,
});
await navigator.serviceWorker.ready;
await registration.paymentManager.enableDelegations(['shippingAddress']);
}
async function runTests(registration) {
await completeAppSetUp(registration);
promise_test(async (t) => {
const request = createRequest();
// Intentionally do not respond to the 'shippingaddresschange' event.
const response = await test_driver.bless('showing a payment sheet', () =>
request.show()
);
const complete_promise = response.complete('success');
assert_equals(response.details.changeShippingAddressReturned, null);
return complete_promise;
}, 'If updateWith(details) is not run, changeShippingAddress() returns null.');
promise_test(async (t) => {
const request = createRequest();
request.addEventListener('shippingaddresschange', (event) => {
assert_equals(request.shippingAddress.organization, '', 'organization should be redacted');
assert_equals(request.shippingAddress.phone, '', 'phone should be redacted');
assert_equals(request.shippingAddress.recipient, '', 'recipient should be redacted');
assert_equals(request.shippingAddress.addressLine.length, 0, 'addressLine should be redacted');
assert_equals(request.shippingAddress.city, 'Reston');
assert_equals(request.shippingAddress.country, 'US');
assert_equals(request.shippingAddress.postalCode, '20190');
assert_equals(request.shippingAddress.region, 'VA');
event.updateWith({
total: {label: 'Total', amount: {currency: 'GBP', value: '0.02'}},
error: 'Error for test',
modifiers: [
{
supportedMethods: methodName,
data: {soup: 'potato'},
total: {
label: 'Modified total',
amount: {currency: 'EUR', value: '0.03'},
},
additionalDisplayItems: [
{
label: 'Modified display item',
amount: {currency: 'INR', value: '0.06'},
},
],
},
{
supportedMethods: methodName + '2',
data: {soup: 'tomato'},
total: {
label: 'Modified total #2',
amount: {currency: 'CHF', value: '0.07'},
},
additionalDisplayItems: [
{
label: 'Modified display item #2',
amount: {currency: 'CAD', value: '0.08'},
},
],
},
],
displayItems: [
{
label: 'Display item',
amount: {currency: 'CNY', value: '0.04'},
},
],
shippingOptions: [
{
id: 'freeShippingOption',
label: 'express global shipping',
amount: {
currency: 'USD',
value: '0',
},
selected: true,
}
],
shippingAddressErrors: {
country: 'US only shipping',
}
});
});
const response = await test_driver.bless('showing a payment sheet', () =>
request.show()
);
const complete_promise = response.complete('success');
const changeShippingAddressReturned =
response.details.changeShippingAddressReturned;
assert_equals(changeShippingAddressReturned.total.currency, 'GBP');
assert_equals(changeShippingAddressReturned.total.value, '0.02');
assert_equals(changeShippingAddressReturned.total.label, undefined);
assert_equals(changeShippingAddressReturned.error, 'Error for test');
assert_equals(changeShippingAddressReturned.modifiers.length, 1);
assert_equals(changeShippingAddressReturned.displayItems, undefined);
assert_equals(changeShippingAddressReturned.shippingOptions.length, 1);
assert_equals(changeShippingAddressReturned.paymentMethodErrors, undefined);
assert_equals(changeShippingAddressReturned.shippingAddressErrors.country, 'US only shipping');
const shipping_option = changeShippingAddressReturned.shippingOptions[0];
assert_equals(shipping_option.id, 'freeShippingOption' );
assert_equals(shipping_option.label, 'express global shipping');
assert_equals(shipping_option.amount.currency, 'USD');
assert_equals(shipping_option.amount.value, '0');
assert_true(shipping_option.selected);
const modifier = changeShippingAddressReturned.modifiers[0];
assert_equals(modifier.supportedMethods, methodName);
assert_equals(modifier.data.soup, 'potato');
assert_equals(modifier.total.label, '');
assert_equals(modifier.total.amount.currency, 'EUR');
assert_equals(modifier.total.amount.value, '0.03');
assert_equals(modifier.additionalDisplayItems, undefined);
return complete_promise;
}, 'The changeShippingAddress() returns some details from the "shippingaddresschange" event\'s updateWith(details) call.');
}
registerAndActiveServiceWorker(
'app-change-shipping-address.js',
'payment-app/',
runTests
);
</script>
This is a testharness.js-based test.
FAIL Tests for PaymentRequestEvent.changeShippingOption() Unhandled rejection: Not allowed to install this payment handler
Harness: the test ran to completion.
<!DOCTYPE html>
<meta charset="utf-8" />
<title>Tests for PaymentRequestEvent.changeShippingOption()</title>
<link rel="manifest" href="/payment-handler/basic-card.json" />
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<script src="register-and-activate-service-worker.js"></script>
<p>If the payment sheet is shown, please authorize the mock payment.</p>
<script>
const methodName = window.location.origin + '/payment-handler/payment-app/';
function createRequest() {
return new PaymentRequest([{supportedMethods: methodName}], {
total: {label: 'Total', amount: {currency: 'USD', value: '0.01'}},
shippingOptions: [{
id: 'freeShippingOption',
label: 'Free global shipping',
amount: {
currency: 'USD',
value: '0',
},
selected: false,
},
{
id: 'expressShippingOption',
label: 'express global shipping',
amount: {
currency: 'USD',
value: '0',
},
selected: true,
}],
}, {requestShipping: true});
}
async function completeAppSetUp(registration) {
await registration.paymentManager.instruments.clear();
await registration.paymentManager.instruments.set('instrument-key', {
name: 'Instrument Name',
method: methodName,
});
await navigator.serviceWorker.ready;
await registration.paymentManager.enableDelegations(['shippingAddress']);
}
async function runTests(registration) {
await completeAppSetUp(registration);
promise_test(async (t) => {
const request = createRequest();
// Intentionally do not respond to the 'shippingoptionchange' event.
const response = await test_driver.bless('showing a payment sheet', () =>
request.show()
);
const complete_promise = response.complete('success');
assert_equals(response.details.changeShippingOptionReturned, null);
return complete_promise;
}, 'If updateWith(details) is not run, changeShippingOption() returns null.');
promise_test(async (t) => {
const request = createRequest();
request.addEventListener('shippingoptionchange', (event) => {
assert_equals(request.shippingOption, 'freeShippingOption');
event.updateWith({
total: {label: 'Total', amount: {currency: 'GBP', value: '0.02'}},
error: 'Error for test',
modifiers: [
{
supportedMethods: methodName,
data: {soup: 'potato'},
total: {
label: 'Modified total',
amount: {currency: 'EUR', value: '0.03'},
},
additionalDisplayItems: [
{
label: 'Modified display item',
amount: {currency: 'INR', value: '0.06'},
},
],
},
{
supportedMethods: methodName + '2',
data: {soup: 'tomato'},
total: {
label: 'Modified total #2',
amount: {currency: 'CHF', value: '0.07'},
},
additionalDisplayItems: [
{
label: 'Modified display item #2',
amount: {currency: 'CAD', value: '0.08'},
},
],
},
],
displayItems: [
{
label: 'Display item',
amount: {currency: 'CNY', value: '0.04'},
},
],
shippingOptions: [
{
id: 'freeShippingOption',
label: 'express global shipping',
amount: {
currency: 'USD',
value: '0',
},
selected: true,
}
],
});
});
const response = await test_driver.bless('showing a payment sheet', () =>
request.show()
);
const complete_promise = response.complete('success');
const changeShippingOptionReturned =
response.details.changeShippingOptionReturned;
assert_equals(changeShippingOptionReturned.total.currency, 'GBP');
assert_equals(changeShippingOptionReturned.total.value, '0.02');
assert_equals(changeShippingOptionReturned.total.label, undefined);
assert_equals(changeShippingOptionReturned.error, 'Error for test');
assert_equals(changeShippingOptionReturned.modifiers.length, 1);
assert_equals(changeShippingOptionReturned.displayItems, undefined);
assert_equals(changeShippingOptionReturned.shippingOptions.length, 1);
assert_equals(changeShippingOptionReturned.paymentMethodErrors, undefined);
assert_equals(changeShippingOptionReturned.shippingAddressErrors, undefined);
const shipping_option = changeShippingOptionReturned.shippingOptions[0];
assert_equals(shipping_option.id, 'freeShippingOption' );
assert_equals(shipping_option.label, 'express global shipping');
assert_equals(shipping_option.amount.currency, 'USD');
assert_equals(shipping_option.amount.value, '0');
assert_true(shipping_option.selected);
const modifier = changeShippingOptionReturned.modifiers[0];
assert_equals(modifier.supportedMethods, methodName);
assert_equals(modifier.data.soup, 'potato');
assert_equals(modifier.total.label, '');
assert_equals(modifier.total.amount.currency, 'EUR');
assert_equals(modifier.total.amount.value, '0.03');
assert_equals(modifier.additionalDisplayItems, undefined);
return complete_promise;
}, 'The changeShippingOption() returns some details from the "shippingoptionchange" event\'s updateWith(details) call.');
}
registerAndActiveServiceWorker(
'app-change-shipping-option.js',
'payment-app/',
runTests
);
</script>
......@@ -1061,6 +1061,8 @@ interface PaymentRequestEvent : ExtendableEvent
getter topOrigin
getter total
method changePaymentMethod
method changeShippingAddress
method changeShippingOption
method constructor
method openWindow
method respondWith
......
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