// Copyright 2022 The Chromium Authors
// 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.ui.fast_checkout;
import androidx.annotation.MainThread;
import org.chromium.chrome.browser.ui.fast_checkout.FastCheckoutProperties.DetailItemType;
import org.chromium.chrome.browser.ui.fast_checkout.FastCheckoutProperties.ScreenType;
import org.chromium.chrome.browser.ui.fast_checkout.data.FastCheckoutAutofillProfile;
import org.chromium.chrome.browser.ui.fast_checkout.data.FastCheckoutCreditCard;
import org.chromium.chrome.browser.ui.fast_checkout.detail_screen.AutofillProfileItemProperties;
import org.chromium.chrome.browser.ui.fast_checkout.detail_screen.CreditCardItemProperties;
import org.chromium.chrome.browser.ui.fast_checkout.detail_screen.FooterItemProperties;
import org.chromium.chrome.browser.ui.fast_checkout.home_screen.HomeScreenCoordinator;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetContent;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController.StateChangeReason;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetObserver;
import org.chromium.components.browser_ui.bottomsheet.EmptyBottomSheetObserver;
import org.chromium.ui.modelutil.MVCListAdapter.ListItem;
import org.chromium.ui.modelutil.MVCListAdapter.ModelList;
import org.chromium.ui.modelutil.PropertyModel;
import java.util.List;
/**
* Contains the logic for the FastCheckout component. It sets the state of the model and reacts to
* events like clicks.
*/
public class FastCheckoutMediator implements FastCheckoutSheetState {
private PropertyModel mModel;
private FastCheckoutComponent.Delegate mDelegate;
private BottomSheetController mBottomSheetController;
private BottomSheetObserver mBottomSheetDismissedObserver;
void initialize(
FastCheckoutComponent.Delegate delegate,
PropertyModel model,
BottomSheetController bottomSheetController) {
mModel = model;
mDelegate = delegate;
mBottomSheetController = bottomSheetController;
mBottomSheetDismissedObserver =
new EmptyBottomSheetObserver() {
@Override
public void onSheetClosed(@BottomSheetController.StateChangeReason int reason) {
super.onSheetClosed(reason);
dismiss(reason);
mBottomSheetController.removeObserver(mBottomSheetDismissedObserver);
}
};
mModel.set(FastCheckoutProperties.HOME_SCREEN_DELEGATE, createHomeScreenDelegate());
mModel.set(
FastCheckoutProperties.DETAIL_SCREEN_BACK_CLICK_HANDLER,
() -> {
setCurrentScreen(FastCheckoutProperties.ScreenType.HOME_SCREEN);
FastCheckoutUserActions.NAVIGATED_BACK_HOME.log();
});
FastCheckoutUserActions.INITIALIZED.log();
}
/** Returns an implementation the {@link HomeScreenCoordinator.Delegate} interface. */
private HomeScreenCoordinator.Delegate createHomeScreenDelegate() {
return new HomeScreenCoordinator.Delegate() {
@Override
public void onOptionsAccepted() {
if (!mModel.get(FastCheckoutProperties.VISIBLE)) {
return; // Dismiss only if not dismissed yet.
}
FastCheckoutUserActions.ACCEPTED.log();
FastCheckoutAutofillProfile profile =
mModel.get(FastCheckoutProperties.SELECTED_PROFILE);
FastCheckoutCreditCard creditCard =
mModel.get(FastCheckoutProperties.SELECTED_CREDIT_CARD);
assert profile != null && creditCard != null;
mModel.set(FastCheckoutProperties.VISIBLE, false);
mDelegate.onOptionsSelected(profile, creditCard);
}
@Override
public void onShowAddressesList() {
setCurrentScreen(FastCheckoutProperties.ScreenType.AUTOFILL_PROFILE_SCREEN);
FastCheckoutUserActions.NAVIGATED_TO_ADDRESSES.log();
}
@Override
public void onShowCreditCardList() {
setCurrentScreen(FastCheckoutProperties.ScreenType.CREDIT_CARD_SCREEN);
FastCheckoutUserActions.NAVIGATED_TO_CREDIT_CARDS.log();
}
};
}
public void showOptions(
List<FastCheckoutAutofillProfile> profiles, List<FastCheckoutCreditCard> creditCards) {
setAutofillProfileItems(profiles);
setCreditCardItems(creditCards);
setCurrentScreen(mModel.get(FastCheckoutProperties.CURRENT_SCREEN));
// Show the bottom sheet.
mModel.set(FastCheckoutProperties.VISIBLE, true);
}
/**
* If set to true, requests to show the bottom sheet. Otherwise, requests to hide the sheet.
* @param isVisible A boolean describing whether to show or hide the sheet.
* @param content The bottom sheet content to show/hide.
* @return True if the request was successful, false otherwise.
*/
public boolean setVisible(boolean isVisible, BottomSheetContent content) {
if (isVisible) {
mBottomSheetController.addObserver(mBottomSheetDismissedObserver);
if (!mBottomSheetController.requestShowContent(content, true)) {
mBottomSheetController.removeObserver(mBottomSheetDismissedObserver);
return false;
}
} else {
mBottomSheetController.hideContent(content, true);
}
return true;
}
/** Dismisses the current bottom sheet. */
public void dismiss(@StateChangeReason int reason) {
if (!mModel.get(FastCheckoutProperties.VISIBLE)) {
return; // Dismiss only if not dismissed yet.
}
FastCheckoutUserActions.DISMISSED.log();
mModel.set(FastCheckoutProperties.VISIBLE, false);
mDelegate.onDismissed();
}
/**
* Sets the Autofill profile items and creates the corresponding models for the profile item
* entries on the Autofill profiles page. If there is a selected Autofill profile prior to
* calling this method, the profile with the same GUID will remain selected. If no prior
* selection was made or this GUID no longer exists, the first Autofill profile is selected.
*
* @param profiles The array of FastCheckoutAutofillProfile to set as Autofill profiles.
*/
public void setAutofillProfileItems(List<FastCheckoutAutofillProfile> profiles) {
assert profiles != null && profiles.size() != 0;
FastCheckoutAutofillProfile previousSelection =
mModel.get(FastCheckoutProperties.SELECTED_PROFILE);
FastCheckoutAutofillProfile newSelection = profiles.get(0);
// Populate all model entries.
ModelList profileItems = new ModelList();
for (FastCheckoutAutofillProfile profile : profiles) {
if (previousSelection != null
&& profile.getGUID().equals(previousSelection.getGUID())) {
newSelection = profile;
}
PropertyModel model =
AutofillProfileItemProperties.create(
/* profile= */ profile,
/* isSelected= */ false,
/* onClickListener= */ () -> {
setSelectedAutofillProfile(profile);
setCurrentScreen(FastCheckoutProperties.ScreenType.HOME_SCREEN);
});
profileItems.add(new ListItem(DetailItemType.PROFILE, model));
}
// Add the footer item.
profileItems.add(
new ListItem(
DetailItemType.FOOTER,
FooterItemProperties.create(
/* label= */ R.string
.fast_checkout_detail_screen_add_autofill_profile_text,
/* onClickHandler= */ () -> {
mDelegate.openAutofillProfileSettings();
FastCheckoutUserActions
.NAVIGATED_TO_ADDRESSES_SETTINGS_VIA_FOOTER
.log();
})));
mModel.set(FastCheckoutProperties.PROFILE_MODEL_LIST, profileItems);
setSelectedAutofillProfile(newSelection);
}
/**
* Sets the selected Autofill profile and updates the IS_SELECTED entry in the models
* of the profile item entries on the Autofill profiles page.
* @param selectedProfile The profile that is to be selected.
*/
public void setSelectedAutofillProfile(FastCheckoutAutofillProfile selectedProfile) {
assert selectedProfile != null;
boolean isInitialSelection = mModel.get(FastCheckoutProperties.SELECTED_PROFILE) == null;
mModel.set(FastCheckoutProperties.SELECTED_PROFILE, selectedProfile);
int foundProfiles = 0;
ModelList allItems = mModel.get(FastCheckoutProperties.PROFILE_MODEL_LIST);
for (ListItem item : allItems) {
if (item.type != DetailItemType.PROFILE) {
continue;
}
boolean isSelected =
selectedProfile.equals(
item.model.get(AutofillProfileItemProperties.AUTOFILL_PROFILE));
boolean wasSelected = item.model.get(AutofillProfileItemProperties.IS_SELECTED);
item.model.set(AutofillProfileItemProperties.IS_SELECTED, isSelected);
if (isSelected) {
++foundProfiles;
if (!isInitialSelection) {
if (wasSelected) {
FastCheckoutUserActions.SELECTED_SAME_ADDRESS.log();
} else {
FastCheckoutUserActions.SELECTED_DIFFERENT_ADDRESS.log();
}
}
}
}
// Exactly one of the models must contain the selected profile.
assert foundProfiles == 1;
}
/**
* Sets the credit card items and creates the corresponding models for the credit card item
* entries on the credit card page. If there is a selected credit card prior to calling this
* method, the card with the same GUID will remain selected. If no prior selection was made or
* this GUID no longer exists, the first credit card is selected.
*
* @param creditCards The array of FastCheckoutCreditCard to set as credit cards.
*/
public void setCreditCardItems(List<FastCheckoutCreditCard> creditCards) {
assert creditCards != null && creditCards.size() != 0;
FastCheckoutCreditCard previousSelection =
mModel.get(FastCheckoutProperties.SELECTED_CREDIT_CARD);
FastCheckoutCreditCard newSelection = creditCards.get(0);
// Populate all model entries.
ModelList cardItems = new ModelList();
for (FastCheckoutCreditCard card : creditCards) {
if (previousSelection != null && card.getGUID().equals(previousSelection.getGUID())) {
newSelection = card;
}
PropertyModel model =
CreditCardItemProperties.create(
/* creditCard= */ card,
/* isSelected= */ false,
/* onClickListener= */ () -> {
setSelectedCreditCard(card);
setCurrentScreen(FastCheckoutProperties.ScreenType.HOME_SCREEN);
});
ListItem item = new ListItem(DetailItemType.CREDIT_CARD, model);
cardItems.add(item);
}
// Add the footer item.
cardItems.add(
new ListItem(
DetailItemType.FOOTER,
FooterItemProperties.create(
/* label= */ R.string
.fast_checkout_detail_screen_add_credit_card_text,
/* onClickHandler= */ () -> {
mDelegate.openCreditCardSettings();
FastCheckoutUserActions
.NAVIGATED_TO_CREDIT_CARDS_SETTINGS_VIA_FOOTER
.log();
})));
mModel.set(FastCheckoutProperties.CREDIT_CARD_MODEL_LIST, cardItems);
setSelectedCreditCard(newSelection);
}
/**
* Sets the selected credit card and updates the IS_SELECTED entry in the models
* of the credit card item entries on the credit card page.
* @param selectedCreditCard The credit card that is to be selected.
*/
public void setSelectedCreditCard(FastCheckoutCreditCard selectedCreditCard) {
assert selectedCreditCard != null;
boolean isInitialSelection =
mModel.get(FastCheckoutProperties.SELECTED_CREDIT_CARD) == null;
mModel.set(FastCheckoutProperties.SELECTED_CREDIT_CARD, selectedCreditCard);
int foundCards = 0;
ModelList allItems = mModel.get(FastCheckoutProperties.CREDIT_CARD_MODEL_LIST);
for (ListItem item : allItems) {
if (item.type != DetailItemType.CREDIT_CARD) {
continue;
}
boolean isSelected =
selectedCreditCard.equals(item.model.get(CreditCardItemProperties.CREDIT_CARD));
boolean wasSelected = item.model.get(CreditCardItemProperties.IS_SELECTED);
item.model.set(CreditCardItemProperties.IS_SELECTED, isSelected);
if (isSelected) {
++foundCards;
if (!isInitialSelection) {
if (wasSelected) {
FastCheckoutUserActions.SELECTED_SAME_CREDIT_CARD.log();
} else {
FastCheckoutUserActions.SELECTED_DIFFERENT_CREDIT_CARD.log();
}
}
}
}
// Exactly one of the models must contain the selected credit card.
assert foundCards == 1;
}
/**
* Selects the currently shown screen on the bottomsheet.
* @param screenType A {@link FastCheckoutProperties.ScreenType} that defines the screen to be
* shown.
*/
public void setCurrentScreen(int screenType) {
if (screenType == FastCheckoutProperties.ScreenType.AUTOFILL_PROFILE_SCREEN) {
mModel.set(
FastCheckoutProperties.DETAIL_SCREEN_TITLE,
R.string.fast_checkout_autofill_profile_sheet_title);
mModel.set(
FastCheckoutProperties.DETAIL_SCREEN_TITLE_DESCRIPTION,
R.string.fast_checkout_autofill_profile_sheet_title_description);
mModel.set(
FastCheckoutProperties.DETAIL_SCREEN_SETTINGS_MENU_TITLE,
R.string.fast_checkout_autofill_profile_settings_button_description);
mModel.set(
FastCheckoutProperties.DETAIL_SCREEN_SETTINGS_CLICK_HANDLER,
() -> {
mDelegate.openAutofillProfileSettings();
FastCheckoutUserActions.NAVIGATED_TO_ADDRESSES_SETTINGS_VIA_ICON.log();
});
mModel.set(
FastCheckoutProperties.DETAIL_SCREEN_MODEL_LIST,
mModel.get(FastCheckoutProperties.PROFILE_MODEL_LIST));
} else if (screenType == ScreenType.CREDIT_CARD_SCREEN) {
mModel.set(
FastCheckoutProperties.DETAIL_SCREEN_TITLE,
R.string.fast_checkout_credit_card_sheet_title);
mModel.set(
FastCheckoutProperties.DETAIL_SCREEN_TITLE_DESCRIPTION,
R.string.fast_checkout_credit_card_sheet_title_description);
mModel.set(
FastCheckoutProperties.DETAIL_SCREEN_SETTINGS_MENU_TITLE,
R.string.fast_checkout_credit_card_settings_button_description);
mModel.set(
FastCheckoutProperties.DETAIL_SCREEN_SETTINGS_CLICK_HANDLER,
() -> {
mDelegate.openCreditCardSettings();
FastCheckoutUserActions.NAVIGATED_TO_CREDIT_CARDS_SETTINGS_VIA_ICON.log();
});
mModel.set(
FastCheckoutProperties.DETAIL_SCREEN_MODEL_LIST,
mModel.get(FastCheckoutProperties.CREDIT_CARD_MODEL_LIST));
}
mModel.set(FastCheckoutProperties.CURRENT_SCREEN, screenType);
// Sets bottom sheet to half height, if enabled. Otherwise to full.
mBottomSheetController.expandSheet();
}
/** Releases the resources used by FastCheckoutMediator. */
@MainThread
public void destroy() {
FastCheckoutUserActions.DESTROYED.log();
mModel.set(FastCheckoutProperties.VISIBLE, false);
}
@Override
public @ScreenType int getCurrentScreen() {
return mModel.get(FastCheckoutProperties.CURRENT_SCREEN);
}
@Override
public int getNumOfAutofillProfiles() {
// The list contains Autofill profiles and one footer at the end. Subtracts 1 to not count
// the footer.
return mModel.get(FastCheckoutProperties.PROFILE_MODEL_LIST).size() - 1;
}
@Override
public int getNumOfCreditCards() {
// The list contains credit cards and one footer at the end. Subtracts 1 to not count the
// footer.
return mModel.get(FastCheckoutProperties.CREDIT_CARD_MODEL_LIST).size() - 1;
}
@Override
public int getContainerHeight() {
return mBottomSheetController.getContainerHeight();
}
}