Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Alessandro/privacy icon animation #3130

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,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 @@ -2353,6 +2354,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 @@ -4418,6 +4420,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 @@ -5650,6 +5660,7 @@
1EEF123E2850A68A003DDE57 /* PrivacyInfoContainerView.swift */,
1E7A71152934E4C700B7EA19 /* OmniBarNotifications */,
1EE411F42857C5130003FE64 /* PrivacyIconAndTrackers */,
9F4CC5252C4E22F9006A96EB /* ContextualOnboarding */,
);
name = OmniBar;
sourceTree = "<group>";
Expand Down Expand Up @@ -7303,6 +7314,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 @@ -2250,8 +2250,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 @@ -2571,9 +2575,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 @@ -71,7 +71,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 @@ -315,13 +316,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 @@ -857,7 +857,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 @@ -1375,6 +1377,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 @@ -2881,7 +2885,7 @@ extension TabViewController: ContextualOnboardingEventDelegate {
}

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

func didTapDismissContextualOnboardingAction() {
Expand Down
Loading
Loading