Skip to content

Commit

Permalink
Alesab/onboarding (#964)
Browse files Browse the repository at this point in the history
Task/Issue URL:
https://app.asana.com/0/1204186595873227/1208077416568671/f
iOS PR: duckduckgo/iOS#3282
macOS PR:  duckduckgo/macos-browser#3149
What kind of version bump will this require?: Minor

**Optional**:

Tech Design URL:
https://app.asana.com/0/481882893211075/1208094950223874/f

**Description**: Extracts code from the onboarding that can be used by
macOS as well
  • Loading branch information
SabrinaTardio authored Aug 28, 2024
1 parent db0a7b4 commit 48acec0
Show file tree
Hide file tree
Showing 29 changed files with 2,101 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,20 @@
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "Onboarding"
BuildableName = "Onboarding"
BlueprintName = "Onboarding"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
Expand Down
22 changes: 20 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import PackageDescription
let package = Package(
name: "BrowserServicesKit",
platforms: [
.iOS("14.0"),
.iOS("15.0"),
.macOS("11.4")
],
products: [
Expand Down Expand Up @@ -41,6 +41,7 @@ let package = Package(
.library(name: "PixelKit", targets: ["PixelKit"]),
.library(name: "PixelKitTestingUtilities", targets: ["PixelKitTestingUtilities"]),
.library(name: "DuckPlayer", targets: ["DuckPlayer"]),
.library(name: "Onboarding", targets: ["Onboarding"])
],
dependencies: [
.package(url: "https://github.com/duckduckgo/duckduckgo-autofill.git", exact: "13.0.0"),
Expand Down Expand Up @@ -389,6 +390,18 @@ let package = Package(
.define("DEBUG", .when(configuration: .debug))
]
),
.target(
name: "Onboarding",
dependencies: [
"BrowserServicesKit"
],
resources: [
.process("Resources")
],
swiftSettings: [
.define("DEBUG", .when(configuration: .debug))
]
),

// MARK: - Test Targets
.testTarget(
Expand Down Expand Up @@ -581,13 +594,18 @@ let package = Package(
"PixelKitTestingUtilities",
]
),

.testTarget(
name: "DuckPlayerTests",
dependencies: [
"DuckPlayer"
]
),
.testTarget(
name: "OnboardingTests",
dependencies: [
"Onboarding"
]
),
],
cxxLanguageStandard: .cxx11
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,315 @@
//
// ContextualDaxDialogContent.swift
//
// 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 SwiftUI
import Combine
import Common

#if canImport(UIKit)
typealias PlatformFont = UIFont
#else
typealias PlatformFont = NSFont
#endif

public struct ContextualDaxDialogContent: View {

public enum Orientation: Equatable {
case verticalStack
case horizontalStack(alignment: VerticalAlignment)
}

let title: String?
let titleFont: Font?
let messageFont: Font?
public let message: NSAttributedString
let list: [ContextualOnboardingListItem]
let listAction: ((_ item: ContextualOnboardingListItem) -> Void)?
let imageName: String?
let customActionView: AnyView?
let orientation: Orientation

private let itemsToAnimate: [DisplayableTypes]

public init(
orientation: Orientation = .verticalStack,
title: String? = nil,
titleFont: Font? = nil,
message: NSAttributedString,
messageFont: Font? = nil,
list: [ContextualOnboardingListItem] = [],
listAction: ((_: ContextualOnboardingListItem) -> Void)? = nil,
imageName: String? = nil,
customActionView: AnyView? = nil
) {
self.title = title
self.titleFont = titleFont
self.message = message
self.messageFont = messageFont
self.list = list
self.listAction = listAction
self.imageName = imageName
self.customActionView = customActionView
self.orientation = orientation

var itemsToAnimate: [DisplayableTypes] = []
if title != nil {
itemsToAnimate.append(.title)
}
itemsToAnimate.append(.message)
if !list.isEmpty {
itemsToAnimate.append(.list)
}
if imageName != nil {
itemsToAnimate.append(.image)
}
if customActionView != nil {
itemsToAnimate.append(.button)
}

self.itemsToAnimate = itemsToAnimate
}

@State private var startTypingTitle: Bool = false
@State private var startTypingMessage: Bool = false
@State private var nonTypingAnimatableItems: NonTypingAnimatableItems = []

public var body: some View {
Group {
if orientation == .verticalStack {
VStack {
typingElements
nonTypingElements
}
} else if case .horizontalStack(let alignment) = orientation {
HStack(alignment: alignment, spacing: 10) {
typingElements
Spacer()
nonTypingElements
}
}
}
.onAppear {
Task { @MainActor in
try await Task.sleep(interval: 0.3)
startAnimating()
}
}
}

@ViewBuilder var typingElements: some View {
VStack(alignment: .leading, spacing: 16) {
titleView
messageView
}
}

@ViewBuilder var nonTypingElements: some View {
VStack(alignment: .leading, spacing: 16) {
listView
.visibility(nonTypingAnimatableItems.contains(.list) ? .visible : .invisible)
imageView
.visibility(nonTypingAnimatableItems.contains(.image) ? .visible : .invisible)
actionView
.visibility(nonTypingAnimatableItems.contains(.button) ? .visible : .invisible)
}
}

@ViewBuilder
private var titleView: some View {
if let title {
let animatingText = AnimatableTypingText(title, startAnimating: $startTypingTitle, onTypingFinished: {
startTypingMessage = true
})

if let titleFont {
animatingText.font(titleFont)
} else {
animatingText
}
}
}

@ViewBuilder
private var messageView: some View {
let animatingText = AnimatableTypingText(message, startAnimating: $startTypingMessage, onTypingFinished: {
animateNonTypingItems()
})
if let messageFont {
animatingText.font(messageFont)
} else {
animatingText
}
}

@ViewBuilder
private var listView: some View {
if let listAction {
ContextualOnboardingListView(list: list, action: listAction)
}
}

@ViewBuilder
private var imageView: some View {
if let imageName {
HStack {
Spacer()
Image(imageName)
Spacer()
}
}
}

@ViewBuilder
private var actionView: some View {
if let customActionView {
customActionView
}
}

enum DisplayableTypes {
case title
case message
case list
case image
case button
}
}

struct NonTypingAnimatableItems: OptionSet {
let rawValue: Int

static let list = NonTypingAnimatableItems(rawValue: 1 << 0)
static let image = NonTypingAnimatableItems(rawValue: 1 << 1)
static let button = NonTypingAnimatableItems(rawValue: 1 << 2)
}

// MARK: - Auxiliary Functions

extension ContextualDaxDialogContent {

private func startAnimating() {
if itemsToAnimate.contains(.title) {
startTypingTitle = true
} else if itemsToAnimate.contains(.message) {
startTypingMessage = true
}
}

private func animateNonTypingItems() {
// Remove typing items and animate sequentially non typing items
let nonTypingItems = itemsToAnimate.filter { $0 != .title && $0 != .message }

nonTypingItems.enumerated().forEach { index, item in
let delayForItem = Metrics.animationDelay * Double(index + 1)
withAnimation(.easeIn(duration: Metrics.animationDuration).delay(delayForItem)) {
switch item {
case .title, .message:
// Typing items. they don't need to animate sequentially.
break
case .list:
nonTypingAnimatableItems.insert(.list)
case .image:
nonTypingAnimatableItems.insert(.image)
case .button:
nonTypingAnimatableItems.insert(.button)
}
}
}
}
}

// MARK: - Metrics

enum Metrics {
static let animationDuration = 0.25
static let animationDelay = 0.3
}

// MARK: - Preview

#Preview("Intro Dialog - text") {
let fullString = "Instantly clear your browsing activity with the Fire Button.\n\n Give it a try! ☝️"
let boldString = "Fire Button."

let attributedString = NSMutableAttributedString(string: fullString)
let boldFontAttribute: [NSAttributedString.Key: Any] = [
.font: PlatformFont.systemFont(ofSize: 15, weight: .bold)
]

if let boldRange = fullString.range(of: boldString) {
let nsBoldRange = NSRange(boldRange, in: fullString)
attributedString.addAttributes(boldFontAttribute, range: nsBoldRange)
}

return ContextualDaxDialogContent(message: attributedString)
.padding()
.preferredColorScheme(.light)
}

#Preview("Intro Dialog - text and button") {
let contextualText = NSMutableAttributedString(string: "Sabrina is the best\n\nBelieve me! ☝️")
return ContextualDaxDialogContent(
message: contextualText,
customActionView: AnyView(Button("Got it!", action: {})))
.padding()
.preferredColorScheme(.light)
}

#Preview("Intro Dialog - title, text, image and button") {
let contextualText = NSMutableAttributedString(string: "Sabrina is the best\n\nBelieve me! ☝️")
return ContextualDaxDialogContent(
title: "Who is the best?",
message: contextualText,
imageName: "Sync-Desktop-New-128",
customActionView: AnyView(Button("Got it!", action: {})))
.padding()
.preferredColorScheme(.light)
}

#Preview("Intro Dialog - title, text, list") {
let contextualText = NSMutableAttributedString(string: "Sabrina is the best!\n\n Alessandro is ok I guess...")
let list = [
ContextualOnboardingListItem.search(title: "Search"),
ContextualOnboardingListItem.site(title: "Website"),
ContextualOnboardingListItem.surprise(title: "Surprise", visibleTitle: "Surpeise me!"),
]
return ContextualDaxDialogContent(
title: "Who is the best?",
message: contextualText,
list: list,
listAction: { _ in })
.padding()
.preferredColorScheme(.light)
}

#Preview("en_GB list") {
ContextualDaxDialogContent(title: "title",
message: NSAttributedString(string: "this is a message"),
list: OnboardingSuggestedSitesProvider(countryProvider: Locale(identifier: "en_GB"), surpriseItemTitle: "surperise").list,
listAction: { _ in })
.padding()
}

#Preview("en_US list") {
ContextualDaxDialogContent(title: "title",
message: NSAttributedString(string: "this is a message"),
list: OnboardingSuggestedSitesProvider(countryProvider: Locale(identifier: "en_US"), surpriseItemTitle: "surprise").list,
listAction: { _ in })
.padding()
}
Loading

0 comments on commit 48acec0

Please sign in to comment.