QR Code Login Initial view state (#2667)

This commit is contained in:
Mauro
2024-04-09 12:21:14 +02:00
committed by GitHub
parent bc75fe1cf8
commit c61135f88c
27 changed files with 623 additions and 93 deletions
@@ -96,7 +96,7 @@ struct AuthenticationStartScreen: View {
var buttons: some View {
VStack(spacing: 16) {
if context.viewState.isQRCodeLoginEnabled {
Button { context.send(viewAction: .loginManually) } label: {
Button { context.send(viewAction: .loginWithQR) } label: {
Label(L10n.screenOnboardingSignInWithQrCode, icon: \.qrCode)
}
.buttonStyle(.compound(.primary))
@@ -0,0 +1,60 @@
//
// Copyright 2022 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.
//
// periphery:ignore:all - this is just a qRCodeLogin remove this comment once generating the final file
import Combine
import SwiftUI
struct QRCodeLoginScreenCoordinatorParameters {
let qrCodeLoginService: QRCodeLoginServiceProtocol
}
enum QRCodeLoginScreenCoordinatorAction {
case cancel
}
final class QRCodeLoginScreenCoordinator: CoordinatorProtocol {
private let viewModel: QRCodeLoginScreenViewModelProtocol
private var cancellables = Set<AnyCancellable>()
private let actionsSubject: PassthroughSubject<QRCodeLoginScreenCoordinatorAction, Never> = .init()
var actionsPublisher: AnyPublisher<QRCodeLoginScreenCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(parameters: QRCodeLoginScreenCoordinatorParameters) {
viewModel = QRCodeLoginScreenViewModel(qrCodeLoginService: parameters.qrCodeLoginService)
}
func start() {
viewModel.actionsPublisher.sink { [weak self] action in
MXLog.info("Coordinator: received view model action: \(action)")
guard let self else { return }
switch action {
case .cancel:
self.actionsSubject.send(.cancel)
}
}
.store(in: &cancellables)
}
func toPresentable() -> AnyView {
AnyView(QRCodeLoginScreen(context: viewModel.context))
}
}
@@ -0,0 +1,67 @@
//
// Copyright 2022 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 QRCodeLoginScreenViewModelAction {
case cancel
}
struct QRCodeLoginScreenViewState: BindableState {
var state: QRCodeLoginState = .initial
private let listItem3AttributedText = {
let boldPlaceholder = "{bold}"
var finalString = AttributedString(L10n.screenQrCodeLoginInitialStateItem3(boldPlaceholder))
var boldString = AttributedString(L10n.screenQrCodeLoginInitialStateItem3Action)
boldString.bold()
finalString.replace(boldPlaceholder, with: boldString)
return finalString
}()
private let listItem4AttributedText = {
let boldPlaceholder = "{bold}"
var finalString = AttributedString(L10n.screenQrCodeLoginInitialStateItem4(boldPlaceholder))
var boldString = AttributedString(L10n.screenQrCodeLoginInitialStateItem4Action)
boldString.bold()
finalString.replace(boldPlaceholder, with: boldString)
return finalString
}()
var listItems: [AttributedString] {
[
AttributedString(L10n.screenQrCodeLoginInitialStateItem1),
AttributedString(L10n.screenQrCodeLoginInitialStateItem2),
listItem3AttributedText,
listItem4AttributedText
]
}
}
enum QRCodeLoginScreenViewAction {
case cancel
case startScan
}
enum QRCodeLoginState {
case initial
case scanning
case error(QRCodeLoginErrorState)
enum QRCodeLoginErrorState {
case noCameraPermission
}
}
@@ -0,0 +1,50 @@
//
// Copyright 2022 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 AVFoundation
import Combine
import SwiftUI
typealias QRCodeLoginScreenViewModelType = StateStoreViewModel<QRCodeLoginScreenViewState, QRCodeLoginScreenViewAction>
class QRCodeLoginScreenViewModel: QRCodeLoginScreenViewModelType, QRCodeLoginScreenViewModelProtocol {
private let qrCodeLoginService: QRCodeLoginServiceProtocol
private let actionsSubject: PassthroughSubject<QRCodeLoginScreenViewModelAction, Never> = .init()
var actionsPublisher: AnyPublisher<QRCodeLoginScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(qrCodeLoginService: QRCodeLoginServiceProtocol) {
self.qrCodeLoginService = qrCodeLoginService
super.init(initialViewState: QRCodeLoginScreenViewState())
}
// MARK: - Public
override func process(viewAction: QRCodeLoginScreenViewAction) {
switch viewAction {
case .cancel:
actionsSubject.send(.cancel)
case .startScan:
Task { await startScanIfPossible() }
}
}
private func startScanIfPossible() async {
state.state = await qrCodeLoginService.requestAuthorizationIfNeeded() ? .scanning : .error(.noCameraPermission)
}
}
@@ -0,0 +1,23 @@
//
// Copyright 2022 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 QRCodeLoginScreenViewModelProtocol {
var actionsPublisher: AnyPublisher<QRCodeLoginScreenViewModelAction, Never> { get }
var context: QRCodeLoginScreenViewModelType.Context { get }
}
@@ -0,0 +1,86 @@
//
// Copyright 2022 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 CONnDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Compound
import SwiftUI
struct QRCodeLoginScreen: View {
@ObservedObject var context: QRCodeLoginScreenViewModel.Context
var body: some View {
NavigationStack {
mainContent
.toolbar { toolbar }
.toolbar(.visible, for: .navigationBar)
.background()
.environment(\.backgroundStyle, AnyShapeStyle(Color.compound.bgSubtleSecondary))
.interactiveDismissDisabled()
}
}
@ViewBuilder
var mainContent: some View {
switch context.viewState.state {
case .initial:
initialContent
case .scanning, .error:
// TODO: Handle states
EmptyView()
}
}
var initialContent: some View {
FullscreenDialog {
VStack(alignment: .leading, spacing: 40) {
VStack(spacing: 16) {
HeroImage(icon: \.computer, style: .subtle)
Text(L10n.screenQrCodeLoginInitialStateTitle)
.foregroundColor(.compound.textPrimary)
.font(.compound.headingMDBold)
.multilineTextAlignment(.center)
}
.padding(.horizontal, 24)
SFNumberedListView(items: context.viewState.listItems)
}
} bottomContent: {
Button(L10n.actionContinue) {
context.send(viewAction: .startScan)
}
.buttonStyle(.compound(.primary))
}
}
@ToolbarContentBuilder
private var toolbar: some ToolbarContent {
ToolbarItem(placement: .cancellationAction) {
Button(L10n.actionCancel) {
context.send(viewAction: .cancel)
}
}
}
}
// MARK: - Previews
struct QRCodeLoginScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = QRCodeLoginScreenViewModel(qrCodeLoginService: QRCodeLoginServiceMock())
static var previews: some View {
QRCodeLoginScreen(context: viewModel.context)
.previewDisplayName("Initial")
}
}
@@ -21,7 +21,7 @@ enum ResetRecoveryKeyScreenViewModelAction {
}
struct ResetRecoveryKeyScreenViewState: BindableState {
let listItem3AttributedText = {
private let listItem3AttributedText = {
let boldPlaceholder = "{bold}"
var finalString = AttributedString(L10n.screenCreateNewRecoveryKeyListItem3(boldPlaceholder))
var boldString = AttributedString(L10n.screenCreateNewRecoveryKeyListItem3ResetAll)
@@ -29,6 +29,16 @@ struct ResetRecoveryKeyScreenViewState: BindableState {
finalString.replace(boldPlaceholder, with: boldString)
return finalString
}()
var listItems: [AttributedString] {
[
AttributedString(L10n.screenCreateNewRecoveryKeyListItem1),
AttributedString(L10n.screenCreateNewRecoveryKeyListItem2),
listItem3AttributedText,
AttributedString(L10n.screenCreateNewRecoveryKeyListItem4),
AttributedString(L10n.screenCreateNewRecoveryKeyListItem5)
]
}
}
enum ResetRecoveryKeyScreenViewAction {
@@ -34,6 +34,7 @@ class ResetRecoveryKeyScreenViewModel: ResetRecoveryKeyScreenViewModelType, Rese
override func process(viewAction: ResetRecoveryKeyScreenViewAction) {
switch viewAction {
case .cancel:
// We might also need to display first an alert and do a logOut API call in some cases
actionsSubject.send(.cancel)
}
}
@@ -38,7 +38,7 @@ struct ResetRecoveryKeyScreen: View {
private var mainContent: some View {
VStack(spacing: 40) {
header
list
SFNumberedListView(items: context.viewState.listItems)
}
}
@@ -53,65 +53,6 @@ struct ResetRecoveryKeyScreen: View {
}
}
private var list: some View {
VStack(alignment: .leading, spacing: 24) {
Label {
Text(L10n.screenCreateNewRecoveryKeyListItem1)
} icon: {
Image(systemSymbol: ._1Circle)
.imageScale(.large)
.fontWeight(.light)
.foregroundColor(.compound.textPlaceholder)
}
.foregroundColor(.compound.textPrimary)
.font(.compound.bodyMD)
Label {
Text(L10n.screenCreateNewRecoveryKeyListItem2)
} icon: {
Image(systemSymbol: ._2Circle)
.imageScale(.large)
.fontWeight(.light)
.foregroundColor(.compound.textPlaceholder)
}
.foregroundColor(.compound.textPrimary)
.font(.compound.bodyMD)
Label {
Text(context.viewState.listItem3AttributedText)
} icon: {
Image(systemSymbol: ._3Circle)
.imageScale(.large)
.fontWeight(.light)
.foregroundColor(.compound.textPlaceholder)
}
.foregroundColor(.compound.textPrimary)
.font(.compound.bodyMD)
Label {
Text(L10n.screenCreateNewRecoveryKeyListItem4)
} icon: {
Image(systemSymbol: ._4Circle)
.imageScale(.large)
.fontWeight(.light)
.foregroundColor(.compound.textPlaceholder)
}
.foregroundColor(.compound.textPrimary)
.font(.compound.bodyMD)
Label {
Text(L10n.screenCreateNewRecoveryKeyListItem5)
} icon: {
Image(systemSymbol: ._5Circle)
.imageScale(.large)
.fontWeight(.light)
.foregroundColor(.compound.textPlaceholder)
}
.foregroundColor(.compound.textPrimary)
.font(.compound.bodyMD)
}
}
@ToolbarContentBuilder
private var toolbar: some ToolbarContent {
ToolbarItem(placement: .cancellationAction) {