| // 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. |
| |
| #include "chrome/browser/promos/promos_utils.h" |
| |
| #include <string> |
| #include <vector> |
| |
| #include "base/containers/contains.h" |
| #include "base/feature_list.h" |
| #include "base/json/values_util.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/notreached.h" |
| #include "base/time/time.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/promos/promos_pref_names.h" |
| #include "chrome/browser/promos/promos_types.h" |
| #include "chrome/common/pref_names.h" |
| #include "components/feature_engagement/public/feature_constants.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/prefs/scoped_user_pref_update.h" |
| #include "components/search/ntp_features.h" |
| #include "components/segmentation_platform/embedder/default_model/device_switcher_model.h" |
| #include "components/sync/base/data_type.h" |
| #include "components/sync/service/sync_service.h" |
| |
| namespace promos_utils { |
| |
| // Max impression count per user, per promo for the iOS desktop promos on |
| // desktop. |
| constexpr int kiOSDesktopPromoMaxImpressionCount = 3; |
| |
| // Total impression count per user in their lifetime, for all iOS desktop |
| // promos. |
| constexpr int kiOSDesktopPromoTotalImpressionCount = 10; |
| |
| // Total amount of opt-outs across any Desktop to iOS promo to block impressions |
| // of other instances of Desktop to iOS promos, per user. |
| constexpr int kiOSDesktopPromoTotalOptOuts = 2; |
| |
| // Minimum time threshold between impressions for a given user to see the iOS |
| // desktop promo on desktop. |
| constexpr base::TimeDelta kiOSDesktopPromoCooldownTime = base::Days(90); |
| |
| // IOSDesktopPromoHistogramType returns the promo histogram type for the given |
| // promo type. New promos should add themselves to this check. |
| std::string IOSDesktopPromoHistogramType(IOSPromoType promo_type) { |
| switch (promo_type) { |
| case IOSPromoType::kPassword: |
| return "PasswordPromo"; |
| case IOSPromoType::kAddress: |
| return "AddressPromo"; |
| case IOSPromoType::kPayment: |
| return "PaymentPromo"; |
| } |
| } |
| |
| base::Time GetMostRecentiOSDesktopNtpPromoTimestamp(PrefService* pref_service) { |
| const base::Value::List& timestamps = pref_service->GetList( |
| promos_prefs::kDesktopToiOSNtpPromoAppearanceTimestamps); |
| base::Value::List::const_iterator most_recent_timestamp_iter = |
| std::max_element(timestamps.begin(), timestamps.end()); |
| if (most_recent_timestamp_iter == timestamps.end()) { |
| return base::Time(); |
| } |
| return base::ValueToTime(*most_recent_timestamp_iter).value_or(base::Time()); |
| } |
| |
| // VerifyIOSDesktopPromoTotalImpressions ensures that each individual user sees |
| // no more than a maximum of these promos total in their lifetime. New promos |
| // should add themselves to this check. If `skip_ntp_promo` is true, then this |
| // call will not include ntp promo data in its checks. This allows calling code |
| // from the NTP promo to add special logic. |
| bool VerifyIOSDesktopPromoTotalImpressions(Profile* profile, |
| bool skip_ntp_promo = false) { |
| int total_desktop_promo_impressions = |
| profile->GetPrefs()->GetInteger( |
| promos_prefs::kDesktopToiOSPasswordPromoImpressionsCounter) + |
| profile->GetPrefs()->GetInteger( |
| promos_prefs::kDesktopToiOSAddressPromoImpressionsCounter) + |
| profile->GetPrefs()->GetInteger( |
| promos_prefs::kDesktopToiOSPaymentPromoImpressionsCounter); |
| |
| if (!skip_ntp_promo) { |
| // The Desktop NTP promo shows 10 times in quick succession, but that only |
| // counts as 1 impression for Desktop to iOS promos in general. |
| const base::Value::List& ntp_promo_appearances = |
| profile->GetPrefs()->GetList( |
| promos_prefs::kDesktopToiOSNtpPromoAppearanceTimestamps); |
| total_desktop_promo_impressions += (ntp_promo_appearances.empty()) ? 0 : 1; |
| } |
| |
| return total_desktop_promo_impressions < kiOSDesktopPromoTotalImpressionCount; |
| } |
| |
| // VerifyIOSDesktopPromoTotalOptOuts verifies that a user hasn't opted-out of |
| // seeing more than the allowed amount of opt-outs for all iOS Desktop promos. |
| // New promos should add themselves to this check. |
| bool VerifyIOSDesktopPromoTotalOptOuts(Profile* profile) { |
| std::vector<bool> promo_opt_outs = { |
| profile->GetPrefs()->GetBoolean( |
| promos_prefs::kDesktopToiOSPasswordPromoOptOut), |
| profile->GetPrefs()->GetBoolean( |
| promos_prefs::kDesktopToiOSAddressPromoOptOut), |
| profile->GetPrefs()->GetBoolean( |
| promos_prefs::kDesktopToiOSPaymentPromoOptOut)}; |
| |
| int total_desktop_promo_opt_outs_counter = |
| std::count(promo_opt_outs.begin(), promo_opt_outs.end(), true); |
| |
| return total_desktop_promo_opt_outs_counter < kiOSDesktopPromoTotalOptOuts; |
| } |
| |
| // VerifyMostRecentPromoTimestamp ensures that each individual user sees a |
| // Desktop to iOS promo a maximum of once per cooldown period. New promos should |
| // add themselves to this check. If `skip_ntp_promo` is true, then this call |
| // will not include ntp promo data in its checks. This allows calling code from |
| // the NTP promo to add special logic. |
| bool VerifyMostRecentPromoTimestamp(Profile* profile, |
| bool skip_ntp_promo = false) { |
| std::vector<base::Time> promos_timestamps = { |
| profile->GetPrefs()->GetTime( |
| promos_prefs::kDesktopToiOSPasswordPromoLastImpressionTimestamp), |
| profile->GetPrefs()->GetTime( |
| promos_prefs::kDesktopToiOSAddressPromoLastImpressionTimestamp), |
| profile->GetPrefs()->GetTime( |
| promos_prefs::kDesktopToiOSPaymentPromoLastImpressionTimestamp), |
| }; |
| |
| if (!skip_ntp_promo) { |
| promos_timestamps.emplace_back( |
| GetMostRecentiOSDesktopNtpPromoTimestamp(profile->GetPrefs())); |
| } |
| |
| auto most_recent_promo_timestamp = |
| std::max_element(promos_timestamps.begin(), promos_timestamps.end()); |
| |
| return *most_recent_promo_timestamp + kiOSDesktopPromoCooldownTime < |
| base::Time::Now(); |
| } |
| |
| // Verify that the user is syncing preferences (for impressions and opt-out |
| // tracking), and that they are syncing the specific datatype needed for a given |
| // promo type. |
| bool VerifySyncingDatatypes(const syncer::SyncService& sync_service, |
| IOSPromoType promo_type) { |
| if (!sync_service.GetActiveDataTypes().Has(syncer::PREFERENCES)) { |
| return false; |
| } |
| |
| switch (promo_type) { |
| case IOSPromoType::kPassword: |
| return sync_service.GetActiveDataTypes().Has(syncer::PASSWORDS); |
| case IOSPromoType::kAddress: |
| return sync_service.GetActiveDataTypes().Has(syncer::CONTACT_INFO); |
| case IOSPromoType::kPayment: |
| return sync_service.GetActiveDataTypes().Has( |
| syncer::AUTOFILL_WALLET_DATA); |
| } |
| } |
| |
| // Checks whether promos in general can currently be shown. |
| bool CanShowPromos() { |
| // Don't show the promo if the local state exists and `kPromotionsEnabled` is |
| // false (likely overridden by policy). `kPromotionsEnabled` does not exist on |
| // Android. |
| #if !BUILDFLAG(IS_ANDROID) |
| PrefService* local_state = g_browser_process->local_state(); |
| if (local_state && !local_state->GetBoolean(prefs::kPromotionsEnabled)) { |
| return false; |
| } |
| #endif // !BUILDFLAG(IS_ANDROID) |
| return true; |
| } |
| |
| // RecordIOSDesktopPromoShownHistogram records which impression (count) was |
| // shown to the user depending on the given promo type. |
| void RecordIOSDesktopPromoShownHistogram(IOSPromoType promo_type, |
| int impression_count) { |
| std::string promo_histogram_type = IOSDesktopPromoHistogramType(promo_type); |
| DesktopIOSPromoImpression promo_impression; |
| switch (impression_count) { |
| case 1: |
| promo_impression = DesktopIOSPromoImpression::kFirstImpression; |
| break; |
| case 2: |
| promo_impression = DesktopIOSPromoImpression::kSecondImpression; |
| break; |
| case 3: |
| promo_impression = DesktopIOSPromoImpression::kThirdImpression; |
| break; |
| default: |
| NOTREACHED(); |
| } |
| base::UmaHistogramEnumeration( |
| "IOS.Desktop." + promo_histogram_type + ".Shown", promo_impression); |
| } |
| |
| // IOSPromoPrefsConfig is a complex struct that needs definition of a |
| // constructor, an explicit out-of-line copy constructor and a destructor. New |
| // promos should add themselves to this function. |
| IOSPromoPrefsConfig::IOSPromoPrefsConfig() = default; |
| IOSPromoPrefsConfig::IOSPromoPrefsConfig(const IOSPromoPrefsConfig&) = default; |
| IOSPromoPrefsConfig::~IOSPromoPrefsConfig() = default; |
| |
| IOSPromoPrefsConfig::IOSPromoPrefsConfig(IOSPromoType promo_type) { |
| switch (promo_type) { |
| case IOSPromoType::kPassword: |
| #if !BUILDFLAG(IS_ANDROID) |
| promo_feature = &feature_engagement::kIPHiOSPasswordPromoDesktopFeature; |
| #endif // !BUILDFLAG(IS_ANDROID) |
| promo_impressions_counter_pref_name = |
| promos_prefs::kDesktopToiOSPasswordPromoImpressionsCounter; |
| promo_opt_out_pref_name = promos_prefs::kDesktopToiOSPasswordPromoOptOut; |
| promo_last_impression_timestamp_pref_name = |
| promos_prefs::kDesktopToiOSPasswordPromoLastImpressionTimestamp; |
| break; |
| case IOSPromoType::kAddress: |
| #if !BUILDFLAG(IS_ANDROID) |
| promo_feature = &feature_engagement::kIPHiOSAddressPromoDesktopFeature; |
| #endif // !BUILDFLAG(IS_ANDROID) |
| promo_impressions_counter_pref_name = |
| promos_prefs::kDesktopToiOSAddressPromoImpressionsCounter; |
| promo_opt_out_pref_name = promos_prefs::kDesktopToiOSAddressPromoOptOut; |
| promo_last_impression_timestamp_pref_name = |
| promos_prefs::kDesktopToiOSAddressPromoLastImpressionTimestamp; |
| break; |
| case IOSPromoType::kPayment: |
| #if !BUILDFLAG(IS_ANDROID) |
| promo_feature = &feature_engagement::kIPHiOSPaymentPromoDesktopFeature; |
| #endif // !BUILDFLAG(IS_ANDROID) |
| promo_impressions_counter_pref_name = |
| promos_prefs::kDesktopToiOSPaymentPromoImpressionsCounter; |
| promo_opt_out_pref_name = promos_prefs::kDesktopToiOSPaymentPromoOptOut; |
| promo_last_impression_timestamp_pref_name = |
| promos_prefs::kDesktopToiOSPaymentPromoLastImpressionTimestamp; |
| break; |
| } |
| } |
| |
| // Registers profile prefs. New promos should add themselves to this function. |
| void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry) { |
| registry->RegisterTimePref( |
| promos_prefs::kDesktopToiOSPasswordPromoLastImpressionTimestamp, |
| base::Time(), user_prefs::PrefRegistrySyncable::SYNCABLE_PREF); |
| registry->RegisterIntegerPref( |
| promos_prefs::kDesktopToiOSPasswordPromoImpressionsCounter, 0, |
| user_prefs::PrefRegistrySyncable::SYNCABLE_PREF); |
| registry->RegisterBooleanPref( |
| promos_prefs::kDesktopToiOSPasswordPromoOptOut, false, |
| user_prefs::PrefRegistrySyncable::SYNCABLE_PREF); |
| |
| registry->RegisterTimePref( |
| promos_prefs::kDesktopToiOSAddressPromoLastImpressionTimestamp, |
| base::Time(), user_prefs::PrefRegistrySyncable::SYNCABLE_PREF); |
| registry->RegisterIntegerPref( |
| promos_prefs::kDesktopToiOSAddressPromoImpressionsCounter, 0, |
| user_prefs::PrefRegistrySyncable::SYNCABLE_PREF); |
| registry->RegisterBooleanPref( |
| promos_prefs::kDesktopToiOSAddressPromoOptOut, false, |
| user_prefs::PrefRegistrySyncable::SYNCABLE_PREF); |
| |
| registry->RegisterTimePref( |
| promos_prefs::kDesktopToiOSPaymentPromoLastImpressionTimestamp, |
| base::Time(), user_prefs::PrefRegistrySyncable::SYNCABLE_PREF); |
| registry->RegisterIntegerPref( |
| promos_prefs::kDesktopToiOSPaymentPromoImpressionsCounter, 0, |
| user_prefs::PrefRegistrySyncable::SYNCABLE_PREF); |
| registry->RegisterBooleanPref( |
| promos_prefs::kDesktopToiOSPaymentPromoOptOut, false, |
| user_prefs::PrefRegistrySyncable::SYNCABLE_PREF); |
| |
| registry->RegisterListPref( |
| promos_prefs::kDesktopToiOSNtpPromoAppearanceTimestamps, {}, |
| user_prefs::PrefRegistrySyncable::SYNCABLE_PREF); |
| registry->RegisterBooleanPref( |
| promos_prefs::kDesktopToiOSNtpPromoDismissed, false, |
| user_prefs::PrefRegistrySyncable::SYNCABLE_PREF); |
| } |
| |
| const base::Feature& GetIOSDesktopPromoFeatureEngagement( |
| IOSPromoType promo_type) { |
| IOSPromoPrefsConfig promo_prefs(promo_type); |
| return *promo_prefs.promo_feature; |
| } |
| |
| // RecordIOSDesktopPromoUserInteractionHistogram records which impression |
| // (count) depending on the promo type. |
| void RecordIOSDesktopPromoUserInteractionHistogram( |
| IOSPromoType promo_type, |
| int impression_count, |
| DesktopIOSPromoAction action) { |
| std::string promo_histogram_type = IOSDesktopPromoHistogramType(promo_type); |
| if (impression_count == 1) { |
| base::UmaHistogramEnumeration( |
| "IOS.Desktop." + promo_histogram_type + ".FirstImpression.Action", |
| action); |
| } else if (impression_count == 2) { |
| base::UmaHistogramEnumeration( |
| "IOS.Desktop." + promo_histogram_type + ".SecondImpression.Action", |
| action); |
| } else if (impression_count == 3) { |
| base::UmaHistogramEnumeration( |
| "IOS.Desktop." + promo_histogram_type + ".ThirdImpression.Action", |
| action); |
| } else { |
| NOTREACHED(); |
| } |
| } |
| |
| bool ShouldShowIOSDesktopPromo(Profile* profile, |
| const syncer::SyncService* sync_service, |
| IOSPromoType promo_type) { |
| if (!CanShowPromos()) { |
| return false; |
| } |
| |
| IOSPromoPrefsConfig promo_prefs(promo_type); |
| |
| // Show the promo if the user hasn't opted out, is not in the cooldown |
| // period and is within the impression limit for this promo. |
| return sync_service && VerifySyncingDatatypes(*sync_service, promo_type) && |
| profile->GetPrefs()->GetInteger( |
| promo_prefs.promo_impressions_counter_pref_name) < |
| kiOSDesktopPromoMaxImpressionCount && |
| VerifyMostRecentPromoTimestamp(profile) && |
| VerifyIOSDesktopPromoTotalImpressions(profile) && |
| VerifyIOSDesktopPromoTotalOptOuts(profile) && |
| !profile->GetPrefs()->GetBoolean(promo_prefs.promo_opt_out_pref_name); |
| } |
| |
| bool ShouldShowIOSDesktopNtpPromo(Profile* profile, |
| const syncer::SyncService* sync_service) { |
| if (!CanShowPromos()) { |
| return false; |
| } |
| |
| PrefService* pref_service = profile->GetPrefs(); |
| |
| // Sync must be active because prefs to track promo display are synced. |
| bool sync_ok = sync_service && |
| sync_service->GetActiveDataTypes().Has(syncer::PREFERENCES); |
| |
| // This promo can appear 10 times itself. |
| int appearance_count = |
| pref_service |
| ->GetList(promos_prefs::kDesktopToiOSNtpPromoAppearanceTimestamps) |
| .size(); |
| bool appearance_count_ok = |
| appearance_count < ntp_features::kNtpMobilePromoImpressionLimit.Get(); |
| |
| bool has_not_dismissed = !profile->GetPrefs()->GetBoolean( |
| promos_prefs::kDesktopToiOSNtpPromoDismissed); |
| |
| bool other_promos_ok = |
| VerifyMostRecentPromoTimestamp(profile, /*skip_ntp_promo=*/true) && |
| VerifyIOSDesktopPromoTotalImpressions(profile, /*skip_ntp_promo=*/true); |
| |
| // Show the promo if the user hasn't opted out, is not in the cooldown |
| // period and is within the impression limit for this promo. |
| return sync_ok && appearance_count_ok && has_not_dismissed && other_promos_ok; |
| } |
| |
| bool UserNotClassifiedAsMobileDeviceSwitcher( |
| const segmentation_platform::ClassificationResult& result) { |
| return result.status == segmentation_platform::PredictionStatus::kSucceeded && |
| !base::Contains( |
| result.ordered_labels, |
| segmentation_platform::DeviceSwitcherModel::kAndroidPhoneLabel) && |
| !base::Contains(result.ordered_labels, |
| segmentation_platform::DeviceSwitcherModel:: |
| kIosPhoneChromeLabel) && |
| !base::Contains( |
| result.ordered_labels, |
| segmentation_platform::DeviceSwitcherModel::kAndroidTabletLabel) && |
| !base::Contains( |
| result.ordered_labels, |
| segmentation_platform::DeviceSwitcherModel::kIosTabletLabel); |
| } |
| |
| void IOSDesktopPromoShown(Profile* profile, IOSPromoType promo_type) { |
| IOSPromoPrefsConfig promo_prefs(promo_type); |
| int new_impression_count = |
| profile->GetPrefs()->GetInteger( |
| promo_prefs.promo_impressions_counter_pref_name) + |
| 1; |
| |
| profile->GetPrefs()->SetInteger( |
| promo_prefs.promo_impressions_counter_pref_name, new_impression_count); |
| profile->GetPrefs()->SetTime( |
| promo_prefs.promo_last_impression_timestamp_pref_name, base::Time::Now()); |
| |
| RecordIOSDesktopPromoShownHistogram(promo_type, new_impression_count); |
| } |
| |
| void IOSDesktopNtpPromoShown(PrefService* pref_service) { |
| ScopedListPrefUpdate update( |
| pref_service, promos_prefs::kDesktopToiOSNtpPromoAppearanceTimestamps); |
| update->Append(base::TimeToValue(base::Time::Now())); |
| } |
| |
| } // namespace promos_utils |