chromium/ios/chrome/browser/ui/push_notification/notifications_opt_in_view_controller.mm

// 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/ui/push_notification/notifications_opt_in_view_controller.h"

#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/ui/symbols/symbols.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_switch_cell.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_switch_item.h"
#import "ios/chrome/browser/shared/ui/table_view/table_view_utils.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/grit/ios_branded_strings.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ui/base/l10n/l10n_util_mac.h"

namespace {
enum SectionIdentifier {
  kNotificationOptions,
};
struct CellConfig {
  int title_id;
  int subtitle_id;
  BOOL on;
  BOOL show_separator;
};
// Radius size of the table view.
CGFloat const kTableViewCornerRadius = 10;
// Table view separator inset.
CGFloat const kTableViewSeparatorInset = 16.0;
// Space above the title.
CGFloat const kSpaceAboveTitle = 20.0;
// Accessibility identifier.
NSString* const kNotificationsOptInScreenAxId = @"NotificationsOptInScreenAxId";
// Constant for the subtitleLabel's width anchor.
CGFloat const kSubtitleWidthConstant = 23.0;
// Title's horizontal margin.
CGFloat const kTitleHorizontalMargin = 25.0;

// Returns the name of the banner image above the title.
NSString* BannerImageName(bool landscape) {
#if BUILDFLAG(IOS_USE_BRANDED_SYMBOLS)
  return landscape ? kChromeNotificationsOptInBannerLandscapeImage
                   : kChromeNotificationsOptInBannerImage;
#else
  return landscape ? kChromiumNotificationsOptInBannerLandscapeImage
                   : kChromiumNotificationsOptInBannerImage;
#endif
}

}  // namespace

@interface NotificationsOptInViewController () <UITableViewDelegate>

@end

@implementation NotificationsOptInViewController {
  UITableView* _tableView;
  NSLayoutConstraint* _tableViewHeightConstraint;
  UITableViewDiffableDataSource<NSNumber*, NSNumber*>* _dataSource;
  UISwitch* _contentToggle;
  UISwitch* _tipsToggle;
  UISwitch* _priceTrackingToggle;
  BOOL _contentNotificationsEnabled;
  BOOL _tipsNotificationsEnabled;
  BOOL _priceTrackingNotificationsEnabled;
}

- (void)viewDidLoad {
  self.titleText = l10n_util::GetNSString(IDS_IOS_NOTIFICATIONS_OPT_IN_TITLE);
  self.subtitleText =
      l10n_util::GetNSString(IDS_IOS_NOTIFICATIONS_OPT_IN_SUBTITLE);
  self.titleHorizontalMargin = kTitleHorizontalMargin;
  self.primaryActionString =
      l10n_util::GetNSString(IDS_IOS_NOTIFICATIONS_OPT_IN_ENABLE_BUTTON);
  self.secondaryActionString =
      l10n_util::GetNSString(IDS_IOS_NOTIFICATIONS_ALERT_CANCEL);
  self.titleTopMarginWhenNoHeaderImage = kSpaceAboveTitle;
  self.bannerName = BannerImageName(IsLandscape(self.view.window));
  self.bannerSize = BannerImageSizeType::kShort;
  self.shouldBannerFillTopSpace = YES;
  self.shouldHideBanner = IsCompactHeight(self.traitCollection);
  self.view.accessibilityIdentifier = kNotificationsOptInScreenAxId;
  _tableView = [self createTableView];
  [self.specificContentView addSubview:_tableView];
  [NSLayoutConstraint activateConstraints:@[
    [_tableView.topAnchor
        constraintEqualToAnchor:self.specificContentView.topAnchor],
    [_tableView.centerXAnchor
        constraintEqualToAnchor:self.specificContentView.centerXAnchor],
    [_tableView.widthAnchor
        constraintEqualToAnchor:self.specificContentView.widthAnchor],
    [_tableView.bottomAnchor
        constraintLessThanOrEqualToAnchor:self.specificContentView
                                              .bottomAnchor],
  ]];
  [self loadModel];
  [self setPrimaryButtonConfiguration];
  [self updatePrimaryButtonState];
  [super viewDidLoad];

  // Add subtitle constraint after it is added to hierarchy.
  [NSLayoutConstraint activateConstraints:@[
    [self.subtitleLabel.widthAnchor
        constraintEqualToAnchor:self.view.widthAnchor
                       constant:-kSubtitleWidthConstant],
  ]];

  self.view.backgroundColor = [UIColor colorNamed:kSecondaryBackgroundColor];
}

- (void)viewWillLayoutSubviews {
  [super viewWillLayoutSubviews];
  [self updateTableViewHeightConstraint];
  self.bannerName = BannerImageName(IsLandscape(self.view.window));
}

- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
  [super traitCollectionDidChange:previousTraitCollection];
  self.shouldHideBanner = IsCompactHeight(self.traitCollection);
}

#pragma mark - PromoStyleViewController

- (UIFontTextStyle)titleLabelFontTextStyle {
  return UIFontTextStyleTitle2;
}

- (UILabel*)createSubtitleLabel {
  UILabel* subtitleLabel = [[UILabel alloc] init];
  subtitleLabel.font =
      [UIFont preferredFontForTextStyle:UIFontTextStyleCallout];
  subtitleLabel.numberOfLines = 0;
  subtitleLabel.textColor = [UIColor colorNamed:kTextSecondaryColor];
  subtitleLabel.text = self.subtitleText;
  subtitleLabel.textAlignment = NSTextAlignmentCenter;
  subtitleLabel.translatesAutoresizingMaskIntoConstraints = NO;
  subtitleLabel.adjustsFontForContentSizeCategory = YES;
  return subtitleLabel;
}

#pragma mark - LegacyChromeTableViewController

- (void)loadModel {
  __weak __typeof(self) weakSelf = self;
  _dataSource = [[UITableViewDiffableDataSource alloc]
      initWithTableView:_tableView
           cellProvider:^UITableViewCell*(UITableView* tableView,
                                          NSIndexPath* indexPath,
                                          NSNumber* itemIdentifier) {
             return [weakSelf
                 cellForTableView:tableView
                        indexPath:indexPath
                   itemIdentifier:static_cast<NotificationsOptInItemIdentifier>(
                                      itemIdentifier.integerValue)];
           }];

  RegisterTableViewCell<TableViewSwitchCell>(_tableView);

  NSDiffableDataSourceSnapshot* snapshot =
      [[NSDiffableDataSourceSnapshot alloc] init];
  [snapshot appendSectionsWithIdentifiers:@[
    @(SectionIdentifier::kNotificationOptions)
  ]];
  if ([self isContentNotificationEnabled]) {
    [snapshot appendItemsWithIdentifiers:@[
      @(NotificationsOptInItemIdentifier::kContent)
    ]];
  }
  [snapshot appendItemsWithIdentifiers:@[
    @(NotificationsOptInItemIdentifier::kTips),
    @(NotificationsOptInItemIdentifier::kPriceTracking)
  ]];
  [_dataSource applySnapshot:snapshot animatingDifferences:NO];
}

#pragma mark - UITableViewDelegate

- (void)tableView:(UITableView*)tableView
      willDisplayCell:(UITableViewCell*)cell
    forRowAtIndexPath:(NSIndexPath*)indexPath {
  cell.backgroundColor = [UIColor colorNamed:kPrimaryBackgroundColor];
}

#pragma mark - NotificationsOptInConsumer

- (void)setOptInItem:(NotificationsOptInItemIdentifier)identifier
             enabled:(BOOL)enabled {
  switch (identifier) {
    case kContent:
      if (_contentToggle) {
        _contentToggle.on = enabled;
      }
      _contentNotificationsEnabled = enabled;
      break;
    case kTips:
      if (_tipsToggle) {
        _tipsToggle.on = enabled;
      }
      _tipsNotificationsEnabled = enabled;
      break;
    case kPriceTracking:
      if (_priceTrackingToggle) {
        _priceTrackingToggle.on = enabled;
      }
      _priceTrackingNotificationsEnabled = enabled;
      break;
  }
}

#pragma mark - Private

// Creates the table view.
- (UITableView*)createTableView {
  UITableView* tableView =
      [[UITableView alloc] initWithFrame:CGRectZero
                                   style:UITableViewStylePlain];
  tableView.layer.cornerRadius = kTableViewCornerRadius;
  tableView.estimatedRowHeight = UITableViewAutomaticDimension;
  tableView.scrollEnabled = NO;
  tableView.showsVerticalScrollIndicator = NO;
  tableView.delegate = self;
  tableView.userInteractionEnabled = YES;
  tableView.translatesAutoresizingMaskIntoConstraints = NO;
  tableView.separatorInset = UIEdgeInsetsZero;
  _tableViewHeightConstraint =
      [tableView.heightAnchor constraintEqualToConstant:0];
  _tableViewHeightConstraint.active = YES;

  return tableView;
}

// Returns the CellConfig for the given itemIdentifier.
- (CellConfig)configForItemIdentifier:
    (NotificationsOptInItemIdentifier)itemIdentifier {
  switch (itemIdentifier) {
    case kContent:
      return {IDS_IOS_CONTENT_NOTIFICATIONS_CONTENT_SETTINGS_TOGGLE_TITLE,
              IDS_IOS_CONTENT_NOTIFICATIONS_CONTENT_SETTINGS_FOOTER_TEXT,
              _contentNotificationsEnabled, YES};
    case kTips:
      return {IDS_IOS_SET_UP_LIST_TIPS_TITLE,
              IDS_IOS_NOTIFICATIONS_OPT_IN_TIPS_SETTINGS_TOGGLE_MESSSAGE,
              _tipsNotificationsEnabled, YES};
    case kPriceTracking:
      return {IDS_IOS_NOTIFICATIONS_OPT_IN_PRICE_TRACKING_TOGGLE_TITLE,
              IDS_IOS_NOTIFICATIONS_OPT_IN_PRICE_TRACKING_TOGGLE_MESSAGE,
              _priceTrackingNotificationsEnabled, YES};
  }
}

// Configures the the table view cells.
- (UITableViewCell*)cellForTableView:(UITableView*)tableView
                           indexPath:(NSIndexPath*)indexPath
                      itemIdentifier:
                          (NotificationsOptInItemIdentifier)itemIdentifier {
  TableViewSwitchCell* cell =
      DequeueTableViewCell<TableViewSwitchCell>(tableView);

  CellConfig config = [self configForItemIdentifier:itemIdentifier];
  [cell configureCellWithTitle:l10n_util::GetNSString(config.title_id)
                      subtitle:l10n_util::GetNSString(config.subtitle_id)
                 switchEnabled:YES
                            on:config.on];

  switch (itemIdentifier) {
    case kContent:
      _contentToggle = cell.switchView;
      break;
    case kTips:
      _tipsToggle = cell.switchView;
      break;
    case kPriceTracking:
      _priceTrackingToggle = cell.switchView;
      break;
  }
  cell.switchView.tag = itemIdentifier;

  // Make the separator invisible on the last row.
  CGFloat separatorInset =
      itemIdentifier == NotificationsOptInItemIdentifier::kMaxValue
          ? tableView.frame.size.width
          : kTableViewSeparatorInset;
  cell.separatorInset = UIEdgeInsetsMake(0.f, separatorInset, 0.f, 0.f);

  [cell.switchView addTarget:self
                      action:@selector(switchToggled:)
            forControlEvents:UIControlEventValueChanged];
  cell.selectionStyle = UITableViewCellSelectionStyleNone;
  cell.backgroundColor = [UIColor colorNamed:kSecondaryBackgroundColor];

  return cell;
}

// Invoked when a notification opt-in switch is toggled.
- (void)switchToggled:(UISwitch*)sender {
  [self.notificationsDelegate
      selectionChangedForItemType:static_cast<NotificationsOptInItemIdentifier>(
                                      sender.tag)
                         selected:sender.on];
  [self updatePrimaryButtonState];
}

// Updates the tableView's height constraint.
- (void)updateTableViewHeightConstraint {
  _tableViewHeightConstraint.constant = _tableView.contentSize.height;
}

// Sets the configurationUpdateHandler for the primaryActionButton to handle the
// button's state changes. The button is blue when enabled, and grayed out when
// disabled.
- (void)setPrimaryButtonConfiguration {
  self.updateHandler = ^(UIButton* incomingButton) {
    UIButtonConfiguration* updatedConfig = incomingButton.configuration;
    switch (incomingButton.state) {
      case UIControlStateDisabled: {
        updatedConfig.background.backgroundColor =
            [UIColor colorNamed:kUpdatedTertiaryBackgroundColor];
        updatedConfig.baseForegroundColor =
            [UIColor colorNamed:kTextTertiaryColor];
        break;
      }
      case UIControlStateNormal: {
        updatedConfig.background.backgroundColor =
            [UIColor colorNamed:kBlueColor];
        updatedConfig.baseForegroundColor =
            [UIColor colorNamed:kBackgroundColor];
        break;
      }
      default:
        break;
    }
    incomingButton.configuration = updatedConfig;
  };
}

// Enables the primary action button if any one of the toggles are on. Disables
// otherwise.
- (void)updatePrimaryButtonState {
  self.primaryButtonEnabled =
      _contentToggle.isOn || _tipsToggle.isOn || _priceTrackingToggle.isOn;
}

@end