Skip to content

Commit 295fcbc

Browse files
Add FXIOS-12322 Implement tracking protection screen in new onboarding with crash & telemetry toggle modal (#27217)
* FXIOS-12237 #26639 Implement Multiple-Selection Onboarding card component * Swiftlint fix * Swiftlint fix * Pr review * Update BrowserKit/Sources/OnboardingKit/Views/OnboardingMultipleChoiceCardView.swift Co-authored-by: lmarceau <[email protected]> * PR updates * FXIOS-12238 #26640 Encapsulate Basic & Multi-Select cards into a single generic Onboarding card * Swiftlint fix * swiftlint fix * Style cards * Improve page change logic * FXIOS-12325 #26845 Integrate Onboarding View into Main Target with Nimbus * Add ui variants * Swiftlint fix * Fix naming * cleanup * swiftlint * swiftlint * Swiftlint * Add cards * Change experiment * Turn off modern ui * FXIOS-12322 #26842 Implement Tracking Protection Screen in New Onboarding with Crash & Telemetry Toggle Modal * Swiftlint fix * Reset strings * Filter --------- Co-authored-by: lmarceau <[email protected]>
1 parent 505e07d commit 295fcbc

File tree

16 files changed

+511
-12
lines changed

16 files changed

+511
-12
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/
4+
5+
import SwiftUI
6+
7+
public struct AttributedLinkText<Action: RawRepresentable>: View where Action.RawValue == String {
8+
let fullText: String
9+
let linkText: String
10+
let action: Action
11+
let linkAction: (Action) -> Void
12+
13+
@State private var attributedString: AttributedString
14+
15+
public init(
16+
fullText: String,
17+
linkText: String,
18+
action: Action,
19+
linkAction: @escaping (Action) -> Void
20+
) {
21+
self.fullText = fullText
22+
self.linkText = linkText
23+
self.action = action
24+
self.linkAction = linkAction
25+
26+
var attrString = AttributedString(fullText)
27+
attrString.foregroundColor = Color(.secondaryLabel)
28+
29+
let actionURL = URL(string: "action://\(action.rawValue)")!
30+
if let range = attrString.range(of: linkText) {
31+
attrString[range].foregroundColor = .blue
32+
attrString[range].link = actionURL
33+
}
34+
35+
self._attributedString = State(initialValue: attrString)
36+
}
37+
38+
public var body: some View {
39+
Text(attributedString)
40+
.fixedSize(horizontal: false, vertical: true)
41+
.accessibilityAddTraits([.isStaticText, .isButton])
42+
.font(.caption)
43+
.multilineTextAlignment(.center)
44+
.environment(\.openURL, OpenURLAction { url in
45+
if url.scheme == "action", let host = url.host,
46+
let action = Action(rawValue: host) {
47+
// Handle in-app navigation
48+
linkAction(action)
49+
return .handled
50+
}
51+
return .systemAction
52+
})
53+
}
54+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"images" : [
3+
{
4+
"filename" : "fxLogo.pdf",
5+
"idiom" : "universal"
6+
}
7+
],
8+
"info" : {
9+
"author" : "xcode",
10+
"version" : 1
11+
}
12+
}
Binary file not shown.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/
4+
5+
import Foundation
6+
7+
public struct EmbeddedLink {
8+
let fullText: String
9+
let linkText: String
10+
let action: TosAction
11+
12+
public init(fullText: String, linkText: String, action: TosAction) {
13+
self.fullText = fullText
14+
self.linkText = linkText
15+
self.action = action
16+
}
17+
}

BrowserKit/Sources/OnboardingKit/Models/OnboardingCardInfoModelProtocol.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public protocol OnboardingCardInfoModelProtocol {
88
associatedtype OnboardingType
99
associatedtype OnboardingPopupActionType
1010
associatedtype OnboardingMultipleChoiceActionType: Hashable
11-
associatedtype OnboardingActionType
11+
associatedtype OnboardingActionType: RawRepresentable where OnboardingActionType.RawValue == String
1212
var cardType: OnboardingCardType { get set }
1313
var name: String { get set }
1414
var order: Int { get set }
@@ -21,6 +21,7 @@ public protocol OnboardingCardInfoModelProtocol {
2121
var onboardingType: OnboardingType { get set }
2222
var a11yIdRoot: String { get set }
2323
var imageID: String { get set }
24+
var embededLinkText: [EmbeddedLink] { get set }
2425

2526
var image: UIImage? { get }
2627

@@ -36,6 +37,7 @@ public protocol OnboardingCardInfoModelProtocol {
3637
onboardingType: OnboardingType,
3738
a11yIdRoot: String,
3839
imageID: String,
39-
instructionsPopup: OnboardingInstructionsPopupInfoModel<OnboardingPopupActionType>?
40+
instructionsPopup: OnboardingInstructionsPopupInfoModel<OnboardingPopupActionType>?,
41+
embededLinkText: [EmbeddedLink]
4042
)
4143
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/
4+
5+
import Foundation
6+
7+
public enum TosAction: String {
8+
case accept
9+
case openTermsOfService
10+
case openPrivacyNotice
11+
case openManageSettings
12+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/
4+
5+
import SwiftUI
6+
7+
public class TosFlowViewModel<VM: OnboardingKit.OnboardingCardInfoModelProtocol>: ObservableObject {
8+
public let configuration: VM
9+
public let onTermsOfServiceTap: () -> Void
10+
public let onPrivacyNoticeTap: () -> Void
11+
public let onManageSettingsTap: () -> Void
12+
public let onComplete: () -> Void
13+
14+
public init(
15+
configuration: VM,
16+
onTermsOfServiceTap: @escaping () -> Void,
17+
onPrivacyNoticeTap: @escaping () -> Void,
18+
onManageSettingsTap: @escaping () -> Void = {},
19+
onComplete: @escaping () -> Void
20+
) {
21+
self.configuration = configuration
22+
self.onTermsOfServiceTap = onTermsOfServiceTap
23+
self.onPrivacyNoticeTap = onPrivacyNoticeTap
24+
self.onManageSettingsTap = onManageSettingsTap
25+
self.onComplete = onComplete
26+
}
27+
28+
public func handleEmbededLinkAction(
29+
action: TosAction
30+
) {
31+
switch action {
32+
case .accept:
33+
onComplete()
34+
case .openTermsOfService:
35+
onTermsOfServiceTap()
36+
case .openPrivacyNotice:
37+
onPrivacyNoticeTap()
38+
case .openManageSettings:
39+
onManageSettingsTap()
40+
}
41+
}
42+
}

BrowserKit/Sources/OnboardingKit/Preview/PreviewModel.swift

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ private struct PreviewModel: OnboardingCardInfoModelProtocol {
1616
var buttons: OnboardingButtons<OnboardingActions>
1717
var multipleChoiceButtons: [OnboardingMultipleChoiceButtonModel<OnboardingMultipleChoiceAction>]
1818
var onboardingType: OnboardingType
19+
var embededLinkText: [EmbeddedLink]
1920

2021
init(
2122
cardType: OnboardingCardType,
@@ -29,12 +30,14 @@ private struct PreviewModel: OnboardingCardInfoModelProtocol {
2930
onboardingType: OnboardingType = .freshInstall,
3031
a11yIdRoot: String,
3132
imageID: String,
32-
instructionsPopup: OnboardingInstructionsPopupInfoModel<OnboardingInstructionsPopupActions>? = nil
33+
instructionsPopup: OnboardingInstructionsPopupInfoModel<OnboardingInstructionsPopupActions>? = nil,
34+
embededLinkText: [EmbeddedLink] = []
3335
) {
3436
self.cardType = cardType; self.name = name; self.order = order; self.title = title; self.body = body
3537
self.link = link; self.buttons = buttons; self.multipleChoiceButtons = multipleChoiceButtons
3638
self.onboardingType = onboardingType; self.a11yIdRoot = a11yIdRoot; self.imageID = imageID
3739
self.instructionsPopup = instructionsPopup
40+
self.embededLinkText = embededLinkText
3841
}
3942
}
4043

@@ -159,6 +162,42 @@ extension PreviewModel {
159162
imageID: "toolbar",
160163
instructionsPopup: nil
161164
)
165+
166+
static let tos = PreviewModel(
167+
cardType: .basic,
168+
name: "tos",
169+
order: 20,
170+
title: "Upgrade your browsing",
171+
body: "Our fastest iOS browser yet\nAutomatic tracking protection\nSync on all your devices",
172+
link: nil,
173+
buttons: .init(
174+
primary: .init(title: "Agree and Continue", action: OnboardingActions.syncSignIn),
175+
secondary: nil
176+
),
177+
multipleChoiceButtons: [],
178+
onboardingType: .freshInstall,
179+
a11yIdRoot: "onboarding_termsOfService",
180+
imageID: "fxHomeHeaderLogoBall",
181+
instructionsPopup: nil,
182+
embededLinkText: [
183+
EmbeddedLink(
184+
fullText: "By continuing, you agree to the Firefox Terms of Use",
185+
linkText: "Firefox Terms of Use",
186+
action: .openTermsOfService
187+
),
188+
EmbeddedLink(
189+
fullText: "Firefox cares about your privacy. Read more in our Privacy Notice",
190+
linkText: "Privacy Notice",
191+
action: .openPrivacyNotice
192+
),
193+
EmbeddedLink(
194+
fullText:
195+
"To help improve the browser, Firefox sends diagnostic and interaction data to Mozilla. Manage settings",
196+
linkText: "Manage settings",
197+
action: .openManageSettings
198+
)
199+
]
200+
)
162201
}
163202

164203
extension PreviewModel {
@@ -210,4 +249,19 @@ extension PreviewModel {
210249
)
211250
}
212251

252+
#Preview("Terms of service") {
253+
TermsOfServiceView(
254+
viewModel: TosFlowViewModel(
255+
configuration: PreviewModel.tos,
256+
onTermsOfServiceTap: {},
257+
onPrivacyNoticeTap: {},
258+
onManageSettingsTap: {},
259+
onComplete: {}
260+
),
261+
windowUUID: .DefaultUITestingUUID,
262+
themeManager: DefaultThemeManager(sharedContainerIdentifier: ""),
263+
onEmbededLinkAction: { _ in }
264+
)
265+
}
266+
213267
#endif
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/
4+
5+
import SwiftUI
6+
import ComponentLibrary
7+
import Common
8+
9+
public struct TermsOfServiceView<VM: OnboardingCardInfoModelProtocol>: View {
10+
@State private var textColor: Color = .clear
11+
@State private var secondaryTextColor: Color = .clear
12+
@State private var cardBackgroundColor: Color = .clear
13+
@State private var secondaryActionColor: Color = .clear
14+
15+
@StateObject private var viewModel: TosFlowViewModel<VM>
16+
let windowUUID: WindowUUID
17+
var themeManager: ThemeManager
18+
public let onEmbededLinkAction: (TosAction) -> Void
19+
20+
public init(
21+
viewModel: TosFlowViewModel<VM>,
22+
windowUUID: WindowUUID,
23+
themeManager: ThemeManager,
24+
onEmbededLinkAction: @escaping (TosAction) -> Void
25+
) {
26+
self._viewModel = StateObject(wrappedValue: viewModel)
27+
self.windowUUID = windowUUID
28+
self.themeManager = themeManager
29+
self.onEmbededLinkAction = onEmbededLinkAction
30+
}
31+
32+
// MARK: - Body
33+
34+
public var body: some View {
35+
GeometryReader { geometry in
36+
// Determine scale factor based on current size vs base metrics
37+
let widthScale = geometry.size.width / UX.CardView.baseWidth
38+
let heightScale = geometry.size.height / UX.CardView.baseHeight
39+
let scale = min(widthScale, heightScale)
40+
41+
ZStack {
42+
MilkyWayMetalView()
43+
.ignoresSafeArea()
44+
ScrollView {
45+
VStack {
46+
VStack(spacing: UX.CardView.spacing * scale) {
47+
Spacer()
48+
imageView(scale: scale)
49+
titleView
50+
bodyView
51+
Spacer()
52+
links
53+
primaryButton
54+
}
55+
.frame(height: geometry.size.height * UX.CardView.cardHeightRatio)
56+
.padding(UX.CardView.verticalPadding * scale)
57+
.background(
58+
RoundedRectangle(cornerRadius: UX.CardView.cornerRadius)
59+
.fill(cardBackgroundColor)
60+
)
61+
.padding(.horizontal, UX.CardView.horizontalPadding * scale)
62+
Spacer()
63+
}
64+
}
65+
.onAppear {
66+
applyTheme(theme: themeManager.getCurrentTheme(for: windowUUID))
67+
}
68+
.onReceive(NotificationCenter.default.publisher(for: .ThemeDidChange)) {
69+
guard let uuid = $0.windowUUID, uuid == windowUUID else { return }
70+
applyTheme(theme: themeManager.getCurrentTheme(for: windowUUID))
71+
}
72+
}
73+
}
74+
}
75+
76+
// MARK: - Subviews
77+
78+
var links: some View {
79+
VStack(alignment: .center, spacing: UX.Onboarding.Spacing.standard) {
80+
ForEach(viewModel.configuration.embededLinkText, id: \.linkText) { link in
81+
AttributedLinkText<TosAction>(
82+
fullText: link.fullText,
83+
linkText: link.linkText,
84+
action: link.action,
85+
linkAction: viewModel.handleEmbededLinkAction(action:)
86+
)
87+
}
88+
}
89+
}
90+
91+
@ViewBuilder
92+
func imageView(scale: CGFloat) -> some View {
93+
if let img = viewModel.configuration.image {
94+
Image(uiImage: img)
95+
.resizable()
96+
.aspectRatio(contentMode: .fit)
97+
.frame(height: UX.CardView.tosImageHeight * scale)
98+
.accessibilityLabel(viewModel.configuration.title)
99+
.accessibility(identifier: "\(viewModel.configuration.a11yIdRoot)ImageView")
100+
}
101+
}
102+
103+
var titleView: some View {
104+
Text(viewModel.configuration.title)
105+
.font(UX.CardView.titleFont)
106+
.fontWeight(.bold)
107+
.foregroundColor(textColor)
108+
.multilineTextAlignment(.center)
109+
.accessibility(identifier: "\(viewModel.configuration.a11yIdRoot)TitleLabel")
110+
.accessibility(addTraits: .isHeader)
111+
}
112+
113+
var bodyView: some View {
114+
Text(viewModel.configuration.body)
115+
.fixedSize(horizontal: false, vertical: true)
116+
.font(UX.CardView.bodyFont)
117+
.foregroundColor(secondaryTextColor)
118+
.multilineTextAlignment(.center)
119+
.accessibility(identifier: "\(viewModel.configuration.a11yIdRoot)DescriptionLabel")
120+
}
121+
122+
var primaryButton: some View {
123+
Button(
124+
viewModel.configuration.buttons.primary.title,
125+
action: {
126+
viewModel.handleEmbededLinkAction(
127+
action: .accept
128+
)
129+
}
130+
)
131+
.font(UX.CardView.primaryActionFont)
132+
.accessibility(identifier: "\(viewModel.configuration.a11yIdRoot)PrimaryButton")
133+
.buttonStyle(PrimaryButtonStyle(theme: themeManager.getCurrentTheme(for: windowUUID)))
134+
}
135+
136+
private func applyTheme(theme: Theme) {
137+
let color = theme.colors
138+
textColor = Color(color.textPrimary)
139+
secondaryTextColor = Color(color.textSecondary)
140+
cardBackgroundColor = Color(color.layer2)
141+
}
142+
}

0 commit comments

Comments
 (0)