| // 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. |
| |
| #include "chrome/browser/fast_checkout/fast_checkout_client_impl.h" |
| #include <cmath> |
| |
| #include "base/containers/flat_set.h" |
| #include "base/guid.h" |
| #include "base/metrics/metrics_hashes.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "chrome/browser/fast_checkout/fast_checkout_accessibility_service_impl.h" |
| #include "chrome/browser/fast_checkout/fast_checkout_capabilities_fetcher_factory.h" |
| #include "chrome/browser/fast_checkout/fast_checkout_enums.h" |
| #include "chrome/browser/fast_checkout/fast_checkout_personal_data_helper_impl.h" |
| #include "chrome/browser/fast_checkout/fast_checkout_trigger_validator_impl.h" |
| #include "chrome/browser/ui/autofill/chrome_autofill_client.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "components/autofill/core/browser/data_model/autofill_profile.h" |
| #include "components/autofill/core/browser/data_model/credit_card.h" |
| #include "components/autofill/core/common/dense_set.h" |
| #include "content/public/browser/web_contents_user_data.h" |
| #include "services/metrics/public/cpp/ukm_builders.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "url/gurl.h" |
| |
| namespace { |
| constexpr base::TimeDelta kSleepBetweenTriggerReparseCalls = base::Seconds(1); |
| constexpr base::TimeDelta kTimeout = base::Minutes(30); |
| |
| constexpr auto kSupportedFormTypes = base::MakeFixedFlatSet<autofill::FormType>( |
| {autofill::FormType::kAddressForm, autofill::FormType::kCreditCardForm}); |
| |
| constexpr auto kAddressFieldTypes = |
| base::MakeFixedFlatSet<autofill::FieldTypeGroup>( |
| {autofill::FieldTypeGroup::kName, autofill::FieldTypeGroup::kEmail, |
| autofill::FieldTypeGroup::kPhoneHome, |
| autofill::FieldTypeGroup::kAddressHome}); |
| |
| bool IsVisibleTextField(const autofill::AutofillField& field) { |
| return field.IsFocusable() && field.IsTextInputElement(); |
| } |
| |
| autofill::AutofillField* GetFieldToFill( |
| const std::vector<std::unique_ptr<autofill::AutofillField>>& fields, |
| bool is_credit_card_form) { |
| for (const std::unique_ptr<autofill::AutofillField>& field : fields) { |
| if (IsVisibleTextField(*field) && field->IsEmpty() && |
| ((!is_credit_card_form && |
| kAddressFieldTypes.contains(field->Type().group())) || |
| (is_credit_card_form && |
| field->Type().GetStorableType() == autofill::CREDIT_CARD_NUMBER))) { |
| return field.get(); |
| } |
| } |
| return nullptr; |
| } |
| |
| bool IsNameOrAddress(autofill::FieldTypeGroup type_group) { |
| return type_group == autofill::FieldTypeGroup::kName || |
| type_group == autofill::FieldTypeGroup::kAddressHome || |
| type_group == autofill::FieldTypeGroup::kAddressBilling; |
| } |
| |
| // Returns `true` if `form` is considered an address form containing only an |
| // `email` field but no `name` or `address` fields. |
| bool IsEmailForm(const autofill::FormStructure& form) { |
| // `kAddressForm` includes email fields. |
| bool is_address_form = |
| form.GetFormTypes().contains(autofill::FormType::kAddressForm); |
| bool has_name_or_address_field = base::ranges::any_of( |
| form.fields().begin(), form.fields().end(), |
| [](const std::unique_ptr<autofill::AutofillField>& field) { |
| autofill::FieldTypeGroup type_group = field->Type().group(); |
| return IsNameOrAddress(type_group) && IsVisibleTextField(*field); |
| }); |
| bool has_focusable_email_field = base::ranges::any_of( |
| form.fields().begin(), form.fields().end(), |
| [](const std::unique_ptr<autofill::AutofillField>& field) { |
| return field->Type().group() == autofill::FieldTypeGroup::kEmail && |
| IsVisibleTextField(*field); |
| }); |
| return is_address_form && has_focusable_email_field && |
| !has_name_or_address_field; |
| } |
| |
| // Returns `true` if `form_signature`'s form is in `forms` and is an email form. |
| bool ContainsEmailFormWithSignature( |
| const std::map<autofill::FormGlobalId, |
| std::unique_ptr<autofill::FormStructure>>& forms, |
| autofill::FormSignature form_signature) { |
| for (auto& [_, form] : forms) { |
| // It is possible to have multiple forms with the same form signature on the |
| // same page where only some are visible to the user. An example could be |
| // shipping and billing address forms. For that reason the `IsEmailForm` |
| // check must not be returned directly to avoid a premature return as we |
| // don't have any control over the order of `forms`. |
| if (form->form_signature() == form_signature && IsEmailForm(*form)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| } // namespace |
| |
| FastCheckoutClientImpl::FastCheckoutClientImpl( |
| content::WebContents* web_contents) |
| : content::WebContentsUserData<FastCheckoutClientImpl>(*web_contents), |
| autofill_client_( |
| autofill::ChromeAutofillClient::FromWebContents(web_contents)), |
| fetcher_(FastCheckoutCapabilitiesFetcherFactory::GetForBrowserContext( |
| web_contents->GetBrowserContext())), |
| personal_data_helper_( |
| std::make_unique<FastCheckoutPersonalDataHelperImpl>(web_contents)), |
| trigger_validator_(std::make_unique<FastCheckoutTriggerValidatorImpl>( |
| autofill_client_, |
| fetcher_, |
| personal_data_helper_.get())), |
| accessibility_service_( |
| std::make_unique<FastCheckoutAccessibilityServiceImpl>()) {} |
| |
| FastCheckoutClientImpl::~FastCheckoutClientImpl() = default; |
| |
| bool FastCheckoutClientImpl::TryToStart( |
| const GURL& url, |
| const autofill::FormData& form, |
| const autofill::FormFieldData& field, |
| base::WeakPtr<autofill::AutofillManager> autofill_manager) { |
| if (!autofill_manager) { |
| return false; |
| } |
| |
| if (!trigger_validator_->ShouldRun(form, field, fast_checkout_ui_state_, |
| is_running_, autofill_manager)) { |
| return false; |
| } |
| |
| autofill_manager_ = autofill_manager; |
| origin_ = url::Origin::Create(url); |
| is_running_ = true; |
| personal_data_manager_observation_.Observe( |
| personal_data_helper_->GetPersonalDataManager()); |
| autofill_manager_observation_.Observe(autofill_manager_.get()); |
| run_id_ = |
| base::HashMetricName(base::GUID::GenerateRandomV4().AsLowercaseString()); |
| |
| SetFormsToFill(); |
| SetShouldSuppressKeyboard(true); |
| |
| fast_checkout_controller_ = CreateFastCheckoutController(); |
| ShowFastCheckoutUI(); |
| |
| fast_checkout_ui_state_ = FastCheckoutUIState::kIsShowing; |
| autofill_client_->HideAutofillPopup( |
| autofill::PopupHidingReason::kOverlappingWithFastCheckoutSurface); |
| |
| return true; |
| } |
| |
| void FastCheckoutClientImpl::ShowFastCheckoutUI() { |
| fast_checkout_controller_->Show( |
| personal_data_helper_->GetProfilesToSuggest(), |
| personal_data_helper_->GetCreditCardsToSuggest()); |
| } |
| |
| void FastCheckoutClientImpl::SetShouldSuppressKeyboard(bool suppress) { |
| if (autofill_manager_) { |
| autofill_manager_->SetShouldSuppressKeyboard(suppress); |
| } |
| } |
| |
| void FastCheckoutClientImpl::OnRunComplete(FastCheckoutRunOutcome run_outcome, |
| bool allow_further_runs) { |
| ukm::builders::Autofill_FastCheckoutRunOutcome builder( |
| GetWebContents().GetPrimaryMainFrame()->GetPageUkmSourceId()); |
| builder.SetRunOutcome(static_cast<int64_t>(run_outcome)); |
| builder.SetRunId(run_id_); |
| builder.Record(ukm::UkmRecorder::Get()); |
| Stop(allow_further_runs); |
| } |
| |
| void FastCheckoutClientImpl::Stop(bool allow_further_runs) { |
| // `OnHidden` is not called if the bottom sheet never managed to show, |
| // e.g. due to a failed onboarding. This ensures that keyboard suppression |
| // stops. |
| SetShouldSuppressKeyboard(false); |
| |
| // Reset run related state. |
| is_running_ = false; |
| form_filling_states_.clear(); |
| form_signatures_to_fill_.clear(); |
| selected_autofill_profile_guid_ = absl::nullopt; |
| selected_credit_card_guid_ = absl::nullopt; |
| timeout_timer_.AbandonAndStop(); |
| credit_card_form_global_id_ = absl::nullopt; |
| run_id_ = 0; |
| // Reset UI related state. |
| fast_checkout_controller_.reset(); |
| // Reset personal data manager observation. |
| personal_data_manager_observation_.Reset(); |
| // Reset `autofill_manager_` and related objects. |
| reparse_timer_.AbandonAndStop(); |
| autofill_manager_observation_.Reset(); |
| autofill_manager_.reset(); |
| |
| if (!allow_further_runs && IsShowing()) { |
| fast_checkout_ui_state_ = FastCheckoutUIState::kWasShown; |
| } else { |
| fast_checkout_ui_state_ = FastCheckoutUIState::kNotShownYet; |
| } |
| } |
| |
| bool FastCheckoutClientImpl::IsShowing() const { |
| return fast_checkout_ui_state_ == FastCheckoutUIState::kIsShowing; |
| } |
| |
| bool FastCheckoutClientImpl::IsRunning() const { |
| return is_running_; |
| } |
| |
| std::unique_ptr<FastCheckoutController> |
| FastCheckoutClientImpl::CreateFastCheckoutController() { |
| return std::make_unique<FastCheckoutControllerImpl>(&GetWebContents(), this); |
| } |
| |
| void FastCheckoutClientImpl::OnHidden() { |
| fast_checkout_ui_state_ = FastCheckoutUIState::kWasShown; |
| SetShouldSuppressKeyboard(false); |
| } |
| |
| void FastCheckoutClientImpl::OnOptionsSelected( |
| std::unique_ptr<autofill::AutofillProfile> selected_profile, |
| std::unique_ptr<autofill::CreditCard> selected_credit_card) { |
| OnHidden(); |
| selected_autofill_profile_guid_ = selected_profile->guid(); |
| selected_credit_card_guid_ = selected_credit_card->guid(); |
| timeout_timer_.Start(FROM_HERE, kTimeout, |
| base::BindOnce(&FastCheckoutClientImpl::OnRunComplete, |
| weak_ptr_factory_.GetWeakPtr(), |
| FastCheckoutRunOutcome::kTimeout, |
| /*allow_further_runs=*/true)); |
| TryToFillForms(); |
| autofill_manager_->TriggerReparseInAllFrames( |
| base::BindOnce(&FastCheckoutClientImpl::OnTriggerReparseFinished, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void FastCheckoutClientImpl::SetFormsToFill() { |
| if (!fetcher_) { |
| return; |
| } |
| DCHECK(form_filling_states_.empty()); |
| DCHECK(form_signatures_to_fill_.empty()); |
| form_signatures_to_fill_ = fetcher_->GetFormsToFill(origin_); |
| } |
| |
| void FastCheckoutClientImpl::OnDismiss() { |
| OnRunComplete(FastCheckoutRunOutcome::kBottomsheetDismissed, |
| /*allow_further_runs=*/false); |
| } |
| |
| void FastCheckoutClientImpl::OnPersonalDataChanged() { |
| if (!IsShowing()) { |
| return; |
| } |
| |
| if (!trigger_validator_->HasValidPersonalData()) { |
| OnRunComplete(FastCheckoutRunOutcome::kInvalidPersonalData, |
| /*allow_further_runs=*/false); |
| } else { |
| ShowFastCheckoutUI(); |
| } |
| } |
| |
| bool FastCheckoutClientImpl::AllFormsAreFilled() const { |
| return base::ranges::all_of(form_filling_states_.begin(), |
| form_filling_states_.end(), |
| [](const auto& pair) { |
| return pair.second == FillingState::kFilled; |
| }) && |
| base::ranges::all_of( |
| form_signatures_to_fill_.begin(), form_signatures_to_fill_.end(), |
| [&](autofill::FormSignature form_signature) { |
| return form_filling_states_.contains(std::make_pair( |
| form_signature, autofill::FormType::kAddressForm)) || |
| form_filling_states_.contains(std::make_pair( |
| form_signature, autofill::FormType::kCreditCardForm)); |
| }); |
| } |
| |
| bool FastCheckoutClientImpl::IsFilling() const { |
| return IsRunning() && selected_autofill_profile_guid_ && |
| selected_credit_card_guid_; |
| } |
| |
| void FastCheckoutClientImpl::OnAfterLoadedServerPredictions() { |
| TryToFillForms(); |
| } |
| |
| void FastCheckoutClientImpl::OnTriggerReparseFinished(bool success) { |
| // `success == true` if `TriggerReparseInAllFrames()` was not called multiple |
| // times in parallel, potentially by another actor. |
| DCHECK(success); |
| if (!reparse_timer_.IsRunning()) { |
| // Trigger reparse in all frames continuously until the run stops. That will |
| // eventually trigger this (`OnAfterLoadedServerPredictions()`) method. |
| reparse_timer_.Start( |
| FROM_HERE, kSleepBetweenTriggerReparseCalls, |
| base::BindOnce( |
| &autofill::AutofillManager::TriggerReparseInAllFrames, |
| autofill_manager_, |
| base::BindOnce(&FastCheckoutClientImpl::OnTriggerReparseFinished, |
| weak_ptr_factory_.GetWeakPtr()))); |
| } |
| } |
| |
| void FastCheckoutClientImpl::TryToFillForms() { |
| if (!IsFilling()) { |
| return; |
| } |
| SetFormFillingStates(); |
| for (const auto& [form_global_id, form] : |
| autofill_manager_->form_structures()) { |
| if (ShouldFillForm(*form, autofill::FormType::kAddressForm)) { |
| autofill::AutofillField* field = |
| GetFieldToFill(form->fields(), /*is_credit_card_form=*/false); |
| autofill::AutofillProfile* autofill_profile = |
| GetSelectedAutofillProfile(); |
| if (field && autofill_profile) { |
| form_filling_states_[std::make_pair(form->form_signature(), |
| autofill::FormType::kAddressForm)] = |
| FillingState::kFilling; |
| static_cast<autofill::BrowserAutofillManager*>(autofill_manager_.get()) |
| ->SetFastCheckoutRunId(autofill::FieldTypeGroup::kAddressHome, |
| run_id_); |
| autofill_manager_->FillProfileForm(*autofill_profile, |
| form->ToFormData(), *field); |
| } |
| } |
| |
| if (ShouldFillForm(*form, autofill::FormType::kCreditCardForm)) { |
| autofill::AutofillField* field = |
| GetFieldToFill(form->fields(), /*is_credit_card_form=*/true); |
| autofill::CreditCard* credit_card = GetSelectedCreditCard(); |
| if (field && !credit_card_form_global_id_ && credit_card) { |
| autofill::CreditCardCvcAuthenticator* cvc_authenticator = |
| autofill_client_->GetCvcAuthenticator(); |
| DCHECK(cvc_authenticator); |
| credit_card_form_global_id_ = form_global_id; |
| cvc_authenticator->GetFullCardRequest()->GetFullCard( |
| *credit_card, autofill::AutofillClient::UnmaskCardReason::kAutofill, |
| weak_ptr_factory_.GetWeakPtr(), |
| cvc_authenticator->GetAsFullCardRequestUIDelegate()); |
| } |
| } |
| } |
| } |
| |
| autofill::AutofillProfile* |
| FastCheckoutClientImpl::GetSelectedAutofillProfile() { |
| autofill::AutofillProfile* autofill_profile = |
| personal_data_helper_->GetPersonalDataManager()->GetProfileByGUID( |
| selected_autofill_profile_guid_.value()); |
| if (!autofill_profile) { |
| OnRunComplete(FastCheckoutRunOutcome::kAutofillProfileDeleted); |
| } |
| return autofill_profile; |
| } |
| |
| autofill::CreditCard* FastCheckoutClientImpl::GetSelectedCreditCard() { |
| autofill::CreditCard* credit_card = |
| personal_data_helper_->GetPersonalDataManager()->GetCreditCardByGUID( |
| selected_credit_card_guid_.value()); |
| if (!credit_card) { |
| OnRunComplete(FastCheckoutRunOutcome::kCreditCardDeleted); |
| } |
| return credit_card; |
| } |
| |
| void FastCheckoutClientImpl::SetFormFillingStates() { |
| for (const auto& [_, form] : autofill_manager_->form_structures()) { |
| // Only attempt to fill forms that were provided by the |
| // `FastCheckoutCapabilitiesFetcher`. |
| if (!form_signatures_to_fill_.contains(form->form_signature())) { |
| continue; |
| } |
| autofill::DenseSet<autofill::FormType> form_types = form->GetFormTypes(); |
| for (autofill::FormType form_type : kSupportedFormTypes) { |
| // Only attempt to fill forms if they match `form_type`. |
| if (!form_types.contains(form_type)) { |
| continue; |
| } |
| auto form_id = std::make_pair(form->form_signature(), form_type); |
| if (!form_filling_states_.contains(form_id)) { |
| form_filling_states_[form_id] = FillingState::kNotFilled; |
| } |
| } |
| } |
| } |
| |
| void FastCheckoutClientImpl::OnFullCardRequestSucceeded( |
| const autofill::payments::FullCardRequest& full_card_request, |
| const autofill::CreditCard& card, |
| const std::u16string& cvc) { |
| if (!IsFilling() || !credit_card_form_global_id_) { |
| return; |
| } |
| if (!autofill_manager_->form_structures().contains( |
| credit_card_form_global_id_.value())) { |
| credit_card_form_global_id_ = absl::nullopt; |
| return; |
| } |
| const std::unique_ptr<autofill::FormStructure>& form = |
| autofill_manager_->form_structures().at( |
| credit_card_form_global_id_.value()); |
| if (autofill::AutofillField* field = |
| GetFieldToFill(form->fields(), /*is_credit_card_form=*/true)) { |
| form_filling_states_[std::make_pair(form->form_signature(), |
| autofill::FormType::kCreditCardForm)] = |
| FillingState::kFilling; |
| static_cast<autofill::BrowserAutofillManager*>(autofill_manager_.get()) |
| ->SetFastCheckoutRunId(autofill::FieldTypeGroup::kCreditCard, run_id_); |
| autofill_manager_->FillCreditCardForm(form->ToFormData(), *field, card, |
| cvc); |
| } |
| credit_card_form_global_id_ = absl::nullopt; |
| } |
| |
| void FastCheckoutClientImpl::OnFullCardRequestFailed( |
| autofill::CreditCard::RecordType card_type, |
| autofill::payments::FullCardRequest::FailureType failure_type) { |
| if (!IsFilling() || !credit_card_form_global_id_) { |
| return; |
| } |
| if (failure_type == |
| autofill::payments::FullCardRequest::FailureType::PROMPT_CLOSED) { |
| OnRunComplete(FastCheckoutRunOutcome::kCvcPopupClosed, |
| /*allow_further_runs=*/false); |
| } else { |
| OnRunComplete(FastCheckoutRunOutcome::kCvcPopupError, |
| /*allow_further_runs=*/false); |
| } |
| } |
| |
| void FastCheckoutClientImpl::OnAfterDidFillAutofillFormData() { |
| if (!IsFilling()) { |
| return; |
| } |
| UpdateFillingStates(); |
| if (AllFormsAreFilled()) { |
| OnRunComplete(FastCheckoutRunOutcome::kSuccess); |
| } |
| } |
| |
| void FastCheckoutClientImpl::UpdateFillingStates() { |
| for (auto& [form_id, filling_state] : form_filling_states_) { |
| const auto& [form_signature, form_type] = form_id; |
| if (form_type == autofill::FormType::kAddressForm && |
| filling_state == FillingState::kFilling) { |
| // Assume that if `OnAfterDidFillAutofillFormData()` is called while |
| // `this` is in filling mode and there's an address form in `kFilling` |
| // state that it got filled. |
| filling_state = FillingState::kFilled; |
| A11yAnnounce(form_signature, /*is_credit_card_form=*/false); |
| } else if (form_type == autofill::FormType::kCreditCardForm) { |
| auto address_form_id = |
| std::make_pair(form_signature, autofill::FormType::kAddressForm); |
| if (form_filling_states_.contains(address_form_id) && |
| form_filling_states_[address_form_id] == FillingState::kFilling) { |
| // Assume that the address part was filled first if the corresponding |
| // form is both an address and a credit card form. |
| continue; |
| } else if (filling_state == FillingState::kFilling) { |
| // Assume that if `OnAfterDidFillAutofillFormData()` is called while |
| // `this` is in filling mode and there's a credit card form in |
| // `kFilling` state - while no address form of the same signature is in |
| // `kFilling` state - that it got filled. |
| filling_state = FillingState::kFilled; |
| A11yAnnounce(form_signature, /*is_credit_card_form=*/true); |
| } |
| } |
| } |
| } |
| |
| void FastCheckoutClientImpl::A11yAnnounce( |
| autofill::FormSignature form_signature, |
| bool is_credit_card_form) { |
| if (is_credit_card_form) { |
| if (autofill::CreditCard* credit_card = GetSelectedCreditCard()) { |
| accessibility_service_->Announce(l10n_util::GetStringFUTF16( |
| IDS_FAST_CHECKOUT_A11Y_CREDIT_CARD_FORM_FILLED, |
| credit_card->HasNonEmptyValidNickname() |
| ? credit_card->nickname() |
| : credit_card->NetworkAndLastFourDigits())); |
| } |
| return; |
| } |
| |
| if (ContainsEmailFormWithSignature(autofill_manager_->form_structures(), |
| form_signature)) { |
| accessibility_service_->Announce( |
| l10n_util::GetStringUTF16(IDS_FAST_CHECKOUT_A11Y_EMAIL_FILLED)); |
| } else if (autofill::AutofillProfile* autofill_profile = |
| GetSelectedAutofillProfile()) { |
| accessibility_service_->Announce(l10n_util::GetStringFUTF16( |
| IDS_FAST_CHECKOUT_A11Y_ADDRESS_FORM_FILLED, |
| base::UTF8ToUTF16(autofill_profile->profile_label()))); |
| } |
| } |
| |
| void FastCheckoutClientImpl::OnAutofillManagerDestroyed() { |
| if (IsRunning()) { |
| if (GetWebContents().IsBeingDestroyed()) { |
| OnRunComplete(FastCheckoutRunOutcome::kTabClosed); |
| } else { |
| OnRunComplete(FastCheckoutRunOutcome::kAutofillManagerDestroyed); |
| } |
| return; |
| } |
| Stop(/*allow_further_runs=*/true); |
| } |
| |
| void FastCheckoutClientImpl::OnAutofillManagerReset() { |
| if (IsShowing()) { |
| OnRunComplete(FastCheckoutRunOutcome::kNavigationWhileBottomsheetWasShown); |
| } |
| } |
| |
| bool FastCheckoutClientImpl::ShouldFillForm( |
| const autofill::FormStructure& form, |
| autofill::FormType expected_form_type) const { |
| // Only attempt to fill forms that were provided by the |
| // `FastCheckoutCapabilitiesFetcher`. |
| if (!form_signatures_to_fill_.contains(form.form_signature())) { |
| return false; |
| } |
| // Only attempt to fill forms if they match `expected_form_type`. |
| if (!form.GetFormTypes().contains(expected_form_type)) { |
| return false; |
| } |
| // Attempt to fill forms once only. |
| return form_filling_states_.at( |
| std::make_pair(form.form_signature(), expected_form_type)) == |
| FillingState::kNotFilled; |
| } |
| |
| void FastCheckoutClientImpl::OnNavigation(const GURL& url, |
| bool is_cart_or_checkout_url) { |
| if (!IsRunning()) { |
| fast_checkout_ui_state_ = FastCheckoutUIState::kNotShownYet; |
| return; |
| } |
| if (url::Origin::Create(url) != origin_) { |
| OnRunComplete(FastCheckoutRunOutcome::kOriginChange); |
| } else if (!is_cart_or_checkout_url) { |
| OnRunComplete(FastCheckoutRunOutcome::kNonCheckoutPage); |
| } |
| } |
| |
| WEB_CONTENTS_USER_DATA_KEY_IMPL(FastCheckoutClientImpl); |