blob: 8a43c46899f116af18a77829b44945582fbdee01 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.payments;
import android.content.Context;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import androidx.collection.ArrayMap;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.browser.app.ChromeActivity;
import org.chromium.chrome.browser.autofill.PersonalDataManager;
import org.chromium.chrome.browser.payments.ui.PaymentUiService;
import org.chromium.components.autofill.EditableOption;
import org.chromium.components.payments.AbortReason;
import org.chromium.components.payments.AndroidPaymentApp;
import org.chromium.components.payments.BrowserPaymentRequest;
import org.chromium.components.payments.ErrorStrings;
import org.chromium.components.payments.Event;
import org.chromium.components.payments.JourneyLogger;
import org.chromium.components.payments.MethodStrings;
import org.chromium.components.payments.PackageManagerDelegate;
import org.chromium.components.payments.PaymentApp;
import org.chromium.components.payments.PaymentAppFactoryDelegate;
import org.chromium.components.payments.PaymentAppFactoryInterface;
import org.chromium.components.payments.PaymentAppService;
import org.chromium.components.payments.PaymentAppType;
import org.chromium.components.payments.PaymentDetailsUpdateServiceHelper;
import org.chromium.components.payments.PaymentFeatureList;
import org.chromium.components.payments.PaymentHandlerHost;
import org.chromium.components.payments.PaymentOptionsUtils;
import org.chromium.components.payments.PaymentRequestParams;
import org.chromium.components.payments.PaymentRequestService;
import org.chromium.components.payments.PaymentRequestServiceUtil;
import org.chromium.components.payments.PaymentRequestSpec;
import org.chromium.components.payments.PaymentRequestUpdateEventListener;
import org.chromium.components.payments.PaymentResponseHelperInterface;
import org.chromium.components.payments.SkipToGPayHelper;
import org.chromium.content_public.browser.RenderFrameHost;
import org.chromium.content_public.browser.WebContents;
import org.chromium.payments.mojom.PayerDetail;
import org.chromium.payments.mojom.PaymentAddress;
import org.chromium.payments.mojom.PaymentComplete;
import org.chromium.payments.mojom.PaymentDetails;
import org.chromium.payments.mojom.PaymentErrorReason;
import org.chromium.payments.mojom.PaymentItem;
import org.chromium.payments.mojom.PaymentMethodData;
import org.chromium.payments.mojom.PaymentOptions;
import org.chromium.payments.mojom.PaymentRequest;
import org.chromium.payments.mojom.PaymentResponse;
import org.chromium.payments.mojom.PaymentValidationErrors;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.url.GURL;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* This is the Clank specific parts of {@link PaymentRequest}, with the parts shared with WebLayer
* living in {@link PaymentRequestService}.
*/
public class ChromePaymentRequestService
implements BrowserPaymentRequest, PaymentUiService.Delegate {
// Null-check is necessary because retainers of ChromePaymentRequestService could still
// reference ChromePaymentRequestService after mPaymentRequestService is set null, e.g.,
// crbug.com/1122148.
@Nullable
private PaymentRequestService mPaymentRequestService;
private final RenderFrameHost mRenderFrameHost;
private final Delegate mDelegate;
private final WebContents mWebContents;
private final JourneyLogger mJourneyLogger;
private final PaymentUiService mPaymentUiService;
private boolean mWasRetryCalled;
private boolean mHasClosed;
private PaymentRequestSpec mSpec;
private boolean mHideServerAutofillCards;
private PaymentHandlerHost mPaymentHandlerHost;
/** A helper to manage the Skip-to-GPay experimental flow. */
private SkipToGPayHelper mSkipToGPayHelper;
private boolean mIsGooglePayBridgeActivated;
/**
* True if the browser has skipped showing the app selector UI (PaymentRequest UI).
*
* <p>In cases where there is a single payment app and the merchant does not request shipping
* or billing, the browser can skip showing UI as the app selector UI is not benefiting the user
* at all.
*/
private boolean mHasSkippedAppSelector;
/** The delegate of this class */
public interface Delegate extends PaymentRequestService.Delegate {
/**
* @return True if the UI can be skipped for "basic-card" scenarios. This will only ever be
* true in tests.
*/
boolean skipUiForBasicCard();
/**
* Create PaymentUiService.
* @param delegate The delegate of this instance.
* @param webContents The WebContents of the merchant page.
* @param isOffTheRecord Whether merchant page is in an isOffTheRecord tab.
* @param journeyLogger The logger of the user journey.
* @param topLevelOrigin The last committed url of webContents.
*/
default PaymentUiService createPaymentUiService(PaymentUiService.Delegate delegate,
PaymentRequestParams params, WebContents webContents, boolean isOffTheRecord,
JourneyLogger journeyLogger, String topLevelOrigin) {
return new PaymentUiService(/*delegate=*/delegate,
/*params=*/params, webContents, isOffTheRecord, journeyLogger, topLevelOrigin);
}
/**
* Looks up the Chrome activity of the given web contents. This can be null. Should never be
* cached, because web contents can change activities, e.g., when user selects "Open in
* Chrome" menu item.
*
* @param webContents The web contents for which to lookup the Chrome activity.
* @return Possibly null Chrome activity that should never be cached.
*/
@Nullable
default ChromeActivity getChromeActivity(WebContents webContents) {
return ChromeActivity.fromWebContents(webContents);
}
/**
* Creates an instance of Android payment app factory.
* @return The instance, can be null for testing.
*/
@Nullable
default PaymentAppFactoryInterface createAndroidPaymentAppFactory() {
return new AndroidPaymentAppFactory();
}
/**
* Creates an instance of service-worker payment app factory.
* @return The instance, can be null for testing.
*/
@Nullable
default PaymentAppFactoryInterface createServiceWorkerPaymentAppFactory() {
return new PaymentAppServiceBridge();
}
/**
* Creates an instance of Autofill payment app factory.
* @return The instance, can be null for testing.
*/
@Nullable
default PaymentAppFactoryInterface createAutofillPaymentAppFactory() {
return new AutofillPaymentAppFactory();
}
/**
* Whether an autofill transaction is allowed to be made.
* @return The instance, can be null for testing.
*/
default boolean canMakeAutofillPayment(Map<String, PaymentMethodData> methodData) {
return AutofillPaymentAppFactory.canMakePayments(methodData);
}
/**
* @param renderFrameHost The frame that issues the payment request.
* @return Whether the WebContents of the merchant frame is alive and visible.
*/
default boolean isWebContentsActive(RenderFrameHost renderFrameHost) {
return PaymentRequestServiceUtil.isWebContentsActive(renderFrameHost);
}
/**
* Creates an instance of PaymentHandlerHost.
* @param webContents The WebContents that issues the payment request.
* @param listener The listener to payment method, shipping address, and shipping option
* change events
* @return The instance.
*/
default PaymentHandlerHost createPaymentHandlerHost(
WebContents webContents, PaymentRequestUpdateEventListener listener) {
return new PaymentHandlerHost(webContents, listener);
}
}
/**
* Builds the PaymentRequest service implementation.
*
* @param paymentRequestService The component side of the PaymentRequest implementation.
* @param delegate The delegate of this class.
*/
public ChromePaymentRequestService(
PaymentRequestService paymentRequestService, Delegate delegate) {
assert paymentRequestService != null;
assert delegate != null;
mPaymentRequestService = paymentRequestService;
mRenderFrameHost = paymentRequestService.getRenderFrameHost();
assert mRenderFrameHost != null;
mDelegate = delegate;
mWebContents = paymentRequestService.getWebContents();
mJourneyLogger = paymentRequestService.getJourneyLogger();
String topLevelOrigin = paymentRequestService.getTopLevelOrigin();
assert topLevelOrigin != null;
mPaymentUiService = mDelegate.createPaymentUiService(/*delegate=*/this,
/*params=*/paymentRequestService, mWebContents,
paymentRequestService.isOffTheRecord(), mJourneyLogger, topLevelOrigin);
mPaymentRequestService = paymentRequestService;
if (PaymentRequestService.getNativeObserverForTest() != null) {
PaymentRequestService.getNativeObserverForTest().onPaymentUiServiceCreated(
mPaymentUiService);
}
}
// Implements BrowserPaymentRequest:
@Override
public PaymentApp getSelectedPaymentApp() {
return mPaymentUiService.getSelectedPaymentApp();
}
// Implements BrowserPaymentRequest:
@Override
public List<PaymentApp> getPaymentApps() {
return mPaymentUiService.getPaymentApps();
}
// Implements BrowserPaymentRequest:
@Override
public boolean hasAnyCompleteApp() {
return mPaymentUiService.hasAnyCompleteAppSuggestion();
}
// Implements BrowserPaymentRequest:
@Override
public void onWhetherGooglePayBridgeEligible(boolean googlePayBridgeEligible,
WebContents webContents, PaymentMethodData[] rawMethodData) {
mIsGooglePayBridgeActivated = googlePayBridgeEligible
&& SkipToGPayHelperUtil.canActivateExperiment(mWebContents, rawMethodData);
}
// Implements BrowserPaymentRequest:
@Override
public void onSpecValidated(PaymentRequestSpec spec) {
mSpec = spec;
mPaymentUiService.initialize(mSpec.getPaymentDetails());
}
// Implements BrowserPaymentRequest:
@Override
public boolean disconnectIfExtraValidationFails(WebContents webContents,
Map<String, PaymentMethodData> methodData, PaymentDetails details,
PaymentOptions options) {
assert methodData != null;
assert details != null;
if (mIsGooglePayBridgeActivated) {
PaymentMethodData data = methodData.get(MethodStrings.GOOGLE_PAY);
mSkipToGPayHelper = new SkipToGPayHelper(options, data.gpayBridgeData);
}
if (!parseAndValidateDetailsFurtherIfNeeded(details)) {
mJourneyLogger.setAborted(AbortReason.INVALID_DATA_FROM_RENDERER);
disconnectFromClientWithDebugMessage(ErrorStrings.INVALID_PAYMENT_DETAILS);
return true;
}
return false;
}
// Implements BrowserPaymentRequest:
@Override
public void modifyQueryForQuotaCreatedIfNeeded(
Map<String, PaymentMethodData> queryForQuota, PaymentOptions paymentOptions) {
if (queryForQuota.containsKey(MethodStrings.BASIC_CARD)
&& PaymentFeatureList.isEnabledOrExperimentalFeaturesEnabled(
PaymentFeatureList.STRICT_HAS_ENROLLED_AUTOFILL_INSTRUMENT)) {
PaymentMethodData paymentMethodData = new PaymentMethodData();
paymentMethodData.stringifiedData =
PaymentOptionsUtils.stringifyRequestedInformation(paymentOptions);
queryForQuota.put("basic-card-payment-options", paymentMethodData);
}
}
// Implements BrowserPaymentRequest:
@Override
public void addPaymentAppFactories(
PaymentAppService service, PaymentAppFactoryDelegate delegate) {
String androidFactoryId = AndroidPaymentAppFactory.class.getName();
if (!service.containsFactory(androidFactoryId)) {
service.addUniqueFactory(mDelegate.createAndroidPaymentAppFactory(), androidFactoryId);
}
String swFactoryId = PaymentAppServiceBridge.class.getName();
if (!service.containsFactory(swFactoryId)) {
service.addUniqueFactory(mDelegate.createServiceWorkerPaymentAppFactory(), swFactoryId);
}
String autofillFactoryId = AutofillPaymentAppFactory.class.getName();
if (!service.containsFactory(autofillFactoryId)) {
service.addUniqueFactory(
mDelegate.createAutofillPaymentAppFactory(), autofillFactoryId);
}
if (mDelegate.canMakeAutofillPayment(mSpec.getMethodData())) {
mPaymentUiService.setAutofillPaymentAppCreator(
AutofillPaymentAppFactory.createAppCreator(
/*delegate=*/delegate));
}
}
// Implements BrowserPaymentRequest:
@Override
public String showOrSkipAppSelector(boolean isShowWaitingForUpdatedDetails, PaymentItem total,
boolean shouldSkipAppSelector) {
ChromeActivity chromeActivity = mDelegate.getChromeActivity(mWebContents);
if (chromeActivity == null) return ErrorStrings.ACTIVITY_NOT_FOUND;
String error = mPaymentUiService.buildPaymentRequestUI(chromeActivity,
/*isWebContentsActive=*/mDelegate.isWebContentsActive(mRenderFrameHost),
/*isShowWaitingForUpdatedDetails=*/isShowWaitingForUpdatedDetails);
if (error != null) return error;
// Calculate skip ui and build ui only after all payment apps are ready and
// request.show() is called.
boolean urlPaymentMethodIdentifiersSupported =
PaymentRequestService.isUrlPaymentMethodIdentifiersSupported(
mSpec.getMethodData().keySet());
// Only allowing payment apps that own their own UIs.
// This excludes AutofillPaymentInstrument as its UI is rendered inline in
// the app selector UI, thus can't be skipped.
if (!urlPaymentMethodIdentifiersSupported && !mDelegate.skipUiForBasicCard()) {
shouldSkipAppSelector = false;
}
if (mSkipToGPayHelper != null) shouldSkipAppSelector = true;
if (shouldSkipAppSelector) {
mHasSkippedAppSelector = true;
} else {
mPaymentUiService.showAppSelector(isShowWaitingForUpdatedDetails);
}
return null;
}
private void dimBackgroundIfNotPaymentHandler(PaymentApp selectedApp) {
if (selectedApp != null
&& selectedApp.getPaymentAppType() == PaymentAppType.SERVICE_WORKER_APP) {
// As bottom-sheet itself has dimming effect, dimming PR is unnecessary for the
// bottom-sheet PH. For now, service worker based payment apps are the only ones that
// can open the bottom-sheet.
return;
}
mPaymentUiService.dimBackground();
}
// Implements BrowserPaymentRequest:
@Override
public String onShowCalledAndAppsQueriedAndDetailsFinalized(boolean isUserGestureShow) {
WindowAndroid windowAndroid = mDelegate.getWindowAndroid(mRenderFrameHost);
if (windowAndroid == null) return ErrorStrings.WINDOW_NOT_FOUND;
Context context = mDelegate.getContext(mRenderFrameHost);
if (context == null) return ErrorStrings.CONTEXT_NOT_FOUND;
// If we are skipping showing the app selector UI, we should call into the payment app
// immediately after we determine the apps are ready and UI is shown.
if (mHasSkippedAppSelector) {
assert !mPaymentUiService.getPaymentApps().isEmpty();
if (isMinimalUiApplicable(isUserGestureShow)) {
if (mPaymentUiService.triggerMinimalUI(windowAndroid, mSpec.getRawTotal(),
this::onMinimalUIReady, this::onMinimalUiConfirmed,
/*dismissObserver=*/
()
-> onUiAborted(AbortReason.ABORTED_BY_USER,
ErrorStrings.USER_CANCELLED))) {
mJourneyLogger.setEventOccurred(Event.SHOWN);
return null;
} else {
return ErrorStrings.MINIMAL_UI_SUPPRESSED;
}
}
assert !mPaymentUiService.getPaymentApps().isEmpty();
PaymentApp selectedApp = mPaymentUiService.getSelectedPaymentApp();
dimBackgroundIfNotPaymentHandler(selectedApp);
mJourneyLogger.setEventOccurred(Event.SKIPPED_SHOW);
invokePaymentApp(null /* selectedShippingAddress */, null /* selectedShippingOption */,
selectedApp);
} else {
mPaymentUiService.createShippingSectionIfNeeded(context);
}
return null;
}
/**
* @param isUserGestureShow Whether PaymentRequest.show() was invoked with a user gesture.
* @return Whether the minimal UI should be shown.
*/
private boolean isMinimalUiApplicable(boolean isUserGestureShow) {
if (!isUserGestureShow || mPaymentUiService.getPaymentApps().size() != 1) {
return false;
}
PaymentApp app = mPaymentUiService.getSelectedPaymentApp();
if (app == null || !app.isReadyForMinimalUI() || TextUtils.isEmpty(app.accountBalance())) {
return false;
}
return PaymentFeatureList.isEnabled(PaymentFeatureList.WEB_PAYMENTS_MINIMAL_UI);
}
private void onMinimalUIReady() {
if (PaymentRequestService.getNativeObserverForTest() != null) {
PaymentRequestService.getNativeObserverForTest().onMinimalUIReady();
}
}
private void onMinimalUiConfirmed(PaymentApp app) {
app.disableShowingOwnUI();
invokePaymentApp(
null /* selectedShippingAddress */, null /* selectedShippingOption */, app);
}
// Implements BrowserPaymentRequest:
@Override
public void modifyMethodDataIfNeeded(@Nullable Map<String, PaymentMethodData> methodDataMap) {
if (!mIsGooglePayBridgeActivated || methodDataMap == null) return;
Map<String, PaymentMethodData> result = new ArrayMap<>();
for (PaymentMethodData methodData : methodDataMap.values()) {
String method = methodData.supportedMethod;
assert !TextUtils.isEmpty(method);
// If skip-to-GPay flow is activated, ignore all other payment methods, which can be
// either "basic-card" or "https://android.com/pay". The latter is safe to ignore
// because merchant has already requested Google Pay.
if (!method.equals(MethodStrings.GOOGLE_PAY)) continue;
if (methodData.gpayBridgeData != null
&& !methodData.gpayBridgeData.stringifiedData.isEmpty()) {
methodData.stringifiedData = methodData.gpayBridgeData.stringifiedData;
}
result.put(method, methodData);
}
methodDataMap.clear();
methodDataMap.putAll(result);
}
// Implements BrowserPaymentRequest:
@Override
@Nullable
public WebContents openPaymentHandlerWindow(
GURL url, boolean isOffTheRecord, long ukmSourceId) {
@Nullable
WebContents paymentHandlerWebContents =
mPaymentUiService.showPaymentHandlerUI(url, isOffTheRecord);
if (paymentHandlerWebContents != null) {
ServiceWorkerPaymentAppBridge.onOpeningPaymentAppWindow(
/*paymentRequestWebContents=*/mWebContents,
/*paymentHandlerWebContents=*/paymentHandlerWebContents);
// UKM for payment app origin should get recorded only when the origin of the invoked
// payment app is shown to the user.
mJourneyLogger.setPaymentAppUkmSourceId(ukmSourceId);
}
return paymentHandlerWebContents;
}
// Implements BrowserPaymentRequest:
@Override
public void onPaymentDetailsUpdated(
PaymentDetails details, boolean hasNotifiedInvokedPaymentApp) {
mPaymentUiService.updateDetailsOnPaymentRequestUI(details);
if (hasNotifiedInvokedPaymentApp) return;
mPaymentUiService.showShippingAddressErrorIfApplicable(details.error);
mPaymentUiService.enableAndUpdatePaymentRequestUIWithPaymentInfo();
}
// Implements BrowserPaymentRequest:
@Override
public String continueShowWithUpdatedDetails(
PaymentDetails details, boolean isFinishedQueryingPaymentApps) {
Context context = mDelegate.getContext(mRenderFrameHost);
if (context == null) return ErrorStrings.CONTEXT_NOT_FOUND;
mPaymentUiService.updateDetailsOnPaymentRequestUI(details);
if (isFinishedQueryingPaymentApps && !mHasSkippedAppSelector) {
mPaymentUiService.enableAndUpdatePaymentRequestUIWithPaymentInfo();
}
return null;
}
// Implements BrowserPaymentRequest:
@Override
public void onPaymentDetailsNotUpdated(@Nullable String selectedShippingOptionError) {
mPaymentUiService.showShippingAddressErrorIfApplicable(selectedShippingOptionError);
mPaymentUiService.enableAndUpdatePaymentRequestUIWithPaymentInfo();
}
// Implements BrowserPaymentRequest:
@Override
public boolean parseAndValidateDetailsFurtherIfNeeded(PaymentDetails details) {
return mSkipToGPayHelper == null || mSkipToGPayHelper.setShippingOptionIfValid(details);
}
// Implements BrowserPaymentRequest:
@Override
public void onInstrumentDetailsLoading() {
assert mPaymentUiService.getSelectedPaymentApp() == null
|| mPaymentUiService.getSelectedPaymentApp().getPaymentAppType()
== PaymentAppType.AUTOFILL;
mPaymentUiService.showProcessingMessage();
}
// Implements PaymentUiService.Delegate:
@Override
public boolean invokePaymentApp(EditableOption selectedShippingAddress,
EditableOption selectedShippingOption, PaymentApp selectedPaymentApp) {
if (mPaymentRequestService == null || mSpec == null || mSpec.isDestroyed()) return false;
selectedPaymentApp.setPaymentHandlerHost(getPaymentHandlerHost());
// Only native apps can use PaymentDetailsUpdateService.
if (selectedPaymentApp.getPaymentAppType() == PaymentAppType.NATIVE_MOBILE_APP) {
PaymentDetailsUpdateServiceHelper.getInstance().initialize(new PackageManagerDelegate(),
((AndroidPaymentApp) selectedPaymentApp).packageName(),
mPaymentRequestService /* PaymentApp.PaymentRequestUpdateEventListener */);
}
PaymentResponseHelperInterface paymentResponseHelper =
new ChromePaymentResponseHelper(selectedShippingAddress, selectedShippingOption,
mPaymentUiService.getSelectedContact(), selectedPaymentApp,
mSpec.getPaymentOptions(), mSkipToGPayHelper != null);
mPaymentRequestService.invokePaymentApp(selectedPaymentApp, paymentResponseHelper);
return !selectedPaymentApp.isAutofillInstrument();
}
private PaymentHandlerHost getPaymentHandlerHost() {
if (mPaymentHandlerHost == null) {
mPaymentHandlerHost = mDelegate.createPaymentHandlerHost(
mWebContents, /*listener=*/mPaymentRequestService);
}
return mPaymentHandlerHost;
}
// Implements PaymentUiService.Delegate:
@Override
public boolean wasRetryCalled() {
return mWasRetryCalled;
}
// Implements PaymentUiService.Delegate:
@Override
public void onUiAborted(@AbortReason int reason, String debugMessage) {
mJourneyLogger.setAborted(reason);
disconnectFromClientWithDebugMessage(debugMessage);
}
private void disconnectFromClientWithDebugMessage(String debugMessage) {
if (mPaymentRequestService != null) {
mPaymentRequestService.disconnectFromClientWithDebugMessage(
debugMessage, PaymentErrorReason.USER_CANCEL);
}
close();
}
// Implements BrowserPaymentRequest:
@Override
public void complete(int result, Runnable onCompleteHandled) {
if (result != PaymentComplete.FAIL && !PaymentPreferencesUtil.isPaymentCompleteOnce()) {
PaymentPreferencesUtil.setPaymentCompleteOnce();
}
mPaymentUiService.onPaymentRequestComplete(result,
/*onMinimalUiErroredAndClosed=*/this::close, onCompleteHandled);
}
// Implements BrowserPaymentRequest:
@Override
public void onRetry(PaymentValidationErrors errors) {
mWasRetryCalled = true;
Context context = mDelegate.getContext(mRenderFrameHost);
if (context == null) {
disconnectFromClientWithDebugMessage(ErrorStrings.CONTEXT_NOT_FOUND);
return;
}
mPaymentUiService.onRetry(context, errors);
}
// Implements BrowserPaymentRequest:
@Override
public void close() {
if (mHasClosed) return;
mHasClosed = true;
if (mPaymentRequestService != null) {
mPaymentRequestService.close();
mPaymentRequestService = null;
}
mPaymentUiService.close();
if (mPaymentHandlerHost != null) {
mPaymentHandlerHost.destroy();
mPaymentHandlerHost = null;
}
PaymentDetailsUpdateServiceHelper.getInstance().reset();
}
// Implements BrowserPaymentRequest:
@Override
public void onPaymentAppCreated(PaymentApp paymentApp) {
mHideServerAutofillCards |= paymentApp.isServerAutofillInstrumentReplacement();
paymentApp.setHaveRequestedAutofillData(mPaymentUiService.haveRequestedAutofillData());
}
// Implements BrowserPaymentRequest:
@Override
public void notifyPaymentUiOfPendingApps(List<PaymentApp> pendingApps) {
if (mHideServerAutofillCards) {
List<PaymentApp> nonServerAutofillCards = new ArrayList<>();
int numberOfPendingApps = pendingApps.size();
for (int i = 0; i < numberOfPendingApps; i++) {
if (!pendingApps.get(i).isServerAutofillInstrument()) {
nonServerAutofillCards.add(pendingApps.get(i));
}
}
pendingApps = nonServerAutofillCards;
}
// Load the validation rules for each unique region code in the credit card billing
// addresses and check for validity.
Set<String> uniqueCountryCodes = new HashSet<>();
for (int i = 0; i < pendingApps.size(); ++i) {
@Nullable
String countryCode = pendingApps.get(i).getCountryCode();
if (countryCode != null && !uniqueCountryCodes.contains(countryCode)) {
uniqueCountryCodes.add(countryCode);
PersonalDataManager.getInstance().loadRulesForAddressNormalization(countryCode);
}
}
mPaymentUiService.setPaymentApps(pendingApps);
int missingFields = 0;
if (mPaymentUiService.getPaymentApps().isEmpty()) {
if (mPaymentUiService.merchantSupportsAutofillCards()) {
// Record all fields if basic-card is supported but no card exists.
missingFields = AutofillPaymentInstrument.CompletionStatus.CREDIT_CARD_EXPIRED
| AutofillPaymentInstrument.CompletionStatus.CREDIT_CARD_NO_CARDHOLDER
| AutofillPaymentInstrument.CompletionStatus.CREDIT_CARD_NO_NUMBER
| AutofillPaymentInstrument.CompletionStatus.CREDIT_CARD_NO_BILLING_ADDRESS;
}
} else {
PaymentApp firstApp = mPaymentUiService.getPaymentApps().get(0);
if (firstApp.isAutofillInstrument()) {
missingFields = ((AutofillPaymentInstrument) (firstApp)).getMissingFields();
}
}
if (missingFields != 0) {
RecordHistogram.recordSparseHistogram(
"PaymentRequest.MissingPaymentFields", missingFields);
}
}
// Implements BrowserPaymentRequest:
@Override
public boolean hasAvailableApps() {
return mPaymentUiService.hasAvailableApps();
}
// Implements BrowserPaymentRequest:
@Override
public boolean isPaymentSheetBasedPaymentAppSupported() {
return mPaymentUiService.canUserAddCreditCard();
}
// Implements BrowserPaymentRequest:
@Override
public void onInstrumentDetailsReady() {
// If the payment app was an Autofill credit card with an identifier, record its use.
PaymentApp selectedPaymentApp = mPaymentUiService.getSelectedPaymentApp();
if (selectedPaymentApp != null
&& selectedPaymentApp.getPaymentAppType() == PaymentAppType.AUTOFILL
&& !selectedPaymentApp.getIdentifier().isEmpty()) {
PersonalDataManager.getInstance().recordAndLogCreditCardUse(
selectedPaymentApp.getIdentifier());
}
// Showing the app selector UI if we were previously skipping it so the loading
// spinner shows up until the merchant notifies that payment was completed.
if (mHasSkippedAppSelector) {
mPaymentUiService.showProcessingMessageAfterUiSkip();
}
}
// Implements BrowserPaymentRequest:
@Override
public boolean patchPaymentResponseIfNeeded(PaymentResponse response) {
return mSkipToGPayHelper == null || mSkipToGPayHelper.patchPaymentResponse(response);
}
// Implements BrowserPaymentRequest:
@Override
public void onInstrumentDetailsError(String errorMessage) {
if (mPaymentUiService.isShowingMinimalUi()) {
mJourneyLogger.setAborted(AbortReason.ABORTED_BY_USER);
mPaymentUiService.closeMinimalUiOnError(this::close);
return;
}
// When skipping UI, any errors/cancel from fetching payment details should abort payment.
if (mHasSkippedAppSelector) {
assert !TextUtils.isEmpty(errorMessage);
mJourneyLogger.setAborted(AbortReason.ABORTED_BY_USER);
disconnectFromClientWithDebugMessage(errorMessage);
} else {
mPaymentUiService.onPayButtonProcessingCancelled();
PaymentDetailsUpdateServiceHelper.getInstance().reset();
}
}
// Implement PaymentUiService.Delegate:
@Override
public void dispatchPayerDetailChangeEventIfNeeded(PayerDetail detail) {
if (mPaymentRequestService == null || !mWasRetryCalled) return;
mPaymentRequestService.onPayerDetailChange(detail);
}
// Implement PaymentUiService.Delegate:
@Override
public void onPaymentRequestUIFaviconNotAvailable() {
if (mPaymentRequestService == null) return;
mPaymentRequestService.warnNoFavicon();
}
// Implement PaymentUiService.Delegate:
@Override
public void onShippingOptionChange(String optionId) {
if (mPaymentRequestService == null) return;
mPaymentRequestService.onShippingOptionChange(optionId);
}
// Implement PaymentUiService.Delegate:
@Override
public void onLeavingCurrentTab(String reason) {
mJourneyLogger.setAborted(AbortReason.ABORTED_BY_USER);
disconnectFromClientWithDebugMessage(reason);
}
// Implement PaymentUiService.Delegate:
@Override
public void onUiServiceError(String error) {
mJourneyLogger.setAborted(AbortReason.OTHER);
disconnectFromClientWithDebugMessage(error);
if (PaymentRequestService.getObserverForTest() != null) {
PaymentRequestService.getObserverForTest().onPaymentRequestServiceShowFailed();
}
}
// Implement PaymentUiService.Delegate:
@Override
public void onShippingAddressChange(PaymentAddress address) {
if (mPaymentRequestService == null) return;
// This updates the line items and the shipping options asynchronously.
mPaymentRequestService.onShippingAddressChange(address);
}
// Implement PaymentUiService.Delegate:
@Override
@Nullable
public Context getContext() {
return mDelegate.getContext(mRenderFrameHost);
}
}