Server Confirmation Screen (#959)

* Add ServerConfirmationScreen.
This commit is contained in:
Doug
2023-05-30 10:31:18 +01:00
committed by GitHub
parent 088ca622a1
commit 74b36ce870
56 changed files with 643 additions and 201 deletions
@@ -0,0 +1,64 @@
//
// 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
import SwiftUI
struct ServerConfirmationScreenCoordinatorParameters {
let authenticationService: AuthenticationServiceProxyProtocol
let authenticationFlow: AuthenticationFlow
}
enum ServerConfirmationScreenCoordinatorAction {
case confirm
case changeServer
}
final class ServerConfirmationScreenCoordinator: CoordinatorProtocol {
private let parameters: ServerConfirmationScreenCoordinatorParameters
private var viewModel: ServerConfirmationScreenViewModelProtocol
private let actionsSubject: PassthroughSubject<ServerConfirmationScreenCoordinatorAction, Never> = .init()
private var cancellables: Set<AnyCancellable> = .init()
var actions: AnyPublisher<ServerConfirmationScreenCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(parameters: ServerConfirmationScreenCoordinatorParameters) {
self.parameters = parameters
viewModel = ServerConfirmationScreenViewModel(authenticationService: parameters.authenticationService,
authenticationFlow: parameters.authenticationFlow)
}
func start() {
viewModel.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .confirm:
self.actionsSubject.send(.confirm)
case .changeServer:
self.actionsSubject.send(.changeServer)
}
}
.store(in: &cancellables)
}
func toPresentable() -> AnyView {
AnyView(ServerConfirmationScreen(context: viewModel.context))
}
}
@@ -0,0 +1,64 @@
//
// 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 ServerConfirmationScreenViewModelAction {
/// The user would like to continue with the current homeserver.
case confirm
/// The user would like to change to a different homeserver.
case changeServer
}
struct ServerConfirmationScreenViewState: BindableState {
/// The homeserver address input by the user.
var homeserverAddress: String
/// The flow being attempted on the selected homeserver.
let authenticationFlow: AuthenticationFlow
/// The screen's title.
var title: String {
switch authenticationFlow {
case .login:
return L10n.screenServerConfirmationTitleLogin(homeserverAddress)
case .register:
return L10n.screenServerConfirmationTitleRegister(homeserverAddress)
}
}
/// The message shown beneath the title.
var message: String {
switch authenticationFlow {
case .login:
if homeserverAddress == "matrix.org" {
return L10n.screenServerConfirmationMessageLoginMatrixDotOrg
} else if homeserverAddress == "element.io" {
return L10n.screenServerConfirmationMessageLoginElementDotIo
} else {
return ""
}
case .register:
return L10n.screenServerConfirmationMessageRegister
}
}
}
enum ServerConfirmationScreenViewAction {
/// The user would like to continue with the current homeserver.
case confirm
/// The user would like to change to a different homeserver.
case changeServer
}
@@ -0,0 +1,52 @@
//
// 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
import SwiftUI
typealias ServerConfirmationScreenViewModelType = StateStoreViewModel<ServerConfirmationScreenViewState, ServerConfirmationScreenViewAction>
class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType, ServerConfirmationScreenViewModelProtocol {
private var actionsSubject: PassthroughSubject<ServerConfirmationScreenViewModelAction, Never> = .init()
var actions: AnyPublisher<ServerConfirmationScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(authenticationService: AuthenticationServiceProxyProtocol, authenticationFlow: AuthenticationFlow) {
super.init(initialViewState: ServerConfirmationScreenViewState(homeserverAddress: authenticationService.homeserver.value.address,
authenticationFlow: authenticationFlow))
authenticationService.homeserver
.receive(on: DispatchQueue.main)
.sink { [weak self] homeserver in
guard let self else { return }
state.homeserverAddress = homeserver.address
}
.store(in: &cancellables)
}
// MARK: - Public
override func process(viewAction: ServerConfirmationScreenViewAction) {
switch viewAction {
case .confirm:
actionsSubject.send(.confirm)
case .changeServer:
actionsSubject.send(.changeServer)
}
}
}
@@ -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 ServerConfirmationScreenViewModelProtocol {
var actions: AnyPublisher<ServerConfirmationScreenViewModelAction, Never> { get }
var context: ServerConfirmationScreenViewModelType.Context { get }
}
@@ -0,0 +1,99 @@
//
// 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 SwiftUI
struct ServerConfirmationScreen: View {
@ObservedObject var context: ServerConfirmationScreenViewModel.Context
var body: some View {
ScrollView {
header
.padding(.top, UIConstants.iconTopPaddingToNavigationBar)
.padding(.horizontal, 16)
.readableFrame()
}
.background(Color.element.background.ignoresSafeArea())
.safeAreaInset(edge: .bottom) {
buttons
.padding(.horizontal, 16)
.padding(.vertical, 16)
.readableFrame()
.background(Color.element.background.ignoresSafeArea())
}
}
/// The main content of the view to be shown in a scroll view.
var header: some View {
VStack(spacing: 8) {
AuthenticationIconImage(image: Image(systemName: "person.crop.circle.fill"))
.padding(.bottom, 8)
Text(context.viewState.title)
.font(.compound.headingMDBold)
.multilineTextAlignment(.center)
.foregroundColor(.element.primaryContent)
.fixedSize(horizontal: false, vertical: true)
Text(context.viewState.message)
.font(.compound.bodyMD)
.multilineTextAlignment(.center)
.foregroundColor(.element.tertiaryContent)
}
.padding(.horizontal, 16)
}
/// The action buttons shown at the bottom of the view.
var buttons: some View {
VStack(spacing: 24) {
Button { context.send(viewAction: .confirm) } label: {
Text(L10n.actionContinue)
}
.buttonStyle(.elementAction(.xLarge))
.accessibilityIdentifier(A11yIdentifiers.serverConfirmationScreen.continue)
Button { context.send(viewAction: .changeServer) } label: {
Text(L10n.screenServerConfirmationChangeServer)
.font(.compound.bodyLGSemibold)
.padding(.vertical, 14)
}
.accessibilityIdentifier(A11yIdentifiers.serverConfirmationScreen.changeServer)
}
}
}
// MARK: - Previews
struct ServerConfirmationScreen_Previews: PreviewProvider {
static let loginViewModel = ServerConfirmationScreenViewModel(authenticationService: MockAuthenticationServiceProxy(),
authenticationFlow: .login)
static let registerViewModel = ServerConfirmationScreenViewModel(authenticationService: MockAuthenticationServiceProxy(),
authenticationFlow: .register)
static var previews: some View {
NavigationStack {
ServerConfirmationScreen(context: loginViewModel.context)
.toolbar(.visible, for: .navigationBar)
}
.previewDisplayName("Login")
NavigationStack {
ServerConfirmationScreen(context: registerViewModel.context)
.toolbar(.visible, for: .navigationBar)
}
.previewDisplayName("Register")
}
}