chromium/ios/chrome/browser/follow/model/follow_tab_helper.mm

// Copyright 2022 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/follow/model/follow_tab_helper.h"

#import "base/functional/callback.h"
#import "base/memory/ptr_util.h"
#import "base/strings/string_util.h"
#import "base/strings/sys_string_conversions.h"
#import "base/strings/utf_string_conversions.h"
#import "base/task/cancelable_task_tracker.h"
#import "base/task/sequenced_task_runner.h"
#import "base/task/single_thread_task_runner.h"
#import "base/time/time.h"
#import "components/feature_engagement/public/feature_constants.h"
#import "components/feature_engagement/public/tracker.h"
#import "components/history/core/browser/history_service.h"
#import "components/history/core/browser/history_types.h"
#import "components/keyed_service/core/service_access_type.h"
#import "ios/chrome/browser/feature_engagement/model/tracker_factory.h"
#import "ios/chrome/browser/follow/model/follow_action_state.h"
#import "ios/chrome/browser/follow/model/follow_features.h"
#import "ios/chrome/browser/follow/model/follow_java_script_feature.h"
#import "ios/chrome/browser/follow/model/follow_menu_updater.h"
#import "ios/chrome/browser/follow/model/follow_service.h"
#import "ios/chrome/browser/follow/model/follow_service_factory.h"
#import "ios/chrome/browser/follow/model/follow_util.h"
#import "ios/chrome/browser/history/model/history_service_factory.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/model/url/url_util.h"
#import "ios/chrome/browser/shared/public/commands/help_commands.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/public/features/system_flags.h"
#import "ios/chrome/browser/signin/model/authentication_service.h"
#import "ios/chrome/browser/signin/model/authentication_service_factory.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/web/public/js_messaging/web_frame.h"
#import "ios/web/public/js_messaging/web_frames_manager.h"
#import "ios/web/public/web_state.h"
#import "ui/base/l10n/l10n_util.h"
#import "url/gurl.h"
#import "url/origin.h"

namespace {

// The prefix of domain name that can be removed. It is used when generating the
// follow item text.
const char kRemovablePrefix[] = "www.";

}  // namespace.

FollowTabHelper::~FollowTabHelper() {
  DCHECK(!web_state_);
}

FollowTabHelper::FollowTabHelper(web::WebState* web_state)
    : web_state_(web_state) {
  // Ensure that follow is not enabled for incognito.
  DCHECK(web_state_);
  DCHECK(!web_state_->GetBrowserState()->IsOffTheRecord());
  web_state_observation_.Observe(web_state_.get());
}

void FollowTabHelper::set_help_handler(id<HelpCommands> help_handler) {
  if (!IsWebChannelsEnabled()) {
    return;
  }
  help_handler_ = help_handler;
}

void FollowTabHelper::SetFollowMenuUpdater(
    id<FollowMenuUpdater> follow_menu_updater) {
  if (!IsWebChannelsEnabled()) {
    return;
  }
  DCHECK(web_state_);
  follow_menu_updater_ = follow_menu_updater;
}

void FollowTabHelper::UpdateFollowMenuItem() {
  if (!IsWebChannelsEnabled()) {
    return;
  }
  if (should_update_follow_item_) {
    FollowJavaScriptFeature::GetInstance()->GetWebPageURLs(
        web_state_,
        base::BindOnce(&FollowTabHelper::UpdateFollowMenuItemWithURL,
                       weak_ptr_factory_.GetWeakPtr()));
  }
}

void FollowTabHelper::RemoveFollowMenuUpdater() {
  if (!IsWebChannelsEnabled()) {
    return;
  }
  follow_menu_updater_ = nil;
  should_update_follow_item_ = true;
}

void FollowTabHelper::DidStartNavigation(
    web::WebState* web_state,
    web::NavigationContext* navigation_context) {
  should_update_follow_item_ = true;
}
void FollowTabHelper::DidRedirectNavigation(
    web::WebState* web_state,
    web::NavigationContext* navigation_context) {
  should_update_follow_item_ = true;
}

void FollowTabHelper::PageLoaded(
    web::WebState* web_state,
    web::PageLoadCompletionStatus load_completion_status) {
  // Do not show follow IPH if the user is not signed in.
  ChromeBrowserState* browserState =
      ChromeBrowserState::FromBrowserState(web_state->GetBrowserState());
  AuthenticationService* authenticationService =
      AuthenticationServiceFactory::GetForBrowserState(browserState);
  if (!authenticationService || !authenticationService->GetPrimaryIdentity(
                                    signin::ConsentLevel::kSignin)) {
    return;
  }

  if (!IsWebChannelsEnabled()) {
    return;
  }

  // Do not show Follow IPH if it is disabled.
  if (!base::FeatureList::IsEnabled(
          feature_engagement::kIPHFollowWhileBrowsingFeature)) {
    return;
  }

  // Record when the page was successfully loaded. Computing whether the
  // IPH needs to be displayed is done asynchronously and the time used
  // to compute this will be removed from the delay before the IPH is
  // displayed.
  const base::Time page_load_time = base::Time::Now();

  // Do not show IPH when browsing non http, https URLs and Chrome URLs, such as
  // NTP, flags, version, sad tab, etc.
  const GURL& url = web_state->GetVisibleURL();
  if (UrlHasChromeScheme(url) || !url.SchemeIsHTTPOrHTTPS()) {
    return;
  }

  switch (load_completion_status) {
    case web::PageLoadCompletionStatus::FAILURE:
      break;
    case web::PageLoadCompletionStatus::SUCCESS:
      FollowJavaScriptFeature::GetInstance()->GetWebPageURLs(
          web_state,
          base::BindOnce(&FollowTabHelper::OnSuccessfulPageLoad,
                         weak_ptr_factory_.GetWeakPtr(), url, page_load_time));
      break;
  }
}

void FollowTabHelper::WebStateDestroyed(web::WebState* web_state) {
  DCHECK_EQ(web_state_, web_state);
  DCHECK(web_state_observation_.IsObservingSource(web_state));
  weak_ptr_factory_.InvalidateWeakPtrs();
  web_state_observation_.Reset();
  web_state_ = nullptr;
}

void FollowTabHelper::OnSuccessfulPageLoad(const GURL& url,
                                           base::Time page_load_time,
                                           WebPageURLs* web_page_urls) {
  DCHECK(web_state_);

  // Don't show follow in-product help (IPH) if there's no help handler. Ex.
  // help_handler_ is nil when link preview page is loaded.
  if (!help_handler_) {
    return;
  }

  // Always show IPH for eligible website if experimental setting is enabled.
  if (experimental_flags::ShouldAlwaysShowFollowIPH()) {
    // A non-nil URL is required to display the IPH (as PresentFollowIPH
    // crash when trying to store a nil URL). Use the -URL property of
    // `web_page_urls`.
    PresentFollowIPH(web_page_urls.URL);
    return;
  }

  feature_engagement::Tracker* feature_engagement_tracker =
      feature_engagement::TrackerFactory::GetForBrowserState(
          ChromeBrowserState::FromBrowserState(web_state_->GetBrowserState()));
  // Do not show follow IPH if the feature engagement conditions are
  // not fulfilled. Ex. Do not show more than 5 Follow IPHs per week.
  if (!feature_engagement_tracker->WouldTriggerHelpUI(
          feature_engagement::kIPHFollowWhileBrowsingFeature)) {
    return;
  }

  NSURL* recommended_url =
      FollowServiceFactory::GetForBrowserState(
          ChromeBrowserState::FromBrowserState(web_state_->GetBrowserState()))
          ->GetRecommendedSiteURL(web_page_urls);

  // Do not show follow IPH if:
  // 1. The site is not recommended;
  // 2. The recommended url is empty (it happens if there's an error when
  // fetching);
  // 3. The IPH was shown too recently.
  if (!recommended_url || recommended_url.absoluteString.length == 0 ||
      !IsFollowIPHShownFrequencyEligible(recommended_url.host)) {
    return;
  }

  // Check if the site has enough visit count.
  history::HistoryService* history_service =
      ios::HistoryServiceFactory::GetForBrowserState(
          ChromeBrowserState::FromBrowserState(web_state_->GetBrowserState()),
          ServiceAccessType::EXPLICIT_ACCESS);

  // Ignore any visits within the last hour so that we do not count
  // the current visit to the page.
  const base::Time end_time =
      page_load_time - GetVisitHistoryExclusiveDuration();
  const base::Time begin_time = page_load_time - GetVisitHistoryDuration();

  // Get daily visit count for `url` from the history service.
  history_service->GetDailyVisitsToOrigin(
      url::Origin::Create(url), begin_time, end_time,
      base::BindOnce(&FollowTabHelper::OnDailyVisitQueryResult,
                     weak_ptr_factory_.GetWeakPtr(), page_load_time,
                     recommended_url),
      &history_task_tracker_);
}

void FollowTabHelper::OnDailyVisitQueryResult(
    base::Time page_load_time,
    NSURL* recommended_url,
    history::DailyVisitsResult result) {
  // Do not display the IPH if there are not enough visits.
  if (result.total_visits < GetNumVisitMin() ||
      result.days_with_visits < GetDailyVisitMin()) {
    return;
  }

  // Check how much time remains before the IPH needs to be displayed.
  const base::TimeDelta elapsed_time = base::Time::Now() - page_load_time;
  if (elapsed_time >= GetShowFollowIPHAfterLoaded()) {
    PresentFollowIPH(recommended_url);
  } else {
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
        FROM_HERE,
        base::BindOnce(&FollowTabHelper::PresentFollowIPH,
                       weak_ptr_factory_.GetWeakPtr(), recommended_url),
        GetShowFollowIPHAfterLoaded() - elapsed_time);
  }
}

void FollowTabHelper::UpdateFollowMenuItemWithURL(WebPageURLs* web_page_urls) {
  DCHECK(web_state_);

  web::WebFrame* web_frame = FollowJavaScriptFeature::GetInstance()
                                 ->GetWebFramesManager(web_state_)
                                 ->GetMainWebFrame();
  // Only update the follow menu item when web_page_urls is not null and when
  // webFrame can be retrieved. Otherwise, leave the option disabled.
  if (web_page_urls && web_frame) {
    const bool followed =
        FollowServiceFactory::GetForBrowserState(
            ChromeBrowserState::FromBrowserState(web_state_->GetBrowserState()))
            ->IsWebSiteFollowed(web_page_urls);

    std::string domain_name = web_frame->GetSecurityOrigin().host();
    if (base::StartsWith(domain_name, kRemovablePrefix)) {
      domain_name = domain_name.substr(strlen(kRemovablePrefix));
    }

    const bool enabled =
        GetFollowActionState(web_state_) == FollowActionStateEnabled;

    [follow_menu_updater_
        updateFollowMenuItemWithWebPage:web_page_urls
                               followed:followed
                             domainName:base::SysUTF8ToNSString(domain_name)
                                enabled:enabled];
  }

  should_update_follow_item_ = false;
}

void FollowTabHelper::PresentFollowIPH(NSURL* recommended_url) {
  DCHECK(help_handler_);
  [help_handler_
      presentInProductHelpWithType:InProductHelpType::kFollowWhileBrowsing];
  StoreFollowIPHDisplayEvent(recommended_url.host);
  if (experimental_flags::ShouldAlwaysShowFollowIPH()) {
    // Remove the follow IPH display event that just added because it's
    // triggered by experimental settings.
    RemoveLastFollowIPHDisplayEvent();
  }
}

WEB_STATE_USER_DATA_KEY_IMPL(FollowTabHelper)