mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 10:33:45 +00:00
Allow the app to be configured to bypass the server selection screen. (#4131)
* Make account provider configuration more flexible. - Change defaultHomeserverAddress to an array of providers (needs UI). - Add allowOtherAccountProviders to prevent the user from manually entering a provider. * Refactor QR code scan failures into a common type. * Validate scanned QR codes against the allowed account providers. * Hide the login flow on the QR code screen when restricted.
This commit is contained in:
+35
-13
@@ -32,14 +32,30 @@ class AuthenticationStartScreenViewModel: AuthenticationStartScreenViewModelType
|
||||
self.appSettings = appSettings
|
||||
self.userIndicatorController = userIndicatorController
|
||||
|
||||
// We only show the "Sign in to …" button when using a provisioning link.
|
||||
let showCreateAccountButton = appSettings.showCreateAccountButton && provisioningParameters == nil
|
||||
let showQRCodeLoginButton = !ProcessInfo.processInfo.isiOSAppOnMac && provisioningParameters == nil
|
||||
let isQRCodeScanningSupported = !ProcessInfo.processInfo.isiOSAppOnMac
|
||||
|
||||
super.init(initialViewState: AuthenticationStartScreenViewState(serverName: provisioningParameters?.accountProvider,
|
||||
showCreateAccountButton: showCreateAccountButton,
|
||||
showQRCodeLoginButton: showQRCodeLoginButton,
|
||||
showReportProblemButton: isBugReportServiceEnabled))
|
||||
let initialViewState = if !appSettings.allowOtherAccountProviders {
|
||||
// We don't show the create account button when custom providers are disallowed.
|
||||
// The assumption here being that if you're running a custom app, your users will already be created.
|
||||
AuthenticationStartScreenViewState(serverName: appSettings.accountProviders.count == 1 ? appSettings.accountProviders[0] : nil,
|
||||
showCreateAccountButton: false,
|
||||
showQRCodeLoginButton: isQRCodeScanningSupported,
|
||||
showReportProblemButton: isBugReportServiceEnabled)
|
||||
} else if let provisioningParameters {
|
||||
// We only show the "Sign in to …" button when using a provisioning link.
|
||||
AuthenticationStartScreenViewState(serverName: provisioningParameters.accountProvider,
|
||||
showCreateAccountButton: false,
|
||||
showQRCodeLoginButton: false,
|
||||
showReportProblemButton: isBugReportServiceEnabled)
|
||||
} else {
|
||||
// The default configuration.
|
||||
AuthenticationStartScreenViewState(serverName: nil,
|
||||
showCreateAccountButton: appSettings.showCreateAccountButton,
|
||||
showQRCodeLoginButton: isQRCodeScanningSupported,
|
||||
showReportProblemButton: isBugReportServiceEnabled)
|
||||
}
|
||||
|
||||
super.init(initialViewState: initialViewState)
|
||||
}
|
||||
|
||||
override func process(viewAction: AuthenticationStartScreenViewAction) {
|
||||
@@ -61,25 +77,31 @@ class AuthenticationStartScreenViewModel: AuthenticationStartScreenViewModelType
|
||||
// MARK: - Private
|
||||
|
||||
private func login() async {
|
||||
if let provisioningParameters {
|
||||
await configureProvisionedServer(with: provisioningParameters)
|
||||
if !appSettings.allowOtherAccountProviders {
|
||||
if appSettings.accountProviders.count == 1 {
|
||||
await configureAccountProvider(appSettings.accountProviders[0])
|
||||
} else {
|
||||
fatalError("WIP: Account provider picker not implemented.")
|
||||
}
|
||||
} else if let provisioningParameters {
|
||||
await configureAccountProvider(provisioningParameters.accountProvider, loginHint: provisioningParameters.loginHint)
|
||||
} else {
|
||||
actionsSubject.send(.login) // No need to configure anything here, continue the flow.
|
||||
}
|
||||
}
|
||||
|
||||
private func configureProvisionedServer(with provisioningParameters: AccountProvisioningParameters) async {
|
||||
private func configureAccountProvider(_ accountProvider: String, loginHint: String? = nil) async {
|
||||
startLoading()
|
||||
defer { stopLoading() }
|
||||
|
||||
guard case .success = await authenticationService.configure(for: provisioningParameters.accountProvider, flow: .login) else {
|
||||
guard case .success = await authenticationService.configure(for: accountProvider, flow: .login) else {
|
||||
// As the server was provisioned, we don't worry about the specifics and show a generic error to the user.
|
||||
displayError()
|
||||
return
|
||||
}
|
||||
|
||||
guard authenticationService.homeserver.value.loginMode.supportsOIDCFlow else {
|
||||
actionsSubject.send(.loginDirectlyWithPassword(loginHint: provisioningParameters.loginHint))
|
||||
actionsSubject.send(.loginDirectlyWithPassword(loginHint: loginHint))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -88,7 +110,7 @@ class AuthenticationStartScreenViewModel: AuthenticationStartScreenViewModelType
|
||||
return
|
||||
}
|
||||
|
||||
switch await authenticationService.urlForOIDCLogin(loginHint: provisioningParameters.loginHint) {
|
||||
switch await authenticationService.urlForOIDCLogin(loginHint: loginHint) {
|
||||
case .success(let oidcData):
|
||||
actionsSubject.send(.loginDirectlyWithOIDC(data: oidcData, window: window))
|
||||
case .failure:
|
||||
|
||||
@@ -10,6 +10,7 @@ import SwiftUI
|
||||
|
||||
struct QRCodeLoginScreenCoordinatorParameters {
|
||||
let qrCodeLoginService: QRCodeLoginServiceProtocol
|
||||
let canSignInManually: Bool
|
||||
let orientationManager: OrientationManagerProtocol
|
||||
let appMediator: AppMediatorProtocol
|
||||
}
|
||||
@@ -33,6 +34,7 @@ final class QRCodeLoginScreenCoordinator: CoordinatorProtocol {
|
||||
|
||||
init(parameters: QRCodeLoginScreenCoordinatorParameters) {
|
||||
viewModel = QRCodeLoginScreenViewModel(qrCodeLoginService: parameters.qrCodeLoginService,
|
||||
canSignInManually: parameters.canSignInManually,
|
||||
appMediator: parameters.appMediator)
|
||||
orientationManager = parameters.orientationManager
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ enum QRCodeLoginScreenViewModelAction {
|
||||
|
||||
struct QRCodeLoginScreenViewState: BindableState {
|
||||
var state: QRCodeLoginState = .initial
|
||||
/// Whether or not it is possible for the screen to start the manual sign in flow. This was added to avoid
|
||||
/// having to handle server configuration when ``AppSettings.allowOtherAccountProviders`` is false.
|
||||
let canSignInManually: Bool
|
||||
|
||||
private static let initialStateListItem3AttributedText = {
|
||||
let boldPlaceholder = "{bold}"
|
||||
@@ -74,14 +77,43 @@ enum QRCodeLoginState: Equatable {
|
||||
}
|
||||
|
||||
enum QRCodeLoginScanningState: Equatable {
|
||||
/// the qr code is scanning
|
||||
/// the QR code is scanning
|
||||
case scanning
|
||||
/// the qr code has been detected and is being processed
|
||||
/// the QR code has been detected and is being processed
|
||||
case connecting
|
||||
/// the qr code has been processed and is invalid
|
||||
case invalid
|
||||
/// the qr code has been processed but it belongs to a device not signed in,
|
||||
case deviceNotSignedIn
|
||||
/// the QR code was scanned, but an error occurred.
|
||||
case scanFailed(Error)
|
||||
|
||||
enum Error: Equatable {
|
||||
/// the QR code has been processed and is invalid
|
||||
case invalid
|
||||
/// the QR code has been processed but it is for an account provider that isn't allowed.
|
||||
case notAllowed(scannedProvider: String, allowedProviders: [String])
|
||||
/// the QR code has been processed but it belongs to a device not signed in
|
||||
case deviceNotSignedIn
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .invalid:
|
||||
L10n.screenQrCodeLoginInvalidScanStateSubtitle
|
||||
case .notAllowed(let scannedProvider, _):
|
||||
L10n.screenChangeServerErrorUnauthorizedHomeserverTitle(scannedProvider)
|
||||
case .deviceNotSignedIn:
|
||||
L10n.screenQrCodeLoginDeviceNotSignedInScanStateSubtitle
|
||||
}
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .invalid:
|
||||
L10n.screenQrCodeLoginInvalidScanStateDescription
|
||||
case .notAllowed(_, let allowedProviders):
|
||||
L10n.screenChangeServerErrorUnauthorizedHomeserverContent(allowedProviders.formatted(.list(type: .and)))
|
||||
case .deviceNotSignedIn:
|
||||
L10n.screenQrCodeLoginDeviceNotSignedInScanStateDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum QRCodeLoginDisplayCodeState: Equatable {
|
||||
@@ -100,39 +132,22 @@ enum QRCodeLoginState: Equatable {
|
||||
|
||||
var isScanning: Bool {
|
||||
switch self {
|
||||
case .scan(let state):
|
||||
return state == .scanning
|
||||
default:
|
||||
return false
|
||||
case .scan(.scanning): true
|
||||
default: false
|
||||
}
|
||||
}
|
||||
|
||||
var isError: Bool {
|
||||
switch self {
|
||||
case .error:
|
||||
return true
|
||||
case let .scan(state):
|
||||
switch state {
|
||||
case .invalid, .deviceNotSignedIn:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
default:
|
||||
return false
|
||||
case .error, .scan(.scanFailed): true
|
||||
default: false
|
||||
}
|
||||
}
|
||||
|
||||
var shouldDisplayCancelButton: Bool {
|
||||
switch self {
|
||||
case .initial:
|
||||
return true
|
||||
case .scan:
|
||||
return true
|
||||
case .error(let error):
|
||||
return error == .noCameraPermission
|
||||
default:
|
||||
return false
|
||||
case .initial, .scan, .error(.noCameraPermission): true
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,10 +22,11 @@ class QRCodeLoginScreenViewModel: QRCodeLoginScreenViewModelType, QRCodeLoginScr
|
||||
private var scanTask: Task<Void, Never>?
|
||||
|
||||
init(qrCodeLoginService: QRCodeLoginServiceProtocol,
|
||||
canSignInManually: Bool,
|
||||
appMediator: AppMediatorProtocol) {
|
||||
self.qrCodeLoginService = qrCodeLoginService
|
||||
self.appMediator = appMediator
|
||||
super.init(initialViewState: QRCodeLoginScreenViewState())
|
||||
super.init(initialViewState: QRCodeLoginScreenViewState(canSignInManually: canSignInManually))
|
||||
setupSubscriptions()
|
||||
}
|
||||
|
||||
@@ -119,9 +120,11 @@ class QRCodeLoginScreenViewModel: QRCodeLoginScreenViewModelType, QRCodeLoginScr
|
||||
MXLog.error("Failed to scan the QR code: \(error)")
|
||||
switch error {
|
||||
case .invalidQRCode:
|
||||
state.state = .scan(.invalid)
|
||||
state.state = .scan(.scanFailed(.invalid))
|
||||
case .providerNotAllowed(let scannedProvider, let allowedProviders):
|
||||
state.state = .scan(.scanFailed(.notAllowed(scannedProvider: scannedProvider, allowedProviders: allowedProviders)))
|
||||
case .deviceNotSignedIn:
|
||||
state.state = .scan(.deviceNotSignedIn)
|
||||
state.state = .scan(.scanFailed(.deviceNotSignedIn))
|
||||
case .cancelled:
|
||||
state.state = .error(.cancelled)
|
||||
case .connectionInsecure:
|
||||
@@ -140,15 +143,15 @@ class QRCodeLoginScreenViewModel: QRCodeLoginScreenViewModelType, QRCodeLoginScr
|
||||
}
|
||||
|
||||
/// Only for mocking initial states
|
||||
fileprivate init(state: QRCodeLoginState) {
|
||||
fileprivate init(state: QRCodeLoginState, canSignInManually: Bool) {
|
||||
qrCodeLoginService = QRCodeLoginServiceMock()
|
||||
appMediator = AppMediatorMock.default
|
||||
super.init(initialViewState: .init(state: state))
|
||||
super.init(initialViewState: .init(state: state, canSignInManually: canSignInManually))
|
||||
}
|
||||
}
|
||||
|
||||
extension QRCodeLoginScreenViewModel {
|
||||
static func mock(state: QRCodeLoginState) -> QRCodeLoginScreenViewModel {
|
||||
QRCodeLoginScreenViewModel(state: state)
|
||||
static func mock(state: QRCodeLoginState, canSignInManually: Bool = true) -> QRCodeLoginScreenViewModel {
|
||||
QRCodeLoginScreenViewModel(state: state, canSignInManually: canSignInManually)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ struct QRCodeLoginScreen: View {
|
||||
Button("") { }
|
||||
.buttonStyle(.compound(.primary))
|
||||
.hidden()
|
||||
case .invalid:
|
||||
case .scanFailed(let error):
|
||||
VStack(spacing: 16) {
|
||||
Button(L10n.screenQrCodeLoginInvalidScanStateRetryButton) {
|
||||
context.send(viewAction: .startScan)
|
||||
@@ -177,7 +177,7 @@ struct QRCodeLoginScreen: View {
|
||||
.buttonStyle(.compound(.primary))
|
||||
|
||||
VStack(spacing: 4) {
|
||||
Label(L10n.screenQrCodeLoginInvalidScanStateSubtitle,
|
||||
Label(error.title,
|
||||
icon: \.errorSolid,
|
||||
iconSize: .medium,
|
||||
relativeTo: .compound.bodyMDSemibold)
|
||||
@@ -185,33 +185,12 @@ struct QRCodeLoginScreen: View {
|
||||
.font(.compound.bodyMDSemibold)
|
||||
.foregroundColor(.compound.textCriticalPrimary)
|
||||
|
||||
Text(L10n.screenQrCodeLoginInvalidScanStateDescription)
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
.font(.compound.bodySM)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
}
|
||||
case .deviceNotSignedIn:
|
||||
VStack(spacing: 16) {
|
||||
Button(L10n.screenQrCodeLoginInvalidScanStateRetryButton) {
|
||||
context.send(viewAction: .startScan)
|
||||
}
|
||||
.buttonStyle(.compound(.primary))
|
||||
|
||||
VStack(spacing: 4) {
|
||||
Label(L10n.screenQrCodeLoginDeviceNotSignedInScanStateSubtitle,
|
||||
icon: \.errorSolid,
|
||||
iconSize: .medium,
|
||||
relativeTo: .compound.bodyMDSemibold)
|
||||
.labelStyle(.custom(spacing: 10))
|
||||
.font(.compound.bodyMDSemibold)
|
||||
.foregroundColor(.compound.textCriticalPrimary)
|
||||
|
||||
Text(L10n.screenQrCodeLoginDeviceNotSignedInScanStateDescription)
|
||||
Text(error.description)
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
.font(.compound.bodySM)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -375,10 +354,12 @@ struct QRCodeLoginScreen: View {
|
||||
.buttonStyle(.compound(.primary))
|
||||
case .linkingNotSupported:
|
||||
VStack(spacing: 16) {
|
||||
Button(L10n.screenOnboardingSignInManually) {
|
||||
context.send(viewAction: .signInManually)
|
||||
if context.viewState.canSignInManually {
|
||||
Button(L10n.screenOnboardingSignInManually) {
|
||||
context.send(viewAction: .signInManually)
|
||||
}
|
||||
.buttonStyle(.compound(.primary))
|
||||
}
|
||||
.buttonStyle(.compound(.primary))
|
||||
|
||||
Button(L10n.actionCancel) {
|
||||
context.send(viewAction: .cancel)
|
||||
@@ -425,9 +406,13 @@ struct QRCodeLoginScreen_Previews: PreviewProvider, TestablePreview {
|
||||
|
||||
static let connectingStateViewModel = QRCodeLoginScreenViewModel.mock(state: .scan(.connecting))
|
||||
|
||||
static let invalidStateViewModel = QRCodeLoginScreenViewModel.mock(state: .scan(.invalid))
|
||||
static let invalidStateViewModel = QRCodeLoginScreenViewModel.mock(state: .scan(.scanFailed(.invalid)))
|
||||
|
||||
static let deviceNotSignedInStateViewModel = QRCodeLoginScreenViewModel.mock(state: .scan(.deviceNotSignedIn))
|
||||
static let notAllowedStateViewModel = QRCodeLoginScreenViewModel.mock(state: .scan(.scanFailed(.notAllowed(scannedProvider: "evil.com",
|
||||
allowedProviders: ["example.com",
|
||||
"server.net"]))))
|
||||
|
||||
static let deviceNotSignedInStateViewModel = QRCodeLoginScreenViewModel.mock(state: .scan(.scanFailed(.deviceNotSignedIn)))
|
||||
|
||||
// Display Code
|
||||
static let deviceCodeStateViewModel = QRCodeLoginScreenViewModel.mock(state: .displayCode(.deviceCode("12")))
|
||||
@@ -440,6 +425,7 @@ struct QRCodeLoginScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static let connectionNotSecureStateViewModel = QRCodeLoginScreenViewModel.mock(state: .error(.connectionNotSecure))
|
||||
|
||||
static let linkingUnsupportedStateViewModel = QRCodeLoginScreenViewModel.mock(state: .error(.linkingNotSupported))
|
||||
static let linkingUnsupportedRestrictedFlowViewModel = QRCodeLoginScreenViewModel.mock(state: .error(.linkingNotSupported), canSignInManually: false)
|
||||
|
||||
static let cancelledStateViewModel = QRCodeLoginScreenViewModel.mock(state: .error(.cancelled))
|
||||
|
||||
@@ -464,6 +450,9 @@ struct QRCodeLoginScreen_Previews: PreviewProvider, TestablePreview {
|
||||
QRCodeLoginScreen(context: invalidStateViewModel.context)
|
||||
.previewDisplayName("Invalid")
|
||||
|
||||
QRCodeLoginScreen(context: notAllowedStateViewModel.context)
|
||||
.previewDisplayName("Not allowed")
|
||||
|
||||
QRCodeLoginScreen(context: deviceNotSignedInStateViewModel.context)
|
||||
.previewDisplayName("Device not signed in")
|
||||
|
||||
@@ -481,6 +470,8 @@ struct QRCodeLoginScreen_Previews: PreviewProvider, TestablePreview {
|
||||
|
||||
QRCodeLoginScreen(context: linkingUnsupportedStateViewModel.context)
|
||||
.previewDisplayName("Linking unsupported")
|
||||
QRCodeLoginScreen(context: linkingUnsupportedRestrictedFlowViewModel.context)
|
||||
.previewDisplayName("Linking unsupported restricted flow")
|
||||
|
||||
QRCodeLoginScreen(context: cancelledStateViewModel.context)
|
||||
.previewDisplayName("Cancelled")
|
||||
|
||||
Reference in New Issue
Block a user