Onboarding flow coordinator and FTUE changes (#2578)

Fixes #2595, fixes #2594, fixes #2593, fixes #2592, fixes #2591
This commit is contained in:
Stefan Ceriu
2024-03-21 14:01:23 +02:00
committed by GitHub
parent 9e6b6ba60a
commit a62c96f1c5
176 changed files with 1886 additions and 912 deletions
@@ -0,0 +1,63 @@
//
// Copyright 2021 New Vector Ltd
//
// 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 Combine
import SwiftUI
enum AnalyticsPromptScreenCoordinatorAction {
case done
}
final class AnalyticsPromptScreenCoordinator: CoordinatorProtocol {
private let analytics: AnalyticsService
private var viewModel: AnalyticsPromptScreenViewModelProtocol
private let actionsSubject: PassthroughSubject<AnalyticsPromptScreenCoordinatorAction, Never> = .init()
private var cancellables = Set<AnyCancellable>()
var actions: AnyPublisher<AnalyticsPromptScreenCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(analytics: AnalyticsService, termsURL: URL) {
self.analytics = analytics
viewModel = AnalyticsPromptScreenViewModel(termsURL: termsURL)
}
// MARK: - Public
func start() {
viewModel.actions
.sink { [weak self] action in
guard let self else { return }
switch action {
case .enable:
MXLog.info("Enable Analytics")
analytics.optIn()
actionsSubject.send(.done)
case .disable:
MXLog.info("Disable Analytics")
analytics.optOut()
actionsSubject.send(.done)
}
}
.store(in: &cancellables)
}
func toPresentable() -> AnyView {
AnyView(AnalyticsPromptScreen(context: viewModel.context))
}
}
@@ -0,0 +1,58 @@
//
// Copyright 2021 New Vector Ltd
//
// 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
enum AnalyticsPromptScreenViewAction {
/// Enable analytics.
case enable
/// Disable analytics.
case disable
}
enum AnalyticsPromptScreenViewModelAction {
/// Enable analytics.
case enable
/// Disable analytics.
case disable
}
struct AnalyticsPromptScreenViewState: BindableState {
/// Attributed strings created from localized HTML.
let strings: AnalyticsPromptScreenStrings
}
/// A collection of strings for the UI that need to be parsed from HTML
struct AnalyticsPromptScreenStrings {
let optInContent: AttributedString
let point1 = L10n.screenAnalyticsPromptDataUsage
let point2 = L10n.screenAnalyticsPromptThirdPartySharing
let point3 = L10n.screenAnalyticsPromptSettings
init(termsURL: URL) {
let content = AttributedString(L10n.screenAnalyticsPromptHelpUsImprove)
// Create the 'read terms' with a placeholder.
let linkPlaceholder = "{link}"
var readTerms = AttributedString(L10n.screenAnalyticsSettingsReadTerms(linkPlaceholder))
var linkString = AttributedString(L10n.screenAnalyticsSettingsReadTermsContentLink)
linkString.link = termsURL
linkString.bold()
readTerms.replace(linkPlaceholder, with: linkString)
optInContent = content + "\n\n" + readTerms
}
}
@@ -0,0 +1,45 @@
//
// Copyright 2021 New Vector Ltd
//
// 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 Combine
import SwiftUI
typealias AnalyticsPromptScreenViewModelType = StateStoreViewModel<AnalyticsPromptScreenViewState, AnalyticsPromptScreenViewAction>
class AnalyticsPromptScreenViewModel: AnalyticsPromptScreenViewModelType, AnalyticsPromptScreenViewModelProtocol {
private var actionsSubject: PassthroughSubject<AnalyticsPromptScreenViewModelAction, Never> = .init()
var actions: AnyPublisher<AnalyticsPromptScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
/// Initialize a view model with the specified prompt type and app display name.
init(termsURL: URL) {
let promptStrings = AnalyticsPromptScreenStrings(termsURL: termsURL)
super.init(initialViewState: AnalyticsPromptScreenViewState(strings: promptStrings))
}
// MARK: - Public
override func process(viewAction: AnalyticsPromptScreenViewAction) {
switch viewAction {
case .enable:
actionsSubject.send(.enable)
case .disable:
actionsSubject.send(.disable)
}
}
}
@@ -0,0 +1,23 @@
//
// Copyright 2021 New Vector Ltd
//
// 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 Combine
@MainActor
protocol AnalyticsPromptScreenViewModelProtocol {
var actions: AnyPublisher<AnalyticsPromptScreenViewModelAction, Never> { get }
var context: AnalyticsPromptScreenViewModelType.Context { get }
}
@@ -0,0 +1,113 @@
//
// Copyright 2021 New Vector Ltd
//
// 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 SwiftUI
/// A prompt that asks the user whether they would like to enable Analytics or not.
struct AnalyticsPromptScreen: View {
@ObservedObject var context: AnalyticsPromptScreenViewModel.Context
var body: some View {
FullscreenDialog(topPadding: UIConstants.startScreenBreakerScreenTopPadding, showBackgroundGradient: true) {
mainContent
} bottomContent: {
buttons
}
.background()
.environment(\.backgroundStyle, AnyShapeStyle(Color.compound.bgCanvasDefault))
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
.interactiveDismissDisabled()
}
/// The main content of the screen that is shown inside the scroll view.
private var mainContent: some View {
VStack(spacing: 40) {
header
checkmarkList
}
}
private var header: some View {
VStack(spacing: 8) {
HeroImage(icon: \.chart)
.padding(.bottom, 8)
Text(L10n.screenAnalyticsPromptTitle(InfoPlistReader.main.bundleDisplayName))
.font(.compound.headingMDBold)
.multilineTextAlignment(.center)
.foregroundColor(.compound.textPrimary)
.accessibilityIdentifier(A11yIdentifiers.analyticsPromptScreen.title)
Text(context.viewState.strings.optInContent)
.font(.compound.bodyMD)
.multilineTextAlignment(.center)
.foregroundColor(.compound.textSecondary)
}
}
@ViewBuilder
private var checkMark: some View {
Image(systemName: "checkmark.circle")
.symbolVariant(.fill)
.symbolRenderingMode(.palette)
.foregroundStyle(Color.compound.iconAccentTertiary, Color.compound.textOnSolidPrimary)
}
/// The list of re-assurances about analytics.
private var checkmarkList: some View {
VStack(alignment: .leading, spacing: 4) {
checkMarkItem(title: context.viewState.strings.point1, position: .top)
checkMarkItem(title: context.viewState.strings.point2, position: .middle)
checkMarkItem(title: context.viewState.strings.point3, position: .bottom)
}
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity)
.environment(\.backgroundStyle, AnyShapeStyle(.compound.bgSubtleSecondary))
}
@ViewBuilder
private func checkMarkItem(title: String, position: ListPosition) -> some View {
RoundedLabelItem(title: title, listPosition: position) {
checkMark
}
}
/// The stack of enable/disable buttons.
private var buttons: some View {
VStack(spacing: 16) {
Button(L10n.actionOk) { context.send(viewAction: .enable) }
.buttonStyle(.compound(.primary))
.accessibilityIdentifier(A11yIdentifiers.analyticsPromptScreen.enable)
Button { context.send(viewAction: .disable) } label: {
Text(L10n.actionNotNow)
.font(.compound.bodyLGSemibold)
.padding(14)
}
.accessibilityIdentifier(A11yIdentifiers.analyticsPromptScreen.notNow)
}
}
}
// MARK: - Previews
struct AnalyticsPromptScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = AnalyticsPromptScreenViewModel(termsURL: ServiceLocator.shared.settings.analyticsConfiguration.termsURL)
static var previews: some View {
AnalyticsPromptScreen(context: viewModel.context)
}
}