// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/autofill/ui_bundled/bottom_sheet/payments_suggestion_bottom_sheet_mediator.h"
#import "base/memory/raw_ptr.h"
#import "base/metrics/histogram_functions.h"
#import "base/strings/sys_string_conversions.h"
#import "components/autofill/core/browser/payments_data_manager.h"
#import "components/autofill/core/browser/personal_data_manager.h"
#import "components/autofill/core/browser/personal_data_manager_observer.h"
#import "components/autofill/core/common/autofill_payments_features.h"
#import "components/autofill/ios/browser/credit_card_util.h"
#import "components/autofill/ios/browser/form_suggestion.h"
#import "components/autofill/ios/browser/personal_data_manager_observer_bridge.h"
#import "components/autofill/ios/form_util/form_activity_params.h"
#import "components/strings/grit/components_strings.h"
#import "ios/chrome/browser/autofill/model/bottom_sheet/autofill_bottom_sheet_java_script_feature.h"
#import "ios/chrome/browser/autofill/model/bottom_sheet/autofill_bottom_sheet_tab_helper.h"
#import "ios/chrome/browser/autofill/model/credit_card/credit_card_data.h"
#import "ios/chrome/browser/autofill/model/form_input_suggestions_provider.h"
#import "ios/chrome/browser/autofill/model/form_suggestion_tab_helper.h"
#import "ios/chrome/browser/shared/model/web_state_list/active_web_state_observation_forwarder.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list_observer_bridge.h"
#import "ios/chrome/browser/autofill/ui_bundled/bottom_sheet/payments_suggestion_bottom_sheet_consumer.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/web/public/js_messaging/web_frames_manager.h"
#import "ios/web/public/web_state_observer_bridge.h"
#import "ui/base/l10n/l10n_util.h"
#import "ui/base/resource/resource_bundle.h"
using PaymentsSuggestionBottomSheetExitReason::kBadProvider;
@interface PaymentsSuggestionBottomSheetMediator () <
CRWWebStateObserver,
PersonalDataManagerObserver,
WebStateListObserving>
@end
@implementation PaymentsSuggestionBottomSheetMediator {
// The WebStateList observed by this mediator and the observer bridge.
raw_ptr<WebStateList> _webStateList;
// Bridge and forwarder for observing WebState events. The forwarder is a
// scoped observation, so the bridge will automatically be removed from the
// relevant observer list.
std::unique_ptr<web::WebStateObserverBridge> _webStateObserver;
std::unique_ptr<ActiveWebStateObservationForwarder>
_activeWebStateObservationForwarder;
// Bridge for observing WebStateList events.
std::unique_ptr<WebStateListObserverBridge> _webStateListObserver;
std::unique_ptr<
base::ScopedObservation<WebStateList, WebStateListObserverBridge>>
_webStateListObservation;
// Personal Data Manager from which we can get Credit Card information.
raw_ptr<autofill::PersonalDataManager> _personalDataManager;
// C++ to ObjC bridge for PersonalDataManagerObserver.
std::unique_ptr<autofill::PersonalDataManagerObserverBridge>
_personalDataManagerObserver;
// Scoped observer used to track registration of the
// PersonalDataManagerObserverBridge.
std::unique_ptr<
base::ScopedObservation<autofill::PersonalDataManager,
autofill::PersonalDataManagerObserver>>
_scopedPersonalDataManagerObservation;
// Information regarding the triggering form for this bottom sheet.
autofill::FormActivityParams _params;
}
#pragma mark - Properties
@synthesize hasCreditCards = _hasCreditCards;
#pragma mark - Initialization
- (instancetype)initWithWebStateList:(WebStateList*)webStateList
params:(const autofill::FormActivityParams&)params
personalDataManager:
(autofill::PersonalDataManager*)personalDataManager {
if ((self = [super init])) {
_params = params;
_hasCreditCards = NO;
_webStateList = webStateList;
if (personalDataManager) {
_personalDataManager = personalDataManager;
_personalDataManagerObserver.reset(
new autofill::PersonalDataManagerObserverBridge(self));
_scopedPersonalDataManagerObservation = std::make_unique<
base::ScopedObservation<autofill::PersonalDataManager,
autofill::PersonalDataManagerObserver>>(
_personalDataManagerObserver.get());
_scopedPersonalDataManagerObservation->Observe(_personalDataManager);
}
// Create and register the observers.
_webStateObserver = std::make_unique<web::WebStateObserverBridge>(self);
_activeWebStateObservationForwarder =
std::make_unique<ActiveWebStateObservationForwarder>(
webStateList, _webStateObserver.get());
_webStateListObserver = std::make_unique<WebStateListObserverBridge>(self);
_webStateListObservation = std::make_unique<
base::ScopedObservation<WebStateList, WebStateListObserverBridge>>(
_webStateListObserver.get());
_webStateListObservation->Observe(_webStateList);
[self setupSuggestionsProvider];
}
return self;
}
#pragma mark - Public
- (void)dealloc {
[self disconnect];
}
- (void)disconnect {
if (_personalDataManager && _personalDataManagerObserver.get()) {
_personalDataManager->RemoveObserver(_personalDataManagerObserver.get());
_personalDataManagerObserver.reset();
}
_scopedPersonalDataManagerObservation.reset();
_webStateListObservation.reset();
_webStateListObserver.reset();
_activeWebStateObservationForwarder.reset();
_webStateObserver.reset();
_webStateList = nullptr;
}
- (autofill::CreditCard*)creditCardForIdentifier:(NSString*)identifier {
CHECK(identifier);
CHECK(_personalDataManager);
return _personalDataManager->payments_data_manager().GetCreditCardByGUID(
base::SysNSStringToUTF8(identifier));
}
- (BOOL)hasCreditCards {
return _hasCreditCards;
}
- (void)logExitReason:(PaymentsSuggestionBottomSheetExitReason)exitReason {
base::UmaHistogramEnumeration("IOS.PaymentsBottomSheet.ExitReason",
exitReason);
}
#pragma mark - Accessors
- (void)setConsumer:(id<PaymentsSuggestionBottomSheetConsumer>)consumer {
_consumer = consumer;
if (!_consumer) {
return;
}
if (!_personalDataManager) {
[_consumer dismiss];
return;
}
const auto& creditCards =
_personalDataManager->payments_data_manager().GetCreditCardsToSuggest();
if (creditCards.empty()) {
[_consumer dismiss];
return;
}
BOOL hasNonLocalCard = NO;
NSMutableArray<CreditCardData*>* creditCardData =
[[NSMutableArray alloc] initWithCapacity:creditCards.size()];
for (const autofill::CreditCard* creditCard : creditCards) {
CHECK(creditCard);
// If the current card is enrolled to be a virtual card, create the virtual
// card and add it to creditCardData array directly before the original
// card.
if (base::FeatureList::IsEnabled(
autofill::features::kAutofillEnableVirtualCards) &&
creditCard->virtual_card_enrollment_state() ==
autofill::CreditCard::VirtualCardEnrollmentState::kEnrolled) {
const autofill::CreditCard virtualCard =
autofill::CreditCard::CreateVirtualCard(*creditCard);
[creditCardData
addObject:[[CreditCardData alloc]
initWithCreditCard:virtualCard
icon:[self
iconForCreditCard:creditCard]]];
}
[creditCardData
addObject:[[CreditCardData alloc]
initWithCreditCard:*creditCard
icon:[self iconForCreditCard:creditCard]]];
hasNonLocalCard |= !autofill::IsCreditCardLocal(*creditCard);
}
[consumer setCreditCardData:creditCardData showGooglePayLogo:hasNonLocalCard];
_hasCreditCards = YES;
}
#pragma mark - PaymentsSuggestionBottomSheetDelegate
- (void)didSelectCreditCard:(CreditCardData*)creditCardData
atIndex:(NSInteger)index {
if (!_webStateList) {
return;
}
web::WebState* activeWebState = _webStateList->GetActiveWebState();
if (!activeWebState) {
return;
}
FormSuggestionTabHelper* tabHelper =
FormSuggestionTabHelper::FromWebState(activeWebState);
DCHECK(tabHelper);
id<FormInputSuggestionsProvider> provider =
tabHelper->GetAccessoryViewProvider();
DCHECK(provider);
if (provider.type != SuggestionProviderTypeAutofill) {
// Last resort safety exit: On the unlikely event that the provider was set
// incorrectly (for example if local predictions and server predictions are
// different), simply exit and open the keyboard.
[self disableBottomSheetAndRefocus:YES];
[self logExitReason:kBadProvider];
return;
}
[self disableBottomSheetAndRefocus:NO];
// Create a form suggestion containing the selected credit card's backend id
// so that the suggestion provider can properly fill the form.
FormSuggestion* suggestion = [FormSuggestion
suggestionWithValue:nil
minorValue:nil
displayDescription:nil
icon:nil
type:((base::FeatureList::IsEnabled(
autofill::features::
kAutofillEnableVirtualCards) &&
([creditCardData recordType] ==
autofill::CreditCard::RecordType::
kVirtualCard))
? autofill::SuggestionType::
kVirtualCreditCardEntry
: autofill::SuggestionType::
kCreditCardEntry)
backendIdentifier:[creditCardData backendIdentifier]
requiresReauth:NO
acceptanceA11yAnnouncement:
base::SysUTF16ToNSString(l10n_util::GetStringUTF16(
IDS_AUTOFILL_A11Y_ANNOUNCE_FILLED_FORM))];
[provider didSelectSuggestion:suggestion atIndex:index params:_params];
}
- (void)disableBottomSheetAndRefocus:(BOOL)refocus {
if (_webStateList) {
web::WebState* activeWebState = _webStateList->GetActiveWebState();
AutofillBottomSheetTabHelper::FromWebState(activeWebState)
->DetachPaymentsListenersForAllFrames(refocus);
}
}
#pragma mark - PersonalDataManagerObserver
- (void)onPersonalDataChanged {
DCHECK(_personalDataManager);
// Refresh the data in the consumer
if (self.consumer) {
[self setConsumer:self.consumer];
}
}
#pragma mark - WebStateListObserving
- (void)didChangeWebStateList:(WebStateList*)webStateList
change:(const WebStateListChange&)change
status:(const WebStateListStatus&)status {
DCHECK_EQ(_webStateList, webStateList);
if (status.active_web_state_change()) {
[self onWebStateChange];
}
}
- (void)webStateListDestroyed:(WebStateList*)webStateList {
DCHECK_EQ(webStateList, _webStateList);
// `disconnect` cleans up all references to `_webStateList` and objects that
// depend on it.
[self disconnect];
[self onWebStateChange];
}
#pragma mark - CRWWebStateObserver
- (void)webStateDestroyed:(web::WebState*)webState {
[self onWebStateChange];
}
- (void)renderProcessGoneForWebState:(web::WebState*)webState {
[self onWebStateChange];
}
#pragma mark - Private
- (void)onWebStateChange {
[self.consumer dismiss];
}
// Make sure the suggestions provider is properly set up. We need to make sure
// that FormSuggestionController's "_provider" member is set, which happens
// within [FormSuggestionController onSuggestionsReady:provider:], before the
// credit card suggestion is selected.
// TODO(crbug.com/40929827): Remove this dependency on suggestions.
- (void)setupSuggestionsProvider {
web::WebState* activeWebState = _webStateList->GetActiveWebState();
if (!activeWebState) {
return;
}
FormSuggestionTabHelper* tabHelper =
FormSuggestionTabHelper::FromWebState(activeWebState);
if (!tabHelper) {
return;
}
id<FormInputSuggestionsProvider> provider =
tabHelper->GetAccessoryViewProvider();
// Setting this to true only when we are retrieving suggestions for the bottom
// sheet. We are not using the results from this call, it is just to set the
// provider so the bottom sheet can fill the fields later.
autofill::FormActivityParams params = _params;
params.has_user_gesture = true;
[provider retrieveSuggestionsForForm:params
webState:activeWebState
accessoryViewUpdateBlock:nil];
}
// Returns the icon associated with the provided credit card.
- (UIImage*)iconForCreditCard:(const autofill::CreditCard*)creditCard {
// Check if custom card art is available.
GURL cardArtURL =
_personalDataManager->payments_data_manager().GetCardArtURL(*creditCard);
if (!cardArtURL.is_empty() && cardArtURL.is_valid()) {
gfx::Image* image = _personalDataManager->payments_data_manager()
.GetCreditCardArtImageForUrl(cardArtURL);
if (image) {
return image->ToUIImage();
}
}
// Otherwise, try to get the default card icon
autofill::Suggestion::Icon icon = creditCard->CardIconForAutofillSuggestion();
return icon == autofill::Suggestion::Icon::kNoIcon
? nil
: ui::ResourceBundle::GetSharedInstance()
.GetNativeImageNamed(
autofill::CreditCard::IconResourceId(icon))
.ToUIImage();
}
@end