Fixes #3147 - Remove the sliding sync proxy wait list

This commit is contained in:
Stefan Ceriu
2024-08-12 12:05:43 +03:00
committed by Stefan Ceriu
parent 6759a66533
commit a0eafe26c3
21 changed files with 1 additions and 542 deletions
@@ -255,34 +255,13 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
case .configuredForOIDC:
// Pop back to the confirmation screen for OIDC login to continue.
navigationStackCoordinator.pop(animated: false)
case .isOnWaitlist(let credentials):
showWaitlistScreen(for: credentials)
}
}
.store(in: &cancellables)
navigationStackCoordinator.push(coordinator)
}
private func showWaitlistScreen(for credentials: WaitlistScreenCredentials) {
let parameters = WaitlistScreenCoordinatorParameters(credentials: credentials,
authenticationService: authenticationService)
let coordinator = WaitlistScreenCoordinator(parameters: parameters)
coordinator.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .signedIn(let userSession):
userHasSignedIn(userSession: userSession)
case .cancel:
navigationStackCoordinator.pop()
}
}
.store(in: &cancellables)
navigationStackCoordinator.push(coordinator)
}
private func userHasSignedIn(userSession: UserSessionProtocol) {
delegate?.authenticationFlowCoordinator(didLoginWithSession: userSession)
}
@@ -21,8 +21,6 @@ enum LoginScreenCoordinatorAction {
case configuredForOIDC
/// Login was successful.
case signedIn(UserSessionProtocol)
/// The user's request to login failed due to being on the proxy waitlist.
case isOnWaitlist(WaitlistScreenCredentials)
}
final class LoginScreenCoordinator: CoordinatorProtocol {
@@ -132,16 +130,7 @@ final class LoginScreenCoordinator: CoordinatorProtocol {
case .failure(let error):
stopLoading()
parameters.analytics.signpost.endLogin()
switch error {
case .isOnWaitlist:
actionsSubject.send(.isOnWaitlist(.init(username: username,
password: password,
initialDeviceName: UIDevice.current.initialDeviceName,
deviceID: nil,
homeserver: authenticationService.homeserver.value)))
default:
handleError(error)
}
handleError(error)
}
}
}
@@ -1,84 +0,0 @@
//
// Copyright 2023, 2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import SwiftUI
/// A standardised layout for waiting dialogs shown during the onboarding flow.
struct WaitingDialog<Content: View, BottomContent: View>: View {
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@Environment(\.verticalSizeClass) private var verticalSizeClass
/// Not using the default to allow the gradient to fill edge to edge.
private let horizontalPadding = 16.0
/// The top padding of the content, used to keep the text below the image.
private var contentTopPadding: CGFloat {
if verticalSizeClass == .compact {
// Reduced value for iPhones in landscape.
return UIConstants.startScreenBreakerScreenTopPadding
} else if horizontalSizeClass == .compact {
// The default value for portrait iPhones.
return 2 * UIConstants.startScreenBreakerScreenTopPadding
} else {
// Larger on iPad specifically for 11" in Landscape.
return 2.7 * UIConstants.startScreenBreakerScreenTopPadding
}
}
/// The main content shown at the top of the layout.
@ViewBuilder var content: () -> Content
/// The content shown at the bottom of the layout.
@ViewBuilder var bottomContent: () -> BottomContent
var body: some View {
FullscreenDialog(topPadding: UIConstants.startScreenBreakerScreenTopPadding, horizontalPadding: 0) {
content()
.padding(.top, contentTopPadding)
.padding(.horizontal, horizontalPadding)
} bottomContent: {
bottomContent()
.padding(.horizontal, horizontalPadding)
}
.background {
background
.ignoresSafeArea(edges: [.horizontal, .bottom])
}
.ignoresSafeArea(edges: [.horizontal, .bottom])
.environment(\.backgroundStyle, AnyShapeStyle(Color.compound.bgCanvasDefault))
.environment(\.colorScheme, .dark)
.toolbar(.visible, for: .navigationBar) // Layout consistency in all states.
.toolbarBackground(.hidden, for: .navigationBar)
.preferredColorScheme(.light) // Has no effect on Previews, works fine in-app.
}
var background: some View {
// Negative spacing to hide a rendering bug that shows the white background.
GeometryReader { geometry in
VStack(spacing: -5) {
Color.white
.frame(maxHeight: UIConstants.startScreenBreakerScreenTopPadding)
Image(asset: Asset.Images.waitingGradient)
.resizable()
.frame(width: horizontalSizeClass == .compact ? nil : geometry.size.width) // Forced for landscape iPhone and nicer iPad.
.scaledToFit()
.layoutPriority(1) // Force for all landscape layouts.
Color.compound.bgCanvasDefault
}
}
}
}
struct WaitingDialog_Previews: PreviewProvider, TestablePreview {
static let viewModel = WaitlistScreenViewModel(homeserver: .mockMatrixDotOrg)
static var previews: some View {
NavigationStack {
WaitlistScreen(context: viewModel.context)
}
}
}
@@ -1,83 +0,0 @@
//
// Copyright 2022-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import SwiftUI
struct WaitlistScreen: View {
@ObservedObject var context: WaitlistScreenViewModel.Context
var body: some View {
WaitingDialog {
content
} bottomContent: {
buttons
}
.navigationBarBackButtonHidden()
.toolbar { toolbar }
}
/// The main content of the view to be shown in a scroll view.
var content: some View {
VStack(spacing: 16) {
Text(context.viewState.title.tinting(".", color: Asset.Colors.brandColor.swiftUIColor))
.font(.compound.headingXLBold)
.multilineTextAlignment(.center)
.foregroundColor(.compound.textPrimary)
.fixedSize(horizontal: false, vertical: true)
Text(context.viewState.message)
.font(.compound.bodyLG)
.multilineTextAlignment(.center)
.foregroundColor(.compound.textPrimary)
}
}
/// The action buttons shown at the bottom of the view.
@ViewBuilder
var buttons: some View {
if let userSession = context.viewState.userSession {
Button { context.send(viewAction: .continue(userSession)) } label: {
Text(L10n.actionContinue)
}
.buttonStyle(.compound(.primary))
}
}
@ToolbarContentBuilder
var toolbar: some ToolbarContent {
if context.viewState.isWaiting {
ToolbarItem(placement: .cancellationAction) {
Button(L10n.actionCancel) {
context.send(viewAction: .cancel)
}
}
}
}
}
// MARK: - Previews
struct WaitlistScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = WaitlistScreenViewModel(homeserver: .mockMatrixDotOrg)
static let successViewModel = {
let viewModel = WaitlistScreenViewModel(homeserver: .mockMatrixDotOrg)
viewModel.update(userSession: UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: "@alice:matrix.org")))))
return viewModel
}()
static var previews: some View {
NavigationStack {
WaitlistScreen(context: viewModel.context)
}
.previewDisplayName("Waiting")
NavigationStack {
WaitlistScreen(context: successViewModel.context)
}
.previewDisplayName("Success")
}
}
@@ -1,119 +0,0 @@
//
// Copyright 2022-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import Combine
import SwiftUI
struct WaitlistScreenCoordinatorParameters {
/// The credentials for the login.
let credentials: WaitlistScreenCredentials
/// The service used to authenticate the user.
let authenticationService: AuthenticationServiceProtocol
/// The service locator for the screen.
var userIndicatorController: UserIndicatorControllerProtocol = ServiceLocator.shared.userIndicatorController
}
enum WaitlistScreenCoordinatorAction {
/// Login was successful after a retry attempt.
case signedIn(UserSessionProtocol)
/// The user would like to try sign in another way.
case cancel
}
final class WaitlistScreenCoordinator: CoordinatorProtocol {
private let parameters: WaitlistScreenCoordinatorParameters
private var viewModel: WaitlistScreenViewModelProtocol
private let actionsSubject: PassthroughSubject<WaitlistScreenCoordinatorAction, Never> = .init()
private var cancellables = Set<AnyCancellable>()
private var refreshCancellable: AnyCancellable?
var actions: AnyPublisher<WaitlistScreenCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(parameters: WaitlistScreenCoordinatorParameters) {
self.parameters = parameters
viewModel = WaitlistScreenViewModel(homeserver: parameters.credentials.homeserver)
refreshCancellable = NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)
.sink { [weak self] _ in
self?.refresh()
}
}
func start() {
viewModel.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .cancel:
actionsSubject.send(.cancel)
case .continue(let userSession):
actionsSubject.send(.signedIn(userSession))
}
}
.store(in: &cancellables)
}
func toPresentable() -> AnyView {
AnyView(WaitlistScreen(context: viewModel.context))
}
// MARK: - Private
/// Refresh the screen by retrying login to see if the waitlist has opened up.
private func refresh() {
guard parameters.credentials.homeserver == parameters.authenticationService.homeserver.value else {
MXLog.warning("Homeserver configuration changed.")
actionsSubject.send(.cancel)
return
}
showRefreshIndicator()
Task {
switch await parameters.authenticationService.login(username: parameters.credentials.username,
password: parameters.credentials.password,
initialDeviceName: parameters.credentials.initialDeviceName,
deviceID: parameters.credentials.deviceID) {
case .success(let userSession):
hideRefreshIndicator()
refreshCancellable = nil
viewModel.update(userSession: userSession)
case .failure(.isOnWaitlist):
hideRefreshIndicator() // Nothing to do, still waiting for availability.
case .failure(.invalidCredentials):
hideRefreshIndicator()
actionsSubject.send(.cancel)
case .failure:
hideRefreshIndicator()
showFailureIndicator()
}
}
}
private static let refreshIndicatorID = "WaitlistCoordinatorRefresh"
private static let failureIndicatorID = "WaitlistCoordinatorFailure"
private func showRefreshIndicator() {
parameters.userIndicatorController.submitIndicator(UserIndicator(id: Self.refreshIndicatorID,
type: .modal,
title: L10n.commonRefreshing,
persistent: true))
}
private func hideRefreshIndicator() {
parameters.userIndicatorController.retractIndicatorWithId(Self.refreshIndicatorID)
}
private func showFailureIndicator() {
parameters.userIndicatorController.submitIndicator(UserIndicator(id: Self.failureIndicatorID,
type: .toast,
title: L10n.errorUnknown,
iconName: "xmark"))
}
}
@@ -1,57 +0,0 @@
//
// Copyright 2022-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import Foundation
enum WaitlistScreenViewModelAction {
case cancel
case `continue`(UserSessionProtocol)
}
/// The user's credentials used to retry login and refresh the waiting list.
struct WaitlistScreenCredentials: CustomStringConvertible, CustomDebugStringConvertible {
let username: String
let password: String
let initialDeviceName: String?
let deviceID: String?
let homeserver: LoginHomeserver
var description: String { "Redacted" }
var debugDescription: String { "Redacted" }
}
struct WaitlistScreenViewState: BindableState {
/// The homeserver the user is waiting for.
let homeserver: LoginHomeserver
/// When refresh was successful, the user session that was returned by the login.
var userSession: UserSessionProtocol?
/// Whether or not the user is still waiting in the queue.
var isWaiting: Bool { userSession == nil }
var title: String {
if isWaiting {
return L10n.screenWaitlistTitle
} else {
return L10n.screenWaitlistTitleSuccess
}
}
var message: String {
if isWaiting {
return L10n.screenWaitlistMessage(InfoPlistReader.main.bundleDisplayName, homeserver.address)
} else {
return L10n.screenWaitlistMessageSuccess(InfoPlistReader.main.bundleDisplayName)
}
}
}
enum WaitlistScreenViewAction {
case cancel
case `continue`(UserSessionProtocol)
}
@@ -1,38 +0,0 @@
//
// Copyright 2022-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import Combine
import SwiftUI
typealias WaitlistScreenViewModelType = StateStoreViewModel<WaitlistScreenViewState, WaitlistScreenViewAction>
class WaitlistScreenViewModel: WaitlistScreenViewModelType, WaitlistScreenViewModelProtocol {
private var actionsSubject: PassthroughSubject<WaitlistScreenViewModelAction, Never> = .init()
var actions: AnyPublisher<WaitlistScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(homeserver: LoginHomeserver) {
super.init(initialViewState: WaitlistScreenViewState(homeserver: homeserver))
}
// MARK: - Public
override func process(viewAction: WaitlistScreenViewAction) {
switch viewAction {
case .cancel:
actionsSubject.send(.cancel)
case .continue(let userSession):
actionsSubject.send(.continue(userSession))
}
}
func update(userSession: UserSessionProtocol) {
state.userSession = userSession
}
}
@@ -1,17 +0,0 @@
//
// Copyright 2022-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import Combine
@MainActor
protocol WaitlistScreenViewModelProtocol {
var actions: AnyPublisher<WaitlistScreenViewModelAction, Never> { get }
var context: WaitlistScreenViewModelType.Context { get }
/// Set a user session on the screen to transition to the success state.
func update(userSession: UserSessionProtocol)
}
@@ -114,10 +114,6 @@ class AuthenticationService: AuthenticationServiceProtocol {
// FIXME: How about we make a proper type in the FFI? 😅
guard let error = error as? ClientError else { return .failure(.failedLoggingIn) }
if error.isElementWaitlist {
return .failure(.isOnWaitlist)
}
switch error.code {
case .forbidden:
return .failure(.invalidCredentials)
@@ -26,7 +26,6 @@ enum AuthenticationServiceError: Error {
case slidingSyncNotAvailable
case accountDeactivated
case failedLoggingIn
case isOnWaitlist
case sessionTokenRefreshNotSupported
}
@@ -25,12 +25,4 @@ extension ClientError {
return first
}
/// Whether or not the error is related to the sliding sync proxy being full.
///
/// This is a temporary error whilst we scale the backend infrastructure.
var isElementWaitlist: Bool {
guard case let .Generic(message) = self else { return false }
return message.contains("IO_ELEMENT_X_WAIT_LIST")
}
}