| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/user_education/common/feature_promo_specification.h" |
| |
| #include <string> |
| |
| #include "base/containers/flat_set.h" |
| #include "base/feature_list.h" |
| #include "base/functional/callback_forward.h" |
| #include "components/strings/grit/components_strings.h" |
| #include "ui/base/accelerators/accelerator.h" |
| #include "ui/base/interaction/element_identifier.h" |
| #include "ui/base/interaction/element_tracker.h" |
| #include "ui/base/l10n/l10n_util.h" |
| |
| namespace user_education { |
| |
| namespace { |
| |
| // This function provides the list of allowed legal promos. |
| // It is not to be modified except by the Frizzle team. |
| bool IsAllowedLegalNotice(const base::Feature& promo_feature) { |
| // Add the text names of allowlisted critical promos here: |
| static const char* const kAllowedPromoNames[] = { |
| "IPH_TrackingProtectionOnboarding", |
| "IPH_TrackingProtectionOffboarding", |
| }; |
| for (const auto* promo_name : kAllowedPromoNames) { |
| if (!strcmp(promo_feature.name, promo_name)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| bool IsAllowedActionableAlert(const base::Feature& promo_feature) { |
| // Add the text names of allowlisted actionable alerts here: |
| static const char* const kAllowedPromoNames[] = { |
| "IPH_DownloadEsbPromo", |
| "IPH_HighEfficiencyMode", |
| }; |
| for (const auto* promo_name : kAllowedPromoNames) { |
| if (!strcmp(promo_feature.name, promo_name)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| bool IsAllowedLegacyPromo(const base::Feature& promo_feature) { |
| // NOTE: LEGACY PROMOS ARE DEPRECATED. |
| // NO NEW ITEMS SHOULD BE ADDED TO THIS LIST, EVER. |
| static const char* const kAllowedPromoNames[] = { |
| "IPH_AutofillExternalAccountProfileSuggestion", |
| "IPH_AutofillVirtualCardSuggestion", |
| "IPH_DesktopPwaInstall", |
| "IPH_DesktopSharedHighlighting", |
| "IPH_GMCCastStartStop", |
| "IPH_PasswordsAccountStorage", |
| "IPH_PriceTrackingInSidePanel", |
| "IPH_ReadingListDiscovery", |
| "IPH_ReadingListInSidePanel", |
| "IPH_TabSearch", |
| "IPH_WebUITabStrip", |
| }; |
| |
| const std::string name = promo_feature.name; |
| for (const auto* promo_name : kAllowedPromoNames) { |
| if (name == promo_name) { |
| return true; |
| } |
| } |
| |
| // Features used for tests have this prefix and are excluded. |
| if (name.starts_with("TEST_")) { |
| return true; |
| } |
| |
| return false; |
| } |
| |
| } // namespace |
| |
| FeaturePromoSpecification::AdditionalConditions::AdditionalConditions() = |
| default; |
| FeaturePromoSpecification::AdditionalConditions::AdditionalConditions( |
| AdditionalConditions&&) noexcept = default; |
| FeaturePromoSpecification::AdditionalConditions& |
| FeaturePromoSpecification::AdditionalConditions::operator=( |
| AdditionalConditions&&) noexcept = default; |
| FeaturePromoSpecification::AdditionalConditions::~AdditionalConditions() = |
| default; |
| |
| void FeaturePromoSpecification::AdditionalConditions::AddAdditionalCondition( |
| const AdditionalCondition& additional_condition) { |
| additional_conditions_.emplace_back(additional_condition); |
| } |
| |
| void FeaturePromoSpecification::AdditionalConditions::AddAdditionalCondition( |
| const char* event_name, |
| Constraint constraint, |
| uint32_t count, |
| std::optional<uint32_t> in_days) { |
| AddAdditionalCondition({event_name, constraint, count, in_days}); |
| } |
| |
| FeaturePromoSpecification::AcceleratorInfo::AcceleratorInfo() = default; |
| FeaturePromoSpecification::AcceleratorInfo::AcceleratorInfo( |
| const AcceleratorInfo& other) = default; |
| FeaturePromoSpecification::AcceleratorInfo::AcceleratorInfo(ValueType value) |
| : value_(value) {} |
| FeaturePromoSpecification::AcceleratorInfo& |
| FeaturePromoSpecification::AcceleratorInfo::operator=( |
| const AcceleratorInfo& other) = default; |
| FeaturePromoSpecification::AcceleratorInfo::~AcceleratorInfo() = default; |
| |
| FeaturePromoSpecification::AcceleratorInfo& |
| FeaturePromoSpecification::AcceleratorInfo::operator=(ValueType value) { |
| value_ = value; |
| return *this; |
| } |
| |
| FeaturePromoSpecification::AcceleratorInfo::operator bool() const { |
| return absl::holds_alternative<ui::Accelerator>(value_) || |
| absl::get<int>(value_); |
| } |
| |
| ui::Accelerator FeaturePromoSpecification::AcceleratorInfo::GetAccelerator( |
| const ui::AcceleratorProvider* provider) const { |
| if (absl::holds_alternative<ui::Accelerator>(value_)) |
| return absl::get<ui::Accelerator>(value_); |
| |
| const int command_id = absl::get<int>(value_); |
| DCHECK_GT(command_id, 0); |
| |
| ui::Accelerator result; |
| DCHECK(provider->GetAcceleratorForCommandId(command_id, &result)); |
| return result; |
| } |
| |
| // static |
| constexpr HelpBubbleArrow FeaturePromoSpecification::kDefaultBubbleArrow; |
| |
| FeaturePromoSpecification::FeaturePromoSpecification() = default; |
| |
| FeaturePromoSpecification::FeaturePromoSpecification( |
| FeaturePromoSpecification&& other) noexcept = default; |
| |
| FeaturePromoSpecification::FeaturePromoSpecification( |
| const base::Feature* feature, |
| PromoType promo_type, |
| ui::ElementIdentifier anchor_element_id, |
| int bubble_body_string_id) |
| : feature_(feature), |
| promo_type_(promo_type), |
| anchor_element_id_(anchor_element_id), |
| bubble_body_string_id_(bubble_body_string_id), |
| custom_action_dismiss_string_id_(IDS_PROMO_DISMISS_BUTTON) { |
| DCHECK_NE(promo_type, PromoType::kUnspecified); |
| DCHECK(bubble_body_string_id_); |
| } |
| |
| FeaturePromoSpecification& FeaturePromoSpecification::operator=( |
| FeaturePromoSpecification&& other) noexcept = default; |
| |
| FeaturePromoSpecification::~FeaturePromoSpecification() = default; |
| |
| |
| std::u16string FeaturePromoSpecification::FormatString( |
| int string_id, |
| const FormatParameters& format_params) { |
| if (!string_id) { |
| CHECK(absl::holds_alternative<NoSubstitution>(format_params)); |
| return std::u16string(); |
| } |
| if (absl::holds_alternative<NoSubstitution>(format_params)) { |
| return l10n_util::GetStringUTF16(string_id); |
| } |
| if (const auto* substitutions = |
| absl::get_if<StringSubstitutions>(&format_params)) { |
| return l10n_util::GetStringFUTF16(string_id, *substitutions, nullptr); |
| } |
| if (const std::u16string* str = |
| absl::get_if<std::u16string>(&format_params)) { |
| return l10n_util::GetStringFUTF16(string_id, *str); |
| } |
| int number = absl::get<int>(format_params); |
| return l10n_util::GetPluralStringFUTF16(string_id, number); |
| } |
| |
| // static |
| FeaturePromoSpecification FeaturePromoSpecification::CreateForToastPromo( |
| const base::Feature& feature, |
| ui::ElementIdentifier anchor_element_id, |
| int body_text_string_id, |
| int accessible_text_string_id, |
| AcceleratorInfo accessible_accelerator) { |
| FeaturePromoSpecification spec(&feature, PromoType::kToast, anchor_element_id, |
| body_text_string_id); |
| CHECK_NE(body_text_string_id, accessible_text_string_id) |
| << "Because toasts are hard to notice and time out quickly, screen " |
| "reader text associated with toasts should differ from the bubble " |
| "text and either provide the accelerator to access the highlighted " |
| "entry point for your feature, or at the very least provide a " |
| "separate description of the screen element appropriate for keyboard " |
| "and low-vision users."; |
| spec.screen_reader_string_id_ = accessible_text_string_id; |
| spec.screen_reader_accelerator_ = std::move(accessible_accelerator); |
| return spec; |
| } |
| |
| // static |
| FeaturePromoSpecification FeaturePromoSpecification::CreateForSnoozePromo( |
| const base::Feature& feature, |
| ui::ElementIdentifier anchor_element_id, |
| int body_text_string_id) { |
| return FeaturePromoSpecification(&feature, PromoType::kSnooze, |
| anchor_element_id, body_text_string_id); |
| } |
| |
| // static |
| FeaturePromoSpecification FeaturePromoSpecification::CreateForSnoozePromo( |
| const base::Feature& feature, |
| ui::ElementIdentifier anchor_element_id, |
| int body_text_string_id, |
| int accessible_text_string_id, |
| AcceleratorInfo accessible_accelerator) { |
| // See `FeaturePromoSpecification::CreateForToastPromo()`. |
| CHECK_NE(body_text_string_id, accessible_text_string_id); |
| FeaturePromoSpecification spec(&feature, PromoType::kSnooze, |
| anchor_element_id, body_text_string_id); |
| spec.screen_reader_string_id_ = accessible_text_string_id; |
| spec.screen_reader_accelerator_ = std::move(accessible_accelerator); |
| return spec; |
| } |
| |
| // static |
| FeaturePromoSpecification FeaturePromoSpecification::CreateForTutorialPromo( |
| const base::Feature& feature, |
| ui::ElementIdentifier anchor_element_id, |
| int body_text_string_id, |
| TutorialIdentifier tutorial_id) { |
| FeaturePromoSpecification spec(&feature, PromoType::kTutorial, |
| anchor_element_id, body_text_string_id); |
| DCHECK(!tutorial_id.empty()); |
| spec.tutorial_id_ = tutorial_id; |
| return spec; |
| } |
| |
| // static |
| FeaturePromoSpecification FeaturePromoSpecification::CreateForCustomAction( |
| const base::Feature& feature, |
| ui::ElementIdentifier anchor_element_id, |
| int body_text_string_id, |
| int custom_action_string_id, |
| CustomActionCallback custom_action_callback) { |
| FeaturePromoSpecification spec(&feature, PromoType::kCustomAction, |
| anchor_element_id, body_text_string_id); |
| spec.custom_action_caption_ = |
| l10n_util::GetStringUTF16(custom_action_string_id); |
| spec.custom_action_callback_ = custom_action_callback; |
| return spec; |
| } |
| |
| // static |
| FeaturePromoSpecification FeaturePromoSpecification::CreateForLegacyPromo( |
| const base::Feature* feature, |
| ui::ElementIdentifier anchor_element_id, |
| int body_text_string_id) { |
| CHECK(!feature || IsAllowedLegacyPromo(*feature)) |
| << "Cannot create promo: " << feature->name |
| << "\nNo new legacy promos may be created; use CreateForToastPromo() " |
| "instead."; |
| return FeaturePromoSpecification(feature, PromoType::kLegacy, |
| anchor_element_id, body_text_string_id); |
| } |
| |
| FeaturePromoSpecification& FeaturePromoSpecification::SetBubbleTitleText( |
| int title_text_string_id) { |
| DCHECK_NE(promo_type_, PromoType::kUnspecified); |
| bubble_title_string_id_ = title_text_string_id; |
| return *this; |
| } |
| |
| FeaturePromoSpecification& FeaturePromoSpecification::SetBubbleIcon( |
| const gfx::VectorIcon* bubble_icon) { |
| DCHECK_NE(promo_type_, PromoType::kUnspecified); |
| bubble_icon_ = bubble_icon; |
| return *this; |
| } |
| |
| FeaturePromoSpecification& FeaturePromoSpecification::SetBubbleArrow( |
| HelpBubbleArrow bubble_arrow) { |
| bubble_arrow_ = bubble_arrow; |
| return *this; |
| } |
| |
| FeaturePromoSpecification& FeaturePromoSpecification::OverrideFocusOnShow( |
| bool focus_on_show) { |
| focus_on_show_override_ = focus_on_show; |
| return *this; |
| } |
| |
| FeaturePromoSpecification& FeaturePromoSpecification::SetPromoSubtype( |
| PromoSubtype promo_subtype) { |
| CHECK_NE(promo_type_, PromoType::kUnspecified); |
| CHECK_NE(promo_type_, PromoType::kSnooze) |
| << "Basic snooze is not compatible with other promo subtypes."; |
| switch (promo_subtype) { |
| case PromoSubtype::kLegalNotice: |
| CHECK(feature_); |
| CHECK(IsAllowedLegalNotice(*feature_)); |
| break; |
| case PromoSubtype::kActionableAlert: |
| CHECK_EQ(promo_type_, PromoType::kCustomAction); |
| CHECK(feature_); |
| CHECK(IsAllowedActionableAlert(*feature_)); |
| break; |
| default: |
| break; |
| } |
| promo_subtype_ = promo_subtype; |
| return *this; |
| } |
| |
| FeaturePromoSpecification& FeaturePromoSpecification::SetAnchorElementFilter( |
| AnchorElementFilter anchor_element_filter) { |
| anchor_element_filter_ = std::move(anchor_element_filter); |
| return *this; |
| } |
| |
| FeaturePromoSpecification& FeaturePromoSpecification::SetInAnyContext( |
| bool in_any_context) { |
| in_any_context_ = in_any_context; |
| return *this; |
| } |
| |
| FeaturePromoSpecification& FeaturePromoSpecification::SetAdditionalConditions( |
| AdditionalConditions additional_conditions) { |
| additional_conditions_ = std::move(additional_conditions); |
| return *this; |
| } |
| |
| FeaturePromoSpecification& FeaturePromoSpecification::SetMetadata( |
| Metadata metadata) { |
| metadata_ = std::move(metadata); |
| return *this; |
| } |
| |
| FeaturePromoSpecification& FeaturePromoSpecification::SetCustomActionIsDefault( |
| bool custom_action_is_default) { |
| DCHECK(!custom_action_callback_.is_null()); |
| custom_action_is_default_ = custom_action_is_default; |
| return *this; |
| } |
| |
| FeaturePromoSpecification& |
| FeaturePromoSpecification::SetCustomActionDismissText( |
| int custom_action_dismiss_string_id) { |
| DCHECK(promo_type_ == PromoType::kCustomAction); |
| custom_action_dismiss_string_id_ = custom_action_dismiss_string_id; |
| return *this; |
| } |
| |
| FeaturePromoSpecification& FeaturePromoSpecification::SetHighlightedMenuItem( |
| const ui::ElementIdentifier highlighted_menu_identifier) { |
| highlighted_menu_identifier_ = highlighted_menu_identifier; |
| return *this; |
| } |
| |
| ui::TrackedElement* FeaturePromoSpecification::GetAnchorElement( |
| ui::ElementContext context) const { |
| auto* const element_tracker = ui::ElementTracker::GetElementTracker(); |
| if (anchor_element_filter_) { |
| return anchor_element_filter_.Run( |
| in_any_context_ ? element_tracker->GetAllMatchingElementsInAnyContext( |
| anchor_element_id_) |
| : element_tracker->GetAllMatchingElements( |
| anchor_element_id_, context)); |
| } else { |
| return in_any_context_ |
| ? element_tracker->GetElementInAnyContext(anchor_element_id_) |
| : element_tracker->GetFirstMatchingElement(anchor_element_id_, |
| context); |
| } |
| } |
| |
| std::ostream& operator<<(std::ostream& oss, |
| FeaturePromoSpecification::PromoType promo_type) { |
| switch (promo_type) { |
| case FeaturePromoSpecification::PromoType::kLegacy: |
| oss << "kLegacy"; |
| break; |
| case FeaturePromoSpecification::PromoType::kToast: |
| oss << "kToast"; |
| break; |
| case FeaturePromoSpecification::PromoType::kSnooze: |
| oss << "kSnooze"; |
| break; |
| case FeaturePromoSpecification::PromoType::kTutorial: |
| oss << "kTutorial"; |
| break; |
| case FeaturePromoSpecification::PromoType::kCustomAction: |
| oss << "kCustomAction"; |
| break; |
| case FeaturePromoSpecification::PromoType::kUnspecified: |
| oss << "kUnspecified"; |
| break; |
| } |
| return oss; |
| } |
| |
| std::ostream& operator<<( |
| std::ostream& oss, |
| FeaturePromoSpecification::PromoSubtype promo_subtype) { |
| switch (promo_subtype) { |
| case FeaturePromoSpecification::PromoSubtype::kNormal: |
| oss << "kNormal"; |
| break; |
| case FeaturePromoSpecification::PromoSubtype::kPerApp: |
| oss << "kPerApp"; |
| break; |
| case FeaturePromoSpecification::PromoSubtype::kLegalNotice: |
| oss << "kLegalNotice"; |
| break; |
| case FeaturePromoSpecification::PromoSubtype::kActionableAlert: |
| oss << "kActionableAlert"; |
| break; |
| } |
| return oss; |
| } |
| |
| } // namespace user_education |