chromium/ios/chrome/browser/price_insights/coordinator/price_insights_modulator.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/price_insights/coordinator/price_insights_modulator.h"

#import "base/i18n/number_formatting.h"
#import "base/metrics/histogram_functions.h"
#import "base/metrics/user_metrics.h"
#import "base/metrics/user_metrics_action.h"
#import "base/strings/sys_string_conversions.h"
#import "components/commerce/core/commerce_constants.h"
#import "components/commerce/core/price_tracking_utils.h"
#import "components/commerce/core/shopping_service.h"
#import "components/image_fetcher/core/image_data_fetcher.h"
#import "components/payments/core/currency_formatter.h"
#import "components/strings/grit/components_strings.h"
#import "ios/chrome/browser/bookmarks/model/bookmark_model_factory.h"
#import "ios/chrome/browser/commerce/model/shopping_service_factory.h"
#import "ios/chrome/browser/contextual_panel/utils/contextual_panel_metrics.h"
#import "ios/chrome/browser/price_insights/model/price_insights_model.h"
#import "ios/chrome/browser/price_insights/ui/price_insights_cell.h"
#import "ios/chrome/browser/price_insights/ui/price_insights_item.h"
#import "ios/chrome/browser/shared/coordinator/alert/alert_coordinator.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
#import "ios/chrome/browser/shared/public/commands/contextual_sheet_commands.h"
#import "ios/chrome/browser/shared/public/commands/price_notifications_commands.h"
#import "ios/chrome/browser/shared/public/commands/snackbar_commands.h"
#import "ios/chrome/browser/ui/price_notifications/price_notifications_price_tracking_mediator.h"
#import "ios/chrome/grit/ios_branded_strings.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/web/public/web_state.h"
#import "services/network/public/cpp/shared_url_loader_factory.h"
#import "ui/base/l10n/l10n_util_mac.h"

namespace {

// The histogram used to record the current price bucket of the product when the
// user clicks on buying options.
const char kPriceInsightsBuyingOptionsClicked[] =
    "Commerce.PriceInsights.BuyingOptionsClicked";

NSDate* getNSDateFromString(std::string date) {
  NSDateFormatter* date_format = [[NSDateFormatter alloc] init];
  [date_format setDateFormat:@"yyyy-MM-dd"];
  NSDate* formated_date =
      [date_format dateFromString:base::SysUTF8ToNSString(date)];
  NSCalendar* calendar = [NSCalendar currentCalendar];
  [calendar setTimeZone:[NSTimeZone timeZoneWithName:@"UTC"]];
  NSDate* midnight_date = [calendar startOfDayForDate:formated_date];
  return midnight_date;
}

}  // namespace

@interface PriceInsightsModulator ()

// The mediator to track/untrack a page and open the buying options URL in a new
// tab.
@property(nonatomic, strong) PriceNotificationsPriceTrackingMediator* mediator;
// A weak reference to a PriceInsightsCell.
@property(nonatomic, weak) PriceInsightsCell* priceInsightsCell;
// The service responsible for interacting with commerce's price data
// infrastructure.
@property(nonatomic, assign) commerce::ShoppingService* shoppingService;
// The price insights item linked to this modulator.
@property(nonatomic, strong) PriceInsightsItem* priceInsightsItem;

@end

@implementation PriceInsightsModulator {
  // Coordinator for displaying alerts.
  AlertCoordinator* _alertCoordinator;
}

#pragma mark - Public

- (void)start {
  PushNotificationService* pushNotificationService =
      GetApplicationContext()->GetPushNotificationService();
  self.shoppingService = commerce::ShoppingServiceFactory::GetForBrowserState(
      self.browser->GetBrowserState());
  bookmarks::BookmarkModel* bookmarkModel =
      ios::BookmarkModelFactory::GetForBrowserState(
          self.browser->GetBrowserState());
  web::WebState* webState =
      self.browser->GetWebStateList()->GetActiveWebState();
  std::unique_ptr<image_fetcher::ImageDataFetcher> imageFetcher =
      std::make_unique<image_fetcher::ImageDataFetcher>(
          self.browser->GetBrowserState()->GetSharedURLLoaderFactory());
  self.mediator = [[PriceNotificationsPriceTrackingMediator alloc]
      initWithShoppingService:self.shoppingService
                bookmarkModel:bookmarkModel
                 imageFetcher:std::move(imageFetcher)
                     webState:webState->GetWeakPtr()
      pushNotificationService:pushNotificationService];
  self.mediator.priceInsightsConsumer = self;
}

- (void)stop {
  self.mediator = nil;
  self.shoppingService = nil;
  self.priceInsightsItem = nil;
  [self dismissAlertCoordinator];
}

- (UICollectionViewCellRegistration*)cellRegistration {
  __weak __typeof(self) weakSelf = self;
  auto handler =
      ^(PriceInsightsCell* cell, NSIndexPath* indexPath, id identifier) {
        weakSelf.priceInsightsCell = cell;
        [weakSelf configureCell:cell];
      };
  return [UICollectionViewCellRegistration
      registrationWithCellClass:[PriceInsightsCell class]
           configurationHandler:handler];
}

- (PanelBlockData*)panelBlockData {
  return [[PanelBlockData alloc] initWithBlockType:[self blockType]
                                  cellRegistration:[self cellRegistration]];
}

#pragma mark - PriceInsightsConsumer

- (void)didStartPriceTrackingWithNotification:(BOOL)granted
                               showCompletion:(BOOL)showCompletion {
  [self.priceInsightsCell updateTrackStatus:YES];

  if (!showCompletion) {
    return;
  }

  __weak PriceInsightsModulator* weakSelf = self;
  NSString* message =
      granted
          ? l10n_util::GetNSString(
                IDS_PRICE_INSIGHTS_SNACKBAR_MESSAGE_TITLE_NOTIFICATION_ENABLED)
          : l10n_util::GetNSString(
                IDS_PRICE_INSIGHTS_SNACKBAR_MESSAGE_TITLE_NOTIFICATION_DISABLED);
  [self displaySnackbar:message
             buttonText:l10n_util::GetNSString(
                            IDS_PRICE_INSIGHTS_SNACKBAR_BUTTON_TITLE)
                 action:^{
                   [weakSelf onPriceNotificationSnackBarClosed];
                 }];
}

- (void)didStopPriceTracking {
  __weak PriceNotificationsPriceTrackingMediator* weakMediator = self.mediator;
  __weak PriceInsightsModulator* weakSelf = self;
  [self.priceInsightsCell updateTrackStatus:NO];
  [self displaySnackbar:l10n_util::GetNSString(
                            IDS_PRICE_INSIGHTS_UNTRACK_SNACKBAR_MESSAGE)
             buttonText:l10n_util::GetNSString(
                            IDS_PRICE_INSIGHTS_UNTRACK_SNACKBAR_BUTTON_TITLE)
                 action:^{
                   [weakMediator
                       priceInsightsTrackItem:weakSelf.priceInsightsItem
                         notificationsGranted:NO
                               showCompletion:NO];
                 }];
}

- (void)didStartNavigationToWebpageWithPriceBucket:
    (commerce::PriceBucket)bucket {
  base::UmaHistogramEnumeration(kPriceInsightsBuyingOptionsClicked, bucket);
}

- (void)presentPushNotificationPermissionAlert {
  NSString* alertTitle = l10n_util::GetNSString(
      IDS_IOS_PRICE_INSIGHTS_PRICE_TRACK_PERMISSION_REDIRECT_ALERT_TITLE);
  NSString* alertMessage = l10n_util::GetNSString(
      IDS_IOS_PRICE_INSIGHTS_PRICE_TRACK_PERMISSION_REDIRECT_ALERT_MESSAGE);
  NSString* closeTitle = l10n_util::GetNSString(
      IDS_IOS_PRICE_INSIGHTS_PRICE_TRACK_PERMISSION_REDIRECT_ALERT_CLOSE);
  NSString* settingsTitle = l10n_util::GetNSString(
      IDS_IOS_PRICE_INSIGHTS_PRICE_TRACK_PERMISSION_REDIRECT_ALERT_REDIRECT);

  __weak PriceInsightsModulator* weakSelf = self;
  [_alertCoordinator stop];
  _alertCoordinator = [[AlertCoordinator alloc]
      initWithBaseViewController:self.baseViewController
                         browser:self.browser
                           title:alertTitle
                         message:alertMessage];
  [_alertCoordinator addItemWithTitle:closeTitle
                               action:^{
                                 [weakSelf onPushNotificationCancel];
                               }
                                style:UIAlertActionStyleCancel];
  [_alertCoordinator addItemWithTitle:settingsTitle
                               action:^{
                                 [weakSelf onPushNotificationSettings];
                               }
                                style:UIAlertActionStyleDefault];
  [_alertCoordinator start];
}

- (void)presentStartPriceTrackingErrorAlert {
  NSString* alertTitle = l10n_util::GetNSString(
      IDS_IOS_PRICE_NOTIFICATIONS_PRICE_TRACK_ERROR_ALERT_TITLE);
  NSString* alertMessage = l10n_util::GetNSString(
      IDS_IOS_PRICE_NOTIFICATIONS_PRICE_TRACK_SUBSCRIBE_ERROR_ALERT_DESCRIPTION);
  NSString* cancelTitle = l10n_util::GetNSString(
      IDS_IOS_PRICE_NOTIFICATIONS_PRICE_TRACK_PERMISSION_REDIRECT_ALERT_CANCEL);
  NSString* tryAgainTitle = l10n_util::GetNSString(
      IDS_IOS_PRICE_NOTIFICATIONS_PRICE_TRACK_ERROR_ALERT_REATTEMPT);

  __weak PriceInsightsModulator* weakSelf = self;
  [_alertCoordinator stop];
  _alertCoordinator = [[AlertCoordinator alloc]
      initWithBaseViewController:self.baseViewController
                         browser:self.browser
                           title:alertTitle
                         message:alertMessage];
  [_alertCoordinator addItemWithTitle:cancelTitle
                               action:^{
                                 [weakSelf dismissAlertCoordinator];
                               }
                                style:UIAlertActionStyleCancel];
  [_alertCoordinator addItemWithTitle:tryAgainTitle
                               action:^{
                                 [weakSelf onStartTrackingRetryForItem];
                               }
                                style:UIAlertActionStyleDefault];
  [_alertCoordinator start];
}

- (void)presentStopPriceTrackingErrorAlert {
  NSString* alertTitle = l10n_util::GetNSString(
      IDS_IOS_PRICE_NOTIFICATIONS_PRICE_TRACK_ERROR_ALERT_TITLE);
  NSString* alertMessage = l10n_util::GetNSString(
      IDS_IOS_PRICE_NOTIFICATIONS_PRICE_TRACK_UNSUBSCRIBE_ERROR_ALERT_DESCRIPTION);
  NSString* cancelTitle = l10n_util::GetNSString(
      IDS_IOS_PRICE_NOTIFICATIONS_PRICE_TRACK_PERMISSION_REDIRECT_ALERT_CANCEL);
  NSString* tryAgainTitle = l10n_util::GetNSString(
      IDS_IOS_PRICE_NOTIFICATIONS_PRICE_TRACK_ERROR_ALERT_REATTEMPT);

  __weak PriceInsightsModulator* weakSelf = self;
  [_alertCoordinator stop];
  _alertCoordinator = [[AlertCoordinator alloc]
      initWithBaseViewController:self.baseViewController
                         browser:self.browser
                           title:alertTitle
                         message:alertMessage];
  [_alertCoordinator addItemWithTitle:cancelTitle
                               action:^{
                                 [weakSelf dismissAlertCoordinator];
                               }
                                style:UIAlertActionStyleCancel];
  [_alertCoordinator addItemWithTitle:tryAgainTitle
                               action:^{
                                 [weakSelf onStopPriceTrackingRetryForItem];
                               }
                                style:UIAlertActionStyleDefault];
  [_alertCoordinator start];
}

#pragma mark - private

// Cell configuration handler helper.
- (void)configureCell:(PriceInsightsCell*)cell {
  cell.viewController = self.baseViewController;
  cell.mutator = self.mediator;
  self.priceInsightsItem = [self priceInsightsItemFromConfig];
  [cell configureWithItem:self.priceInsightsItem];
}

// Dismisses and removes the current alert coordinator.
- (void)dismissAlertCoordinator {
  [_alertCoordinator stop];
  _alertCoordinator = nil;
}

// Creates a PriceInsightsItem object from the current item configuration.
- (PriceInsightsItem*)priceInsightsItemFromConfig {
  PriceInsightsItemConfiguration* config =
      static_cast<PriceInsightsItemConfiguration*>(
          self.itemConfiguration.get());
  DCHECK(config->product_info.has_value());

  PriceInsightsItem* item = [[PriceInsightsItem alloc] init];
  std::string product_title =
      config->product_info->product_cluster_title.empty()
          ? config->product_info->title
          : config->product_info->product_cluster_title;
  item.title = base::SysUTF8ToNSString(product_title);
  item.currency = config->product_info->currency_code;
  item.country = config->product_info->country_code;
  item.canPriceTrack = config->can_price_track;
  item.productURL =
      self.browser->GetWebStateList()->GetActiveWebState()->GetVisibleURL();

  if (item.canPriceTrack &&
      config->product_info->product_cluster_id.has_value()) {
    item.clusterId = config->product_info->product_cluster_id.value();
    // TODO: b/355423868 - Use the async version of IsSubscribed.
    item.isPriceTracked = self.shoppingService->IsSubscribedFromCache(
        commerce::BuildUserSubscriptionForClusterId(item.clusterId));
  }

  if (!config->price_insights_info.has_value()) {
    return item;
  }

  if (config->price_insights_info->has_multiple_catalogs &&
      config->price_insights_info->catalog_attributes.has_value()) {
    item.variants = base::SysUTF8ToNSString(
        config->price_insights_info->catalog_attributes.value());
  }

  NSMutableDictionary* priceHistory = [[NSMutableDictionary alloc] init];
  for (std::tuple<std::string, int64_t> history :
       config->price_insights_info->catalog_history_prices) {
    NSDate* date = getNSDateFromString(std::get<0>(history));
    float amount = static_cast<float>(std::get<1>(history)) /
                   static_cast<float>(commerce::kToMicroCurrency);
    priceHistory[date] = @(amount);
  }
  item.priceHistory = priceHistory;
  item.buyingOptionsURL = config->price_insights_info->jackpot_url.has_value()
                              ? config->price_insights_info->jackpot_url.value()
                              : GURL();
  return item;
}

// Displays a snackbar message.
- (void)displaySnackbar:(NSString*)message
             buttonText:(NSString*)buttonText
                 action:(void (^)(void))action {
  CommandDispatcher* dispatcher = self.browser->GetCommandDispatcher();
  id<SnackbarCommands> snackbarHandler =
      HandlerForProtocol(dispatcher, SnackbarCommands);
  [snackbarHandler showSnackbarWithMessage:message
                                buttonText:buttonText
                             messageAction:action
                          completionAction:nil];
}

// Callback invoked when the user chooses to retry stopping price tracking after
// an initial error.
- (void)onStopPriceTrackingRetryForItem {
  [self.mediator priceInsightsStopTrackingItem:self.priceInsightsItem];
  [self dismissAlertCoordinator];
}

// Callback is invoked when the user chooses to retry starting price tracking
// after an initial error.
- (void)onStartTrackingRetryForItem {
  [self.mediator tryPriceInsightsTrackItem:self.priceInsightsItem];
  [self dismissAlertCoordinator];
}

// Callback invoked when the user chooses to close push notifications prompt
// during.
- (void)onPushNotificationCancel {
  [self.mediator priceInsightsTrackItem:self.priceInsightsItem
                   notificationsGranted:NO
                         showCompletion:YES];
  [self dismissAlertCoordinator];
}

// Callback invoked when the user chooses to open settings.
- (void)onPushNotificationSettings {
  NSString* settingURL = UIApplicationOpenSettingsURLString;
  if (@available(iOS 15.4, *)) {
    settingURL = UIApplicationOpenNotificationSettingsURLString;
  }

  [[UIApplication sharedApplication] openURL:[NSURL URLWithString:settingURL]
                                     options:{}
                           completionHandler:nil];
  [self.mediator priceInsightsTrackItem:self.priceInsightsItem
                   notificationsGranted:NO
                         showCompletion:YES];
  [self dismissAlertCoordinator];
}

// Callback invoked when the notification snackbar closes.
- (void)onPriceNotificationSnackBarClosed {
  CommandDispatcher* dispatcher = self.browser->GetCommandDispatcher();
  __weak id<PriceNotificationsCommands> weakPriceNotificationsHandler =
      HandlerForProtocol(dispatcher, PriceNotificationsCommands);
  __weak id<ContextualSheetCommands> weakContextualSheetHandler =
      HandlerForProtocol(dispatcher, ContextualSheetCommands);

  base::RecordAction(base::UserMetricsAction("MobileMenuPriceNotifications"));
  base::UmaHistogramEnumeration(
      "IOS.ContextualPanel.DismissedReason",
      ContextualPanelDismissedReason::BlockInteraction);
  [weakContextualSheetHandler closeContextualSheet];
  [weakPriceNotificationsHandler showPriceNotifications];
}

@end