mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
SwiftUI NavigationController and UserNotificationControllers (#309)
* Fixes #286 - Adopted the new SwiftUI NavigationStack based NavigationController throughout the application * Fixes #315 - Implemented new user notification components on top of SwiftUI and the new navigation flows * Add home screen fade animation between skeletons and real rooms * Bump the danger-swift version used on the CI and swiftlint with it * Renamed Splash to Onboarding, Empty to Splash
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
protocol AuthenticationCoordinatorDelegate: AnyObject {
|
||||
@@ -22,42 +22,31 @@ protocol AuthenticationCoordinatorDelegate: AnyObject {
|
||||
didLoginWithSession userSession: UserSessionProtocol)
|
||||
}
|
||||
|
||||
class AuthenticationCoordinator: Coordinator, Presentable {
|
||||
class AuthenticationCoordinator: CoordinatorProtocol {
|
||||
private let authenticationService: AuthenticationServiceProxyProtocol
|
||||
private let navigationRouter: NavigationRouter
|
||||
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
|
||||
private var activityIndicator: UserIndicator?
|
||||
|
||||
var childCoordinators: [Coordinator] = []
|
||||
private let navigationController: NavigationController
|
||||
|
||||
weak var delegate: AuthenticationCoordinatorDelegate?
|
||||
|
||||
init(authenticationService: AuthenticationServiceProxyProtocol,
|
||||
navigationRouter: NavigationRouter) {
|
||||
navigationController: NavigationController) {
|
||||
self.authenticationService = authenticationService
|
||||
self.navigationRouter = navigationRouter
|
||||
|
||||
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: navigationRouter.toPresentable())
|
||||
self.navigationController = navigationController
|
||||
}
|
||||
|
||||
func start() {
|
||||
showSplashScreen()
|
||||
showOnboarding()
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
navigationRouter.toPresentable()
|
||||
}
|
||||
|
||||
func stop() {
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// Shows the splash screen as the root view in the navigation stack.
|
||||
private func showSplashScreen() {
|
||||
let coordinator = SplashScreenCoordinator()
|
||||
|
||||
private func showOnboarding() {
|
||||
let coordinator = OnboardingCoordinator()
|
||||
|
||||
coordinator.callback = { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
@@ -66,12 +55,7 @@ class AuthenticationCoordinator: Coordinator, Presentable {
|
||||
}
|
||||
}
|
||||
|
||||
coordinator.start()
|
||||
add(childCoordinator: coordinator)
|
||||
|
||||
navigationRouter.setRootModule(coordinator) { [weak self] in
|
||||
self?.remove(childCoordinator: coordinator)
|
||||
}
|
||||
navigationController.setRootCoordinator(coordinator)
|
||||
}
|
||||
|
||||
private func startAuthentication() async {
|
||||
@@ -89,7 +73,8 @@ class AuthenticationCoordinator: Coordinator, Presentable {
|
||||
|
||||
private func showServerSelectionScreen() {
|
||||
let parameters = ServerSelectionCoordinatorParameters(authenticationService: authenticationService,
|
||||
hasModalPresentation: false)
|
||||
userNotificationController: ServiceLocator.shared.userNotificationController,
|
||||
isModallyPresented: false)
|
||||
let coordinator = ServerSelectionCoordinator(parameters: parameters)
|
||||
|
||||
coordinator.callback = { [weak self] action in
|
||||
@@ -103,34 +88,24 @@ class AuthenticationCoordinator: Coordinator, Presentable {
|
||||
}
|
||||
}
|
||||
|
||||
coordinator.start()
|
||||
add(childCoordinator: coordinator)
|
||||
|
||||
navigationRouter.push(coordinator) { [weak self] in
|
||||
self?.remove(childCoordinator: coordinator)
|
||||
}
|
||||
navigationController.push(coordinator)
|
||||
}
|
||||
|
||||
private func showLoginScreen() {
|
||||
let parameters = LoginCoordinatorParameters(authenticationService: authenticationService,
|
||||
navigationRouter: navigationRouter)
|
||||
navigationController: navigationController)
|
||||
let coordinator = LoginCoordinator(parameters: parameters)
|
||||
|
||||
|
||||
coordinator.callback = { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
|
||||
switch action {
|
||||
case .signedIn(let userSession):
|
||||
self.delegate?.authenticationCoordinator(self, didLoginWithSession: userSession)
|
||||
}
|
||||
}
|
||||
|
||||
coordinator.start()
|
||||
add(childCoordinator: coordinator)
|
||||
|
||||
navigationRouter.push(coordinator) { [weak self] in
|
||||
self?.remove(childCoordinator: coordinator)
|
||||
}
|
||||
|
||||
navigationController.push(coordinator)
|
||||
}
|
||||
|
||||
private func showAnalyticsPrompt(with userSession: UserSessionProtocol) {
|
||||
@@ -141,22 +116,20 @@ class AuthenticationCoordinator: Coordinator, Presentable {
|
||||
guard let self else { return }
|
||||
self.delegate?.authenticationCoordinator(self, didLoginWithSession: userSession)
|
||||
}
|
||||
|
||||
coordinator.start()
|
||||
add(childCoordinator: coordinator)
|
||||
|
||||
navigationRouter.setRootModule(coordinator, hideNavigationBar: true, animated: true) { [weak self] in
|
||||
self?.remove(childCoordinator: coordinator)
|
||||
}
|
||||
|
||||
navigationController.setRootCoordinator(coordinator)
|
||||
}
|
||||
|
||||
/// Show a blocking activity indicator.
|
||||
static let loadingIndicatorIdentifier = "AuthenticationCoordinatorLoading"
|
||||
|
||||
private func startLoading() {
|
||||
activityIndicator = indicatorPresenter.present(.loading(label: ElementL10n.loading, isInteractionBlocking: true))
|
||||
ServiceLocator.shared.userNotificationController.submitNotification(UserNotification(id: Self.loadingIndicatorIdentifier,
|
||||
type: .modal,
|
||||
title: ElementL10n.loading,
|
||||
persistent: true))
|
||||
}
|
||||
|
||||
/// Hide the currently displayed activity indicator.
|
||||
private func stopLoading() {
|
||||
activityIndicator = nil
|
||||
ServiceLocator.shared.userNotificationController.retractNotificationWithId(Self.loadingIndicatorIdentifier)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ struct LoginCoordinatorParameters {
|
||||
/// The service used to authenticate the user.
|
||||
let authenticationService: AuthenticationServiceProxyProtocol
|
||||
/// The navigation router used to present the server selection screen.
|
||||
let navigationRouter: NavigationRouterType
|
||||
let navigationController: NavigationController
|
||||
}
|
||||
|
||||
enum LoginCoordinatorAction {
|
||||
@@ -29,14 +29,10 @@ enum LoginCoordinatorAction {
|
||||
case signedIn(UserSessionProtocol)
|
||||
}
|
||||
|
||||
final class LoginCoordinator: Coordinator, Presentable {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
final class LoginCoordinator: CoordinatorProtocol {
|
||||
private let parameters: LoginCoordinatorParameters
|
||||
private let loginHostingController: UIViewController
|
||||
private var loginViewModel: LoginViewModelProtocol
|
||||
private var viewModel: LoginViewModelProtocol
|
||||
private let hostingController: UIViewController
|
||||
/// Passed to the OIDC service to provide a view controller from which to present the authentication session.
|
||||
private let oidcUserAgent: OIDExternalUserAgentIOS?
|
||||
|
||||
@@ -47,14 +43,8 @@ final class LoginCoordinator: Coordinator, Presentable {
|
||||
}
|
||||
|
||||
private var authenticationService: AuthenticationServiceProxyProtocol { parameters.authenticationService }
|
||||
private var navigationRouter: NavigationRouterType { parameters.navigationRouter }
|
||||
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
|
||||
private var activityIndicator: UserIndicator?
|
||||
|
||||
// MARK: Public
|
||||
private var navigationController: NavigationController { parameters.navigationController }
|
||||
|
||||
// Must be used only internally
|
||||
var childCoordinators: [Coordinator] = []
|
||||
var callback: (@MainActor (LoginCoordinatorAction) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
@@ -62,22 +52,16 @@ final class LoginCoordinator: Coordinator, Presentable {
|
||||
init(parameters: LoginCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
let viewModel = LoginViewModel(homeserver: parameters.authenticationService.homeserver)
|
||||
loginViewModel = viewModel
|
||||
viewModel = LoginViewModel(homeserver: parameters.authenticationService.homeserver)
|
||||
|
||||
let view = LoginScreen(context: viewModel.context)
|
||||
loginHostingController = UIHostingController(rootView: view)
|
||||
|
||||
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: loginHostingController)
|
||||
oidcUserAgent = OIDExternalUserAgentIOS(presenting: loginHostingController)
|
||||
hostingController = UIHostingController(rootView: LoginScreen(context: viewModel.context))
|
||||
oidcUserAgent = OIDExternalUserAgentIOS(presenting: hostingController)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func start() {
|
||||
MXLog.debug("Did start.")
|
||||
|
||||
loginViewModel.callback = { [weak self] action in
|
||||
viewModel.callback = { [weak self] action in
|
||||
guard let self else { return }
|
||||
MXLog.debug("LoginViewModel did callback with result: \(action).")
|
||||
|
||||
@@ -95,51 +79,52 @@ final class LoginCoordinator: Coordinator, Presentable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
loginHostingController
|
||||
}
|
||||
|
||||
func stop() {
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(LoginScreen(context: viewModel.context))
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// Show a blocking activity indicator whilst saving.
|
||||
static let loadingIndicatorIdentifier = "LoginCoordinatorLoading"
|
||||
|
||||
private func startLoading(isInteractionBlocking: Bool) {
|
||||
activityIndicator = indicatorPresenter.present(.loading(label: ElementL10n.loading, isInteractionBlocking: isInteractionBlocking))
|
||||
ServiceLocator.shared.userNotificationController.submitNotification(UserNotification(id: Self.loadingIndicatorIdentifier,
|
||||
type: .modal,
|
||||
title: ElementL10n.loading,
|
||||
persistent: true))
|
||||
|
||||
if !isInteractionBlocking {
|
||||
loginViewModel.update(isLoading: true)
|
||||
viewModel.update(isLoading: true)
|
||||
}
|
||||
}
|
||||
|
||||
/// Show a non-blocking indicator that an operation was successful.
|
||||
private func indicateSuccess() {
|
||||
activityIndicator = indicatorPresenter.present(.success(label: ElementL10n.dialogTitleSuccess))
|
||||
}
|
||||
|
||||
/// Show a non-blocking indicator that an operation failed.
|
||||
private func indicateFailure() {
|
||||
activityIndicator = indicatorPresenter.present(.error(label: ElementL10n.dialogTitleError))
|
||||
}
|
||||
|
||||
/// Hide the currently displayed activity indicator.
|
||||
private func stopLoading() {
|
||||
loginViewModel.update(isLoading: false)
|
||||
activityIndicator = nil
|
||||
viewModel.update(isLoading: false)
|
||||
ServiceLocator.shared.userNotificationController.retractNotificationWithId(Self.loadingIndicatorIdentifier)
|
||||
}
|
||||
|
||||
private func indicateSuccess() {
|
||||
ServiceLocator.shared.userNotificationController.submitNotification(UserNotification(title: ElementL10n.dialogTitleSuccess))
|
||||
}
|
||||
|
||||
private func indicateFailure() {
|
||||
ServiceLocator.shared.userNotificationController.submitNotification(UserNotification(title: ElementL10n.dialogTitleError))
|
||||
}
|
||||
|
||||
/// Processes an error to either update the flow or display it to the user.
|
||||
private func handleError(_ error: AuthenticationServiceError) {
|
||||
switch error {
|
||||
case .invalidCredentials:
|
||||
loginViewModel.displayError(.alert(ElementL10n.authInvalidLoginParam))
|
||||
viewModel.displayError(.alert(ElementL10n.authInvalidLoginParam))
|
||||
case .accountDeactivated:
|
||||
loginViewModel.displayError(.alert(ElementL10n.authInvalidLoginDeactivatedAccount))
|
||||
viewModel.displayError(.alert(ElementL10n.authInvalidLoginDeactivatedAccount))
|
||||
default:
|
||||
loginViewModel.displayError(.alert(ElementL10n.unknownError))
|
||||
viewModel.displayError(.alert(ElementL10n.unknownError))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,44 +189,43 @@ final class LoginCoordinator: Coordinator, Presentable {
|
||||
|
||||
/// Updates the view model with a different homeserver.
|
||||
private func updateViewModel() {
|
||||
loginViewModel.update(homeserver: authenticationService.homeserver)
|
||||
viewModel.update(homeserver: authenticationService.homeserver)
|
||||
indicateSuccess()
|
||||
}
|
||||
|
||||
/// Presents the server selection screen as a modal.
|
||||
private func presentServerSelectionScreen() {
|
||||
MXLog.debug("PresentServerSelectionScreen")
|
||||
let serverSelectionNavigationController = NavigationController()
|
||||
|
||||
let userNotificationController = UserNotificationController(rootCoordinator: serverSelectionNavigationController)
|
||||
|
||||
let parameters = ServerSelectionCoordinatorParameters(authenticationService: authenticationService,
|
||||
hasModalPresentation: true)
|
||||
userNotificationController: userNotificationController,
|
||||
isModallyPresented: true)
|
||||
|
||||
let coordinator = ServerSelectionCoordinator(parameters: parameters)
|
||||
coordinator.callback = { [weak self, weak coordinator] action in
|
||||
guard let self, let coordinator = coordinator else { return }
|
||||
self.serverSelectionCoordinator(coordinator, didCompleteWith: action)
|
||||
}
|
||||
|
||||
coordinator.start()
|
||||
add(childCoordinator: coordinator)
|
||||
serverSelectionNavigationController.setRootCoordinator(coordinator)
|
||||
|
||||
let modalRouter = NavigationRouter(navigationController: ElementNavigationController())
|
||||
modalRouter.setRootModule(coordinator)
|
||||
|
||||
navigationRouter.present(modalRouter, animated: true)
|
||||
navigationController.presentSheet(userNotificationController)
|
||||
}
|
||||
|
||||
/// Handles the result from the server selection modal, dismissing it after updating the view.
|
||||
private func serverSelectionCoordinator(_ coordinator: ServerSelectionCoordinator,
|
||||
didCompleteWith action: ServerSelectionCoordinatorAction) {
|
||||
navigationRouter.dismissModule(animated: true) { [weak self] in
|
||||
if action == .updated {
|
||||
self?.updateViewModel()
|
||||
}
|
||||
|
||||
self?.remove(childCoordinator: coordinator)
|
||||
if action == .updated {
|
||||
updateViewModel()
|
||||
}
|
||||
|
||||
navigationController.dismissSheet()
|
||||
}
|
||||
|
||||
/// Shows the forgot password screen.
|
||||
private func showForgotPasswordScreen() {
|
||||
loginViewModel.displayError(.alert("Not implemented."))
|
||||
viewModel.displayError(.alert("Not implemented."))
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -27,18 +27,18 @@ enum MockServerSelectionScreenState: CaseIterable {
|
||||
switch self {
|
||||
case .matrix:
|
||||
return ServerSelectionViewModel(homeserverAddress: "https://matrix.org",
|
||||
hasModalPresentation: true)
|
||||
isModallyPresented: true)
|
||||
case .emptyAddress:
|
||||
return ServerSelectionViewModel(homeserverAddress: "",
|
||||
hasModalPresentation: true)
|
||||
isModallyPresented: true)
|
||||
case .invalidAddress:
|
||||
let viewModel = ServerSelectionViewModel(homeserverAddress: "thisisbad",
|
||||
hasModalPresentation: true)
|
||||
isModallyPresented: true)
|
||||
viewModel.displayError(.footerMessage(ElementL10n.unknownError))
|
||||
return viewModel
|
||||
case .nonModal:
|
||||
return ServerSelectionViewModel(homeserverAddress: "https://matrix.org",
|
||||
hasModalPresentation: false)
|
||||
isModallyPresented: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+22
-44
@@ -19,8 +19,9 @@ import SwiftUI
|
||||
struct ServerSelectionCoordinatorParameters {
|
||||
/// The service used to authenticate the user.
|
||||
let authenticationService: AuthenticationServiceProxyProtocol
|
||||
let userNotificationController: UserNotificationControllerProtocol
|
||||
/// Whether the screen is presented modally or within a navigation stack.
|
||||
let hasModalPresentation: Bool
|
||||
let isModallyPresented: Bool
|
||||
}
|
||||
|
||||
enum ServerSelectionCoordinatorAction {
|
||||
@@ -28,45 +29,25 @@ enum ServerSelectionCoordinatorAction {
|
||||
case dismiss
|
||||
}
|
||||
|
||||
final class ServerSelectionCoordinator: Coordinator, Presentable {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
final class ServerSelectionCoordinator: CoordinatorProtocol {
|
||||
private let parameters: ServerSelectionCoordinatorParameters
|
||||
private let serverSelectionHostingController: UIViewController
|
||||
private var serverSelectionViewModel: ServerSelectionViewModelProtocol
|
||||
|
||||
private let userNotificationController: UserNotificationControllerProtocol
|
||||
private var viewModel: ServerSelectionViewModelProtocol
|
||||
private var authenticationService: AuthenticationServiceProxyProtocol { parameters.authenticationService }
|
||||
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
|
||||
private var loadingIndicator: UserIndicator?
|
||||
|
||||
// MARK: Public
|
||||
|
||||
// Must be used only internally
|
||||
var childCoordinators: [Coordinator] = []
|
||||
var callback: (@MainActor (ServerSelectionCoordinatorAction) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(parameters: ServerSelectionCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
let viewModel = ServerSelectionViewModel(homeserverAddress: parameters.authenticationService.homeserver.address,
|
||||
hasModalPresentation: parameters.hasModalPresentation)
|
||||
let view = ServerSelectionScreen(context: viewModel.context)
|
||||
serverSelectionViewModel = viewModel
|
||||
serverSelectionHostingController = UIHostingController(rootView: view)
|
||||
|
||||
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: serverSelectionHostingController)
|
||||
viewModel = ServerSelectionViewModel(homeserverAddress: parameters.authenticationService.homeserver.address,
|
||||
isModallyPresented: parameters.isModallyPresented)
|
||||
userNotificationController = parameters.userNotificationController
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func start() {
|
||||
MXLog.debug("Did start.")
|
||||
|
||||
serverSelectionViewModel.callback = { [weak self] action in
|
||||
viewModel.callback = { [weak self] action in
|
||||
guard let self else { return }
|
||||
MXLog.debug("ServerSelectionViewModel did callback with action: \(action).")
|
||||
|
||||
@@ -79,27 +60,24 @@ final class ServerSelectionCoordinator: Coordinator, Presentable {
|
||||
}
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
serverSelectionHostingController
|
||||
}
|
||||
|
||||
func stop() {
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// Show an activity indicator whilst loading.
|
||||
/// - Parameters:
|
||||
/// - label: The label to show on the indicator.
|
||||
/// - isInteractionBlocking: Whether the indicator should block any user interaction.
|
||||
private func startLoading(label: String = ElementL10n.loading, isInteractionBlocking: Bool = true) {
|
||||
loadingIndicator = indicatorPresenter.present(.loading(label: label, isInteractionBlocking: isInteractionBlocking))
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(ServerSelectionScreen(context: viewModel.context))
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func startLoading(label: String = ElementL10n.loading) {
|
||||
userNotificationController.submitNotification(UserNotification(type: .modal,
|
||||
title: label,
|
||||
persistent: true))
|
||||
}
|
||||
|
||||
/// Hide the currently displayed activity indicator.
|
||||
private func stopLoading() {
|
||||
loadingIndicator = nil
|
||||
userNotificationController.retractAllNotifications()
|
||||
}
|
||||
|
||||
/// Updates the login flow using the supplied homeserver address, or shows an error when this isn't possible.
|
||||
@@ -122,9 +100,9 @@ final class ServerSelectionCoordinator: Coordinator, Presentable {
|
||||
private func handleError(_ error: AuthenticationServiceError) {
|
||||
switch error {
|
||||
case .invalidServer, .invalidHomeserverAddress:
|
||||
serverSelectionViewModel.displayError(.footerMessage(ElementL10n.loginErrorHomeserverNotFound))
|
||||
viewModel.displayError(.footerMessage(ElementL10n.loginErrorHomeserverNotFound))
|
||||
default:
|
||||
serverSelectionViewModel.displayError(.footerMessage(ElementL10n.unknownError))
|
||||
viewModel.displayError(.footerMessage(ElementL10n.unknownError))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ struct ServerSelectionViewState: BindableState {
|
||||
/// An error message to be shown in the text field footer.
|
||||
var footerErrorMessage: String?
|
||||
/// Whether the screen is presented modally or within a navigation stack.
|
||||
var hasModalPresentation: Bool
|
||||
var isModallyPresented: Bool
|
||||
|
||||
/// The message to show in the text field footer.
|
||||
var footerMessage: String {
|
||||
@@ -42,7 +42,7 @@ struct ServerSelectionViewState: BindableState {
|
||||
|
||||
/// The title shown on the confirm button.
|
||||
var buttonTitle: String {
|
||||
hasModalPresentation ? ElementL10n.actionConfirm : ElementL10n.actionNext
|
||||
isModallyPresented ? ElementL10n.actionConfirm : ElementL10n.actionNext
|
||||
}
|
||||
|
||||
/// The text field is showing an error.
|
||||
|
||||
+4
-4
@@ -28,13 +28,13 @@ class ServerSelectionViewModel: ServerSelectionViewModelType, ServerSelectionVie
|
||||
var callback: (@MainActor (ServerSelectionViewModelAction) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(homeserverAddress: String, hasModalPresentation: Bool) {
|
||||
|
||||
init(homeserverAddress: String, isModallyPresented: Bool) {
|
||||
let bindings = ServerSelectionBindings(homeserverAddress: homeserverAddress)
|
||||
super.init(initialViewState: ServerSelectionViewState(bindings: bindings,
|
||||
hasModalPresentation: hasModalPresentation))
|
||||
isModallyPresented: isModallyPresented))
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: ServerSelectionViewAction) async {
|
||||
|
||||
+2
-1
@@ -44,6 +44,7 @@ struct ServerSelectionScreen: View {
|
||||
.background(Color.element.background, ignoresSafeAreaEdges: .all)
|
||||
.toolbar { toolbar }
|
||||
.alert(item: $context.alertInfo) { $0.alert }
|
||||
.interactiveDismissDisabled()
|
||||
}
|
||||
|
||||
/// The title, message and icon at the top of the screen.
|
||||
@@ -91,7 +92,7 @@ struct ServerSelectionScreen: View {
|
||||
@ToolbarContentBuilder
|
||||
var toolbar: some ToolbarContent {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
if context.viewState.hasModalPresentation {
|
||||
if context.viewState.isModallyPresented {
|
||||
Button { context.send(viewAction: .dismiss) } label: {
|
||||
Text(ElementL10n.actionCancel)
|
||||
}
|
||||
|
||||
@@ -40,55 +40,35 @@ enum SoftLogoutCoordinatorResult: CustomStringConvertible {
|
||||
}
|
||||
}
|
||||
|
||||
final class SoftLogoutCoordinator: Coordinator, Presentable {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
final class SoftLogoutCoordinator: CoordinatorProtocol {
|
||||
private let parameters: SoftLogoutCoordinatorParameters
|
||||
private let softLogoutHostingController: UIViewController
|
||||
private var softLogoutViewModel: SoftLogoutViewModelProtocol
|
||||
private var viewModel: SoftLogoutViewModelProtocol
|
||||
private let hostingController: UIViewController
|
||||
/// Passed to the OIDC service to provide a view controller from which to present the authentication session.
|
||||
private let oidcUserAgent: OIDExternalUserAgentIOS?
|
||||
|
||||
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
|
||||
private var loadingIndicator: UserIndicator?
|
||||
private var successIndicator: UserIndicator?
|
||||
|
||||
/// The wizard used to handle the registration flow.
|
||||
private var authenticationService: AuthenticationServiceProxyProtocol { parameters.authenticationService }
|
||||
|
||||
// MARK: Public
|
||||
|
||||
// Must be used only internally
|
||||
var childCoordinators: [Coordinator] = []
|
||||
var callback: (@MainActor (SoftLogoutCoordinatorResult) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
@MainActor init(parameters: SoftLogoutCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
let homeserver = parameters.authenticationService.homeserver
|
||||
|
||||
let viewModel = SoftLogoutViewModel(credentials: parameters.credentials,
|
||||
homeserver: homeserver,
|
||||
keyBackupNeeded: parameters.keyBackupNeeded)
|
||||
softLogoutViewModel = viewModel
|
||||
|
||||
let view = SoftLogoutScreen(context: viewModel.context)
|
||||
softLogoutHostingController = UIHostingController(rootView: view)
|
||||
viewModel = SoftLogoutViewModel(credentials: parameters.credentials,
|
||||
homeserver: homeserver,
|
||||
keyBackupNeeded: parameters.keyBackupNeeded)
|
||||
|
||||
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: softLogoutHostingController)
|
||||
oidcUserAgent = OIDExternalUserAgentIOS(presenting: softLogoutHostingController)
|
||||
hostingController = UIHostingController(rootView: SoftLogoutScreen(context: viewModel.context))
|
||||
oidcUserAgent = OIDExternalUserAgentIOS(presenting: hostingController)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func start() {
|
||||
MXLog.debug("[SoftLogoutCoordinator] did start.")
|
||||
|
||||
softLogoutViewModel.callback = { [weak self] result in
|
||||
viewModel.callback = { [weak self] result in
|
||||
guard let self else { return }
|
||||
MXLog.debug("[SoftLogoutCoordinator] SoftLogoutViewModel did complete with result: \(result).")
|
||||
|
||||
@@ -105,31 +85,36 @@ final class SoftLogoutCoordinator: Coordinator, Presentable {
|
||||
}
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
softLogoutHostingController
|
||||
}
|
||||
|
||||
func stop() {
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(SoftLogoutScreen(context: viewModel.context))
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
static let loadingIndicatorIdentifier = "SoftLogoutLoading"
|
||||
|
||||
/// Show an activity indicator whilst loading.
|
||||
@MainActor private func startLoading() {
|
||||
loadingIndicator = indicatorPresenter.present(.loading(label: ElementL10n.loading, isInteractionBlocking: true))
|
||||
ServiceLocator.shared.userNotificationController.submitNotification(UserNotification(id: Self.loadingIndicatorIdentifier,
|
||||
type: .modal,
|
||||
title: ElementL10n.loading,
|
||||
persistent: true))
|
||||
}
|
||||
|
||||
/// Hide the currently displayed activity indicator.
|
||||
@MainActor private func stopLoading() {
|
||||
loadingIndicator = nil
|
||||
ServiceLocator.shared.userNotificationController.retractNotificationWithId(Self.loadingIndicatorIdentifier)
|
||||
}
|
||||
|
||||
/// Shows the forgot password screen.
|
||||
@MainActor private func showForgotPasswordScreen() {
|
||||
MXLog.debug("[SoftLogoutCoordinator] showForgotPasswordScreen")
|
||||
|
||||
softLogoutViewModel.displayError(.alert("Not implemented."))
|
||||
viewModel.displayError(.alert("Not implemented."))
|
||||
}
|
||||
|
||||
/// Login with the supplied username and password.
|
||||
@@ -177,11 +162,11 @@ final class SoftLogoutCoordinator: Coordinator, Presentable {
|
||||
private func handleError(_ error: AuthenticationServiceError) {
|
||||
switch error {
|
||||
case .invalidCredentials:
|
||||
softLogoutViewModel.displayError(.alert(ElementL10n.authInvalidLoginParam))
|
||||
viewModel.displayError(.alert(ElementL10n.authInvalidLoginParam))
|
||||
case .accountDeactivated:
|
||||
softLogoutViewModel.displayError(.alert(ElementL10n.authInvalidLoginDeactivatedAccount))
|
||||
viewModel.displayError(.alert(ElementL10n.authInvalidLoginDeactivatedAccount))
|
||||
default:
|
||||
softLogoutViewModel.displayError(.alert(ElementL10n.unknownError))
|
||||
viewModel.displayError(.alert(ElementL10n.unknownError))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user