Skip to content

Commit

Permalink
Contextual Onboarding - Privacy Icon animation (#3130)
Browse files Browse the repository at this point in the history
  • Loading branch information
alessandroboron committed Aug 5, 2024
1 parent 88b9d93 commit 2fa6587
Show file tree
Hide file tree
Showing 15 changed files with 211 additions and 42 deletions.
12 changes: 12 additions & 0 deletions DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,7 @@
9F4CC51D2C48D240006A96EB /* CoreDataDatabaseTestUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4CC51C2C48D240006A96EB /* CoreDataDatabaseTestUtilities.swift */; };
9F4CC51F2C48D758006A96EB /* ContextualDaxDialogsFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4CC51E2C48D758006A96EB /* ContextualDaxDialogsFactoryTests.swift */; };
9F4CC5242C4A4F0D006A96EB /* SwiftUITestUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4CC5232C4A4F0D006A96EB /* SwiftUITestUtilities.swift */; };
9F4CC5272C4E230C006A96EB /* PrivacyIconContextualOnboardingAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4CC5262C4E230C006A96EB /* PrivacyIconContextualOnboardingAnimator.swift */; };
9F5E5AAC2C3D0FCD00165F54 /* ContextualDaxDialogsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F5E5AAB2C3D0FCD00165F54 /* ContextualDaxDialogsFactory.swift */; };
9F5E5AB02C3E4C6000165F54 /* ContextualOnboardingPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F5E5AAF2C3E4C6000165F54 /* ContextualOnboardingPresenter.swift */; };
9F5E5AB22C3E606D00165F54 /* ContextualOnboardingPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F5E5AB12C3E606D00165F54 /* ContextualOnboardingPresenterTests.swift */; };
Expand Down Expand Up @@ -2385,6 +2386,7 @@
9F4CC51C2C48D240006A96EB /* CoreDataDatabaseTestUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataDatabaseTestUtilities.swift; sourceTree = "<group>"; };
9F4CC51E2C48D758006A96EB /* ContextualDaxDialogsFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextualDaxDialogsFactoryTests.swift; sourceTree = "<group>"; };
9F4CC5232C4A4F0D006A96EB /* SwiftUITestUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUITestUtilities.swift; sourceTree = "<group>"; };
9F4CC5262C4E230C006A96EB /* PrivacyIconContextualOnboardingAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyIconContextualOnboardingAnimator.swift; sourceTree = "<group>"; };
9F5E5AAB2C3D0FCD00165F54 /* ContextualDaxDialogsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextualDaxDialogsFactory.swift; sourceTree = "<group>"; };
9F5E5AAF2C3E4C6000165F54 /* ContextualOnboardingPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextualOnboardingPresenter.swift; sourceTree = "<group>"; };
9F5E5AB12C3E606D00165F54 /* ContextualOnboardingPresenterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextualOnboardingPresenterTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4500,6 +4502,14 @@
name = Onboarding;
sourceTree = "<group>";
};
9F4CC5252C4E22F9006A96EB /* ContextualOnboarding */ = {
isa = PBXGroup;
children = (
9F4CC5262C4E230C006A96EB /* PrivacyIconContextualOnboardingAnimator.swift */,
);
name = ContextualOnboarding;
sourceTree = "<group>";
};
9F5E5AAA2C3D0FAA00165F54 /* ContextualOnboarding */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -5732,6 +5742,7 @@
1EEF123E2850A68A003DDE57 /* PrivacyInfoContainerView.swift */,
1E7A71152934E4C700B7EA19 /* OmniBarNotifications */,
1EE411F42857C5130003FE64 /* PrivacyIconAndTrackers */,
9F4CC5252C4E22F9006A96EB /* ContextualOnboarding */,
);
name = OmniBar;
sourceTree = "<group>";
Expand Down Expand Up @@ -7400,6 +7411,7 @@
C1B7B52528941F2A0098FD6A /* RemoteMessagingClient.swift in Sources */,
3132FA2827A0788400DD7A12 /* PassKitPreviewHelper.swift in Sources */,
6FA343922C3D3C3B00470677 /* FavoriteIconView.swift in Sources */,
9F4CC5272C4E230C006A96EB /* PrivacyIconContextualOnboardingAnimator.swift in Sources */,
8505836C219F424500ED4EDB /* TextFieldWithInsets.swift in Sources */,
CBD4F13F279EBFAF00B20FD7 /* HomeMessageViewModel.swift in Sources */,
1E4DCF4A27B6A38000961E25 /* DownloadListRepresentable.swift in Sources */,
Expand Down
42 changes: 32 additions & 10 deletions DuckDuckGo/DaxDialogs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,21 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic {
let type: SpecType

func format(args: CVarArg...) -> BrowsingSpec {
return BrowsingSpec(message: String(format: message, arguments: args),
cta: cta,
highlightAddressBar: highlightAddressBar,
pixelName: pixelName,
type: type)
format(message: message, args: args)
}

func format(message: String, args: CVarArg...) -> BrowsingSpec {
withUpdatedMessage(String(format: message, arguments: args))
}

func withUpdatedMessage(_ message: String) -> BrowsingSpec {
BrowsingSpec(
message: message,
cta: cta,
highlightAddressBar: highlightAddressBar,
pixelName: pixelName,
type: type
)
}
}

Expand Down Expand Up @@ -246,7 +256,11 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic {
}

var shouldShowFireButtonPulse: Bool {
return nonDDGBrowsingMessageSeen && !fireButtonBrowsingMessageSeenOrExpired && isEnabled
if isNewOnboarding {
nonDDGBrowsingMessageSeen && fireButtonBrowsingMessageSeenOrExpired && isEnabled
} else {
nonDDGBrowsingMessageSeen && !fireButtonBrowsingMessageSeenOrExpired && isEnabled
}
}

func isStillOnboarding() -> Bool {
Expand Down Expand Up @@ -508,13 +522,21 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic {

case 1:
settings.browsingWithTrackersShown = true
spec = BrowsingSpec.withOneTracker.format(args: entitiesBlocked[0])
let args = entitiesBlocked[0]
spec = if isNewOnboarding {
BrowsingSpec.withOneTracker.format(message: UserText.DaxOnboardingExperiment.ContextualOnboarding.daxDialogBrowsingWithOneTracker, args: args)
} else {
BrowsingSpec.withOneTracker.format(args: args)
}

default:
settings.browsingWithTrackersShown = true
spec = BrowsingSpec.withMultipleTrackers.format(args: entitiesBlocked.count - 2,
entitiesBlocked[0],
entitiesBlocked[1])
let args: [CVarArg] = [entitiesBlocked.count - 2, entitiesBlocked[0], entitiesBlocked[1]]
spec = if isNewOnboarding {
BrowsingSpec.withMultipleTrackers.format(message: UserText.DaxOnboardingExperiment.ContextualOnboarding.daxDialogBrowsingWithMultipleTrackers, args: args)
} else {
BrowsingSpec.withMultipleTrackers.format(args: args)
}
}
// New Contextual onboarding doesn't highlight the address bar. This checks prevents to cancel the lottie animation.
if isNewOnboarding {
Expand Down
16 changes: 11 additions & 5 deletions DuckDuckGo/MainViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2258,8 +2258,12 @@ extension MainViewController: TabDelegate {
showFireButtonPulse()
}

func tabDidRequestPrivacyDashboardButtonPulse(tab: TabViewController) {
showPrivacyDashboardButtonPulse()
func tabDidRequestPrivacyDashboardButtonPulse(tab: TabViewController, animated: Bool) {
if animated {
showPrivacyDashboardButtonPulse()
} else {
dismissPrivacyDashboardButtonPulse()
}
}

func tabDidRequestSearchBarRect(tab: TabViewController) -> CGRect {
Expand Down Expand Up @@ -2581,9 +2585,11 @@ extension MainViewController: AutoClearWorker {
}

private func showPrivacyDashboardButtonPulse() {
// TODO:
// 1. Wait for Lottie Tracker animation to complete
// 2. Animate Privacy Icon with pulsing animation similar to `showFireButtonPulse()`.
viewCoordinator.omniBar.showOrScheduleOnboardingPrivacyIconAnimation()
}

private func dismissPrivacyDashboardButtonPulse() {
viewCoordinator.omniBar.dismissOnboardingPrivacyIconAnimation()
}

}
Expand Down
28 changes: 22 additions & 6 deletions DuckDuckGo/OmniBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ class OmniBar: UIView {

private var privacyIconAndTrackersAnimator = PrivacyIconAndTrackersAnimator()
private var notificationAnimator = OmniBarNotificationAnimator()

private let privacyIconContextualOnboardingAnimator = PrivacyIconContextualOnboardingAnimator()

// Set up a view to add a custom icon to the Omnibar
private var customIconView: UIImageView = UIImageView(frame: CGRect(x: 4, y: 8, width: 26, height: 26))

Expand Down Expand Up @@ -316,13 +317,28 @@ class OmniBar: UIView {
func showOrScheduleCookiesManagedNotification(isCosmetic: Bool) {
let type: OmniBarNotificationType = isCosmetic ? .cookiePopupHidden : .cookiePopupManaged

enqueueAnimationIfNeeded { [weak self] in
guard let self else { return }
self.notificationAnimator.showNotification(type, in: self)
}
}

func showOrScheduleOnboardingPrivacyIconAnimation() {
enqueueAnimationIfNeeded { [weak self] in
guard let self else { return }
self.privacyIconContextualOnboardingAnimator.showPrivacyIconAnimation(in: self)
}
}

func dismissOnboardingPrivacyIconAnimation() {
privacyIconContextualOnboardingAnimator.dismissPrivacyIconAnimation()
}

private func enqueueAnimationIfNeeded(_ block: @escaping () -> Void) {
if privacyIconAndTrackersAnimator.state == .completed {
notificationAnimator.showNotification(type, in: self)
block()
} else {
privacyIconAndTrackersAnimator.onAnimationCompletion = { [weak self] in
guard let self = self else { return }
self.notificationAnimator.showNotification(type, in: self)
}
privacyIconAndTrackersAnimator.onAnimationCompletion(block)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,16 @@ protocol ContextualOnboardingPresenting {
final class ContextualOnboardingPresenter: ContextualOnboardingPresenting {
private let variantManager: VariantManager
private let daxDialogsFactory: ContextualDaxDialogsFactory
private let appSettings: AppSettings

init(variantManager: VariantManager, daxDialogsFactory: ContextualDaxDialogsFactory = ExperimentContextualDaxDialogsFactory()) {
init(
variantManager: VariantManager,
daxDialogsFactory: ContextualDaxDialogsFactory = ExperimentContextualDaxDialogsFactory(),
appSettings: AppSettings = AppUserDefaults()
) {
self.variantManager = variantManager
self.daxDialogsFactory = daxDialogsFactory
self.appSettings = appSettings
}

func presentContextualOnboarding(for spec: DaxDialogs.BrowsingSpec, in vc: TabViewOnboardingDelegate) {
Expand Down Expand Up @@ -71,8 +77,14 @@ private extension ContextualOnboardingPresenter {
$0.removeFromSuperview()
}

// Adjust message hand emoji based on address bar position
let platformSpecificMessage = spec.message.replacingOccurrences(
of: "☝️",
with: appSettings.currentAddressBarPosition == .bottom ? "👇" : "☝️"
)
let platformSpecificSpec = spec.withUpdatedMessage(platformSpecificMessage)
// Ask the Dax Dialogs Factory for a view for the given spec
let controller = daxDialogsFactory.makeView(for: spec, delegate: vc)
let controller = daxDialogsFactory.makeView(for: platformSpecificSpec, delegate: vc)
controller.view.isHidden = true
controller.view.alpha = 0

Expand Down
14 changes: 10 additions & 4 deletions DuckDuckGo/OnboardingExperiment/DaxDialogs/DaxDialogView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ struct DaxDialogView<Content: View>: View {

@State private var logoPosition: DaxDialogLogoPosition

private let matchLogoAnimation: (id: String, namespace: Namespace.ID)
private let matchLogoAnimation: (id: String, namespace: Namespace.ID)?
private let showDialogBox: Binding<Bool>
private let cornerRadius: CGFloat
private let arrowSize: CGSize
Expand All @@ -55,7 +55,7 @@ struct DaxDialogView<Content: View>: View {

init(
logoPosition: DaxDialogLogoPosition,
matchLogoAnimation: (String, Namespace.ID) = ("", Namespace().wrappedValue),
matchLogoAnimation: (String, Namespace.ID)? = nil,
showDialogBox: Binding<Bool> = .constant(true),
cornerRadius: CGFloat = 16.0,
arrowSize: CGSize = .init(width: 16.0, height: 8.0),
Expand Down Expand Up @@ -109,12 +109,18 @@ struct DaxDialogView<Content: View>: View {
Metrics.stackSpacing + arrowSize.height
}

@ViewBuilder
private var daxLogo: some View {
Image(.daxIconExperiment)
let icon = Image(.daxIconExperiment)
.resizable()
.matchedGeometryEffect(id: matchLogoAnimation.id, in: matchLogoAnimation.namespace)
.aspectRatio(contentMode: .fill)
.frame(width: Metrics.DaxLogo.size, height: Metrics.DaxLogo.size)

if let matchLogoAnimation {
icon.matchedGeometryEffect(id: matchLogoAnimation.id, in: matchLogoAnimation.namespace)
} else {
icon
}
}

private var wrappedContent: some View {
Expand Down
17 changes: 11 additions & 6 deletions DuckDuckGo/PrivacyIconAndTrackersAnimator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ final class PrivacyIconAndTrackersAnimator {

private(set) var state: State = .notStarted

var onAnimationCompletion: (() -> Void)?
private var animationCompletionObservers: [() -> Void] = []

func configure(_ container: PrivacyInfoContainerView, with privacyInfo: PrivacyInfo) {
state = .notStarted
isAnimatingForDaxDialog = false
Expand Down Expand Up @@ -96,8 +96,8 @@ final class PrivacyIconAndTrackersAnimator {

if completed {
self?.state = .completed
self?.onAnimationCompletion?()
self?.onAnimationCompletion = nil
self?.animationCompletionObservers.forEach { action in action() }
self?.animationCompletionObservers = []
}
}
}
Expand Down Expand Up @@ -143,8 +143,8 @@ final class PrivacyIconAndTrackersAnimator {

func completeForNoAnimation() {
state = .completed
onAnimationCompletion?()
onAnimationCompletion = nil
animationCompletionObservers.forEach { action in action() }
animationCompletionObservers = []
}

func cancelAnimations(in omniBar: OmniBar) {
Expand All @@ -169,4 +169,9 @@ final class PrivacyIconAndTrackersAnimator {
func resetImageProvider() {
trackerAnimationImageProvider.reset()
}

func onAnimationCompletion(_ completion: @escaping () -> Void) {
animationCompletionObservers.append(completion)
}

}
33 changes: 33 additions & 0 deletions DuckDuckGo/PrivacyIconContextualOnboardingAnimator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// PrivacyIconContextualOnboardingAnimator.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

final class PrivacyIconContextualOnboardingAnimator {

func showPrivacyIconAnimation(in omniBar: OmniBar) {
guard let window = omniBar.window else { return }
ViewHighlighter.showIn(window, focussedOnView: omniBar.privacyInfoContainer.privacyIcon, scale: .custom(3))
}

func dismissPrivacyIconAnimation() {
ViewHighlighter.hideAll()
}

}
4 changes: 2 additions & 2 deletions DuckDuckGo/TabDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ protocol TabDelegate: AnyObject {
func tabDidRequestForgetAll(tab: TabViewController)

func tabDidRequestFireButtonPulse(tab: TabViewController)
func tabDidRequestPrivacyDashboardButtonPulse(tab: TabViewController)

func tabDidRequestPrivacyDashboardButtonPulse(tab: TabViewController, animated: Bool)

func tabDidRequestSearchBarRect(tab: TabViewController) -> CGRect

Expand Down
8 changes: 6 additions & 2 deletions DuckDuckGo/TabViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -869,7 +869,9 @@ class TabViewController: UIViewController {

@IBSegueAction
private func makePrivacyDashboardViewController(coder: NSCoder) -> PrivacyDashboardViewController? {
PrivacyDashboardViewController(coder: coder,
delegate?.tabDidRequestPrivacyDashboardButtonPulse(tab: self, animated: false)

return PrivacyDashboardViewController(coder: coder,
privacyInfo: privacyInfo,
entryPoint: .dashboard,
privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager,
Expand Down Expand Up @@ -1384,6 +1386,8 @@ extension TabViewController: WKNavigationDelegate {
guard let spec = DaxDialogs.shared.nextBrowsingMessageIfShouldShow(for: privacyInfo) else {
// Dismiss Contextual onboarding if there's no message to show.
contextualOnboardingPresenter.dismissContextualOnboardingIfNeeded(from: self)
// Dismiss privacy dashbooard pulse animation when no browsing dialog to show.
delegate?.tabDidRequestPrivacyDashboardButtonPulse(tab: self, animated: false)

if DaxDialogs.shared.shouldShowFireButtonPulse {
delegate?.tabDidRequestFireButtonPulse(tab: self)
Expand Down Expand Up @@ -2847,7 +2851,7 @@ extension TabViewController: ContextualOnboardingEventDelegate {
}

func didShowContextualOnboardingTrackersDialog() {
delegate?.tabDidRequestPrivacyDashboardButtonPulse(tab: self)
delegate?.tabDidRequestPrivacyDashboardButtonPulse(tab: self, animated: true)
}

func didTapDismissContextualOnboardingAction() {
Expand Down
Loading

0 comments on commit 2fa6587

Please sign in to comment.