diff --git a/Client/Frontend/Browser/BrowserViewController/BrowserViewController+URLBarDelegate.swift b/Client/Frontend/Browser/BrowserViewController/BrowserViewController+URLBarDelegate.swift index 8757bc49292c..83116f76589b 100644 --- a/Client/Frontend/Browser/BrowserViewController/BrowserViewController+URLBarDelegate.swift +++ b/Client/Frontend/Browser/BrowserViewController/BrowserViewController+URLBarDelegate.swift @@ -127,6 +127,31 @@ extension BrowserViewController: URLBarDelegate { navigationHandler?.showFakespotFlow(productURL: productURL) } + func urlBarPresentCFR(at sourceView: UIView) { + let contextualViewProvider = ContextualHintViewProvider(forHintType: .shoppingExperience, + with: profile) + + let contextHintVC = ContextualHintViewController(with: contextualViewProvider) + + contextHintVC.configure( + anchor: sourceView, + withArrowDirection: isBottomSearchBar ? .down : .up, + andDelegate: self, + presentedUsing: { + self.present(contextHintVC, animated: true) + TelemetryWrapper.recordEvent(category: .action, + method: .navigate, + object: .shoppingButton, + value: .shoppingCFRsDisplayed) + }, + andActionForButton: { [weak self] in + guard let self else { return } + guard let productURL = self.urlBar.currentURL else { return } + self.navigationHandler?.showFakespotFlow(productURL: productURL) + }, + overlayState: overlayManager) + } + func urlBarDidPressQRButton(_ urlBar: URLBarView) { let qrCodeViewController = QRCodeViewController() qrCodeViewController.qrCodeDelegate = self diff --git a/Client/Frontend/ContextualHint/ContextualHintCopyProvider.swift b/Client/Frontend/ContextualHint/ContextualHintCopyProvider.swift index 3951131567c7..328aa30a2d54 100644 --- a/Client/Frontend/ContextualHint/ContextualHintCopyProvider.swift +++ b/Client/Frontend/ContextualHint/ContextualHintCopyProvider.swift @@ -3,6 +3,8 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import Foundation +import Shared +import Common enum ContextualHintCopyType { case action, description @@ -15,9 +17,12 @@ struct ContextualHintCopyProvider: FeatureFlaggable { /// Arrow direction infuences toolbar copy, so it exists here. private var arrowDirection: UIPopoverArrowDirection? + private let prefs: Prefs - init(arrowDirecton: UIPopoverArrowDirection? = nil) { + init(profile: Profile = AppContainer.shared.resolve(), + arrowDirecton: UIPopoverArrowDirection? = nil) { self.arrowDirection = arrowDirecton + self.prefs = profile.prefs } // MARK: - Public interface @@ -59,6 +64,9 @@ struct ContextualHintCopyProvider: FeatureFlaggable { case .toolbarLocation: return getToolbarDescriptionCopy(with: arrowDirection) + + case .shoppingExperience: + descriptionCopy = getShoppingCopy(.description) } return descriptionCopy @@ -72,6 +80,8 @@ struct ContextualHintCopyProvider: FeatureFlaggable { actionCopy = CFRStrings.TabsTray.InactiveTabs.Action case .toolbarLocation: actionCopy = CFRStrings.Toolbar.SearchBarPlacementButtonText + case .shoppingExperience: + actionCopy = getShoppingCopy(.action) case .jumpBackIn, .jumpBackInSyncedTab: actionCopy = "" @@ -96,4 +106,16 @@ struct ContextualHintCopyProvider: FeatureFlaggable { default: return "" } } + + private func getShoppingCopy(_ copyType: ContextualHintCopyType) -> String { + let hasOptedIn = prefs.boolForKey(PrefsKeys.Shopping2023OptIn) ?? false + var copy: String + switch copyType { + case .action: + copy = hasOptedIn ? CFRStrings.Shopping.OptedInAction : CFRStrings.Shopping.NotOptedInAction + case .description: + copy = hasOptedIn ? CFRStrings.Shopping.OptedInBody : CFRStrings.Shopping.NotOptedInBody + } + return copy + } } diff --git a/Client/Frontend/ContextualHint/ContextualHintEligibilityUtility.swift b/Client/Frontend/ContextualHint/ContextualHintEligibilityUtility.swift index 74518ffc48b0..a8aa85171c7b 100644 --- a/Client/Frontend/ContextualHint/ContextualHintEligibilityUtility.swift +++ b/Client/Frontend/ContextualHint/ContextualHintEligibilityUtility.swift @@ -3,6 +3,7 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import Foundation +import Shared /// Public interface for contextual hint consumers protocol ContextualHintEligibilityUtilityProtocol { @@ -40,6 +41,8 @@ struct ContextualHintEligibilityUtility: ContextualHintEligibilityUtilityProtoco hintTypeShouldBePresented = isSearchBarLocationFeatureEnabled case .inactiveTabs: hintTypeShouldBePresented = true + case .shoppingExperience: + return canPresentShoppingCFR } return hintTypeShouldBePresented && !hasAlreadyBeenPresented(hintType) @@ -92,6 +95,38 @@ struct ContextualHintEligibilityUtility: ContextualHintEligibilityUtilityProtoco return shouldCheckToolbarHasShown } + // There are 2 types of CFRs + // + // Shopping CFR-1: The user has not opted in for the Shopping Experience + // Shopping CFR-2: The user has opted in for the Shopping Experience + private var canPresentShoppingCFR: Bool { + guard !hasAlreadyBeenPresented(.shoppingExperience) else { + // Retrieve the counter for shopping onboarding CFRs + let cfrCounter = profile.prefs.intForKey(PrefsKeys.ContextualHints.shoppingOnboardingCFRsCounterKey.rawValue) ?? 1 + // Check if the user has opted in for Shopping Experience + let hasOptedIn = profile.prefs.boolForKey(PrefsKeys.Shopping2023OptIn) ?? false + // Retrieve the last timestamp for Fakespot CFRs + let lastTimestamp = profile.prefs.timestampForKey(PrefsKeys.FakespotLastCFRTimestamp) + // Check if 24 hours have passed since the last timestamp + let hasTimePassed = lastTimestamp != nil ? Date.hasTimePassedBy(hours: 24, lastTimestamp: lastTimestamp!) : false + + if cfrCounter == 1, !hasOptedIn, hasTimePassed { + // - Display CFR-1 + profile.prefs.setInt(2, forKey: PrefsKeys.ContextualHints.shoppingOnboardingCFRsCounterKey.rawValue) + return true + } else if cfrCounter < 3, hasOptedIn, hasTimePassed { + // - Display CFR-2 + profile.prefs.setInt(3, forKey: PrefsKeys.ContextualHints.shoppingOnboardingCFRsCounterKey.rawValue) + return true + } + return false + } + // - Display CFR-1 + profile.prefs.setInt(1, forKey: PrefsKeys.ContextualHints.shoppingOnboardingCFRsCounterKey.rawValue) + profile.prefs.setTimestamp(Date.now(), forKey: PrefsKeys.FakespotLastCFRTimestamp) + return true + } + private func hasAlreadyBeenPresented(_ hintType: ContextualHintType) -> Bool { guard let contextualHintData = profile.prefs.boolForKey(prefsKey(for: hintType)) else { return false } diff --git a/Client/Frontend/ContextualHint/ContextualHintPrefsKeysProvider.swift b/Client/Frontend/ContextualHint/ContextualHintPrefsKeysProvider.swift index 8cba3cc18f4c..402a28c319b5 100644 --- a/Client/Frontend/ContextualHint/ContextualHintPrefsKeysProvider.swift +++ b/Client/Frontend/ContextualHint/ContextualHintPrefsKeysProvider.swift @@ -18,6 +18,7 @@ extension ContextualHintPrefsKeysProvider { case .jumpBackIn: return CFRPrefsKeys.jumpBackinKey.rawValue case .jumpBackInSyncedTab: return CFRPrefsKeys.jumpBackInSyncedTabKey.rawValue case .toolbarLocation: return CFRPrefsKeys.toolbarOnboardingKey.rawValue + case .shoppingExperience: return CFRPrefsKeys.shoppingOnboardingKey.rawValue } } } diff --git a/Client/Frontend/ContextualHint/ContextualHintViewController.swift b/Client/Frontend/ContextualHint/ContextualHintViewController.swift index 48561528d0e4..f1e09ea13fb5 100644 --- a/Client/Frontend/ContextualHint/ContextualHintViewController.swift +++ b/Client/Frontend/ContextualHint/ContextualHintViewController.swift @@ -161,7 +161,7 @@ class ContextualHintViewController: UIViewController, OnViewDismissable, Themeab hintView.configure(viewModel: viewModel) applyTheme() - if viewProvider.shouldPresentContextualHint() && shouldStartTimer { + if shouldStartTimer { viewProvider.startTimer() } diff --git a/Client/Frontend/ContextualHint/ContextualHintViewProvider.swift b/Client/Frontend/ContextualHint/ContextualHintViewProvider.swift index 829ac551e53d..f725c727e12b 100644 --- a/Client/Frontend/ContextualHint/ContextualHintViewProvider.swift +++ b/Client/Frontend/ContextualHint/ContextualHintViewProvider.swift @@ -17,6 +17,7 @@ enum ContextualHintType: String { case jumpBackInSyncedTab = "JumpBackInSyncedTab" case inactiveTabs = "InactiveTabs" case toolbarLocation = "ToolbarLocation" + case shoppingExperience = "ShoppingExperience" } class ContextualHintViewProvider: ContextualHintPrefsKeysProvider, SearchBarLocationProvider { @@ -111,7 +112,8 @@ class ContextualHintViewProvider: ContextualHintPrefsKeysProvider, SearchBarLoca var isActionType: Bool { switch hintType { case .inactiveTabs, - .toolbarLocation: + .toolbarLocation, + .shoppingExperience: return true default: return false diff --git a/Client/Frontend/Fakespot/FakespotOptInCardViewModel.swift b/Client/Frontend/Fakespot/FakespotOptInCardViewModel.swift index ec161352871a..f36a19a81067 100644 --- a/Client/Frontend/Fakespot/FakespotOptInCardViewModel.swift +++ b/Client/Frontend/Fakespot/FakespotOptInCardViewModel.swift @@ -116,6 +116,7 @@ struct FakespotOptInCardViewModel { func onTapMainButton() { prefs.setBool(true, forKey: PrefsKeys.Shopping2023OptIn) + prefs.setTimestamp(Date.now(), forKey: PrefsKeys.FakespotLastCFRTimestamp) TelemetryWrapper.recordEvent(category: .action, method: .tap, object: .shoppingOptIn) diff --git a/Client/Frontend/Fakespot/Views/FakespotSettingsCardView.swift b/Client/Frontend/Fakespot/Views/FakespotSettingsCardView.swift index 15a01c657aa7..7be2c2f2b411 100644 --- a/Client/Frontend/Fakespot/Views/FakespotSettingsCardView.swift +++ b/Client/Frontend/Fakespot/Views/FakespotSettingsCardView.swift @@ -26,7 +26,7 @@ class FakespotSettingsCardViewModel { var dismissViewController: (() -> Void)? var isReviewQualityCheckOn: Bool { - get { return prefs.boolForKey(PrefsKeys.Shopping2023OptIn) ?? true } + get { return prefs.boolForKey(PrefsKeys.Shopping2023OptIn) ?? false } set { prefs.setBool(newValue, forKey: PrefsKeys.Shopping2023OptIn) } } diff --git a/Client/Frontend/Strings.swift b/Client/Frontend/Strings.swift index e543ac3ec741..552888de73f2 100644 --- a/Client/Frontend/Strings.swift +++ b/Client/Frontend/Strings.swift @@ -172,6 +172,29 @@ extension String { value: "Move the toolbar to the top if that’s more your style.", comment: "Contextual hints are little popups that appear for the users informing them of new features. This one indicates a user can navigate to the Settings page to move the search bar to the top.") } + + public struct Shopping { + public static let NotOptedInBody = MZLocalizedString( + key: "", // "ContextualHints.Shopping.NotOptedIn.v120" + tableName: "Shopping", + value: "Find out if you can trust this product’s reviews — before you buy.", + comment: "Contextual hints are little popups that appear for the users informing them of new features. This one indicates that a user can tap on the shopping button to start using the Shopping feature.") + public static let NotOptedInAction = MZLocalizedString( + key: "", // "ContextualHints.Shopping.NotOptedInAction.v120" + tableName: "Shopping", + value: "Try review checker", + comment: "Contextual hints are little popups that appear for the users informing them of new features. This one is a call to action for the popup describing the Shopping feature. It indicates that a user can go directly to the Shopping feature by tapping the text of the action.") + public static let OptedInBody = MZLocalizedString( + key: "", // "ContextualHints.Shopping.OptedInBody.v120" + tableName: "Shopping", + value: "Are these reviews reliable? Check now to see an adjusted rating.", + comment: "Contextual hints are little popups that appear for the users informing them of new features. This one appears after the user has opted in and informs him if he wants use the review checker by tapping the Shopping button.") + public static let OptedInAction = MZLocalizedString( + key: "", // "ContextualHints.Shopping.OptedInAction.v120" + tableName: "Shopping", + value: "Open review checker", + comment: "Contextual hints are little popups that appear for the users informing them of new features. This is a call to action for the popup that appears after the user has opted in for the Shopping feature. It indicates that a user can directly open the review checker by tapping the text of the action.") + } } } diff --git a/Client/Frontend/Toolbar+URLBar/TabLocationView.swift b/Client/Frontend/Toolbar+URLBar/TabLocationView.swift index 0cb16df296d5..9b780417cd32 100644 --- a/Client/Frontend/Toolbar+URLBar/TabLocationView.swift +++ b/Client/Frontend/Toolbar+URLBar/TabLocationView.swift @@ -15,6 +15,7 @@ protocol TabLocationViewDelegate: AnyObject { func tabLocationViewDidBeginDragInteraction(_ tabLocationView: TabLocationView) func tabLocationViewDidTapShare(_ tabLocationView: TabLocationView, button: UIButton) func tabLocationViewDidTapShopping(_ tabLocationView: TabLocationView, button: UIButton) + func tabLocationViewPresentCFR(at sourceView: UIView) /// - returns: whether the long-press was handled by the delegate; i.e. return `false` when the conditions for even starting handling long-press were not satisfied @discardableResult @@ -290,6 +291,7 @@ class TabLocationView: UIView, FeatureFlaggable { shoppingButton.isHidden = shouldHideButton if !shouldHideButton { TelemetryWrapper.recordEvent(category: .action, method: .view, object: .shoppingButton) + delegate?.tabLocationViewPresentCFR(at: shoppingButton) } } diff --git a/Client/Frontend/Toolbar+URLBar/URLBarView.swift b/Client/Frontend/Toolbar+URLBar/URLBarView.swift index 0aad17678541..b4bbd6da9c8c 100644 --- a/Client/Frontend/Toolbar+URLBar/URLBarView.swift +++ b/Client/Frontend/Toolbar+URLBar/URLBarView.swift @@ -47,6 +47,7 @@ protocol URLBarDelegate: AnyObject { func urlBarDidBeginDragInteraction(_ urlBar: URLBarView) func urlBarDidPressShare(_ urlBar: URLBarView, shareView: UIView) func urlBarDidPressShopping(_ urlBar: URLBarView, shoppingButton: UIButton) + func urlBarPresentCFR(at sourceView: UIView) } protocol URLBarViewProtocol { @@ -766,6 +767,10 @@ extension URLBarView: TabLocationViewDelegate { delegate?.urlBarDidPressShopping(self, shoppingButton: button) } + func tabLocationViewPresentCFR(at sourceView: UIView) { + delegate?.urlBarPresentCFR(at: sourceView) + } + func tabLocationViewLocationAccessibilityActions(_ tabLocationView: TabLocationView) -> [UIAccessibilityCustomAction]? { return delegate?.urlBarLocationAccessibilityActions(self) } diff --git a/Client/Telemetry/TelemetryWrapper.swift b/Client/Telemetry/TelemetryWrapper.swift index b6e8b226764b..7fac53b63316 100644 --- a/Client/Telemetry/TelemetryWrapper.swift +++ b/Client/Telemetry/TelemetryWrapper.swift @@ -647,6 +647,7 @@ extension TelemetryWrapper { case bookmarkItem = "bookmark-item" case searchSuggestion = "search-suggestion" case searchHighlights = "search-highlights" + case shoppingCFRsDisplayed = "shopping-cfrs-displayed" case awesomebarShareTap = "awesomebar-share-tap" } @@ -1068,6 +1069,8 @@ extension TelemetryWrapper { GleanMetrics.Shopping.surfaceLearnMoreClicked.record() case (.action, .tap, .shoppingLearnMoreReviewQualityButton, _, _): GleanMetrics.Shopping.surfaceShowQualityExplainerClicked.record() + case (.action, .navigate, .shoppingButton, .shoppingCFRsDisplayed, _): + GleanMetrics.Shopping.addressBarFeatureCalloutDisplayed.record() // MARK: Onboarding case (.action, .view, .onboardingCardView, _, let extras): diff --git a/Client/metrics.yaml b/Client/metrics.yaml index 91e3e151da21..fee4632f8a0f 100755 --- a/Client/metrics.yaml +++ b/Client/metrics.yaml @@ -816,6 +816,18 @@ shopping: - fx-ios-data-stewards@mozilla.com expires: "2024-09-20" + address_bar_feature_callout_displayed: + type: event + description: | + Records an event everytime a shopping CFR is shown on the screen + bugs: + - https://github.com/mozilla-mobile/firefox-ios/issues/15461 + data_reviews: + - https://github.com/mozilla-mobile/firefox-ios/pull/16593 + notification_emails: + - fx-ios-data-stewards@mozilla.com + expires: "2024-09-25" + # Key Commands key_commands: press_key_command_action: diff --git a/Shared/Prefs.swift b/Shared/Prefs.swift index 377b6e0340fc..3ddf05c4b4d9 100644 --- a/Shared/Prefs.swift +++ b/Shared/Prefs.swift @@ -92,6 +92,8 @@ public struct PrefsKeys { case jumpBackInSyncedTabConfiguredKey = "JumpBackInSyncedTabConfigured" case inactiveTabsKey = "ContextualHintInactiveTabs" case toolbarOnboardingKey = "ContextualHintToolbarOnboardingKey" + case shoppingOnboardingKey = "ShoppingOnboardingCFRKey" + case shoppingOnboardingCFRsCounterKey = "ShoppingOnboardingCFRsCounterKey" } // Activity Stream @@ -140,6 +142,9 @@ public struct PrefsKeys { // The last timestamp we polled FxA for missing send tabs public static let PollCommandsTimestamp = "PollCommandsTimestamp" + // The last recorded CFR timestamp + public static let FakespotLastCFRTimestamp = "FakespotLastCFRTimestamp" + // Representing whether or not the last user session was private public static let LastSessionWasPrivate = "wasLastSessionPrivate" diff --git a/Shared/TimeConstants.swift b/Shared/TimeConstants.swift index 52b965ad6f6f..8ca59cb77fa3 100644 --- a/Shared/TimeConstants.swift +++ b/Shared/TimeConstants.swift @@ -141,6 +141,20 @@ extension Date { second: second) } + /// Checks if a specified amount of time in hours has passed since a given timestamp. + /// + /// - Parameters: + /// - hours: The number of hours to check for elapsed time. + /// - lastTimestamp: The timestamp to compare against. + /// + /// - Returns: `true` if the specified time in hours has passed since the lastTimestamp; `false` otherwise. + public static func hasTimePassedBy(hours: Timestamp, + lastTimestamp: Timestamp) -> Bool { + let millisecondsInAnHour: Timestamp = 3_600_000 // Convert 1 hour to milliseconds + let timeDifference = Date.now() - lastTimestamp + return timeDifference >= hours * millisecondsInAnHour + } + static func - (lhs: Date, rhs: Date) -> TimeInterval { return lhs.timeIntervalSinceReferenceDate - rhs.timeIntervalSinceReferenceDate } diff --git a/Tests/ClientTests/Fakespot/FakespotSettingsCardViewModelTests.swift b/Tests/ClientTests/Fakespot/FakespotSettingsCardViewModelTests.swift index f63d38718781..7a6c78b93b74 100644 --- a/Tests/ClientTests/Fakespot/FakespotSettingsCardViewModelTests.swift +++ b/Tests/ClientTests/Fakespot/FakespotSettingsCardViewModelTests.swift @@ -25,7 +25,7 @@ final class FakespotSettingsCardViewModelTests: XCTestCase { func testInitialViewModelValues() { XCTAssertEqual(viewModel.areAdsEnabled, true) - XCTAssertEqual(viewModel.isReviewQualityCheckOn, true) + XCTAssertEqual(viewModel.isReviewQualityCheckOn, false) } func testGetUserPrefsAfterSettingPrefs() { @@ -41,7 +41,7 @@ final class FakespotSettingsCardViewModelTests: XCTestCase { mockProfile.prefs.setBool(false, forKey: "") XCTAssertEqual(viewModel.areAdsEnabled, true) - XCTAssertEqual(viewModel.isReviewQualityCheckOn, true) + XCTAssertEqual(viewModel.isReviewQualityCheckOn, false) } func testSwitchValueChangedUpdatesPrefs() { diff --git a/Tests/ClientTests/Frontend/ContextualHints/ContextualHintEligibilityUtilityTests.swift b/Tests/ClientTests/Frontend/ContextualHints/ContextualHintEligibilityUtilityTests.swift index ef40ec30497c..4a22b566c477 100644 --- a/Tests/ClientTests/Frontend/ContextualHints/ContextualHintEligibilityUtilityTests.swift +++ b/Tests/ClientTests/Frontend/ContextualHints/ContextualHintEligibilityUtilityTests.swift @@ -139,4 +139,55 @@ class ContextualHintEligibilityUtilityTests: XCTestCase { let result = subject.canPresent(.jumpBackInSyncedTab) XCTAssertFalse(result) } + + // Test Shopping CFRs + func test_canPresentShoppingCFR_FirstDisplay_UserHasNotOptedIn() { + subject = ContextualHintEligibilityUtility(with: profile, overlayState: overlayState) + let result = subject.canPresent(.shoppingExperience) + XCTAssertTrue(result) + } + + func test_canPresentShoppingCFR_SecondDisplay_UserHasNotOptedIn_TimeHasPassed() { + let lastTimestamp: Timestamp = 1695719918000 // Date and time (GMT): Tuesday, 26 September 2023 09:18:38 + + profile.prefs.setBool(true, forKey: CFRPrefsKeys.shoppingOnboardingKey.rawValue) + profile.prefs.setTimestamp(lastTimestamp, forKey: PrefsKeys.FakespotLastCFRTimestamp) + profile.prefs.setBool(false, forKey: PrefsKeys.Shopping2023OptIn) + + let result = subject.canPresent(.shoppingExperience) + XCTAssertTrue(result) + } + + func test_canPresentShoppingCFR_SecondDisplay_UserHasOptedIn_TimeHasPassed() { + let lastTimestamp: Timestamp = 1695719918000 // Date and time (GMT): Tuesday, 26 September 2023 09:18:38 + + profile.prefs.setBool(true, forKey: CFRPrefsKeys.shoppingOnboardingKey.rawValue) + profile.prefs.setTimestamp(lastTimestamp, forKey: PrefsKeys.FakespotLastCFRTimestamp) + profile.prefs.setBool(true, forKey: PrefsKeys.Shopping2023OptIn) + + let result = subject.canPresent(.shoppingExperience) + XCTAssertTrue(result) + } + + func test_canPresentShoppingCFR_SecondDisplay_UserHasNotOptedIn_TimeHasNotPassed() { + let lastTimestamp: Timestamp = Date.now() + + profile.prefs.setBool(true, forKey: CFRPrefsKeys.shoppingOnboardingKey.rawValue) + profile.prefs.setTimestamp(lastTimestamp, forKey: PrefsKeys.FakespotLastCFRTimestamp) + profile.prefs.setBool(false, forKey: PrefsKeys.Shopping2023OptIn) + + let result = subject.canPresent(.shoppingExperience) + XCTAssertFalse(result) + } + + func test_canPresentShoppingCFR_SecondDisplay_UserHasOptedIn_TimeHasNotPassed() { + let lastTimestamp: Timestamp = Date.now() + + profile.prefs.setBool(true, forKey: CFRPrefsKeys.shoppingOnboardingKey.rawValue) + profile.prefs.setTimestamp(lastTimestamp, forKey: PrefsKeys.FakespotLastCFRTimestamp) + profile.prefs.setBool(true, forKey: PrefsKeys.Shopping2023OptIn) + + let result = subject.canPresent(.shoppingExperience) + XCTAssertFalse(result) + } } diff --git a/Tests/ClientTests/TabLocationViewTests.swift b/Tests/ClientTests/TabLocationViewTests.swift index 4cf634a8ac2e..2dcc16e08426 100644 --- a/Tests/ClientTests/TabLocationViewTests.swift +++ b/Tests/ClientTests/TabLocationViewTests.swift @@ -27,6 +27,7 @@ class TabLocationViewTests: XCTestCase { // A mock delegate class MockTabLocationViewDelegate: TabLocationViewDelegate { + func tabLocationViewPresentCFR(at sourceView: UIView) {} func tabLocationViewDidTapLocation(_ tabLocationView: TabLocationView) {} func tabLocationViewDidLongPressLocation(_ tabLocationView: TabLocationView) {} func tabLocationViewDidTapReaderMode(_ tabLocationView: TabLocationView) {} diff --git a/Tests/ClientTests/TelemetryWrapperTests.swift b/Tests/ClientTests/TelemetryWrapperTests.swift index ec795211827f..78539b75fc7a 100644 --- a/Tests/ClientTests/TelemetryWrapperTests.swift +++ b/Tests/ClientTests/TelemetryWrapperTests.swift @@ -307,6 +307,11 @@ class TelemetryWrapperTests: XCTestCase { testEventMetricRecordingSuccess(metric: GleanMetrics.Shopping.surfaceShowQualityExplainerClicked) } + func test_addressBarFeatureCalloutDisplayed_GleanIsCalled() { + TelemetryWrapper.recordEvent(category: .action, method: .navigate, object: .shoppingButton, value: .shoppingCFRsDisplayed) + testEventMetricRecordingSuccess(metric: GleanMetrics.Shopping.addressBarFeatureCalloutDisplayed) + } + // MARK: - Onboarding func test_onboardingSelectWallpaperWithExtras_GleanIsCalled() { let wallpaperNameKey = TelemetryWrapper.EventExtraKey.wallpaperName.rawValue diff --git a/Tests/SharedTests/DateExtensionsTests.swift b/Tests/SharedTests/DateExtensionsTests.swift index b791889e7937..7eb7a130d9c1 100644 --- a/Tests/SharedTests/DateExtensionsTests.swift +++ b/Tests/SharedTests/DateExtensionsTests.swift @@ -55,4 +55,16 @@ class DateExtensionsTests: XCTestCase { return Calendar(identifier: .gregorian).date(from: dateComponents) } + + func test_hasTimePassedBy() { + let tenHoursInMilliseconds: Timestamp = 3_600_000 * 10 + + let lastTimestamp: Timestamp = Date.now() - tenHoursInMilliseconds // Assuming 10 hours difference. + + XCTAssertTrue(Date.hasTimePassedBy(hours: 10, lastTimestamp: lastTimestamp)) + + XCTAssertTrue(Date.hasTimePassedBy(hours: 5, lastTimestamp: lastTimestamp)) + + XCTAssertFalse(Date.hasTimePassedBy(hours: 30, lastTimestamp: lastTimestamp)) + } }