Skip to content

Commit

Permalink
[ads] Add search result ad clicked InfoBar on iOS
Browse files Browse the repository at this point in the history
  • Loading branch information
aseren committed Sep 4, 2024
1 parent 643eca2 commit f681d88
Show file tree
Hide file tree
Showing 9 changed files with 250 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,10 @@ extension BrowserViewController: TabManagerDelegate {
self.downloadToast = downloadToast
}

if let searchResultAdClickedInfoBar = toast as? SearchResultAdClickedInfoBar {
self.searchResultAdClickedInfoBar = searchResultAdClickedInfoBar
}

// If BVC isnt visible hold on to this toast until viewDidAppear
if view.window == nil {
pendingToast = toast
Expand Down Expand Up @@ -241,6 +245,13 @@ extension BrowserViewController: TabManagerDelegate {
)
}

func hideToastsOnNavigationStartIfNeeded(_ tabManager: TabManager) {
if tabManager.selectedTab?.braveSearchResultAdManager == nil {
searchResultAdClickedInfoBar?.dismiss(false)
searchResultAdClickedInfoBar = nil
}
}

func tabManagerDidRemoveAllTabs(_ tabManager: TabManager, toast: ButtonToast?) {
guard let toast = toast, !privateBrowsingManager.isPrivateBrowsing else {
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ extension BrowserViewController: WKNavigationDelegate {
}
}

hideToastsOnNavigationStartIfNeeded(tabManager)

resetRedirectChain(webView)

// Append source URL to redirect chain
Expand Down Expand Up @@ -414,13 +416,25 @@ extension BrowserViewController: WKNavigationDelegate {
if navigationAction.targetFrame?.isMainFrame == true,
BraveSearchManager.isValidURL(requestURL)
{

if let braveSearchResultAdManager = tab?.braveSearchResultAdManager,
braveSearchResultAdManager.isSearchResultAdClickedURL(requestURL),
navigationAction.navigationType == .linkActivated
{
braveSearchResultAdManager.maybeTriggerSearchResultAdClickedEvent(requestURL)
tab?.braveSearchResultAdManager = nil
let showSearchResultAdClickedPrivacyNotice =
rewards.ads.shouldShowSearchResultAdClickedInfoBar()

braveSearchResultAdManager.maybeTriggerSearchResultAdClickedEvent(
requestURL,
completion: { [weak self] success in
guard let self, success, showSearchResultAdClickedPrivacyNotice else {
return
}
let searchResultClickedInfobar = SearchResultAdClickedInfoBar(
tabManager: self.tabManager
)
self.show(toast: searchResultClickedInfobar, duration: nil)
}
)
} else {
// The Brave-Search-Ads header should be added with a negative value when all
// of the following conditions are met:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,8 @@ public class BrowserViewController: UIViewController {
var downloadToast: DownloadToast?
/// A toast which is active and not yet dismissed
var activeButtonToast: Toast?
/// An infobar displaying a privacy notice when a search result ad is clicked
var searchResultAdClickedInfoBar: SearchResultAdClickedInfoBar?
/// A boolean to determine If AddToListActivity should be added
var addToPlayListActivityItem: (enabled: Bool, item: PlaylistInfo?)?
/// A boolean to determine if OpenInPlaylistActivity should be shown
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class BraveSearchResultAdManager: NSObject {
)
}

func maybeTriggerSearchResultAdClickedEvent(_ url: URL) {
func maybeTriggerSearchResultAdClickedEvent(_ url: URL, completion: @escaping ((Bool) -> Void)) {
guard let placementId = getPlacementID(url) else {
return
}
Expand All @@ -58,7 +58,7 @@ class BraveSearchResultAdManager: NSObject {
rewards.ads.triggerSearchResultAdEvent(
searchResultAd,
eventType: .clicked,
completion: { _ in }
completion: completion
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

import DesignSystem
import Foundation
import Shared
import SnapKit
import SwiftUI
import UIKit

struct SearchResultAdClickedInfoBarUX {
static let toastHeight: CGFloat = 100.0
static let toastPadding: CGFloat = 10.0
static let toastCloseButtonWidth: CGFloat = 20.0
static let toastLabelFont = UIFont.systemFont(ofSize: 15, weight: .semibold)
static let toastBackgroundColor = UIColor(braveSystemName: .schemesOnPrimaryFixed)
static let learnMoreUrl = "https://support.brave.com/hc/en-us/articles/360026361072-Brave-Ads-FAQ"
}

class SearchResultAdClickedInfoBar: Toast, UITextViewDelegate {
let tabManager: TabManager

init(tabManager: TabManager) {
self.tabManager = tabManager

super.init(frame: .zero)

self.tapDismissalMode = .outsideTap

self.clipsToBounds = true
self.addSubview(
createView(
Strings.searchResultAdsClickedPrivacyNoticeToastTitle + " "
)
)

self.toastView.backgroundColor = SearchResultAdClickedInfoBarUX.toastBackgroundColor

self.toastView.snp.makeConstraints { make in
make.left.right.height.equalTo(self)
self.animationConstraint =
make.top.equalTo(self).offset(SearchResultAdClickedInfoBarUX.toastHeight).constraint
}

self.snp.makeConstraints { make in
make.height.equalTo(SearchResultAdClickedInfoBarUX.toastHeight)
}
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

fileprivate func createView(
_ labelText: String
) -> UIView {
let horizontalStackView = UIStackView()
horizontalStackView.axis = .horizontal
horizontalStackView.alignment = .center
horizontalStackView.spacing = SearchResultAdClickedInfoBarUX.toastPadding

let label = UITextView()
label.textAlignment = .left
label.textColor = .white
label.font = SearchResultAdClickedInfoBarUX.toastLabelFont
label.backgroundColor = SearchResultAdClickedInfoBarUX.toastBackgroundColor
label.isEditable = false
label.isScrollEnabled = false
label.isSelectable = true
label.delegate = self

let learnMoreText = Strings.learnMore.withNonBreakingSpace
let attributes: [NSAttributedString.Key: Any] = [
.foregroundColor: UIColor.white,
.font: SearchResultAdClickedInfoBarUX.toastLabelFont,
]

let linkAttributes: [NSAttributedString.Key: Any] = [
.font: SearchResultAdClickedInfoBarUX.toastLabelFont,
.foregroundColor: UIColor.white,
.underlineStyle: 1,
]
label.linkTextAttributes = linkAttributes

let nsLabelAttributedString = NSMutableAttributedString(
string: labelText,
attributes: attributes
)
let nsLinkAttributedString = NSMutableAttributedString(
string: learnMoreText,
attributes: linkAttributes
)

if let url = URL(string: SearchResultAdClickedInfoBarUX.learnMoreUrl) {
let learnMoreRange = NSRange(location: 0, length: learnMoreText.count)
nsLinkAttributedString.addAttribute(.link, value: url, range: learnMoreRange)
nsLabelAttributedString.append(nsLinkAttributedString)
label.isUserInteractionEnabled = true
}
label.attributedText = nsLabelAttributedString

horizontalStackView.addArrangedSubview(label)

if let buttonImage = UIImage(braveSystemNamed: "leo.close") {
let button = UIButton()
button.setImage(buttonImage, for: .normal)
button.imageView?.contentMode = .scaleAspectFit
button.imageView?.tintColor = .white
button.imageView?.preferredSymbolConfiguration = .init(
font: .preferredFont(for: .title3, weight: .regular),
scale: .small
)

button.imageView?.snp.makeConstraints {
$0.width.equalTo(SearchResultAdClickedInfoBarUX.toastCloseButtonWidth)
}

button.addGestureRecognizer(
UITapGestureRecognizer(target: self, action: #selector(buttonPressed))
)

horizontalStackView.addArrangedSubview(button)
}

toastView.addSubview(horizontalStackView)

horizontalStackView.snp.makeConstraints { make in
make.centerX.equalTo(toastView)
make.centerY.equalTo(toastView)
make.width.equalTo(toastView.snp.width).offset(
-2 * SearchResultAdClickedInfoBarUX.toastPadding
)
}

return toastView
}

func textView(
_ textView: UITextView,
shouldInteractWith url: URL,
in characterRange: NSRange,
interaction: UITextItemInteraction
) -> Bool {
self.tabManager.addTabAndSelect(
URLRequest(url: URL(string: SearchResultAdClickedInfoBarUX.learnMoreUrl)!),
isPrivate: false
)
dismiss(true)
return false
}

@objc func buttonPressed(_ gestureRecognizer: UIGestureRecognizer) {
dismiss(true)
}

override func showToast(
viewController: UIViewController? = nil,
delay: DispatchTimeInterval = SimpleToastUX.toastDelayBefore,
duration: DispatchTimeInterval?,
makeConstraints: @escaping (ConstraintMaker) -> Void,
completion: (() -> Void)? = nil
) {
super.showToast(
viewController: viewController,
delay: delay,
duration: duration,
makeConstraints: makeConstraints
)
}
}
15 changes: 15 additions & 0 deletions ios/brave-ios/Sources/Brave/Frontend/Browser/Toast.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ class Toast: UIView {

var displayState = State.dismissed

var tapDismissalMode: TapDismissalMode = .anyTap

lazy var gestureRecognizer: UITapGestureRecognizer = {
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
gestureRecognizer.cancelsTouchesInView = false
Expand Down Expand Up @@ -97,6 +99,14 @@ class Toast: UIView {
}

@objc func handleTap(_ gestureRecognizer: UIGestureRecognizer) {
if tapDismissalMode == .outsideTap {
let location = gestureRecognizer.location(in: self)
// Check if the tap was inside the toast view
if self.point(inside: location, with: nil) {
return
}
}

dismiss(false)
}

Expand All @@ -106,4 +116,9 @@ class Toast: UIView {
case pendingDismiss
case dismissed
}

enum TapDismissalMode {
case outsideTap
case anyTap
}
}
10 changes: 10 additions & 0 deletions ios/brave-ios/Sources/Shared/SharedStrings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -337,3 +337,13 @@ extension Strings {
comment: "Question shown to user when tapping a link that opens the App Store app"
)
}

// Search result ad clicked privacy notice toast title
extension Strings {
public static let searchResultAdsClickedPrivacyNoticeToastTitle = NSLocalizedString(
"SearchResultAdsClickedPrivacyNoticeToastTitle",
bundle: .module,
value: "You’ve just clicked on a Brave Search ad. Unlike Big Tech, we measure ad performance anonymously and preserve your privacy.",
comment: "Toast displayed after a user clicked a search result ad for the first time."
)
}
5 changes: 5 additions & 0 deletions ios/browser/api/ads/brave_ads.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ OBJC_EXPORT
/// Returns `true` if the user opted-in to search result ads.
- (BOOL)isOptedInToSearchResultAds;

/// Returns `true` if the privacy notice infobar should be displayed when a user
/// clicks on a search result ad. This should be called before calling
/// `triggerSearchResultAdEvent` for the click.
- (BOOL)shouldShowSearchResultAdClickedInfoBar;

/// Used to notify the ads service that the user has opted-in/opted-out to
/// Brave News.
- (void)notifyBraveNewsIsEnabledPreferenceDidChange:(BOOL)isEnabled;
Expand Down
20 changes: 17 additions & 3 deletions ios/browser/api/ads/brave_ads.mm
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,11 @@ - (BOOL)isOptedInToSearchResultAds {
brave_ads::prefs::kOptedInToSearchResultAds);
}

- (BOOL)shouldShowSearchResultAdClickedInfoBar {
return self.profilePrefService->GetBoolean(
brave_ads::prefs::kShouldShowSearchResultAdClickedInfoBar);
}

- (void)notifyBraveNewsIsEnabledPreferenceDidChange:(BOOL)isEnabled {
[self setProfilePref:brave_news::prefs::kBraveNewsOptedIn
value:base::Value(isEnabled)];
Expand Down Expand Up @@ -1632,10 +1637,19 @@ - (void)triggerSearchResultAdEvent:
return;
}

const auto mojom_event_type =
static_cast<brave_ads::mojom::SearchResultAdEventType>(eventType);
ads->TriggerSearchResultAdEvent(
searchResultAd.cppObjPtr,
static_cast<brave_ads::mojom::SearchResultAdEventType>(eventType),
base::BindOnce(completion));
searchResultAd.cppObjPtr, mojom_event_type,
base::BindOnce(^(const bool success) {
if (success &&
mojom_event_type ==
brave_ads::mojom::SearchResultAdEventType::kClicked) {
self.profilePrefService->SetBoolean(
brave_ads::prefs::kShouldShowSearchResultAdClickedInfoBar, false);
}
completion(success);
}));
}

- (void)purgeOrphanedAdEventsForType:(BraveAdsAdType)adType
Expand Down

0 comments on commit f681d88

Please sign in to comment.