| // Copyright 2018 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.autofill_assistant; |
| |
| import android.content.Context; |
| import android.os.Handler; |
| import android.support.v4.util.ArrayMap; |
| import android.text.TextUtils; |
| import android.view.ViewGroup; |
| |
| import org.chromium.base.Callback; |
| import org.chromium.chrome.R; |
| import org.chromium.chrome.browser.ChromeActivity; |
| import org.chromium.chrome.browser.autofill.PersonalDataManager; |
| import org.chromium.chrome.browser.autofill.PersonalDataManager.AutofillProfile; |
| import org.chromium.chrome.browser.autofill.PersonalDataManager.CreditCard; |
| import org.chromium.chrome.browser.autofill_assistant.ui.PaymentRequestUI; |
| import org.chromium.chrome.browser.payments.AddressEditor; |
| import org.chromium.chrome.browser.payments.AutofillAddress; |
| import org.chromium.chrome.browser.payments.AutofillContact; |
| import org.chromium.chrome.browser.payments.AutofillPaymentApp; |
| import org.chromium.chrome.browser.payments.AutofillPaymentInstrument; |
| import org.chromium.chrome.browser.payments.BasicCardUtils; |
| import org.chromium.chrome.browser.payments.CardEditor; |
| import org.chromium.chrome.browser.payments.ContactEditor; |
| import org.chromium.chrome.browser.payments.ShippingStrings; |
| import org.chromium.chrome.browser.payments.ui.ContactDetailsSection; |
| import org.chromium.chrome.browser.payments.ui.PaymentInformation; |
| import org.chromium.chrome.browser.payments.ui.SectionInformation; |
| import org.chromium.chrome.browser.payments.ui.ShoppingCart; |
| import org.chromium.chrome.browser.ssl.SecurityStateModel; |
| import org.chromium.chrome.browser.widget.prefeditor.Completable; |
| import org.chromium.chrome.browser.widget.prefeditor.EditableOption; |
| import org.chromium.components.url_formatter.UrlFormatter; |
| import org.chromium.content_public.browser.WebContents; |
| import org.chromium.payments.mojom.PaymentMethodData; |
| import org.chromium.payments.mojom.PaymentOptions; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * This class simplifies payment request UX to get payment information for Autofill Assistant. |
| * |
| * TODO(crbug.com/806868): Refactor shared codes with PaymentRequestImpl to a common place when the |
| * UX is fixed. |
| */ |
| public class AutofillAssistantPaymentRequest { |
| private static final String BASIC_CARD_PAYMENT_METHOD = "basic-card"; |
| private static final Comparator<Completable> COMPLETENESS_COMPARATOR = |
| (a, b) -> (b.isComplete() ? 1 : 0) - (a.isComplete() ? 1 : 0); |
| |
| private final WebContents mWebContents; |
| private final PaymentOptions mPaymentOptions; |
| private final String mTitle; |
| private final CardEditor mCardEditor; |
| private final AddressEditor mAddressEditor; |
| private final Map<String, PaymentMethodData> mMethodData; |
| private final Handler mHandler = new Handler(); |
| |
| private PaymentRequestUI mUI; |
| private ContactEditor mContactEditor; |
| private SectionInformation mPaymentMethodsSection; |
| private SectionInformation mShippingAddressesSection; |
| private ContactDetailsSection mContactSection; |
| private Callback<SelectedPaymentInformation> mCallback; |
| |
| /** The class to return payment information. */ |
| public class SelectedPaymentInformation { |
| /** Whether selection succeed. */ |
| public boolean succeed; |
| |
| /** Selected payment card. */ |
| public CreditCard card; |
| |
| /** Selected shipping address. */ |
| public AutofillProfile address; |
| |
| /** Payer's contact name. */ |
| public String payerName; |
| |
| /** Payer's contact email. */ |
| public String payerEmail; |
| |
| /** Payer's contact phone. */ |
| public String payerPhone; |
| |
| /** The terms and conditions accepted checkbox state. */ |
| public boolean isTermsAndConditionsAccepted; |
| } |
| |
| /** |
| * Constructor of AutofillAssistantPaymentRequest. |
| * |
| * @webContents The web contents of the payment request associated with. |
| * @paymentOptions The options to request payment information. |
| * @title The title to display in the payment request. |
| * @supportedBasicCardNetworks Optional array of supported basic card networks (see {@link |
| * BasicCardUtils}). If non-empty, only the specified card networks |
| * will be available for the basic-card payment method. |
| */ |
| public AutofillAssistantPaymentRequest(WebContents webContents, PaymentOptions paymentOptions, |
| String title, String[] supportedBasicCardNetworks) { |
| mWebContents = webContents; |
| mPaymentOptions = paymentOptions; |
| mTitle = title; |
| |
| // This feature should only works in non-incognito mode. |
| mAddressEditor = new AddressEditor(/* emailFieldIncluded= */ true, /* saveToDisk= */ true); |
| mCardEditor = new CardEditor(mWebContents, mAddressEditor, /* observerForTest= */ null); |
| |
| // Only enable 'basic-card' payment method. |
| PaymentMethodData methodData = new PaymentMethodData(); |
| methodData.supportedMethod = BASIC_CARD_PAYMENT_METHOD; |
| |
| // Apply basic-card filter if specified |
| if (supportedBasicCardNetworks.length > 0) { |
| ArrayList<Integer> filteredNetworks = new ArrayList<>(); |
| Map<String, Integer> networks = getNetworkIdentifiers(); |
| for (int i = 0; i < supportedBasicCardNetworks.length; i++) { |
| assert networks.containsKey(supportedBasicCardNetworks[i]); |
| filteredNetworks.add(networks.get(supportedBasicCardNetworks[i])); |
| } |
| |
| methodData.supportedNetworks = new int[filteredNetworks.size()]; |
| for (int i = 0; i < filteredNetworks.size(); i++) { |
| methodData.supportedNetworks[i] = filteredNetworks.get(i); |
| } |
| } |
| |
| mMethodData = new ArrayMap<>(); |
| mMethodData.put(BASIC_CARD_PAYMENT_METHOD, methodData); |
| mCardEditor.addAcceptedPaymentMethodIfRecognized(methodData); |
| } |
| |
| /** |
| * Show payment request UI to ask for payment information. |
| * |
| * Replace |container| with the payment request UI and restore it when the payment request UI is |
| * closed. |
| * |
| * @param container View to replace with the payment request. |
| * @param callback The callback to return payment information. |
| */ |
| /* package */ void show(ViewGroup container, Callback<SelectedPaymentInformation> callback) { |
| // Do not expect calling show multiple times. |
| assert mCallback == null; |
| assert mUI == null; |
| |
| mCallback = callback; |
| buildUI(ChromeActivity.fromWebContents(mWebContents)); |
| |
| mUI.show(container); |
| } |
| |
| private void buildUI(ChromeActivity activity) { |
| assert activity != null; |
| |
| mPaymentMethodsSection = new SectionInformation(PaymentRequestUI.DataType.PAYMENT_METHODS, |
| SectionInformation.NO_SELECTION, |
| (new AutofillPaymentApp(mWebContents)) |
| .getInstruments(mMethodData, /*forceReturnServerCards=*/true)); |
| if (!mPaymentMethodsSection.isEmpty() && mPaymentMethodsSection.getItem(0).isComplete()) { |
| mPaymentMethodsSection.setSelectedItemIndex(0); |
| } |
| |
| List<AutofillProfile> profiles = null; |
| if (mPaymentOptions.requestShipping || mPaymentOptions.requestPayerName |
| || mPaymentOptions.requestPayerPhone || mPaymentOptions.requestPayerEmail) { |
| profiles = PersonalDataManager.getInstance().getProfilesToSuggest( |
| /* includeNameInLabel= */ false); |
| } |
| |
| if (mPaymentOptions.requestShipping) { |
| createShippingSection(activity, Collections.unmodifiableList(profiles)); |
| } |
| |
| if (mPaymentOptions.requestPayerName || mPaymentOptions.requestPayerPhone |
| || mPaymentOptions.requestPayerEmail) { |
| mContactEditor = new ContactEditor(mPaymentOptions.requestPayerName, |
| mPaymentOptions.requestPayerPhone, mPaymentOptions.requestPayerEmail, |
| /* saveToDisk= */ true); |
| mContactSection = new ContactDetailsSection(activity, |
| Collections.unmodifiableList(profiles), mContactEditor, |
| /* journeyLogger= */ null); |
| } |
| |
| mUI = new PaymentRequestUI(activity, this, mPaymentOptions.requestShipping, |
| /* requestShippingOption= */ false, |
| mPaymentOptions.requestPayerName || mPaymentOptions.requestPayerPhone |
| || mPaymentOptions.requestPayerEmail, |
| /* canAddCards= */ true, /* showDataSource= */ true, |
| mTitle.isEmpty() ? mWebContents.getTitle() : mTitle, |
| UrlFormatter.formatUrlForSecurityDisplay(mWebContents.getLastCommittedUrl()), |
| SecurityStateModel.getSecurityLevelForWebContents(mWebContents), |
| new ShippingStrings(mPaymentOptions.shippingType)); |
| // This payment request is embedded in another flow, so update the 'Pay' button text to |
| // 'Confirm'. |
| mUI.updatePayButtonText(R.string.autofill_assistant_payment_info_confirm); |
| |
| mAddressEditor.setEditorDialog(mUI.getEditorDialog()); |
| mCardEditor.setEditorDialog(mUI.getCardEditorDialog()); |
| if (mContactEditor != null) mContactEditor.setEditorDialog(mUI.getEditorDialog()); |
| } |
| |
| private void createShippingSection( |
| Context context, List<AutofillProfile> unmodifiableProfiles) { |
| List<AutofillAddress> addresses = new ArrayList<>(); |
| |
| for (int i = 0; i < unmodifiableProfiles.size(); i++) { |
| AutofillProfile profile = unmodifiableProfiles.get(i); |
| mAddressEditor.addPhoneNumberIfValid(profile.getPhoneNumber()); |
| |
| // Only suggest addresses that have a street address. |
| if (!TextUtils.isEmpty(profile.getStreetAddress())) { |
| addresses.add(new AutofillAddress(context, profile)); |
| } |
| } |
| |
| // Suggest complete addresses first. |
| Collections.sort(addresses, COMPLETENESS_COMPARATOR); |
| |
| // Automatically select the first address if one is complete. |
| int firstCompleteAddressIndex = SectionInformation.NO_SELECTION; |
| if (!addresses.isEmpty() && addresses.get(0).isComplete()) { |
| firstCompleteAddressIndex = 0; |
| |
| // The initial label for the selected shipping address should not include the |
| // country. |
| addresses.get(firstCompleteAddressIndex).setShippingAddressLabelWithoutCountry(); |
| } |
| |
| mShippingAddressesSection = new SectionInformation( |
| PaymentRequestUI.DataType.SHIPPING_ADDRESSES, firstCompleteAddressIndex, addresses); |
| } |
| |
| /** Close payment request. */ |
| /* package */ void close() { |
| if (mUI != null) { |
| // Close the UI immediately and do not wait for finishing animations. |
| mUI.close(/* shouldCloseImmediately = */ true, () -> {}); |
| mUI = null; |
| } |
| |
| // Do not call callback if closed by caller. |
| mCallback = null; |
| } |
| |
| public void getDefaultPaymentInformation(Callback<PaymentInformation> callback) { |
| mHandler.post(() -> { |
| if (mUI != null) { |
| callback.onResult(new PaymentInformation(/* shoppingCart= */ null, |
| mShippingAddressesSection, |
| /* shippingOptions= */ null, mContactSection, mPaymentMethodsSection)); |
| } |
| }); |
| } |
| |
| public void getShoppingCart(Callback<ShoppingCart> callback) { |
| // Do not display anything for shopping cart. |
| mHandler.post(() -> callback.onResult(null)); |
| } |
| |
| public void getSectionInformation( |
| @PaymentRequestUI.DataType int optionType, Callback<SectionInformation> callback) { |
| mHandler.post(() -> { |
| if (optionType == PaymentRequestUI.DataType.SHIPPING_ADDRESSES) { |
| callback.onResult(mShippingAddressesSection); |
| } else if (optionType == PaymentRequestUI.DataType.CONTACT_DETAILS) { |
| callback.onResult(mContactSection); |
| } else if (optionType == PaymentRequestUI.DataType.PAYMENT_METHODS) { |
| assert mPaymentMethodsSection != null; |
| callback.onResult(mPaymentMethodsSection); |
| } else { |
| // Only support above sections for now. |
| assert false; |
| } |
| }); |
| } |
| |
| @PaymentRequestUI.SelectionResult |
| public int onSectionOptionSelected(@PaymentRequestUI.DataType int optionType, |
| EditableOption option, Callback<PaymentInformation> checkedCallback) { |
| if (optionType == PaymentRequestUI.DataType.SHIPPING_ADDRESSES) { |
| AutofillAddress address = (AutofillAddress) option; |
| if (address.isComplete()) { |
| mShippingAddressesSection.setSelectedItem(option); |
| } else { |
| editAddress(address); |
| return PaymentRequestUI.SelectionResult.EDITOR_LAUNCH; |
| } |
| } else if (optionType == PaymentRequestUI.DataType.CONTACT_DETAILS) { |
| AutofillContact contact = (AutofillContact) option; |
| if (contact.isComplete()) { |
| mContactSection.setSelectedItem(option); |
| } else { |
| editContact(contact); |
| return PaymentRequestUI.SelectionResult.EDITOR_LAUNCH; |
| } |
| } else if (optionType == PaymentRequestUI.DataType.PAYMENT_METHODS) { |
| AutofillPaymentInstrument card = (AutofillPaymentInstrument) option; |
| if (card.isComplete()) { |
| mPaymentMethodsSection.setSelectedItem(option); |
| } else { |
| editCard(card); |
| return PaymentRequestUI.SelectionResult.EDITOR_LAUNCH; |
| } |
| } else { |
| // Only support above sections for now. |
| assert false; |
| } |
| |
| return PaymentRequestUI.SelectionResult.NONE; |
| } |
| |
| @PaymentRequestUI.SelectionResult |
| public int onSectionEditOption(@PaymentRequestUI.DataType int optionType, EditableOption option, |
| Callback<PaymentInformation> checkedCallback) { |
| if (optionType == PaymentRequestUI.DataType.SHIPPING_ADDRESSES) { |
| editAddress((AutofillAddress) option); |
| return PaymentRequestUI.SelectionResult.EDITOR_LAUNCH; |
| } |
| |
| if (optionType == PaymentRequestUI.DataType.CONTACT_DETAILS) { |
| editContact((AutofillContact) option); |
| return PaymentRequestUI.SelectionResult.EDITOR_LAUNCH; |
| } |
| |
| if (optionType == PaymentRequestUI.DataType.PAYMENT_METHODS) { |
| editCard((AutofillPaymentInstrument) option); |
| return PaymentRequestUI.SelectionResult.EDITOR_LAUNCH; |
| } |
| |
| // Only support above sections for now. |
| assert false; |
| return PaymentRequestUI.SelectionResult.NONE; |
| } |
| |
| @PaymentRequestUI.SelectionResult |
| public int onSectionAddOption(@PaymentRequestUI.DataType int optionType, |
| Callback<PaymentInformation> checkedCallback) { |
| if (optionType == PaymentRequestUI.DataType.SHIPPING_ADDRESSES) { |
| editAddress(null); |
| return PaymentRequestUI.SelectionResult.EDITOR_LAUNCH; |
| } else if (optionType == PaymentRequestUI.DataType.CONTACT_DETAILS) { |
| editContact(null); |
| return PaymentRequestUI.SelectionResult.EDITOR_LAUNCH; |
| } else if (optionType == PaymentRequestUI.DataType.PAYMENT_METHODS) { |
| editCard(null); |
| return PaymentRequestUI.SelectionResult.EDITOR_LAUNCH; |
| } |
| |
| // Only support above sections for now. |
| assert false; |
| return PaymentRequestUI.SelectionResult.NONE; |
| } |
| |
| private void editAddress(final AutofillAddress toEdit) { |
| mAddressEditor.edit(toEdit, new Callback<AutofillAddress>() { |
| @Override |
| public void onResult(AutofillAddress editedAddress) { |
| if (mUI == null) return; |
| |
| if (editedAddress != null) { |
| // Sets or updates the shipping address label. |
| editedAddress.setShippingAddressLabelWithCountry(); |
| |
| mCardEditor.updateBillingAddressIfComplete(editedAddress); |
| |
| // A partial or complete address came back from the editor (could have been from |
| // adding/editing or cancelling out of the edit flow). |
| if (!editedAddress.isComplete()) { |
| // If the address is not complete, deselect it (editor can return incomplete |
| // information when cancelled). |
| mShippingAddressesSection.setSelectedItemIndex( |
| SectionInformation.NO_SELECTION); |
| } else { |
| if (toEdit == null) { |
| // Address is complete and user was in the "Add flow": add an item to |
| // the list. |
| mShippingAddressesSection.addAndSelectItem(editedAddress); |
| } |
| |
| if (mContactSection != null) { |
| // Update |mContactSection| with the new/edited address, which will |
| // update an existing item or add a new one to the end of the list. |
| mContactSection.addOrUpdateWithAutofillAddress(editedAddress); |
| mUI.updateSection( |
| PaymentRequestUI.DataType.CONTACT_DETAILS, mContactSection); |
| } |
| } |
| |
| mUI.updateSection(PaymentRequestUI.DataType.SHIPPING_ADDRESSES, |
| mShippingAddressesSection); |
| } |
| } |
| }); |
| } |
| |
| private void editContact(final AutofillContact toEdit) { |
| mContactEditor.edit(toEdit, new Callback<AutofillContact>() { |
| @Override |
| public void onResult(AutofillContact editedContact) { |
| if (mUI == null) return; |
| |
| if (editedContact != null) { |
| // A partial or complete contact came back from the editor (could have been from |
| // adding/editing or cancelling out of the edit flow). |
| if (!editedContact.isComplete()) { |
| // If the contact is not complete according to the requirements of the flow, |
| // deselect it (editor can return incomplete information when cancelled). |
| mContactSection.setSelectedItemIndex(SectionInformation.NO_SELECTION); |
| } else if (toEdit == null) { |
| // Contact is complete and we were in the "Add flow": add an item to the |
| // list. |
| mContactSection.addAndSelectItem(editedContact); |
| } |
| // If contact is complete and (toEdit != null), no action needed: the contact |
| // was already selected in the UI. |
| } |
| // If |editedContact| is null, the user has cancelled out of the "Add flow". No |
| // action to take (if a contact was selected in the UI, it will stay selected). |
| |
| mUI.updateSection(PaymentRequestUI.DataType.CONTACT_DETAILS, mContactSection); |
| } |
| }); |
| } |
| |
| private void editCard(final AutofillPaymentInstrument toEdit) { |
| mCardEditor.edit(toEdit, new Callback<AutofillPaymentInstrument>() { |
| @Override |
| public void onResult(AutofillPaymentInstrument editedCard) { |
| if (mUI == null) return; |
| |
| if (editedCard != null) { |
| // A partial or complete card came back from the editor (could have been from |
| // adding/editing or cancelling out of the edit flow). |
| if (!editedCard.isComplete()) { |
| // If the card is not complete, deselect it (editor can return incomplete |
| // information when cancelled). |
| mPaymentMethodsSection.setSelectedItemIndex( |
| SectionInformation.NO_SELECTION); |
| } else if (toEdit == null) { |
| // Card is complete and we were in the "Add flow": add an item to the list. |
| mPaymentMethodsSection.addAndSelectItem(editedCard); |
| } |
| // If card is complete and (toEdit != null), no action needed: the card was |
| // already selected in the UI. |
| } |
| // If |editedCard| is null, the user has cancelled out of the "Add flow". No action |
| // to take (if another card was selected prior to the add flow, it will stay |
| // selected). |
| |
| mUI.updateSection( |
| PaymentRequestUI.DataType.PAYMENT_METHODS, mPaymentMethodsSection); |
| } |
| }); |
| } |
| |
| /** |
| * @return a complete map of string identifiers to BasicCardNetworks. |
| */ |
| private static Map<String, Integer> getNetworkIdentifiers() { |
| Map<Integer, String> networksByInt = BasicCardUtils.getNetworks(); |
| Map<String, Integer> networksByString = new HashMap<>(); |
| for (Map.Entry<Integer, String> entry : networksByInt.entrySet()) { |
| networksByString.put(entry.getValue(), entry.getKey()); |
| } |
| return networksByString; |
| } |
| |
| public boolean onPayClicked(EditableOption selectedShippingAddress, |
| EditableOption selectedShippingOption, EditableOption selectedPaymentMethod, |
| boolean isTermsAndConditionsAccepted) { |
| if (mCallback != null) { |
| SelectedPaymentInformation selectedPaymentInformation = |
| new SelectedPaymentInformation(); |
| |
| selectedPaymentInformation.isTermsAndConditionsAccepted = isTermsAndConditionsAccepted; |
| selectedPaymentInformation.card = |
| ((AutofillPaymentInstrument) selectedPaymentMethod).getCard(); |
| if (mPaymentOptions.requestShipping && selectedShippingAddress != null) { |
| selectedPaymentInformation.address = |
| ((AutofillAddress) selectedShippingAddress).getProfile(); |
| } |
| if (mPaymentOptions.requestPayerName || mPaymentOptions.requestPayerPhone |
| || mPaymentOptions.requestPayerEmail) { |
| EditableOption selectedContact = |
| mContactSection != null ? mContactSection.getSelectedItem() : null; |
| if (selectedContact != null) { |
| selectedPaymentInformation.payerName = |
| ((AutofillContact) selectedContact).getPayerName(); |
| selectedPaymentInformation.payerPhone = |
| ((AutofillContact) selectedContact).getPayerPhone(); |
| selectedPaymentInformation.payerEmail = |
| ((AutofillContact) selectedContact).getPayerEmail(); |
| } |
| } |
| selectedPaymentInformation.succeed = true; |
| mCallback.onResult(selectedPaymentInformation); |
| mCallback = null; |
| } |
| |
| return false; |
| } |
| |
| public void onDismiss() { |
| if (mCallback != null) { |
| SelectedPaymentInformation selectedPaymentInformation = |
| new SelectedPaymentInformation(); |
| selectedPaymentInformation.succeed = false; |
| mCallback.onResult(selectedPaymentInformation); |
| mCallback = null; |
| } |
| |
| close(); |
| } |
| |
| public void onCardAndAddressSettingsClicked() { |
| // TODO(crbug.com/806868): Allow user to control cards and addresses. |
| } |
| } |