| // Copyright 2024 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/tips_notifications/model/utils.h" |
| |
| #import "base/strings/string_number_conversions.h" |
| #import "base/strings/string_split.h" |
| #import "base/strings/sys_string_conversions.h" |
| #import "base/time/time.h" |
| #import "components/prefs/pref_service.h" |
| #import "ios/chrome/browser/push_notification/model/constants.h" |
| #import "ios/chrome/browser/shared/public/features/features.h" |
| #import "ios/chrome/grit/ios_branded_strings.h" |
| #import "ios/chrome/grit/ios_strings.h" |
| #import "ui/base/device_form_factor.h" |
| #import "ui/base/l10n/l10n_util_mac.h" |
| |
| namespace { |
| |
| // Holds the l10n string ids for tips notification content. |
| struct ContentIDs { |
| int title; |
| int body; |
| }; |
| |
| // Returns the string id of the body text for the Docking promo notification. |
| int DockingBodyID() { |
| if (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET) { |
| return IDS_IOS_NOTIFICATIONS_TIPS_DOCKING_BODY_IPAD; |
| } |
| return IDS_IOS_NOTIFICATIONS_TIPS_DOCKING_BODY_IPHONE; |
| } |
| |
| // Returns the ContentIDs for the given `type`. |
| ContentIDs ContentIDsForType(TipsNotificationType type) { |
| switch (type) { |
| case TipsNotificationType::kDefaultBrowser: |
| return {IDS_IOS_NOTIFICATIONS_TIPS_DEFAULT_BROWSER_TITLE, |
| IDS_IOS_NOTIFICATIONS_TIPS_DEFAULT_BROWSER_BODY}; |
| case TipsNotificationType::kWhatsNew: |
| return {IDS_IOS_NOTIFICATIONS_TIPS_WHATS_NEW_TITLE, |
| IDS_IOS_NOTIFICATIONS_TIPS_WHATS_NEW_BODY}; |
| case TipsNotificationType::kSignin: |
| return {IDS_IOS_NOTIFICATIONS_TIPS_SIGNIN_TITLE, |
| IDS_IOS_NOTIFICATIONS_TIPS_SIGNIN_BODY}; |
| case TipsNotificationType::kSetUpListContinuation: |
| return {IDS_IOS_NOTIFICATIONS_TIPS_SETUPLIST_CONTINUATION_TITLE, |
| IDS_IOS_NOTIFICATIONS_TIPS_SETUPLIST_CONTINUATION_BODY}; |
| case TipsNotificationType::kDocking: |
| return {IDS_IOS_NOTIFICATIONS_TIPS_DOCKING_TITLE, DockingBodyID()}; |
| case TipsNotificationType::kOmniboxPosition: |
| return {IDS_IOS_NOTIFICATIONS_TIPS_OMNIBOX_POSITION_TITLE, |
| IDS_IOS_NOTIFICATIONS_TIPS_OMNIBOX_POSITION_BODY}; |
| case TipsNotificationType::kLens: |
| return {IDS_IOS_NOTIFICATIONS_TIPS_LENS_TITLE, |
| IDS_IOS_NOTIFICATIONS_TIPS_LENS_BODY}; |
| case TipsNotificationType::kEnhancedSafeBrowsing: |
| return {IDS_IOS_NOTIFICATIONS_TIPS_ENHANCED_SAFE_BROWSING_TITLE, |
| IDS_IOS_NOTIFICATIONS_TIPS_ENHANCED_SAFE_BROWSING_BODY}; |
| case TipsNotificationType::kCPE: |
| return {IDS_IOS_NOTIFICATIONS_TIPS_CPE_TITLE, |
| IDS_IOS_NOTIFICATIONS_TIPS_CPE_BODY}; |
| case TipsNotificationType::kLensOverlay: |
| return {IDS_IOS_NOTIFICATIONS_TIPS_LENS_OVERLAY_TITLE, |
| IDS_IOS_NOTIFICATIONS_TIPS_LENS_OVERLAY_BODY}; |
| case TipsNotificationType::kTrustedVaultKeyRetrieval: |
| return {IDS_IOS_NOTIFICATIONS_TIPS_TRUSTED_VAULT_KEY_RETRIVAL_TITLE, |
| IDS_IOS_NOTIFICATIONS_TIPS_TRUSTED_VAULT_KEY_RETRIVAL_BODY}; |
| case TipsNotificationType::kIncognitoLock: |
| case TipsNotificationType::kError: |
| NOTREACHED(); |
| } |
| } |
| |
| // Returns the default trigger TimeDelta for the given `user_type` and |
| // `notification_type` depending on whether this is for a reactivation |
| // notification or not. |
| base::TimeDelta DefaultTriggerDelta( |
| bool for_reactivation, |
| TipsNotificationUserType user_type, |
| std::optional<TipsNotificationType> notification_type) { |
| if (notification_type.has_value() && |
| notification_type.value() == |
| TipsNotificationType::kTrustedVaultKeyRetrieval) { |
| // We need to use a short trigger delta for the notification type |
| // `kTrustedVaultKeyRetrieval` because we want to ensure that users fix the |
| // issue as soon as possible. The trigger delta of 5 minutes in this case |
| // has been chosen arbitrarily. |
| return base::Minutes(5); |
| } |
| if (for_reactivation) { |
| return base::Days(7); |
| } |
| switch (user_type) { |
| case TipsNotificationUserType::kUnknown: |
| return base::Days(3); |
| case TipsNotificationUserType::kLessEngaged: |
| return base::Days(21); |
| case TipsNotificationUserType::kActiveSeeker: |
| return base::Days(7); |
| } |
| } |
| |
| // Parses a field trial param as a comma separated list of integers, casts the |
| // integers as type T, and returns a vector with elements of type T. |
| template <typename T> |
| std::vector<T> GetFieldTrialParamByFeatureAsVector( |
| const base::Feature& feature, |
| const std::string& param_name, |
| const std::vector<T> default_values) { |
| std::string param_string = |
| GetFieldTrialParamValueByFeature(feature, param_name); |
| if (param_string.length() == 0) { |
| return default_values; |
| } |
| |
| std::vector<T> values; |
| const std::vector<std::string> items = base::SplitString( |
| param_string, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY); |
| for (std::string_view item : items) { |
| int value; |
| if (base::StringToInt(item, &value) && value >= 0 && |
| value <= int(T::kMaxValue)) { |
| values.push_back(static_cast<T>(value)); |
| } |
| } |
| return values; |
| } |
| |
| // A bitfield with all notification types from the enum enabled. |
| constexpr int kAllNotificationBits = |
| (1 << (int(TipsNotificationType::kMaxValue) + 1)) - 1; |
| // A bitfield with all notification types from the enum enabled, except for |
| // kError. |
| constexpr int kEnableAllNotifications = |
| kAllNotificationBits - (1 << int(TipsNotificationType::kError)); |
| |
| } // namespace |
| |
| NSString* const kTipsNotificationId = @"kTipsNotificationId"; |
| NSString* const kTipsNotificationTypeKey = @"kTipsNotificationTypeKey"; |
| NSString* const kReactivationKey = @"kReactivationKey"; |
| const char kTipsNotificationsSentPref[] = "tips_notifications.sent_bitfield"; |
| const char kTipsNotificationsLastSent[] = "tips_notifiations.last_sent"; |
| const char kTipsNotificationsLastTriggered[] = |
| "tips_notifiations.last_triggered"; |
| const char kTipsNotificationsLastRequestedTime[] = |
| "tips_notifications.last_requested.time"; |
| const char kTipsNotificationsUserType[] = "tips_notifications.user_type"; |
| const char kTipsNotificationsDismissCount[] = |
| "tips_notifications.dismiss_count"; |
| const char kReactivationNotificationsCanceledCount[] = |
| "reactivation_notifications.canceled_count"; |
| |
| // User defaults key for the experimental setting to force a particular |
| // notification type. |
| NSString* const kForcedTipsNotificationType = @"ForcedTipsNotificationType"; |
| // User defaults key for the experimental setting to force a specified |
| // trigger time in seconds. |
| NSString* const kTipsNotificationTrigger = @"TipsNotificationTrigger"; |
| |
| bool IsTipsNotification(UNNotificationRequest* request) { |
| return [request.identifier isEqualToString:kTipsNotificationId]; |
| } |
| |
| bool IsProactiveTipsNotification(UNNotificationRequest* request) { |
| return [request.content.userInfo[kReactivationKey] isEqual:@YES]; |
| } |
| |
| NSDictionary* UserInfoForTipsNotificationType(TipsNotificationType type, |
| bool for_reactivation, |
| std::string_view profile_name) { |
| return @{ |
| kTipsNotificationId : @YES, |
| kTipsNotificationTypeKey : @(static_cast<int>(type)), |
| kReactivationKey : for_reactivation ? @YES : @NO, |
| kOriginatingProfileNameKey : base::SysUTF8ToNSString(profile_name), |
| }; |
| } |
| |
| std::optional<TipsNotificationType> ParseTipsNotificationType( |
| UNNotificationRequest* request) { |
| NSDictionary* user_info = request.content.userInfo; |
| NSNumber* type = user_info[kTipsNotificationTypeKey]; |
| if (type == nil) { |
| return std::nullopt; |
| } |
| return static_cast<TipsNotificationType>(type.integerValue); |
| } |
| |
| UNNotificationContent* ContentForTipsNotificationType( |
| TipsNotificationType type, |
| bool for_reactivation, |
| std::string_view profile_name) { |
| UNMutableNotificationContent* content = |
| [[UNMutableNotificationContent alloc] init]; |
| ContentIDs content_ids = ContentIDsForType(type); |
| content.title = l10n_util::GetNSString(content_ids.title); |
| content.body = l10n_util::GetNSString(content_ids.body); |
| content.userInfo = |
| UserInfoForTipsNotificationType(type, for_reactivation, profile_name); |
| content.sound = UNNotificationSound.defaultSound; |
| return content; |
| } |
| |
| base::TimeDelta TipsNotificationTriggerDelta( |
| bool for_reactivation, |
| TipsNotificationUserType user_type, |
| std::optional<TipsNotificationType> notification_type) { |
| base::TimeDelta default_trigger = |
| DefaultTriggerDelta(for_reactivation, user_type, notification_type); |
| if (for_reactivation) { |
| return GetFieldTrialParamByFeatureAsTimeDelta( |
| kIOSReactivationNotifications, |
| kIOSReactivationNotificationsTriggerTimeParam, default_trigger); |
| } |
| if (int setting = TipsNotificationTriggerExperimentalSetting()) { |
| return base::Seconds(setting); |
| } |
| return default_trigger; |
| } |
| |
| int TipsNotificationsEnabledBitfield() { |
| return kEnableAllNotifications; |
| } |
| |
| std::vector<TipsNotificationType> TipsNotificationsTypesOrder( |
| bool for_reactivation) { |
| if (for_reactivation) { |
| std::vector<TipsNotificationType> notification_types{ |
| TipsNotificationType::kLens, |
| TipsNotificationType::kEnhancedSafeBrowsing, |
| TipsNotificationType::kWhatsNew, |
| }; |
| if (IsIOSTrustedVaultNotificationEnabled()) { |
| notification_types.insert( |
| notification_types.begin(), |
| TipsNotificationType::kTrustedVaultKeyRetrieval); |
| } |
| return GetFieldTrialParamByFeatureAsVector<TipsNotificationType>( |
| kIOSReactivationNotifications, kIOSReactivationNotificationsOrderParam, |
| notification_types); |
| } else if (IsIOSExpandedTipsEnabled()) { |
| return GetFieldTrialParamByFeatureAsVector<TipsNotificationType>( |
| kIOSExpandedTips, kIOSExpandedTipsOrderParam, |
| { |
| TipsNotificationType::kEnhancedSafeBrowsing, |
| TipsNotificationType::kWhatsNew, |
| TipsNotificationType::kLens, |
| TipsNotificationType::kOmniboxPosition, |
| TipsNotificationType::kSetUpListContinuation, |
| TipsNotificationType::kDefaultBrowser, |
| TipsNotificationType::kDocking, |
| TipsNotificationType::kSignin, |
| TipsNotificationType::kLensOverlay, |
| TipsNotificationType::kCPE, |
| }); |
| } |
| return { |
| TipsNotificationType::kEnhancedSafeBrowsing, |
| TipsNotificationType::kWhatsNew, |
| TipsNotificationType::kLens, |
| TipsNotificationType::kOmniboxPosition, |
| TipsNotificationType::kSetUpListContinuation, |
| TipsNotificationType::kDefaultBrowser, |
| TipsNotificationType::kDocking, |
| TipsNotificationType::kSignin, |
| }; |
| } |
| |
| NotificationType NotificationTypeForTipsNotificationType( |
| TipsNotificationType type) { |
| switch (type) { |
| case TipsNotificationType::kDefaultBrowser: |
| return NotificationType::kTipsDefaultBrowser; |
| case TipsNotificationType::kWhatsNew: |
| return NotificationType::kTipsWhatsNew; |
| case TipsNotificationType::kSignin: |
| return NotificationType::kTipsSignin; |
| case TipsNotificationType::kSetUpListContinuation: |
| return NotificationType::kTipsSetUpListContinuation; |
| case TipsNotificationType::kDocking: |
| return NotificationType::kTipsDocking; |
| case TipsNotificationType::kOmniboxPosition: |
| return NotificationType::kTipsOmniboxPosition; |
| case TipsNotificationType::kLens: |
| return NotificationType::kTipsLens; |
| case TipsNotificationType::kEnhancedSafeBrowsing: |
| return NotificationType::kTipsEnhancedSafeBrowsing; |
| case TipsNotificationType::kLensOverlay: |
| return NotificationType::kTipsLensOverlay; |
| case TipsNotificationType::kCPE: |
| return NotificationType::kTipsCPE; |
| case TipsNotificationType::kIncognitoLock: |
| return NotificationType::kTipsIncognitoLock; |
| case TipsNotificationType::kTrustedVaultKeyRetrieval: |
| return NotificationType::kTipsTrustedVaultKeyRetrieval; |
| case TipsNotificationType::kError: |
| NOTREACHED(); |
| } |
| } |
| |
| std::optional<TipsNotificationType> ForcedTipsNotificationType() { |
| int int_value = [[NSUserDefaults standardUserDefaults] |
| integerForKey:kForcedTipsNotificationType] - |
| 1; |
| if (int_value < 0 || int_value > int(TipsNotificationType::kMaxValue)) { |
| return std::nullopt; |
| } |
| return static_cast<TipsNotificationType>(int_value); |
| } |
| |
| int TipsNotificationTriggerExperimentalSetting() { |
| return [[NSUserDefaults standardUserDefaults] |
| integerForKey:kTipsNotificationTrigger]; |
| } |
| |
| TipsNotificationUserType GetTipsNotificationUserType(PrefService* local_state) { |
| return static_cast<TipsNotificationUserType>( |
| local_state->GetInteger(kTipsNotificationsUserType)); |
| } |
| |
| void SetTipsNotificationUserType(PrefService* local_state, |
| TipsNotificationUserType user_type) { |
| local_state->SetInteger(kTipsNotificationsUserType, int(user_type)); |
| } |