| // Copyright 2018 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/bubble/bubble_presenter.h" |
| |
| #import "base/functional/bind.h" |
| #import "base/metrics/histogram_functions.h" |
| #import "base/metrics/user_metrics.h" |
| #import "base/metrics/user_metrics_action.h" |
| #import "components/content_settings/core/browser/host_content_settings_map.h" |
| #import "components/feature_engagement/public/event_constants.h" |
| #import "components/feature_engagement/public/feature_constants.h" |
| #import "components/feature_engagement/public/tracker.h" |
| #import "ios/chrome/browser/feature_engagement/tracker_factory.h" |
| #import "ios/chrome/browser/shared/model/url/chrome_url_constants.h" |
| #import "ios/chrome/browser/shared/model/url/url_util.h" |
| #import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h" |
| #import "ios/chrome/browser/shared/public/features/system_flags.h" |
| #import "ios/chrome/browser/shared/ui/util/named_guide.h" |
| #import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h" |
| #import "ios/chrome/browser/shared/ui/util/util_swift.h" |
| #import "ios/chrome/browser/ui/bubble/bubble_presenter_delegate.h" |
| #import "ios/chrome/browser/ui/bubble/bubble_util.h" |
| #import "ios/chrome/browser/ui/bubble/bubble_view_controller_presenter.h" |
| #import "ios/chrome/common/ui/util/ui_util.h" |
| #import "ios/chrome/grit/ios_chromium_strings.h" |
| #import "ios/chrome/grit/ios_strings.h" |
| #import "ios/web/public/ui/crw_web_view_proxy.h" |
| #import "ios/web/public/ui/crw_web_view_scroll_view_proxy.h" |
| #import "ios/web/public/web_state.h" |
| #import "ui/base/device_form_factor.h" |
| #import "ui/base/l10n/l10n_util.h" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| namespace { |
| const CGFloat kBubblePresentationDelay = 1; |
| } // namespace |
| |
| @interface BubblePresenter () |
| |
| // Used to display the bottom toolbar tip in-product help promotion bubble. |
| // `nil` if the tip bubble has not yet been presented. Once the bubble is |
| // dismissed, it remains allocated so that `userEngaged` remains accessible. |
| @property(nonatomic, strong) |
| BubbleViewControllerPresenter* bottomToolbarTipBubblePresenter; |
| // Used to display the long press on toolbar buttons tip in-product help |
| // promotion bubble. `nil` if the tip bubble has not yet been presented. Once |
| // the bubble is dismissed, it remains allocated so that `userEngaged` remains |
| // accessible. |
| @property(nonatomic, strong) |
| BubbleViewControllerPresenter* longPressToolbarTipBubblePresenter; |
| // Used to display the new tab tip in-product help promotion bubble. `nil` if |
| // the new tab tip bubble has not yet been presented. Once the bubble is |
| // dismissed, it remains allocated so that `userEngaged` remains accessible. |
| @property(nonatomic, strong) |
| BubbleViewControllerPresenter* tabTipBubblePresenter; |
| @property(nonatomic, strong, readwrite) |
| BubbleViewControllerPresenter* incognitoTabTipBubblePresenter; |
| @property(nonatomic, strong) |
| BubbleViewControllerPresenter* discoverFeedHeaderMenuTipBubblePresenter; |
| @property(nonatomic, strong) |
| BubbleViewControllerPresenter* readingListTipBubblePresenter; |
| @property(nonatomic, strong) |
| BubbleViewControllerPresenter* followWhileBrowsingBubbleTipPresenter; |
| @property(nonatomic, strong) |
| BubbleViewControllerPresenter* defaultPageModeTipBubblePresenter; |
| @property(nonatomic, strong) |
| BubbleViewControllerPresenter* whatsNewBubblePresenter; |
| @property(nonatomic, strong) BubbleViewControllerPresenter* |
| priceNotificationsWhileBrowsingBubbleTipPresenter; |
| @property(nonatomic, strong) |
| BubbleViewControllerPresenter* tabPinnedBubbleTipPresenter; |
| @property(nonatomic, assign) WebStateList* webStateList; |
| @property(nonatomic, assign) feature_engagement::Tracker* engagementTracker; |
| @property(nonatomic, assign) HostContentSettingsMap* settingsMap; |
| // Whether the presenter is started. |
| @property(nonatomic, assign, getter=isStarted) BOOL started; |
| |
| @end |
| |
| @implementation BubblePresenter |
| |
| #pragma mark - Public |
| |
| - (instancetype)initWithTracker:(feature_engagement::Tracker*)engagementTracker |
| hostContentSettingsMap:(HostContentSettingsMap*)settingsMap |
| webStateList:(WebStateList*)webStateList { |
| self = [super init]; |
| if (self) { |
| DCHECK(webStateList); |
| _webStateList = webStateList; |
| _engagementTracker = engagementTracker; |
| _settingsMap = settingsMap; |
| self.started = YES; |
| } |
| return self; |
| } |
| |
| - (void)stop { |
| self.started = NO; |
| self.webStateList = nullptr; |
| self.engagementTracker = nullptr; |
| self.settingsMap = nullptr; |
| } |
| |
| - (void)showHelpBubbleIfEligible { |
| if (!self.engagementTracker) { |
| return; |
| } |
| // Waits to present the bubbles until the feature engagement tracker database |
| // is fully initialized. |
| __weak BubblePresenter* weakSelf = self; |
| void (^onInitializedBlock)(bool) = ^(bool successfullyLoaded) { |
| if (!successfullyLoaded) |
| return; |
| dispatch_after( |
| dispatch_time(DISPATCH_TIME_NOW, |
| (int64_t)(kBubblePresentationDelay * NSEC_PER_SEC)), |
| dispatch_get_main_queue(), ^{ |
| [weakSelf presentBubbles]; |
| }); |
| }; |
| |
| // Because the new tab tip occurs on startup, the feature engagement |
| // tracker's database is not guaranteed to be loaded by this time. For the |
| // bubble to appear properly, a callback is used to guarantee the event data |
| // is loaded before the check to see if the promotion should be displayed. |
| self.engagementTracker->AddOnInitializedCallback( |
| base::BindRepeating(onInitializedBlock)); |
| } |
| |
| - (void)showLongPressHelpBubbleIfEligible { |
| if (!self.engagementTracker) { |
| return; |
| } |
| // Waits to present the bubble until the feature engagement tracker database |
| // is fully initialized. |
| __weak BubblePresenter* weakSelf = self; |
| void (^onInitializedBlock)(bool) = ^(bool successfullyLoaded) { |
| if (!successfullyLoaded) |
| return; |
| [weakSelf presentLongPressBubble]; |
| }; |
| |
| // Because the new tab tip occurs on startup, the feature engagement |
| // tracker's database is not guaranteed to be loaded by this time. For the |
| // bubble to appear properly, a callback is used to guarantee the event data |
| // is loaded before the check to see if the promotion should be displayed. |
| self.engagementTracker->AddOnInitializedCallback( |
| base::BindRepeating(onInitializedBlock)); |
| } |
| |
| - (void)hideAllHelpBubbles { |
| [self.tabTipBubblePresenter dismissAnimated:NO]; |
| [self.incognitoTabTipBubblePresenter dismissAnimated:NO]; |
| [self.bottomToolbarTipBubblePresenter dismissAnimated:NO]; |
| [self.longPressToolbarTipBubblePresenter dismissAnimated:NO]; |
| [self.discoverFeedHeaderMenuTipBubblePresenter dismissAnimated:NO]; |
| [self.readingListTipBubblePresenter dismissAnimated:NO]; |
| [self.followWhileBrowsingBubbleTipPresenter dismissAnimated:NO]; |
| [self.priceNotificationsWhileBrowsingBubbleTipPresenter dismissAnimated:NO]; |
| [self.tabPinnedBubbleTipPresenter dismissAnimated:NO]; |
| [self.whatsNewBubblePresenter dismissAnimated:NO]; |
| [self.defaultPageModeTipBubblePresenter dismissAnimated:NO]; |
| } |
| |
| - (void)userEnteredTabSwitcher { |
| if (self.tabTipBubblePresenter.userEngaged) { |
| base::RecordAction(base::UserMetricsAction("NewTabTipTargetSelected")); |
| } |
| } |
| |
| - (void)toolsMenuDisplayed { |
| if (self.incognitoTabTipBubblePresenter.userEngaged) { |
| base::RecordAction( |
| base::UserMetricsAction("NewIncognitoTabTipTargetSelected")); |
| } |
| } |
| |
| - (void)presentDiscoverFeedHeaderTipBubble { |
| BubbleArrowDirection arrowDirection = BubbleArrowDirectionDown; |
| NSString* text = |
| l10n_util::GetNSStringWithFixup(IDS_IOS_DISCOVER_FEED_HEADER_IPH); |
| |
| UIView* menuButton = [self.layoutGuideCenter |
| referencedViewUnderName:kDiscoverFeedHeaderMenuGuide]; |
| // Checks "canPresentBubble" after checking that the NTP with feed is visible. |
| // This ensures that the feature tracker doesn't trigger the IPH event if the |
| // bubble isn't shown, which would prevent it from ever being shown again. |
| if (!menuButton || ![self canPresentBubble]) { |
| return; |
| } |
| CGPoint discoverFeedHeaderAnchor = |
| [menuButton.superview convertPoint:menuButton.frame.origin toView:nil]; |
| // Anchor the IPH 1/3 of the way through the button. Anchoring it midway |
| // doesn't work since the button is too close to the edge, which would cause |
| // the bubble to bleed out the screen. |
| discoverFeedHeaderAnchor.x += menuButton.frame.size.width / 3; |
| |
| // If the feature engagement tracker does not consider it valid to display |
| // the tip, then end early to prevent the potential reassignment of the |
| // existing `discoverFeedHeaderMenuTipBubblePresenter` to nil. |
| BubbleViewControllerPresenter* presenter = [self |
| presentBubbleForFeature:feature_engagement::kIPHDiscoverFeedHeaderFeature |
| direction:arrowDirection |
| alignment:BubbleAlignmentTrailing |
| text:text |
| voiceOverAnnouncement:nil |
| anchorPoint:discoverFeedHeaderAnchor]; |
| if (!presenter) |
| return; |
| |
| self.discoverFeedHeaderMenuTipBubblePresenter = presenter; |
| } |
| |
| - (void)presentFollowWhileBrowsingTipBubble { |
| if (![self canPresentBubble]) |
| return; |
| |
| BubbleArrowDirection arrowDirection = |
| IsSplitToolbarMode(self.rootViewController) ? BubbleArrowDirectionDown |
| : BubbleArrowDirectionUp; |
| NSString* text = l10n_util::GetNSString(IDS_IOS_FOLLOW_WHILE_BROWSING_IPH); |
| CGPoint toolsMenuAnchor = [self anchorPointToGuide:kToolsMenuGuide |
| direction:arrowDirection]; |
| |
| // If the feature engagement tracker does not consider it valid to display |
| // the tip, then end early to prevent the potential reassignment of the |
| // existing `followWhileBrowsingBubbleTipPresenter` to nil. |
| BubbleViewControllerPresenter* presenter = [self |
| presentBubbleForFeature:feature_engagement::kIPHFollowWhileBrowsingFeature |
| direction:arrowDirection |
| alignment:BubbleAlignmentTrailing |
| text:text |
| voiceOverAnnouncement:l10n_util::GetNSString( |
| IDS_IOS_FOLLOW_WHILE_BROWSING_IPH) |
| anchorPoint:toolsMenuAnchor]; |
| if (!presenter) |
| return; |
| |
| self.followWhileBrowsingBubbleTipPresenter = presenter; |
| } |
| |
| - (void)presentDefaultSiteViewTipBubble { |
| if (![self canPresentBubble]) |
| return; |
| web::WebState* currentWebState = self.webStateList->GetActiveWebState(); |
| if (!currentWebState || |
| ShouldLoadUrlInDesktopMode(currentWebState->GetVisibleURL(), |
| self.settingsMap)) { |
| return; |
| } |
| |
| BubbleArrowDirection arrowDirection = |
| IsSplitToolbarMode(self.rootViewController) ? BubbleArrowDirectionDown |
| : BubbleArrowDirectionUp; |
| NSString* text = l10n_util::GetNSString(IDS_IOS_DEFAULT_PAGE_MODE_TIP); |
| CGPoint toolsMenuAnchor = [self anchorPointToGuide:kToolsMenuGuide |
| direction:arrowDirection]; |
| |
| // If the feature engagement tracker does not consider it valid to display |
| // the tip, then end early to prevent the potential reassignment of the |
| // existing presenter to nil. |
| BubbleViewControllerPresenter* presenter = [self |
| presentBubbleForFeature:feature_engagement::kIPHDefaultSiteViewFeature |
| direction:arrowDirection |
| alignment:BubbleAlignmentTrailing |
| text:text |
| voiceOverAnnouncement:l10n_util::GetNSString( |
| IDS_IOS_DEFAULT_PAGE_MODE_TIP_VOICE_OVER) |
| anchorPoint:toolsMenuAnchor]; |
| if (!presenter) |
| return; |
| |
| self.defaultPageModeTipBubblePresenter = presenter; |
| } |
| |
| - (void)presentWhatsNewBottomToolbarBubble { |
| if (![self canPresentBubble]) |
| return; |
| |
| BubbleArrowDirection arrowDirection = |
| IsSplitToolbarMode(self.rootViewController) ? BubbleArrowDirectionDown |
| : BubbleArrowDirectionUp; |
| NSString* text = l10n_util::GetNSString(IDS_IOS_WHATS_NEW_IPH_TEXT); |
| CGPoint toolsMenuAnchor = [self anchorPointToGuide:kToolsMenuGuide |
| direction:arrowDirection]; |
| |
| // If the feature engagement tracker does not consider it valid to display |
| // the tip, then end early to prevent the potential reassignment of the |
| // existing `whatsNewBubblePresenter` to nil. |
| BubbleViewControllerPresenter* presenter = [self |
| presentBubbleForFeature:feature_engagement::kIPHWhatsNewFeature |
| direction:arrowDirection |
| alignment:BubbleAlignmentTrailing |
| text:text |
| voiceOverAnnouncement:l10n_util::GetNSString(IDS_IOS_WHATS_NEW_IPH_TEXT) |
| anchorPoint:toolsMenuAnchor]; |
| if (!presenter) |
| return; |
| |
| self.whatsNewBubblePresenter = presenter; |
| } |
| |
| - (void)presentPriceNotificationsWhileBrowsingTipBubble { |
| if (![self canPresentBubble]) |
| return; |
| |
| BubbleArrowDirection arrowDirection = |
| IsSplitToolbarMode(self.rootViewController) ? BubbleArrowDirectionDown |
| : BubbleArrowDirectionUp; |
| NSString* text = l10n_util::GetNSString( |
| IDS_IOS_PRICE_NOTIFICATIONS_PRICE_TRACK_TOAST_IPH_TEXT); |
| CGPoint toolsMenuAnchor = [self anchorPointToGuide:kToolsMenuGuide |
| direction:arrowDirection]; |
| |
| // If the feature engagement tracker does not consider it valid to display |
| // the tip, then end early to prevent the potential reassignment of the |
| // existing `whatsNewBubblePresenter` to nil. |
| BubbleViewControllerPresenter* presenter = |
| [self presentBubbleForFeature: |
| feature_engagement::kIPHPriceNotificationsWhileBrowsingFeature |
| direction:arrowDirection |
| alignment:BubbleAlignmentTrailing |
| text:text |
| voiceOverAnnouncement:text |
| anchorPoint:toolsMenuAnchor]; |
| if (!presenter) |
| return; |
| |
| self.priceNotificationsWhileBrowsingBubbleTipPresenter = presenter; |
| } |
| |
| - (void)presentTabPinnedBubble { |
| if (!IsSplitToolbarMode(self.rootViewController)) { |
| // Don't show the tip if the user sees the tap strip. |
| return; |
| } |
| if (![self canPresentBubble]) { |
| return; |
| } |
| |
| BubbleArrowDirection arrowDirection = BubbleArrowDirectionDown; |
| NSString* text = |
| l10n_util::GetNSString(IDS_IOS_PINNED_TAB_OVERFLOW_ACTION_IPH_TEXT); |
| NSString* voiceOverAnnouncement = l10n_util::GetNSString( |
| IDS_IOS_PINNED_TAB_OVERFLOW_ACTION_IPH_VOICE_OVER_ANNOUNCEMENT); |
| CGPoint tabGridAnchor = [self anchorPointToGuide:kTabSwitcherGuide |
| direction:arrowDirection]; |
| |
| // If the feature engagement tracker does not consider it valid to display |
| // the tip, then end early to prevent the potential reassignment of the |
| // existing `tabPinnedBubbleTipPresenter` to nil. |
| BubbleViewControllerPresenter* presenter = |
| [self presentBubbleForFeature:feature_engagement::kIPHTabPinnedFeature |
| direction:arrowDirection |
| alignment:BubbleAlignmentTrailing |
| text:text |
| voiceOverAnnouncement:voiceOverAnnouncement |
| anchorPoint:tabGridAnchor]; |
| if (!presenter) { |
| return; |
| } |
| |
| self.tabPinnedBubbleTipPresenter = presenter; |
| } |
| |
| #pragma mark - Private |
| |
| - (void)presentBubbles { |
| // If the tip bubble has already been presented and the user is still |
| // considered engaged, it can't be overwritten or set to `nil` or else it will |
| // reset the `userEngaged` property. Once the user is not engaged, the bubble |
| // can be safely overwritten or set to `nil`. |
| if (!self.tabTipBubblePresenter.userEngaged) |
| [self presentNewTabTipBubble]; |
| if (!self.incognitoTabTipBubblePresenter.userEngaged) |
| [self presentNewIncognitoTabTipBubble]; |
| |
| // The bottom toolbar and Discover feed header menu don't use the |
| // isUserEngaged, so don't check if the user is engaged here. |
| [self presentBottomToolbarTipBubble]; |
| } |
| |
| - (void)presentLongPressBubble { |
| if (self.longPressToolbarTipBubblePresenter.userEngaged) |
| return; |
| |
| if (![self canPresentBubble]) |
| return; |
| |
| BubbleArrowDirection arrowDirection = |
| IsSplitToolbarMode(self.rootViewController) ? BubbleArrowDirectionDown |
| : BubbleArrowDirectionUp; |
| NSString* text = |
| l10n_util::GetNSString(IDS_IOS_LONG_PRESS_TOOLBAR_IPH_PROMOTION_TEXT); |
| CGPoint tabGridButtonAnchor = [self anchorPointToGuide:kTabSwitcherGuide |
| direction:arrowDirection]; |
| |
| // If the feature engagement tracker does not consider it valid to display |
| // the tip, then end early to prevent the potential reassignment of the |
| // existing `longPressToolbarTipBubblePresenter` to nil. |
| BubbleViewControllerPresenter* presenter = [self |
| presentBubbleForFeature:feature_engagement::kIPHLongPressToolbarTipFeature |
| direction:arrowDirection |
| alignment:BubbleAlignmentTrailing |
| text:text |
| voiceOverAnnouncement: |
| l10n_util::GetNSString( |
| IDS_IOS_LONG_PRESS_TOOLBAR_IPH_PROMOTION_VOICE_OVER) |
| anchorPoint:tabGridButtonAnchor]; |
| if (!presenter) |
| return; |
| |
| self.longPressToolbarTipBubblePresenter = presenter; |
| } |
| |
| // Presents and returns a bubble view controller for the `feature` with an arrow |
| // `direction`, an arrow `alignment` and a `text` on an `anchorPoint`. |
| - (BubbleViewControllerPresenter*) |
| presentBubbleForFeature:(const base::Feature&)feature |
| direction:(BubbleArrowDirection)direction |
| alignment:(BubbleAlignment)alignment |
| text:(NSString*)text |
| voiceOverAnnouncement:(NSString*)voiceOverAnnouncement |
| anchorPoint:(CGPoint)anchorPoint { |
| DCHECK(self.engagementTracker); |
| BubbleViewControllerPresenter* presenter = |
| [self bubblePresenterForFeature:feature |
| direction:direction |
| alignment:alignment |
| text:text]; |
| if (!presenter) |
| return nil; |
| presenter.voiceOverAnnouncement = voiceOverAnnouncement; |
| if ([presenter canPresentInView:self.rootViewController.view |
| anchorPoint:anchorPoint] && |
| ([self shouldForcePresentBubbleForFeature:feature] || |
| self.engagementTracker->ShouldTriggerHelpUI(feature))) { |
| [presenter presentInViewController:self.rootViewController |
| view:self.rootViewController.view |
| anchorPoint:anchorPoint]; |
| } |
| return presenter; |
| } |
| |
| // Presents a bubble associated with the bottom toolbar tip in-product help |
| // promotion. |
| - (void)presentBottomToolbarTipBubble { |
| if (!IsSplitToolbarMode(self.rootViewController)) |
| return; |
| |
| if (![self canPresentBubble]) |
| return; |
| |
| BubbleArrowDirection arrowDirection = BubbleArrowDirectionDown; |
| NSString* text = l10n_util::GetNSStringWithFixup( |
| IDS_IOS_BOTTOM_TOOLBAR_IPH_PROMOTION_TEXT); |
| CGPoint newTabButtonAnchor = [self anchorPointToGuide:kNewTabButtonGuide |
| direction:arrowDirection]; |
| |
| // If the feature engagement tracker does not consider it valid to display |
| // the tip, then end early to prevent the potential reassignment of the |
| // existing `bottomToolbarTipBubblePresenter` to nil. |
| BubbleViewControllerPresenter* presenter = [self |
| presentBubbleForFeature:feature_engagement::kIPHBottomToolbarTipFeature |
| direction:arrowDirection |
| alignment:BubbleAlignmentCenter |
| text:text |
| voiceOverAnnouncement: |
| l10n_util::GetNSString( |
| IDS_IOS_BOTTOM_TOOLBAR_IPH_PROMOTION_VOICE_OVER) |
| anchorPoint:newTabButtonAnchor]; |
| if (!presenter) |
| return; |
| |
| self.bottomToolbarTipBubblePresenter = presenter; |
| self.engagementTracker->NotifyEvent( |
| feature_engagement::events::kBottomToolbarOpened); |
| } |
| |
| // Optionally presents a bubble associated with the new tab tip in-product help |
| // promotion. If the feature engagement tracker determines it is valid to show |
| // the new tab tip, then it initializes `tabTipBubblePresenter` and presents |
| // the bubble. If it is not valid to show the new tab tip, |
| // `tabTipBubblePresenter` is set to `nil` and no bubble is shown. |
| - (void)presentNewTabTipBubble { |
| if (![self canPresentBubble]) |
| return; |
| |
| // Do not present the new tab tips on NTP. |
| web::WebState* currentWebState = self.webStateList->GetActiveWebState(); |
| if (!currentWebState || |
| currentWebState->GetVisibleURL() == kChromeUINewTabURL) { |
| return; |
| } |
| |
| BubbleArrowDirection arrowDirection = |
| IsSplitToolbarMode(self.rootViewController) ? BubbleArrowDirectionDown |
| : BubbleArrowDirectionUp; |
| NSString* text = |
| l10n_util::GetNSStringWithFixup(IDS_IOS_NEW_TAB_IPH_PROMOTION_TEXT); |
| CGPoint tabSwitcherAnchor = [self anchorPointToGuide:kTabSwitcherGuide |
| direction:arrowDirection]; |
| |
| // If the feature engagement tracker does not consider it valid to display |
| // the new tab tip, then end early to prevent the potential reassignment |
| // of the existing `tabTipBubblePresenter` to nil. |
| BubbleViewControllerPresenter* presenter = |
| [self presentBubbleForFeature:feature_engagement::kIPHNewTabTipFeature |
| direction:arrowDirection |
| alignment:BubbleAlignmentTrailing |
| text:text |
| voiceOverAnnouncement:nil |
| anchorPoint:tabSwitcherAnchor]; |
| if (!presenter) |
| return; |
| |
| self.tabTipBubblePresenter = presenter; |
| } |
| |
| // Presents a bubble associated with the new incognito tab tip in-product help |
| // promotion. |
| - (void)presentNewIncognitoTabTipBubble { |
| if (![self canPresentBubble]) |
| return; |
| |
| BubbleArrowDirection arrowDirection = |
| IsSplitToolbarMode(self.rootViewController) ? BubbleArrowDirectionDown |
| : BubbleArrowDirectionUp; |
| NSString* text = l10n_util::GetNSStringWithFixup( |
| IDS_IOS_NEW_INCOGNITO_TAB_IPH_PROMOTION_TEXT); |
| |
| CGPoint toolsButtonAnchor = |
| [self anchorPointToGuide:kToolsMenuGuide direction:arrowDirection]; |
| |
| // If the feature engagement tracker does not consider it valid to display |
| // the incognito tab tip, then end early to prevent the potential reassignment |
| // of the existing `incognitoTabTipBubblePresenter` to nil. |
| BubbleViewControllerPresenter* presenter = [self |
| presentBubbleForFeature:feature_engagement::kIPHNewIncognitoTabTipFeature |
| direction:arrowDirection |
| alignment:BubbleAlignmentTrailing |
| text:text |
| voiceOverAnnouncement:nil |
| anchorPoint:toolsButtonAnchor]; |
| if (!presenter) |
| return; |
| |
| self.incognitoTabTipBubblePresenter = presenter; |
| } |
| |
| #pragma mark - Private Utils |
| |
| // Returns the anchor point for a bubble with an `arrowDirection` pointing to a |
| // `guideName`. The point is in the window coordinates. |
| - (CGPoint)anchorPointToGuide:(GuideName*)guideName |
| direction:(BubbleArrowDirection)arrowDirection { |
| UILayoutGuide* guide = |
| [self.layoutGuideCenter makeLayoutGuideNamed:guideName]; |
| DCHECK(guide); |
| [self.rootViewController.view addLayoutGuide:guide]; |
| CGPoint anchorPoint = |
| bubble_util::AnchorPoint(guide.layoutFrame, arrowDirection); |
| CGPoint anchorPointInWindow = |
| [guide.owningView convertPoint:anchorPoint |
| toView:guide.owningView.window]; |
| [self.rootViewController.view removeLayoutGuide:guide]; |
| return anchorPointInWindow; |
| } |
| |
| // Returns whether the tab can present a bubble tip. |
| - (BOOL)canPresentBubble { |
| // If BubblePresenter has been stopped, do not present the bubble. |
| if (!self.started) { |
| return NO; |
| } |
| // If the BVC is not visible, do not present the bubble. |
| if (![self.delegate rootViewVisibleForBubblePresenter:self]) { |
| return NO; |
| } |
| // Do not present the bubble if there is no current tab. |
| if (!self.webStateList->GetActiveWebState()) { |
| return NO; |
| } |
| // Do not present the bubble if the tab is not scrolled to the top. |
| if (![self isTabScrolledToTop]) { |
| return NO; |
| } |
| return YES; |
| } |
| |
| - (BOOL)isTabScrolledToTop { |
| // If NTP exists, check if it is scrolled to top. |
| if ([self.delegate isNTPActiveForBubblePresenter:self]) { |
| return [self.delegate isNTPScrolledToTopForBubblePresenter:self]; |
| } |
| web::WebState* currentWebState = self.webStateList->GetActiveWebState(); |
| CRWWebViewScrollViewProxy* scrollProxy = |
| currentWebState->GetWebViewProxy().scrollViewProxy; |
| CGPoint scrollOffset = scrollProxy.contentOffset; |
| UIEdgeInsets contentInset = scrollProxy.contentInset; |
| return AreCGFloatsEqual(scrollOffset.y, -contentInset.top); |
| } |
| |
| // Returns a bubble associated with an in-product help promotion if |
| // it is valid to show the promotion and `nil` otherwise. `feature` is the |
| // base::Feature object associated with the given promotion. `direction` is the |
| // direction the bubble's arrow is pointing. `alignment` is the alignment of the |
| // arrow on the button. `text` is the text displayed by the bubble. |
| - (BubbleViewControllerPresenter*) |
| bubblePresenterForFeature:(const base::Feature&)feature |
| direction:(BubbleArrowDirection)direction |
| alignment:(BubbleAlignment)alignment |
| text:(NSString*)text { |
| DCHECK(self.engagementTracker); |
| if ([self shouldForcePresentBubbleForFeature:feature] || |
| self.engagementTracker->WouldTriggerHelpUI(feature)) { |
| // Capture `weakSelf` instead of the feature engagement tracker object |
| // because `weakSelf` will safely become `nil` if it is deallocated, whereas |
| // the feature engagement tracker will remain pointing to invalid memory if |
| // its owner (the ChromeBrowserState) is deallocated. |
| __weak BubblePresenter* weakSelf = self; |
| ProceduralBlockWithSnoozeAction dismissalCallback = |
| ^(feature_engagement::Tracker::SnoozeAction snoozeAction) { |
| [weakSelf featureDismissed:feature withSnooze:snoozeAction]; |
| }; |
| |
| BubbleViewControllerPresenter* bubbleViewControllerPresenter = |
| [[BubbleViewControllerPresenter alloc] |
| initDefaultBubbleWithText:text |
| arrowDirection:direction |
| alignment:alignment |
| isLongDurationBubble:[self isLongDurationBubble:feature] |
| dismissalCallback:dismissalCallback]; |
| |
| return bubbleViewControllerPresenter; |
| } |
| return nil; |
| } |
| |
| - (void)featureDismissed:(const base::Feature&)feature |
| withSnooze: |
| (feature_engagement::Tracker::SnoozeAction)snoozeAction { |
| if (!self.engagementTracker) { |
| return; |
| } |
| self.engagementTracker->DismissedWithSnooze(feature, snoozeAction); |
| } |
| |
| // Returns YES if the bubble for `feature` has a long duration. |
| - (BOOL)isLongDurationBubble:(const base::Feature&)feature { |
| // Display follow iph bubble with long duration. |
| return feature.name == |
| feature_engagement::kIPHFollowWhileBrowsingFeature.name; |
| } |
| |
| // Return YES if the bubble should always be presented. Ex. if force present |
| // bubble set by system experimental settings. |
| - (BOOL)shouldForcePresentBubbleForFeature:(const base::Feature&)feature { |
| // Always present follow IPH if it's triggered by system experimental |
| // settings. |
| if (feature.name == feature_engagement::kIPHFollowWhileBrowsingFeature.name && |
| experimental_flags::ShouldAlwaysShowFollowIPH()) { |
| return YES; |
| } |
| |
| return NO; |
| } |
| |
| @end |