chromium/ios/chrome/browser/ui/content_suggestions/parcel_tracking/parcel_tracking_view.mm

// 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.

#import "ios/chrome/browser/ui/content_suggestions/parcel_tracking/parcel_tracking_view.h"

#import "base/i18n/time_formatting.h"
#import "base/strings/sys_string_conversions.h"
#import "base/time/time.h"
#import "ios/chrome/browser/shared/ui/symbols/symbols.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/ui/content_suggestions/parcel_tracking/parcel_tracking_commands.h"
#import "ios/chrome/browser/ui/content_suggestions/parcel_tracking/parcel_tracking_item.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/public/provider/chrome/browser/branded_images/branded_images_api.h"
#import "ui/base/l10n/l10n_util_mac.h"
#import "url/gurl.h"

namespace {

// Spacing between Icon container and the text StackView.
const CGFloat kIconContainerTextSpacing = 20.0f;

// Corner radius of the Icon container.
const CGFloat kIconContainerCornerRadius = 12.0f;

// Icon container size.
const CGFloat kIconContainerWidth = 72.0f;

// Margin between icon and its container when using the default image.
const CGFloat kIconContainerMargin = 18.0f;

// Margin between icon and its container when using the carrier's logo.
const CGFloat kIconContainerMarginForCarrierLogo = 8.0f;

// Size of the icon.
const CGFloat kIconSize = 53.0f;

// Spacing between text StackView subviews.
const CGFloat kTextStackViewSpacing = 5.0f;

// Spacing between status bars.
const CGFloat kStatusBarViewSpacing = 6.0f;

// Status bar configurations.
const CGFloat kStatusBarWidth = 61.0f;
const CGFloat kStatusBarHeight = 6.0f;
const CGFloat kStatusBarCornerRadius = 3.0f;
const CGFloat kStatusBarBottomMarginViewHeight = .01f;

BOOL isInProgressState(ParcelState state) {
  return state == ParcelState::kPickedUp || state == ParcelState::kHandedOff ||
         state == ParcelState::kWithCarrier;
}

}  // namespace

// Represents a status bar in the ParcelStatusBarView.
@interface ParcelStatusBarView : UIView

// Configures the view to reflect `hasError` and if it should have `lighterTone`
// coloring.
- (void)configureAsError:(BOOL)hasError lighterTone:(BOOL)lighterTone;

@end

@implementation ParcelStatusBarView

- (instancetype)initWithFrame:(CGRect)frame {
  self = [super initWithFrame:frame];
  if (self) {
    self.layer.cornerRadius = kStatusBarCornerRadius;
    self.layer.masksToBounds = YES;

    [NSLayoutConstraint activateConstraints:@[
      [self.widthAnchor constraintEqualToConstant:kStatusBarWidth],
      [self.heightAnchor constraintEqualToConstant:kStatusBarHeight],
    ]];

    [self setContentHuggingPriority:UILayoutPriorityRequired
                            forAxis:UILayoutConstraintAxisVertical];
    [self
        setContentCompressionResistancePriority:UILayoutPriorityRequired
                                        forAxis:UILayoutConstraintAxisVertical];
  }
  return self;
}

- (void)configureAsError:(BOOL)hasError lighterTone:(BOOL)lighterTone {
  NSString* colorName;
  if (hasError) {
    colorName = lighterTone ? kRed100Color : kRed400Color;
  } else {
    colorName = lighterTone ? kGreen100Color : kGreen400Color;
  }
  self.backgroundColor = [UIColor colorNamed:colorName];
}

@end

@implementation ParcelTrackingModuleView {
  UIView* _imageContainer;
  ParcelStatusBarView* _firstStatusBar;
  ParcelStatusBarView* _secondStatusBar;
  ParcelStatusBarView* _thirdStatusBar;
  GURL _parcelTrackingURL;
  UIImageView* _iconImageView;
  UILabel* _titleLabel;
  UILabel* _subtitleLabel;
  UITapGestureRecognizer* _tapGestureRecognizer;
  BOOL _useCarrierLogo;
}

- (instancetype)initWithFrame:(CGRect)frame {
  self = [super initWithFrame:CGRectZero];
  if (self) {
    [self constructView];
    self.isAccessibilityElement = YES;
  }
  return self;
}

- (void)configureView:(ParcelTrackingItem*)config {
  _parcelTrackingURL = config.trackingURL;
  _iconImageView.image = [self iconImageForParcelType:config.parcelType];
  _imageContainer.layer.borderWidth = [self iconBorderWidth];

  NSString* carrierName;
  switch (config.parcelType) {
    case ParcelType::kUSPS:
      carrierName =
          l10n_util::GetNSString(IDS_IOS_PARCEL_TRACKING_CARRIER_USPS);
      break;
    case ParcelType::kUPS:
      carrierName = l10n_util::GetNSString(IDS_IOS_PARCEL_TRACKING_CARRIER_UPS);
      break;
    case ParcelType::kFedex:
      carrierName =
          l10n_util::GetNSString(IDS_IOS_PARCEL_TRACKING_CARRIER_FEDEX);
      break;
    default:
      break;
  }
  _subtitleLabel.text = l10n_util::GetNSStringF(
      IDS_IOS_CONTENT_SUGGESTIONS_PARCEL_TRACKING_MODULE_PACKAGE_INFORMATION,
      base::SysNSStringToUTF16(carrierName),
      base::SysNSStringToUTF16(config.parcelID));

  [self updateViewForParcelStatus:config.status
                     deliveryTime:config.estimatedDeliveryTime];

  self.accessibilityLabel = [NSString
      stringWithFormat:@"%@, %@", _titleLabel.text, _subtitleLabel.text];
}

- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
  [super traitCollectionDidChange:previousTraitCollection];
  if (previousTraitCollection.userInterfaceStyle !=
      self.traitCollection.userInterfaceStyle) {
    _imageContainer.layer.borderColor =
        [UIColor colorNamed:kGrey200Color].CGColor;
    _imageContainer.layer.borderWidth = [self iconBorderWidth];
  }
  if (previousTraitCollection.preferredContentSizeCategory !=
      self.traitCollection.preferredContentSizeCategory) {
    _titleLabel.font =
        CreateDynamicFont(UIFontTextStyleFootnote, UIFontWeightSemibold);
  }
}

- (void)constructView {
  _titleLabel = [[UILabel alloc] init];
  _titleLabel.isAccessibilityElement = NO;
  _titleLabel.font =
      CreateDynamicFont(UIFontTextStyleFootnote, UIFontWeightSemibold);
  _titleLabel.adjustsFontForContentSizeCategory = YES;
  _titleLabel.numberOfLines = 0;
  _titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
  _titleLabel.textColor = [UIColor colorNamed:kGreen600Color];
  [_titleLabel setContentHuggingPriority:UILayoutPriorityDefaultLow
                                 forAxis:UILayoutConstraintAxisVertical];
  [_titleLabel
      setContentCompressionResistancePriority:UILayoutPriorityRequired
                                      forAxis:UILayoutConstraintAxisVertical];

  _subtitleLabel = [[UILabel alloc] init];
  _subtitleLabel.numberOfLines = 1;
  _subtitleLabel.font =
      [UIFont preferredFontForTextStyle:UIFontTextStyleFootnote];
  _subtitleLabel.adjustsFontForContentSizeCategory = YES;
  _subtitleLabel.textColor = [UIColor colorNamed:kTextSecondaryColor];

  _firstStatusBar = [[ParcelStatusBarView alloc] init];
  _secondStatusBar = [[ParcelStatusBarView alloc] init];
  _thirdStatusBar = [[ParcelStatusBarView alloc] init];
  UIStackView* statusBarStackView =
      [[UIStackView alloc] initWithArrangedSubviews:@[
        _firstStatusBar, _secondStatusBar, _thirdStatusBar
      ]];
  statusBarStackView.axis = UILayoutConstraintAxisHorizontal;
  statusBarStackView.alignment = UIStackViewAlignmentCenter;
  statusBarStackView.spacing = kStatusBarViewSpacing;

  // Add empty view to serve as spacing between status bars and subtitle that
  // dynamically expands vertically to fill space.
  UIView* emptySpaceFiller = [[UIView alloc] init];
  [emptySpaceFiller setContentHuggingPriority:UILayoutPriorityDefaultLow
                                      forAxis:UILayoutConstraintAxisVertical];
  // Add empty view to trigger spacing between status bars and bottom alignment
  // with the image.
  UIView* statusBarBottomMarginView = [[UIView alloc] init];
  [NSLayoutConstraint activateConstraints:@[
    [statusBarBottomMarginView.heightAnchor
        constraintEqualToConstant:kStatusBarBottomMarginViewHeight]
  ]];
  UIStackView* rightVerticalStackView =
      [[UIStackView alloc] initWithArrangedSubviews:@[
        _titleLabel, _subtitleLabel, emptySpaceFiller, statusBarStackView,
        statusBarBottomMarginView
      ]];
  rightVerticalStackView.axis = UILayoutConstraintAxisVertical;
  rightVerticalStackView.alignment = UIStackViewAlignmentLeading;
  rightVerticalStackView.spacing = kTextStackViewSpacing;

  _iconImageView = [[UIImageView alloc] init];
  _iconImageView.contentMode = UIViewContentModeScaleAspectFit;
  _iconImageView.translatesAutoresizingMaskIntoConstraints = NO;

  // Container allows for margins between icon a border.
  _imageContainer = [[UIView alloc] init];
  _imageContainer.translatesAutoresizingMaskIntoConstraints = NO;
  _imageContainer.layer.cornerRadius = kIconContainerCornerRadius;
  _imageContainer.layer.masksToBounds = YES;
  _imageContainer.layer.borderColor =
      [UIColor colorNamed:kGrey200Color].CGColor;
  [_imageContainer addSubview:_iconImageView];
  CGFloat containerMargin = _useCarrierLogo ? kIconContainerMarginForCarrierLogo
                                            : kIconContainerMargin;
  AddSameConstraintsWithInsets(
      _iconImageView, _imageContainer,
      NSDirectionalEdgeInsetsMake(containerMargin, containerMargin,
                                  containerMargin, containerMargin));

  UIStackView* horizontalStackView = [[UIStackView alloc]
      initWithArrangedSubviews:@[ _imageContainer, rightVerticalStackView ]];
  horizontalStackView.translatesAutoresizingMaskIntoConstraints = NO;
  horizontalStackView.axis = UILayoutConstraintAxisHorizontal;
  horizontalStackView.alignment = UIStackViewAlignmentTrailing;
  horizontalStackView.spacing = kIconContainerTextSpacing;
  [self addSubview:horizontalStackView];
  AddSameConstraints(horizontalStackView, self);

  [NSLayoutConstraint activateConstraints:@[
    [_imageContainer.widthAnchor constraintEqualToConstant:kIconContainerWidth],
    [_imageContainer.heightAnchor
        constraintEqualToAnchor:_imageContainer.widthAnchor],
    [rightVerticalStackView.topAnchor
        constraintLessThanOrEqualToAnchor:_imageContainer.topAnchor],
  ]];

  // Set up the tap gesture recognizer.
  _tapGestureRecognizer =
      [[UITapGestureRecognizer alloc] initWithTarget:self
                                              action:@selector(handleTap:)];
  [self addGestureRecognizer:_tapGestureRecognizer];
}

// Returns the appropriate icon image for a `parcelType`.
- (UIImage*)iconImageForParcelType:(ParcelType)parcelType {
#if !BUILDFLAG(IOS_USE_BRANDED_SYMBOLS)
  _useCarrierLogo = NO;
  if (@available(iOS 16.0, *)) {
    return DefaultSymbolWithPointSize(kBoxTruckFillSymbol, kIconSize);
  } else {
    return DefaultSymbolWithPointSize(kShippingBoxFillSymbol, kIconSize);
  }
#else
  switch (parcelType) {
    case ParcelType::kUPS:
      _useCarrierLogo = YES;
      return [UIImage imageNamed:kUPSCarrierImage];
    case ParcelType::kFedex:
      _useCarrierLogo = YES;
      return [UIImage imageNamed:kFedexCarrierImage];
    default:
      _useCarrierLogo = NO;
      if (@available(iOS 16.0, *)) {
        return DefaultSymbolWithPointSize(kBoxTruckFillSymbol, kIconSize);
      } else {
        return DefaultSymbolWithPointSize(kShippingBoxFillSymbol, kIconSize);
      }
  }
#endif
}

// Updates the title and status bars based on the parcel `status` and
// `estimatedDeliveryTime`.
- (void)updateViewForParcelStatus:(ParcelState)status
                     deliveryTime:
                         (std::optional<base::Time>)estimatedDeliveryTime {
  NSString* dateString =
      estimatedDeliveryTime.has_value()
          ? base::SysUTF16ToNSString(base::LocalizedTimeFormatWithPattern(
                *estimatedDeliveryTime, "EEEE MMMM d"))
          : nil;
  NSString* imageColorName;
  NSString* imageContainerColorName;

  // If the parcel is in progress but the estimated delivered date was not set
  // on the server side, we cannot show the user much useful information. As
  // such, we treat those cases visually like they are in the new parcel state.
  if (!estimatedDeliveryTime.has_value() && isInProgressState(status)) {
    status = ParcelState::kNew;
  }

  // Configure the status bars (and title text color if needed) depending on
  // status.
  switch (status) {
    case ParcelState::kNew:
      _titleLabel.text = l10n_util::GetNSString(
          IDS_IOS_CONTENT_SUGGESTIONS_PARCEL_TRACKING_MODULE_PACKAGE_NEW_STATUS);
      [_firstStatusBar configureAsError:NO lighterTone:NO];
      [_secondStatusBar configureAsError:NO lighterTone:YES];
      [_thirdStatusBar configureAsError:NO lighterTone:YES];
      imageColorName = kGreen300Color;
      imageContainerColorName = kStaticGreen50Color;
      break;
    case ParcelState::kLabelCreated:
      _titleLabel.text = l10n_util::GetNSString(
          IDS_IOS_CONTENT_SUGGESTIONS_PARCEL_TRACKING_MODULE_PACKAGE_LABEL_CREATED_STATUS);
      [_firstStatusBar configureAsError:NO lighterTone:NO];
      [_secondStatusBar configureAsError:NO lighterTone:YES];
      [_thirdStatusBar configureAsError:NO lighterTone:YES];
      imageColorName = kGreen300Color;
      imageContainerColorName = kStaticGreen50Color;
      break;
    case ParcelState::kFinished: {
      if (!estimatedDeliveryTime.has_value()) {
        _titleLabel.text = l10n_util::GetNSString(
            IDS_IOS_CONTENT_SUGGESTIONS_PARCEL_TRACKING_MODULE_PACKAGE_DELIVERED_STATUS);
      } else {
        // Use Today date descriptor if the delivery day matches the current day
        if ([[NSCalendar currentCalendar]
                         isDate:estimatedDeliveryTime->ToNSDate()
                inSameDayAsDate:[NSDate date]]) {
          dateString = l10n_util::GetNSString(
              IDS_IOS_CONTENT_SUGGESTIONS_PARCEL_TRACKING_MODULE_PACKAGE_DELIVERED_TODAY);
        }
        _titleLabel.text = [NSString
            stringWithFormat:
                @"%@ %@",
                l10n_util::GetNSString(
                    IDS_IOS_CONTENT_SUGGESTIONS_PARCEL_TRACKING_MODULE_PACKAGE_DELIVERED_STATUS),
                dateString];
      }
      [_firstStatusBar configureAsError:NO lighterTone:NO];
      [_secondStatusBar configureAsError:NO lighterTone:NO];
      [_thirdStatusBar configureAsError:NO lighterTone:NO];
      imageColorName = kGreen300Color;
      imageContainerColorName = kStaticGreen50Color;
      break;
    }
    case ParcelState::kAtPickupLocation:
      _titleLabel.text = l10n_util::GetNSString(
          IDS_IOS_CONTENT_SUGGESTIONS_PARCEL_TRACKING_MODULE_PACKAGE_READY_PICKUP_STATUS);
      [_firstStatusBar configureAsError:NO lighterTone:NO];
      [_secondStatusBar configureAsError:NO lighterTone:NO];
      [_thirdStatusBar configureAsError:NO lighterTone:NO];
      imageColorName = kGreen300Color;
      imageContainerColorName = kStaticGreen50Color;
      break;
    case ParcelState::kPickedUp:
    case ParcelState::kHandedOff:
    case ParcelState::kWithCarrier:
      _titleLabel.text = l10n_util::GetNSStringF(
          IDS_IOS_CONTENT_SUGGESTIONS_PARCEL_TRACKING_MODULE_PACKAGE_ARRIVING_STATUS,
          base::SysNSStringToUTF16(dateString));
      [_firstStatusBar configureAsError:NO lighterTone:NO];
      [_secondStatusBar configureAsError:NO lighterTone:NO];
      [_thirdStatusBar configureAsError:NO lighterTone:YES];
      imageColorName = kGreen300Color;
      imageContainerColorName = kStaticGreen50Color;
      break;
    case ParcelState::kOutForDelivery:
      _titleLabel.text = l10n_util::GetNSStringF(
          IDS_IOS_CONTENT_SUGGESTIONS_PARCEL_TRACKING_MODULE_PACKAGE_ARRIVING_STATUS,
          base::SysNSStringToUTF16(l10n_util::GetNSString(
              IDS_IOS_CONTENT_SUGGESTIONS_PARCEL_TRACKING_MODULE_PACKAGE_DELIVERED_TODAY)));
      [_firstStatusBar configureAsError:NO lighterTone:NO];
      [_secondStatusBar configureAsError:NO lighterTone:NO];
      [_thirdStatusBar configureAsError:NO lighterTone:YES];
      imageColorName = kGreen300Color;
      imageContainerColorName = kStaticGreen50Color;
      break;
    case ParcelState::kDeliveryFailed:
      _titleLabel.text = l10n_util::GetNSString(
          IDS_IOS_CONTENT_SUGGESTIONS_PARCEL_TRACKING_MODULE_PACKAGE_DELIVERY_ATTEMPTED_STATUS);
      _titleLabel.textColor = [UIColor colorNamed:kRed600Color];
      [_firstStatusBar configureAsError:YES lighterTone:NO];
      [_secondStatusBar configureAsError:YES lighterTone:NO];
      [_thirdStatusBar configureAsError:YES lighterTone:YES];
      imageColorName = kRed300Color;
      imageContainerColorName = kRed50Color;
      break;
    case ParcelState::kError:
      _titleLabel.text = l10n_util::GetNSString(
          IDS_IOS_CONTENT_SUGGESTIONS_PARCEL_TRACKING_MODULE_PACKAGE_ERROR_STATUS);
      _titleLabel.textColor = [UIColor colorNamed:kRed600Color];
      [_firstStatusBar configureAsError:YES lighterTone:NO];
      [_secondStatusBar configureAsError:YES lighterTone:NO];
      [_thirdStatusBar configureAsError:YES lighterTone:YES];
      imageColorName = kRed300Color;
      imageContainerColorName = kRed50Color;
      break;
    case ParcelState::kCancelled:
      _titleLabel.text = l10n_util::GetNSString(
          IDS_IOS_CONTENT_SUGGESTIONS_PARCEL_TRACKING_MODULE_PACKAGE_CANCELLED_STATUS);
      _titleLabel.textColor = [UIColor colorNamed:kTextPrimaryColor];
      // No status bars.
      [_firstStatusBar removeFromSuperview];
      [_secondStatusBar removeFromSuperview];
      [_thirdStatusBar removeFromSuperview];
      imageColorName = kGrey400Color;
      imageContainerColorName = kGrey100Color;
      break;
    case ParcelState::kUndeliverable:
      _titleLabel.text = l10n_util::GetNSString(
          IDS_IOS_CONTENT_SUGGESTIONS_PARCEL_TRACKING_MODULE_PACKAGE_UNDELIVERABLE_STATUS);
      _titleLabel.textColor = [UIColor colorNamed:kTextPrimaryColor];
      // No status bars.
      [_firstStatusBar removeFromSuperview];
      [_secondStatusBar removeFromSuperview];
      [_thirdStatusBar removeFromSuperview];
      imageColorName = kGrey400Color;
      imageContainerColorName = kGrey100Color;
      break;
    case ParcelState::kReturnToSender:
    case ParcelState::kReturnCompleted:
      _titleLabel.text = l10n_util::GetNSString(
          IDS_IOS_CONTENT_SUGGESTIONS_PARCEL_TRACKING_MODULE_PACKAGE_RETURNED_TO_SENDER_STATUS);
      _titleLabel.textColor = [UIColor colorNamed:kTextPrimaryColor];
      // No status bars.
      [_firstStatusBar removeFromSuperview];
      [_secondStatusBar removeFromSuperview];
      [_thirdStatusBar removeFromSuperview];
      imageColorName = kGrey400Color;
      imageContainerColorName = kGrey100Color;
      break;
    default:
      break;
  }

  if (!_useCarrierLogo) {
    _imageContainer.backgroundColor =
        [UIColor colorNamed:imageContainerColorName];
    _iconImageView.tintColor = [UIColor colorNamed:imageColorName];
  }
}

- (void)handleTap:(UITapGestureRecognizer*)sender {
  if (sender.state == UIGestureRecognizerStateEnded) {
    [self.commandHandler loadParcelTrackingPage:_parcelTrackingURL];
  }
}

// Returns the icon container's border width.
- (CGFloat)iconBorderWidth {
  if (!_useCarrierLogo &&
      self.traitCollection.userInterfaceStyle != UIUserInterfaceStyleDark) {
    return 0;
  }
  return 1;
}

#pragma mark - Testing category methods

- (NSString*)titleLabelTextForTesting {
  return self->_titleLabel.text;
}

@end