| // 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.text.TextUtils; |
| |
| import androidx.annotation.Nullable; |
| import androidx.collection.ArrayMap; |
| |
| import org.chromium.base.metrics.RecordHistogram; |
| import org.chromium.chrome.R; |
| import org.chromium.chrome.browser.app.ChromeActivity; |
| import org.chromium.chrome.browser.autofill.PersonalDataManager; |
| import org.chromium.chrome.browser.payments.handler.PaymentHandlerCoordinator; |
| import org.chromium.chrome.browser.payments.ui.PaymentRequestUI; |
| import org.chromium.chrome.browser.payments.ui.PaymentUiService; |
| import org.chromium.chrome.browser.payments.ui.SectionInformation; |
| import org.chromium.components.autofill.EditableOption; |
| import org.chromium.components.payments.AbortReason; |
| 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.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.PaymentRequestService; |
| import org.chromium.components.payments.PaymentRequestServiceUtil; |
| import org.chromium.components.payments.PaymentRequestSpec; |
| import org.chromium.components.payments.PaymentResponseHelperInterface; |
| import org.chromium.components.payments.Section; |
| 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.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; |
| |
| /** |
| * There are a few situations were the Payment Request can appear, from a code perspective, to |
| * be shown more than once. This boolean is used to make sure it is only logged once. |
| */ |
| private boolean mDidRecordShowEvent; |
| |
| /** A helper to manage the Skip-to-GPay experimental flow. */ |
| private SkipToGPayHelper mSkipToGPayHelper; |
| private boolean mIsGooglePayBridgeActivated; |
| |
| /** 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(); |
| } |
| |
| /** |
| * 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 = new PaymentUiService(/*delegate=*/this, |
| /*params=*/paymentRequestService, mWebContents, |
| paymentRequestService.isOffTheRecord(), mJourneyLogger, topLevelOrigin); |
| mPaymentRequestService = paymentRequestService; |
| if (PaymentRequestService.getNativeObserverForTest() != null) { |
| PaymentRequestService.getNativeObserverForTest().onPaymentUiServiceCreated( |
| mPaymentUiService); |
| } |
| } |
| |
| // 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 onQueryForQuotaCreated( |
| 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(new AndroidPaymentAppFactory(), androidFactoryId); |
| } |
| String swFactoryId = PaymentAppServiceBridge.class.getName(); |
| if (!service.containsFactory(swFactoryId)) { |
| service.addUniqueFactory(new PaymentAppServiceBridge(), swFactoryId); |
| } |
| |
| String autofillFactoryId = AutofillPaymentAppFactory.class.getName(); |
| if (!service.containsFactory(autofillFactoryId)) { |
| service.addUniqueFactory(new AutofillPaymentAppFactory(), autofillFactoryId); |
| } |
| if (AutofillPaymentAppFactory.canMakePayments(mSpec.getMethodData())) { |
| mPaymentUiService.setAutofillPaymentAppCreator( |
| AutofillPaymentAppFactory.createAppCreator( |
| /*delegate=*/delegate)); |
| } |
| } |
| |
| @Override |
| public boolean isShowingUi() { |
| return mPaymentUiService.isShowingUI(); |
| } |
| |
| // Implements BrowserPaymentRequest: |
| @Override |
| public String showAppSelector(boolean isShowWaitingForUpdatedDetails, PaymentItem total, |
| PaymentOptions paymentOptions, boolean isUserGestureShow) { |
| // Send AppListReady signal when all apps are created and request.show() is called. |
| if (PaymentRequestService.getNativeObserverForTest() != null) { |
| PaymentRequestService.getNativeObserverForTest().onAppListReady( |
| mPaymentUiService.getPaymentMethodsSection().getItems(), total); |
| } |
| // Calculate skip ui and build ui only after all payment apps are ready and |
| // request.show() is called. |
| mPaymentUiService.calculateWhetherShouldSkipShowingPaymentRequestUi(isUserGestureShow, |
| mDelegate.skipUiForBasicCard(), mSpec.getPaymentOptions(), |
| mSpec.getMethodData().keySet()); |
| ChromeActivity chromeActivity = ChromeActivity.fromWebContents(mWebContents); |
| if (chromeActivity == null) return ErrorStrings.ACTIVITY_NOT_FOUND; |
| String error = mPaymentUiService.buildPaymentRequestUI(chromeActivity, |
| /*isWebContentsActive=*/ |
| PaymentRequestServiceUtil.isWebContentsActive(mRenderFrameHost), |
| /*isShowWaitingForUpdatedDetails=*/isShowWaitingForUpdatedDetails); |
| if (error != null) return error; |
| if (!mPaymentUiService.shouldSkipShowingPaymentRequestUi() && mSkipToGPayHelper == null) { |
| mPaymentUiService.getPaymentRequestUI().show(isShowWaitingForUpdatedDetails); |
| } |
| return null; |
| } |
| |
| private void dimBackgroundIfNotBottomSheetPaymentHandler(PaymentApp selectedApp) { |
| // Putting isEnabled() last is intentional. It's to ensure not to confused the unexecuted |
| // group and the disabled in A/B testing. |
| if (selectedApp != null |
| && selectedApp.getPaymentAppType() == PaymentAppType.SERVICE_WORKER_APP |
| && PaymentHandlerCoordinator.isEnabled()) { |
| // When the Payment Handler (PH) UI is based on Activity, dimming the Payment |
| // Request (PR) UI does not dim the PH; when it's based on bottom-sheet, dimming |
| // the PR dims both UIs. 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.getPaymentRequestUI().dimBackground(); |
| } |
| |
| // Implements BrowserPaymentRequest: |
| @Override |
| public String triggerPaymentAppUiSkipIfApplicable(boolean isUserGestureShow) { |
| // If we are skipping showing the Payment Request UI, we should call into the payment app |
| // immediately after we determine the apps are ready and UI is shown. |
| if ((mPaymentUiService.shouldSkipShowingPaymentRequestUi() || mSkipToGPayHelper != null)) { |
| assert !mPaymentUiService.getPaymentMethodsSection().isEmpty(); |
| assert mPaymentUiService.getPaymentRequestUI() != null; |
| |
| if (isMinimalUiApplicable(isUserGestureShow)) { |
| ChromeActivity chromeActivity = ChromeActivity.fromWebContents(mWebContents); |
| if (chromeActivity == null) return ErrorStrings.ACTIVITY_NOT_FOUND; |
| if (mPaymentUiService.triggerMinimalUI(chromeActivity, mSpec.getRawTotal(), |
| this::onMinimalUIReady, this::onMinimalUiConfirmed, |
| /*dismissObserver=*/ |
| () |
| -> onUiAborted(AbortReason.ABORTED_BY_USER, |
| ErrorStrings.USER_CANCELLED))) { |
| mDidRecordShowEvent = true; |
| mJourneyLogger.setEventOccurred(Event.SHOWN); |
| return null; |
| } else { |
| return ErrorStrings.MINIMAL_UI_SUPPRESSED; |
| } |
| } |
| |
| assert !mPaymentUiService.getPaymentMethodsSection().isEmpty(); |
| PaymentApp selectedApp = |
| (PaymentApp) mPaymentUiService.getPaymentMethodsSection().getSelectedItem(); |
| dimBackgroundIfNotBottomSheetPaymentHandler(selectedApp); |
| mDidRecordShowEvent = true; |
| mJourneyLogger.setEventOccurred(Event.SKIPPED_SHOW); |
| assert mSpec.getRawTotal() != null; |
| // The total amount in details should be finalized at this point. So it is safe to |
| // record the triggered transaction amount. |
| mJourneyLogger.recordTransactionAmount(mSpec.getRawTotal().amount.currency, |
| mSpec.getRawTotal().amount.value, false /*completed*/); |
| invokePaymentApp(null /* selectedShippingAddress */, null /* selectedShippingOption */, |
| selectedApp); |
| } |
| 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.getPaymentMethodsSection() == null |
| || mPaymentUiService.getPaymentMethodsSection().getSize() != 1) { |
| return false; |
| } |
| |
| PaymentApp app = |
| (PaymentApp) mPaymentUiService.getPaymentMethodsSection().getSelectedItem(); |
| 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) { |
| mJourneyLogger.recordTransactionAmount(mSpec.getRawTotal().amount.currency, |
| mSpec.getRawTotal().amount.value, false /*completed*/); |
| app.disableShowingOwnUI(); |
| invokePaymentApp( |
| null /* selectedShippingAddress */, null /* selectedShippingOption */, app); |
| } |
| |
| // Implements BrowserPaymentRequest: |
| @Override |
| public void modifyMethodData(@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; |
| |
| if (mPaymentUiService.shouldShowShippingSection() |
| && (mPaymentUiService.getUiShippingOptions().isEmpty() |
| || !TextUtils.isEmpty(details.error)) |
| && mPaymentUiService.getShippingAddressesSection().getSelectedItem() != null) { |
| mPaymentUiService.getShippingAddressesSection().getSelectedItem().setInvalid(); |
| mPaymentUiService.getShippingAddressesSection().setSelectedItemIndex( |
| SectionInformation.INVALID_SELECTION); |
| mPaymentUiService.getShippingAddressesSection().setErrorMessage(details.error); |
| } |
| |
| boolean providedInformationToPaymentRequestUI = |
| mPaymentUiService.enableAndUpdatePaymentRequestUIWithPaymentInfo(); |
| if (providedInformationToPaymentRequestUI) recordShowEventAndTransactionAmount(); |
| } |
| |
| // Implements BrowserPaymentRequest: |
| @Override |
| public String continueShow(boolean isFinishedQueryingPaymentApps, boolean isUserGestureShow) { |
| ChromeActivity chromeActivity = ChromeActivity.fromWebContents(mWebContents); |
| if (chromeActivity == null) return ErrorStrings.ACTIVITY_NOT_FOUND; |
| |
| mPaymentUiService.updateDetailsOnPaymentRequestUI(mSpec.getPaymentDetails()); |
| |
| // Do not create shipping section When UI is not built yet. This happens when the show |
| // promise gets resolved before all apps are ready. |
| if (mPaymentUiService.getPaymentRequestUI() != null |
| && mPaymentUiService.shouldShowShippingSection()) { |
| mPaymentUiService.createShippingSectionForPaymentRequestUI(chromeActivity); |
| } |
| |
| // Triggered transaction amount gets recorded when both of the following conditions are met: |
| // 1- Either Event.Shown or Event.SKIPPED_SHOW bits are set showing that transaction is |
| // triggered (mDidRecordShowEvent == true). 2- The total amount in details won't change |
| // (mPaymentRequestService.isShowWaitingForUpdatedDetails() == false). Record the |
| // transaction amount only when the triggered condition is already met. Otherwise it will |
| // get recorded when triggered condition becomes true. |
| if (mDidRecordShowEvent) { |
| assert mSpec.getRawTotal() != null; |
| mJourneyLogger.recordTransactionAmount(mSpec.getRawTotal().amount.currency, |
| mSpec.getRawTotal().amount.value, false /*completed*/); |
| } |
| |
| if (isFinishedQueryingPaymentApps |
| && !mPaymentUiService.shouldSkipShowingPaymentRequestUi()) { |
| boolean providedInformationToPaymentRequestUI = |
| mPaymentUiService.enableAndUpdatePaymentRequestUIWithPaymentInfo(); |
| if (providedInformationToPaymentRequestUI) recordShowEventAndTransactionAmount(); |
| } |
| return null; |
| } |
| |
| // Implements BrowserPaymentRequest: |
| @Override |
| public void onPaymentDetailsNotUpdated(@Nullable String selectedShippingOptionError) { |
| if (mPaymentUiService.shouldShowShippingSection() |
| && (mPaymentUiService.getUiShippingOptions().isEmpty() |
| || !TextUtils.isEmpty(mSpec.selectedShippingOptionError())) |
| && mPaymentUiService.getShippingAddressesSection().getSelectedItem() != null) { |
| mPaymentUiService.getShippingAddressesSection().getSelectedItem().setInvalid(); |
| mPaymentUiService.getShippingAddressesSection().setSelectedItemIndex( |
| SectionInformation.INVALID_SELECTION); |
| mPaymentUiService.getShippingAddressesSection().setErrorMessage( |
| selectedShippingOptionError); |
| } |
| |
| boolean providedInformationToPaymentRequestUI = |
| mPaymentUiService.enableAndUpdatePaymentRequestUIWithPaymentInfo(); |
| if (providedInformationToPaymentRequestUI) recordShowEventAndTransactionAmount(); |
| } |
| |
| // Implements BrowserPaymentRequest: |
| @Override |
| public boolean parseAndValidateDetailsFurtherIfNeeded(PaymentDetails details) { |
| return mSkipToGPayHelper == null || mSkipToGPayHelper.setShippingOptionIfValid(details); |
| } |
| |
| // Implements PaymentUiService.Delegate: |
| @Override |
| public void recordShowEventAndTransactionAmount() { |
| if (mDidRecordShowEvent) return; |
| mDidRecordShowEvent = true; |
| mJourneyLogger.setEventOccurred(Event.SHOWN); |
| // Record the triggered transaction amount only when the total amount in details is |
| // finalized (i.e. mPaymentRequestService.isShowWaitingForUpdatedDetails() == false). |
| // Otherwise it will get recorded when the updated details become available. |
| if (mPaymentRequestService != null |
| && !mPaymentRequestService.isShowWaitingForUpdatedDetails()) { |
| assert mSpec.getRawTotal() != null; |
| mJourneyLogger.recordTransactionAmount(mSpec.getRawTotal().amount.currency, |
| mSpec.getRawTotal().amount.value, false /*completed*/); |
| } |
| } |
| |
| // Implements BrowserPaymentRequest: |
| @Override |
| public void onInstrumentDetailsLoading() { |
| if (mPaymentUiService.getPaymentRequestUI() == null) { |
| return; |
| } |
| |
| assert mPaymentUiService.getSelectedPaymentAppType() == PaymentAppType.AUTOFILL; |
| |
| mPaymentUiService.getPaymentRequestUI().showProcessingMessage(); |
| } |
| |
| // Implements PaymentUiService.Delegate: |
| @Override |
| public boolean invokePaymentApp(EditableOption selectedShippingAddress, |
| EditableOption selectedShippingOption, PaymentApp selectedPaymentApp) { |
| if (mPaymentRequestService == null || mSpec == null || mSpec.isDestroyed()) return false; |
| EditableOption selectedContact = mPaymentUiService.getContactSection() != null |
| ? mPaymentUiService.getContactSection().getSelectedItem() |
| : null; |
| 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, selectedContact, |
| selectedPaymentApp, mSpec.getPaymentOptions(), mSkipToGPayHelper != null); |
| mPaymentRequestService.invokePaymentApp(selectedPaymentApp, paymentResponseHelper); |
| return !selectedPaymentApp.isAutofillInstrument(); |
| } |
| |
| private PaymentHandlerHost getPaymentHandlerHost() { |
| if (mPaymentHandlerHost == null) { |
| mPaymentHandlerHost = |
| new PaymentHandlerHost(mWebContents, /*delegate=*/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 retry(PaymentValidationErrors errors) { |
| mWasRetryCalled = true; |
| mPaymentUiService.onRetry(errors); |
| } |
| |
| // Implements BrowserPaymentRequest: |
| @Override |
| public void close() { |
| if (mHasClosed) return; |
| mHasClosed = true; |
| |
| if (mPaymentRequestService != null) { |
| mPaymentRequestService.close(); |
| mPaymentRequestService = null; |
| } |
| |
| mPaymentUiService.close(); |
| SettingsAutofillAndPaymentsObserver.getInstance().unregisterObserver(mPaymentUiService); |
| |
| 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.rankPaymentAppsForPaymentRequestUI(pendingApps); |
| |
| // Possibly pre-select the first app on the list. |
| int selection = !pendingApps.isEmpty() && pendingApps.get(0).canPreselect() |
| ? 0 |
| : SectionInformation.NO_SELECTION; |
| |
| // The list of payment apps is ready to display. |
| mPaymentUiService.setPaymentMethodsSection( |
| new SectionInformation(PaymentRequestUI.DataType.PAYMENT_METHODS, selection, |
| new ArrayList<>(pendingApps))); |
| |
| // Record the number suggested payment methods and whether at least one of them was |
| // complete. |
| mJourneyLogger.setNumberOfSuggestionsShown(Section.PAYMENT_METHOD, pendingApps.size(), |
| !pendingApps.isEmpty() && pendingApps.get(0).isComplete()); |
| |
| int missingFields = 0; |
| if (pendingApps.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 if (pendingApps.get(0).isAutofillInstrument()) { |
| missingFields = ((AutofillPaymentInstrument) (pendingApps.get(0))).getMissingFields(); |
| } |
| if (missingFields != 0) { |
| RecordHistogram.recordSparseHistogram( |
| "PaymentRequest.MissingPaymentFields", missingFields); |
| } |
| |
| mPaymentUiService.updateAppModifiedTotals(); |
| |
| SettingsAutofillAndPaymentsObserver.getInstance().registerObserver(mPaymentUiService); |
| } |
| |
| // 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 method was an Autofill credit card with an identifier, record its use. |
| PaymentApp selectedPaymentMethod = |
| (PaymentApp) mPaymentUiService.getPaymentMethodsSection().getSelectedItem(); |
| if (selectedPaymentMethod != null |
| && selectedPaymentMethod.getPaymentAppType() == PaymentAppType.AUTOFILL |
| && !selectedPaymentMethod.getIdentifier().isEmpty()) { |
| PersonalDataManager.getInstance().recordAndLogCreditCardUse( |
| selectedPaymentMethod.getIdentifier()); |
| } |
| |
| // Showing the payment request UI if we were previously skipping it so the loading |
| // spinner shows up until the merchant notifies that payment was completed. |
| if (mPaymentUiService.shouldSkipShowingPaymentRequestUi() |
| && mPaymentUiService.getPaymentRequestUI() != null) { |
| mPaymentUiService.getPaymentRequestUI().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.getMinimalUI() != null) { |
| mJourneyLogger.setAborted(AbortReason.ABORTED_BY_USER); |
| mPaymentUiService.getMinimalUI().showErrorAndClose( |
| /*observer=*/this::close, R.string.payments_error_message); |
| return; |
| } |
| |
| // When skipping UI, any errors/cancel from fetching payment details should abort payment. |
| if (mPaymentUiService.shouldSkipShowingPaymentRequestUi()) { |
| assert !TextUtils.isEmpty(errorMessage); |
| mJourneyLogger.setAborted(AbortReason.ABORTED_BY_USER); |
| disconnectFromClientWithDebugMessage(errorMessage); |
| } else { |
| mPaymentUiService.getPaymentRequestUI().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); |
| } |
| } |