Skip to content

Commit

Permalink
Refresh toast updates (#3552)
Browse files Browse the repository at this point in the history
Task/Issue URL:
https://app.asana.com/0/72649045549333/1207889813347128/f

**Description**:
We’re introducing a behavioral toast on macOS, which required moving logic from the iOS client into BSK. Some of this logic has beenupdated—specifically, we now only support an event for 3 consecutive refreshes, whereas previously, we had multiple events.
  • Loading branch information
jaceklyp authored Nov 9, 2024
1 parent 0f5996e commit 1ae8ae8
Show file tree
Hide file tree
Showing 41 changed files with 235 additions and 585 deletions.
14 changes: 9 additions & 5 deletions Core/PixelEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -671,8 +671,10 @@ extension Pixel {
case toggleReportDoNotSend
case toggleReportDismiss

case userBehaviorReloadTwiceWithin12Seconds
case userBehaviorReloadThreeTimesWithin20Seconds
case pageRefreshThreeTimesWithin20Seconds

case siteNotWorkingShown
case siteNotWorkingWebsiteIsBroken

// MARK: History
case historyStoreLoadFailed
Expand Down Expand Up @@ -1494,9 +1496,11 @@ extension Pixel.Event {
// MARK: - Apple Ad Attribution
case .appleAdAttribution: return "m_apple-ad-attribution"

// MARK: - User behavior
case .userBehaviorReloadTwiceWithin12Seconds: return "m_reload-twice-within-12-seconds"
case .userBehaviorReloadThreeTimesWithin20Seconds: return "m_reload-three-times-within-20-seconds"
// MARK: - Page refresh toasts
case .pageRefreshThreeTimesWithin20Seconds: return "m_reload-three-times-within-20-seconds"

case .siteNotWorkingShown: return "m_site-not-working_shown"
case .siteNotWorkingWebsiteIsBroken: return "m_site-not-working_website-is-broken"

// MARK: - History debug
case .historyStoreLoadFailed: return "m_debug_history-store-load-failed"
Expand Down
4 changes: 1 addition & 3 deletions Core/UserDefaultsPropertyWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,7 @@ public struct UserDefaultsWrapper<T> {

case appleAdAttributionReportCompleted = "com.duckduckgo.ios.appleAdAttributionReport.completed"

case didRefreshTimestamp = "com.duckduckgo.ios.userBehavior.didRefreshTimestamp"
case didDoubleRefreshTimestamp = "com.duckduckgo.ios.userBehavior.didDoubleRefreshTimestamp"
case didRefreshCounter = "com.duckduckgo.ios.userBehavior.didRefreshCounter"
case refreshTimestamps = "com.duckduckgo.ios.pageRefreshMonitor.refreshTimestamps"
case lastBrokenSiteToastShownDate = "com.duckduckgo.ios.userBehavior.lastBrokenSiteToastShownDate"
case toastDismissStreakCounter = "com.duckduckgo.ios.userBehavior.toastDismissStreakCounter"

Expand Down
66 changes: 30 additions & 36 deletions DuckDuckGo.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/DuckDuckGo/BrowserServicesKit",
"state" : {
"revision" : "26cc3c597990db8a0f8aa4be743b25ce65076c95",
"version" : "207.1.0"
"revision" : "17154907fe86c75942331ed6d037694c666ddd95",
"version" : "208.0.0"
}
},
{
Expand Down
6 changes: 4 additions & 2 deletions DuckDuckGo/AppDependencyProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import Subscription
import Common
import NetworkProtection
import RemoteMessaging
import PageRefreshMonitor

protocol DependencyProvider {

Expand All @@ -39,7 +40,7 @@ protocol DependencyProvider {
var autofillNeverPromptWebsitesManager: AutofillNeverPromptWebsitesManager { get }
var configurationManager: ConfigurationManager { get }
var configurationStore: ConfigurationStore { get }
var userBehaviorMonitor: UserBehaviorMonitor { get }
var pageRefreshMonitor: PageRefreshMonitor { get }
var subscriptionManager: SubscriptionManager { get }
var accountManager: AccountManager { get }
var vpnFeatureVisibility: DefaultNetworkProtectionVisibility { get }
Expand Down Expand Up @@ -72,7 +73,8 @@ final class AppDependencyProvider: DependencyProvider {
let configurationManager: ConfigurationManager
let configurationStore = ConfigurationStore()

let userBehaviorMonitor = UserBehaviorMonitor()
let pageRefreshMonitor = PageRefreshMonitor(onDidDetectRefreshPattern: PageRefreshMonitor.onDidDetectRefreshPattern,
store: PageRefreshStore())

// Subscription
let subscriptionManager: SubscriptionManager
Expand Down
30 changes: 30 additions & 0 deletions DuckDuckGo/AppPageRefreshMonitor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// AppPageRefreshMonitor.swift
// DuckDuckGo
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Core
import Common
import PageRefreshMonitor

extension PageRefreshMonitor {

static let onDidDetectRefreshPattern: () -> Void = {
Pixel.fire(pixel: .pageRefreshThreeTimesWithin20Seconds)
}

}
106 changes: 0 additions & 106 deletions DuckDuckGo/BrokenSitePromptLimiter.swift

This file was deleted.

32 changes: 32 additions & 0 deletions DuckDuckGo/BrokenSitePromptLimiterStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// BrokenSitePromptLimiterStore.swift
// DuckDuckGo
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation
import BrokenSitePrompt
import Core

final class BrokenSitePromptLimiterStore: BrokenSitePromptLimiterStoring {

@UserDefaultsWrapper(key: .lastBrokenSiteToastShownDate, defaultValue: .distantPast)
var lastToastShownDate: Date

@UserDefaultsWrapper(key: .toastDismissStreakCounter, defaultValue: 0)
var toastDismissStreakCounter: Int

}
2 changes: 1 addition & 1 deletion DuckDuckGo/BrokenSitePromptView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ struct BrokenSitePromptView: View {
Button(UserText.siteNotWorkingDismiss, action: viewModel.onDidDismiss)
.buttonStyle(GhostButtonStyle())
.fixedSize()
Button(UserText.siteNotWorkingWebsiteIsBroken, action: viewModel.onDidSubmit)
Button(UserText.siteNotWorkingReportBrokenSite, action: viewModel.onDidSubmit)
.buttonStyle(PrimaryButtonStyle(compact: true))
.fixedSize()
}
Expand Down
29 changes: 15 additions & 14 deletions DuckDuckGo/MainViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import SwiftUI
import NetworkProtection
import Onboarding
import os.log
import PageRefreshMonitor
import BrokenSitePrompt

class MainViewController: UIViewController {

Expand Down Expand Up @@ -301,7 +303,7 @@ class MainViewController: UIViewController {
findInPageView.delegate = self
findInPageBottomLayoutConstraint.constant = 0
registerForKeyboardNotifications()
registerForUserBehaviorEvents()
registerForPageRefreshPatterns()
registerForSyncFeatureFlagsUpdates()

decorate()
Expand Down Expand Up @@ -497,11 +499,11 @@ class MainViewController: UIViewController {
keyboardShowing = false
}

private func registerForUserBehaviorEvents() {
private func registerForPageRefreshPatterns() {
NotificationCenter.default.addObserver(
self,
selector: #selector(attemptToShowBrokenSitePrompt(_:)),
name: .userBehaviorDidMatchBrokenSiteCriteria,
name: .pageRefreshMonitorDidDetectRefreshPattern,
object: nil)
}

Expand Down Expand Up @@ -1095,8 +1097,7 @@ class MainViewController: UIViewController {
}

private func hideNotificationBarIfBrokenSitePromptShown(afterRefresh: Bool = false) {
guard brokenSitePromptViewHostingController != nil,
let event = brokenSitePromptEvent?.rawValue else { return }
guard brokenSitePromptViewHostingController != nil else { return }
brokenSitePromptViewHostingController = nil
hideNotification()
}
Expand Down Expand Up @@ -1341,12 +1342,11 @@ class MainViewController: UIViewController {
}

private var brokenSitePromptViewHostingController: UIHostingController<BrokenSitePromptView>?
private var brokenSitePromptEvent: UserBehaviorEvent?
lazy private var brokenSitePromptLimiter = BrokenSitePromptLimiter()
lazy private var brokenSitePromptLimiter = BrokenSitePromptLimiter(privacyConfigManager: ContentBlocking.shared.privacyConfigurationManager,
store: BrokenSitePromptLimiterStore())

@objc func attemptToShowBrokenSitePrompt(_ notification: Notification) {
guard brokenSitePromptLimiter.shouldShowToast(),
let event = notification.userInfo?[UserBehaviorEvent.Key.event] as? UserBehaviorEvent,
let url = currentTab?.url, !url.isDuckDuckGo,
notificationView == nil,
!isPad,
Expand All @@ -1356,18 +1356,18 @@ class MainViewController: UIViewController {
// We're using async to ensure the view dismissal happens on the first runloop after a refresh. This prevents the scenario where the view briefly appears and then immediately disappears after a refresh.
brokenSitePromptLimiter.didShowToast()
DispatchQueue.main.async {
self.showBrokenSitePrompt(after: event)
self.showBrokenSitePrompt()
}
}

private func showBrokenSitePrompt(after event: UserBehaviorEvent) {
let host = makeBrokenSitePromptViewHostingController(event: event)
private func showBrokenSitePrompt() {
let host = makeBrokenSitePromptViewHostingController()
brokenSitePromptViewHostingController = host
brokenSitePromptEvent = event
Pixel.fire(pixel: .siteNotWorkingShown)
showNotification(with: host.view)
}

private func makeBrokenSitePromptViewHostingController(event: UserBehaviorEvent) -> UIHostingController<BrokenSitePromptView> {
private func makeBrokenSitePromptViewHostingController() -> UIHostingController<BrokenSitePromptView> {
let viewModel = BrokenSitePromptViewModel(onDidDismiss: { [weak self] in
Task { @MainActor in
self?.hideNotification()
Expand All @@ -1376,10 +1376,11 @@ class MainViewController: UIViewController {
}
}, onDidSubmit: { [weak self] in
Task { @MainActor in
self?.segueToReportBrokenSite(entryPoint: .prompt(event.rawValue))
self?.segueToReportBrokenSite(entryPoint: .prompt)
self?.hideNotification()
self?.brokenSitePromptLimiter.didOpenReport()
self?.brokenSitePromptViewHostingController = nil
Pixel.fire(pixel: .siteNotWorkingWebsiteIsBroken)
}
})
return UIHostingController(rootView: BrokenSitePromptView(viewModel: viewModel), ignoreSafeArea: true)
Expand Down
29 changes: 29 additions & 0 deletions DuckDuckGo/PageRefreshStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// PageRefreshStore.swift
// DuckDuckGo
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation
import Core
import PageRefreshMonitor

final class PageRefreshStore: PageRefreshStoring {

@UserDefaultsWrapper(key: .refreshTimestamps, defaultValue: [])
var refreshTimestamps: [Date]

}
Loading

0 comments on commit 1ae8ae8

Please sign in to comment.