// 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/content_suggestions/parcel_tracking/parcel_tracking_mediator.h"
#import "base/functional/bind.h"
#import "base/functional/callback.h"
#import "base/strings/sys_string_conversions.h"
#import "components/commerce/core/shopping_service.h"
#import "components/prefs/ios/pref_observer_bridge.h"
#import "components/prefs/pref_change_registrar.h"
#import "ios/chrome/browser/ntp/shared/metrics/home_metrics.h"
#import "ios/chrome/browser/ntp/ui_bundled/new_tab_page_metrics_delegate.h"
#import "ios/chrome/browser/parcel_tracking/features.h"
#import "ios/chrome/browser/parcel_tracking/metrics.h"
#import "ios/chrome/browser/parcel_tracking/parcel_tracking_prefs.h"
#import "ios/chrome/browser/parcel_tracking/parcel_tracking_util.h"
#import "ios/chrome/browser/parcel_tracking/tracking_source.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_constants.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/browser/url_loading/model/url_loading_browser_agent.h"
#import "ios/chrome/browser/url_loading/model/url_loading_params.h"
@interface ParcelTrackingMediator () <PrefObserverDelegate>
@end
@implementation ParcelTrackingMediator {
raw_ptr<commerce::ShoppingService> _shoppingService;
NSArray<ParcelTrackingItem*>* _parcelTrackingItems;
UrlLoadingBrowserAgent* _URLLoadingBrowserAgent;
raw_ptr<PrefService> _prefService;
// Bridge to listen to pref changes.
std::unique_ptr<PrefObserverBridge> _prefObserverBridge;
// Registrar for pref changes notifications.
PrefChangeRegistrar _prefChangeRegistrar;
base::CancelableOnceCallback<commerce::GetParcelStatusCallback::RunType>
_parcelFetchTimeoutClosure;
}
- (instancetype)
initWithShoppingService:(commerce::ShoppingService*)shoppingService
URLLoadingBrowserAgent:(UrlLoadingBrowserAgent*)URLLoadingBrowserAgent
prefService:(PrefService*)prefService {
self = [super init];
if (self) {
_shoppingService = shoppingService;
_URLLoadingBrowserAgent = URLLoadingBrowserAgent;
_prefService = prefService;
_prefObserverBridge = std::make_unique<PrefObserverBridge>(self);
_prefChangeRegistrar.Init(_prefService);
if (IsHomeCustomizationEnabled()) {
_prefObserverBridge->ObserveChangesForPreference(
prefs::kHomeCustomizationMagicStackParcelTrackingEnabled,
&_prefChangeRegistrar);
} else {
_prefObserverBridge->ObserveChangesForPreference(kParcelTrackingDisabled,
&_prefChangeRegistrar);
}
}
return self;
}
- (void)disconnect {
_shoppingService = nil;
_URLLoadingBrowserAgent = nil;
_delegate = nil;
_prefChangeRegistrar.RemoveAll();
_prefObserverBridge.reset();
_prefService = nullptr;
}
- (void)reset {
_parcelTrackingItems = nil;
}
- (void)fetchTrackedParcels {
_parcelFetchTimeoutClosure.Cancel();
__weak ParcelTrackingMediator* weakSelf = self;
_parcelFetchTimeoutClosure.Reset(base::BindOnce(
^(bool success,
std::unique_ptr<std::vector<commerce::ParcelTrackingStatus>> parcels) {
ParcelTrackingMediator* strongSelf = weakSelf;
if (!strongSelf || !success || !strongSelf.delegate) {
return;
}
[strongSelf parcelStatusesSuccessfullyReceived:std::move(parcels)];
}));
_shoppingService->GetAllParcelStatuses(_parcelFetchTimeoutClosure.callback());
}
#pragma mark - Public
- (ParcelTrackingItem*)parcelTrackingItemToShow {
if ([_parcelTrackingItems count] > 1) {
ParcelTrackingItem* itemToShow = _parcelTrackingItems[0];
itemToShow.shouldShowSeeMore = YES;
return itemToShow;
}
return _parcelTrackingItems[0];
}
- (NSArray<ParcelTrackingItem*>*)allParcelTrackingItems {
return _parcelTrackingItems;
}
- (void)disableModule {
DisableParcelTracking(_prefService);
_shoppingService->StopTrackingAllParcels(base::BindOnce(^(bool){
}));
[self.delegate parcelTrackingDisabled];
}
- (void)untrackParcel:(NSString*)parcelID {
_shoppingService->StopTrackingParcel(
base::SysNSStringToUTF8(parcelID), base::BindOnce(^(bool) {
parcel_tracking::RecordParcelsUntracked(
TrackingSource::kMagicStackModule, 1);
}));
}
- (void)trackParcel:(NSString*)parcelID carrier:(ParcelType)carrier {
commerce::ParcelIdentifier::Carrier carrierValue =
[self carrierValueForParcelType:carrier];
_shoppingService->StartTrackingParcels(
{std::make_pair(carrierValue, base::SysNSStringToUTF8(parcelID))},
std::string(),
base::BindOnce(
^(bool, std::unique_ptr<std::vector<commerce::ParcelTrackingStatus>>){
}));
}
- (void)setDelegate:(id<ParcelTrackingMediatorDelegate>)delegate {
if (delegate == _delegate) {
return;
}
_delegate = delegate;
if (_delegate) {
[self fetchTrackedParcels];
}
}
#pragma mark - ParcelTrackingCommands
- (void)loadParcelTrackingPage:(GURL)parcelTrackingURL {
[self.NTPMetricsDelegate parcelTrackingOpened];
[self.delegate logMagicStackEngagementForType:ContentSuggestionsModuleType::
kParcelTracking];
_URLLoadingBrowserAgent->Load(UrlLoadParams::InCurrentTab(parcelTrackingURL));
}
#pragma mark - PrefObserverDelegate
- (void)onPreferenceChanged:(const std::string&)preferenceName {
if (preferenceName == kParcelTrackingDisabled) {
if (IsParcelTrackingDisabled(_prefService)) {
[self disableModule];
}
}
if (preferenceName ==
prefs::kHomeCustomizationMagicStackParcelTrackingEnabled) {
CHECK(IsHomeCustomizationEnabled());
if (IsParcelTrackingDisabled(_prefService)) {
[self disableModule];
}
}
}
#pragma mark - Private
// Handles a parcel tracking status fetch result from the
// commerce::ShoppingService.
- (void)parcelStatusesSuccessfullyReceived:
(std::unique_ptr<std::vector<commerce::ParcelTrackingStatus>>)
parcelStatuses {
NSMutableArray* parcelItems = [NSMutableArray array];
for (auto iter = parcelStatuses->begin(); iter != parcelStatuses->end();
++iter) {
ParcelTrackingItem* item = [[ParcelTrackingItem alloc] init];
item.parcelType = [self parcelTypeforCarrierValue:iter->carrier];
item.estimatedDeliveryTime = iter->estimated_delivery_time;
item.parcelID = base::SysUTF8ToNSString(iter->tracking_id);
item.trackingURL = iter->tracking_url;
item.status = (ParcelState)iter->state;
item.commandHandler = self;
[parcelItems addObject:item];
if (iter->estimated_delivery_time.has_value() &&
*iter->estimated_delivery_time < base::Time::Now() - base::Days(2)) {
// Parcel was delivered more than two days ago, make this the last time it
// is shown by stopping tracking.
_shoppingService->StopTrackingParcel(iter->tracking_id,
base::BindOnce(^(bool){
}));
}
}
if ([parcelItems count] > 0) {
_parcelTrackingItems = parcelItems;
[self logParcelTrackingFreshnessSignalIfApplicable];
[self.delegate newParcelsAvailable];
}
}
// Logs a freshness signal for the Parcel Tracking module if there is at least
// one parcel with an estimated delivery date within the next two days.
- (void)logParcelTrackingFreshnessSignalIfApplicable {
for (ParcelTrackingItem* item in _parcelTrackingItems) {
base::Time now = base::Time::Now();
if (item.estimatedDeliveryTime.has_value() &&
*item.estimatedDeliveryTime > now &&
*item.estimatedDeliveryTime < now + base::Days(2)) {
RecordModuleFreshnessSignal(
ContentSuggestionsModuleType::kParcelTracking);
return;
}
}
}
// Maps the carrier int value into a ParcelType.
- (ParcelType)parcelTypeforCarrierValue:(int)carrier {
if (carrier == 1) {
return ParcelType::kFedex;
} else if (carrier == 2) {
return ParcelType::kUPS;
} else if (carrier == 4) {
return ParcelType::kUSPS;
}
return ParcelType::kUnkown;
}
- (commerce::ParcelIdentifier::Carrier)carrierValueForParcelType:
(ParcelType)parcelType {
switch (parcelType) {
case ParcelType::kUSPS:
return commerce::ParcelIdentifier::Carrier(4);
case ParcelType::kUPS:
return commerce::ParcelIdentifier::Carrier(2);
case ParcelType::kFedex:
return commerce::ParcelIdentifier::Carrier(1);
default:
return commerce::ParcelIdentifier::Carrier(0);
}
}
@end