mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 16:24:11 +00:00
Show an alert when entering an account provider that requires Element Pro. (#4326)
This commit is contained in:
@@ -869,12 +869,14 @@
|
||||
A4C29D373986AFE4559696D5 /* SecureBackupKeyBackupScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4525E8C0FBDD27D1ACE90952 /* SecureBackupKeyBackupScreenViewModelProtocol.swift */; };
|
||||
A4E885358D7DD5A072A06824 /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = CCE5BF78B125320CBF3BB834 /* PostHog */; };
|
||||
A51C65E5A3C9F2464A91A380 /* AuthenticationClientBuilderFactoryMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0554FEA301486A8CFA475D5A /* AuthenticationClientBuilderFactoryMock.swift */; };
|
||||
A52090A4FE0DB826578DFC03 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0724EBDFE8BB4C9E5547C57D /* Client.swift */; };
|
||||
A583B70939707197B0B21DFC /* DeferredFulfillmentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEBB74427E24AF30CDB131B7 /* DeferredFulfillmentTests.swift */; };
|
||||
A588572ED0EB18D947B32A5E /* SendInviteConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F276F31C1AEC19E52B951B62 /* SendInviteConfirmationView.swift */; };
|
||||
A5B455D1A6DADF7476F7B417 /* EmojiProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BCCE3D12B0A9C6E559B5B5A /* EmojiProviderProtocol.swift */; };
|
||||
A5B9EF45C7B8ACEB4954AE36 /* LoginScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9780389F8A53E4D26E23DD03 /* LoginScreenViewModelProtocol.swift */; };
|
||||
A5D551E5691749066E0E0C44 /* RoomDetailsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 837B440C4705E4B899BCB899 /* RoomDetailsScreenViewModel.swift */; };
|
||||
A5FD8284744E2FECFC842FC1 /* TraceLogPack.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7149BDDE47F8AD104E644E2 /* TraceLogPack.swift */; };
|
||||
A63DA8A6D229889273A53D99 /* ElementWellKnownHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0142FAE169CC4619D8A30262 /* ElementWellKnownHook.swift */; };
|
||||
A64B52D9F73F9A6B95AF24FE /* UserDetailsEditScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CD503F5E0938FE53C7C6E7 /* UserDetailsEditScreenCoordinator.swift */; };
|
||||
A6B83EB78F025D21B6EBA90C /* CompoundIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 044E501B8331B339874D1B96 /* CompoundIcon.swift */; };
|
||||
A6D4C5EEA85A6A0ABA1559D6 /* RoomDetailsEditScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16D09C79746BDCD9173EB3A7 /* RoomDetailsEditScreenModels.swift */; };
|
||||
@@ -1399,6 +1401,7 @@
|
||||
00E5B2CBEF8F96424F095508 /* RoomDetailsEditScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenViewModelTests.swift; sourceTree = "<group>"; };
|
||||
011AFA4990C585D157829679 /* DeclineAndBlockScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeclineAndBlockScreenViewModel.swift; sourceTree = "<group>"; };
|
||||
012A284622B32052015F1F89 /* ReadMarkerRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadMarkerRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||
0142FAE169CC4619D8A30262 /* ElementWellKnownHook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementWellKnownHook.swift; sourceTree = "<group>"; };
|
||||
018194CAFBE80720FECCEDEE /* ZoomTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomTransition.swift; sourceTree = "<group>"; };
|
||||
01B795AAAB7B8747FE2FF311 /* LogViewerScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewerScreenModels.swift; sourceTree = "<group>"; };
|
||||
01C4C7DB37597D7D8379511A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
@@ -1435,6 +1438,7 @@
|
||||
06B098A612DCB5A7358EECD5 /* DeveloperOptionsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreenModels.swift; sourceTree = "<group>"; };
|
||||
06F27F588F9059128E17C669 /* WindowManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowManagerProtocol.swift; sourceTree = "<group>"; };
|
||||
06FAE373A7F20780BA84B59C /* MessageForwardingScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||
0724EBDFE8BB4C9E5547C57D /* Client.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Client.swift; sourceTree = "<group>"; };
|
||||
07579F9C29001E40715F3014 /* NotificationSettingsChatType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsChatType.swift; sourceTree = "<group>"; };
|
||||
077B01C13BBA2996272C5FB5 /* ProcessInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessInfo.swift; sourceTree = "<group>"; };
|
||||
077D7C3BE199B6E5DDEC07EC /* AppCoordinatorStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinatorStateMachine.swift; sourceTree = "<group>"; };
|
||||
@@ -3271,6 +3275,7 @@
|
||||
3865AD7B7249C939D7C69C33 /* CertificateValidatorHook.swift */,
|
||||
7AC0CD1CAFD3F8B057F9AEA5 /* ClientBuilderHook.swift */,
|
||||
8B89D6C760E8CAE29CA28FB1 /* CompoundHook.swift */,
|
||||
0142FAE169CC4619D8A30262 /* ElementWellKnownHook.swift */,
|
||||
B343C5255FB408DDE853CFDF /* RoomScreenHook.swift */,
|
||||
);
|
||||
path = Hooks;
|
||||
@@ -4674,6 +4679,7 @@
|
||||
8039515BAA53B7C3275AC64A /* Client */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0724EBDFE8BB4C9E5547C57D /* Client.swift */,
|
||||
18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */,
|
||||
6033779EB37259F27F938937 /* ClientProxyProtocol.swift */,
|
||||
);
|
||||
@@ -7261,6 +7267,7 @@
|
||||
E14E469CD97550D0FC58F3CA /* CancellableTask.swift in Sources */,
|
||||
DF8F1211F2B0B56F0FCCA5C2 /* CertificateValidatorHook.swift in Sources */,
|
||||
D885B783B95AD7832D4EF5DD /* CharacterSet.swift in Sources */,
|
||||
A52090A4FE0DB826578DFC03 /* Client.swift in Sources */,
|
||||
C80E06ED97CE52704A46C148 /* ClientBuilder.swift in Sources */,
|
||||
87CEA3E07B602705BC2D2A20 /* ClientBuilderHook.swift in Sources */,
|
||||
1950A80CD198BED283DFC2CE /* ClientProxy.swift in Sources */,
|
||||
@@ -7336,6 +7343,7 @@
|
||||
07CC13C5729C24255348CBBD /* ElementCallWidgetDriver.swift in Sources */,
|
||||
370AF5BFCD4384DD455479B6 /* ElementCallWidgetDriverProtocol.swift in Sources */,
|
||||
A87DC550659C5176AC1829DE /* ElementTextFieldStyle.swift in Sources */,
|
||||
A63DA8A6D229889273A53D99 /* ElementWellKnownHook.swift in Sources */,
|
||||
7C1A7B594B2F8143F0DD0005 /* ElementXAttributeScope.swift in Sources */,
|
||||
3A08584ECDD4A4541DBF21F8 /* EmojiLoaderProtocol.swift in Sources */,
|
||||
340D39DB87F3800D53A6A621 /* EmojiPickerScreen.swift in Sources */,
|
||||
|
||||
@@ -659,6 +659,9 @@
|
||||
"screen_change_account_provider_other" = "Other";
|
||||
"screen_change_account_provider_subtitle" = "Use a different account provider, such as your own private server or a work account.";
|
||||
"screen_change_account_provider_title" = "Change account provider";
|
||||
"screen_change_server_error_element_pro_required_action_ios" = "App Store";
|
||||
"screen_change_server_error_element_pro_required_message" = "The Element Pro app is required on %1$@. Please download it from the store.";
|
||||
"screen_change_server_error_element_pro_required_title" = "Element Pro required";
|
||||
"screen_change_server_error_invalid_homeserver" = "We couldn't reach this homeserver. Please check that you have entered the homeserver URL correctly. If the URL is correct, contact your homeserver administrator for further help.";
|
||||
"screen_change_server_error_invalid_well_known" = "Server isn't available due to an issue in the .well-known file:\n%1$@";
|
||||
"screen_change_server_error_no_sliding_sync_message" = "The selected account provider does not support sliding sync. An upgrade to the server is needed to use %1$@.";
|
||||
|
||||
@@ -33,6 +33,11 @@ class AppHooks: AppHooksProtocol {
|
||||
certificateValidatorHook = hook
|
||||
}
|
||||
|
||||
private(set) var elementWellKnownHook: ElementWellKnownHookProtocol = DefaultElementWellKnownHook()
|
||||
func registerElementWellKnownHook(_ hook: ElementWellKnownHookProtocol) {
|
||||
elementWellKnownHook = hook
|
||||
}
|
||||
|
||||
private(set) var roomScreenHook: RoomScreenHookProtocol = DefaultRoomScreenHook()
|
||||
func registerRoomScreenHook(_ hook: RoomScreenHookProtocol) {
|
||||
roomScreenHook = hook
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// Copyright 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
protocol ElementWellKnownHookProtocol {
|
||||
func validate(using client: ClientProtocol) async -> Result<Void, ElementWellKnownError>
|
||||
}
|
||||
|
||||
struct DefaultElementWellKnownHook: ElementWellKnownHookProtocol {
|
||||
/// A best effort implementation to let Element X advertise to users when they should be using
|
||||
/// Element Pro. In an ideal world the backend would be able to validate the client's requests
|
||||
/// instead of relying on it to check a well-known file for this.
|
||||
func validate(using client: ClientProtocol) async -> Result<Void, ElementWellKnownError> {
|
||||
guard case let .success(wellKnownData) = await client.elementWellKnown() else {
|
||||
// Nothing to check, carry on as normal.
|
||||
return .success(())
|
||||
}
|
||||
|
||||
do {
|
||||
let wellKnown = try JSONDecoder().decode(ElementWellKnown.self, from: wellKnownData)
|
||||
if wellKnown.enforceElementPro == true {
|
||||
let serverName = client.server() ?? client.homeserver()
|
||||
let displayableServerName = LoginHomeserver(address: serverName, loginMode: .unknown).address
|
||||
return .failure(.elementProRequired(serverName: displayableServerName))
|
||||
} else {
|
||||
return .success(())
|
||||
}
|
||||
} catch {
|
||||
// If it doesn't decode we have to assume it's a 404 page or similar.
|
||||
return .success(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct ElementWellKnown: Decodable {
|
||||
var version: Int
|
||||
var enforceElementPro: Bool?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case version
|
||||
case enforceElementPro = "enforce_element_pro"
|
||||
}
|
||||
}
|
||||
|
||||
enum ElementWellKnownError: Error {
|
||||
case elementProRequired(serverName: String)
|
||||
}
|
||||
@@ -189,6 +189,9 @@ final class AppSettings {
|
||||
private(set) var elementWebHosts = ["app.element.io", "staging.element.io", "develop.element.io"]
|
||||
/// The domain that account provisioning links will be hosted on - used for handling the links.
|
||||
private(set) var accountProvisioningHost = "mobile.element.io"
|
||||
/// The App Store URL for Element Pro, shown to the user when a homeserver requires that app.
|
||||
/// **Note:** This property isn't overridable as it in unexpected for forks to come across the error (or to even have a "Pro" app).
|
||||
let elementProAppStoreURL: URL = "https://apps.apple.com/app/element-pro-for-work/id6502951615"
|
||||
|
||||
@UserPreference(key: UserDefaultsKeys.appAppearance, defaultValue: .system, storageType: .userDefaults(store))
|
||||
var appAppearance: AppAppearance
|
||||
|
||||
@@ -363,6 +363,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
let parameters = ServerSelectionScreenCoordinatorParameters(authenticationService: authenticationService,
|
||||
authenticationFlow: authenticationFlow,
|
||||
appSettings: appSettings,
|
||||
userIndicatorController: userIndicatorController)
|
||||
let coordinator = ServerSelectionScreenCoordinator(parameters: parameters)
|
||||
|
||||
@@ -408,6 +409,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
|
||||
let parameters = LoginScreenCoordinatorParameters(authenticationService: authenticationService,
|
||||
loginHint: loginHint,
|
||||
userIndicatorController: userIndicatorController,
|
||||
appSettings: appSettings,
|
||||
analytics: analytics)
|
||||
let coordinator = LoginScreenCoordinator(parameters: parameters)
|
||||
|
||||
|
||||
@@ -1258,6 +1258,14 @@ internal enum L10n {
|
||||
internal static var screenChangeAccountProviderSubtitle: String { return L10n.tr("Localizable", "screen_change_account_provider_subtitle") }
|
||||
/// Change account provider
|
||||
internal static var screenChangeAccountProviderTitle: String { return L10n.tr("Localizable", "screen_change_account_provider_title") }
|
||||
/// App Store
|
||||
internal static var screenChangeServerErrorElementProRequiredActionIos: String { return L10n.tr("Localizable", "screen_change_server_error_element_pro_required_action_ios") }
|
||||
/// The Element Pro app is required on %1$@. Please download it from the store.
|
||||
internal static func screenChangeServerErrorElementProRequiredMessage(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "screen_change_server_error_element_pro_required_message", String(describing: p1))
|
||||
}
|
||||
/// Element Pro required
|
||||
internal static var screenChangeServerErrorElementProRequiredTitle: String { return L10n.tr("Localizable", "screen_change_server_error_element_pro_required_title") }
|
||||
/// We couldn't reach this homeserver. Please check that you have entered the homeserver URL correctly. If the URL is correct, contact your homeserver administrator for further help.
|
||||
internal static var screenChangeServerErrorInvalidHomeserver: String { return L10n.tr("Localizable", "screen_change_server_error_invalid_homeserver") }
|
||||
/// Server isn't available due to an issue in the .well-known file:
|
||||
|
||||
@@ -25,11 +25,18 @@ extension AuthenticationClientFactoryMock {
|
||||
supportsOIDCCreatePrompt: false,
|
||||
supportsPasswordLogin: false)),
|
||||
"server.net": ClientSDKMock(configuration: .init(serverAddress: "server.net",
|
||||
homeserverURL: "https://matrix.example.com",
|
||||
homeserverURL: "https://matrix.server.net",
|
||||
slidingSyncVersion: .native,
|
||||
oidcLoginURL: nil,
|
||||
supportsOIDCCreatePrompt: false,
|
||||
supportsPasswordLogin: false))
|
||||
supportsPasswordLogin: false)),
|
||||
"secure.gov": ClientSDKMock(configuration: .init(serverAddress: "secure.gov",
|
||||
homeserverURL: "https://ess.secure.gov",
|
||||
slidingSyncVersion: .native,
|
||||
oidcLoginURL: "https://auth.secure.gov/oidc",
|
||||
supportsOIDCCreatePrompt: false,
|
||||
supportsPasswordLogin: false,
|
||||
elementWellKnown: "{\"version\":1,\"enforce_element_pro\":true}"))
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ extension ClientSDKMock {
|
||||
var oidcLoginURL: String? = "https://account.matrix.org/authorize"
|
||||
var supportsOIDCCreatePrompt = true
|
||||
var supportsPasswordLogin = true
|
||||
var elementWellKnown: String?
|
||||
var validCredentials = (username: "alice", password: "12345678")
|
||||
|
||||
// MARK: Session
|
||||
@@ -51,6 +52,14 @@ extension ClientSDKMock {
|
||||
|
||||
userIdReturnValue = configuration.userID
|
||||
sessionReturnValue = configuration.session
|
||||
getUrlUrlClosure = { url in
|
||||
guard url.contains(".well-known/element/element.json") else { throw MockError.generic }
|
||||
if let elementWellKnown = configuration.elementWellKnown {
|
||||
return elementWellKnown
|
||||
} else {
|
||||
throw MockError.generic
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ struct LoginScreenCoordinatorParameters {
|
||||
/// An optional hint that can be used to pre-fill the form.
|
||||
let loginHint: String?
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
let appSettings: AppSettings
|
||||
let analytics: AnalyticsService
|
||||
}
|
||||
|
||||
@@ -46,6 +47,7 @@ final class LoginScreenCoordinator: CoordinatorProtocol {
|
||||
viewModel = LoginScreenViewModel(authenticationService: parameters.authenticationService,
|
||||
loginHint: parameters.loginHint,
|
||||
userIndicatorController: parameters.userIndicatorController,
|
||||
appSettings: parameters.appSettings,
|
||||
analytics: parameters.analytics)
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,8 @@ enum LoginScreenErrorType: Hashable {
|
||||
case invalidWellKnownAlert(String)
|
||||
/// An alert that allows the user to learn about sliding sync.
|
||||
case slidingSyncAlert
|
||||
/// An alert that informs the user that Element Pro should be used for a particular server.
|
||||
case elementProAlert
|
||||
/// An alert that informs the user that login failed due to a refresh token being returned.
|
||||
case refreshTokenAlert
|
||||
/// The response from the homeserver was unexpected.
|
||||
|
||||
@@ -13,6 +13,7 @@ typealias LoginScreenViewModelType = StateStoreViewModelV2<LoginScreenViewState,
|
||||
class LoginScreenViewModel: LoginScreenViewModelType, LoginScreenViewModelProtocol {
|
||||
private let authenticationService: AuthenticationServiceProtocol
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
private let appSettings: AppSettings
|
||||
private let analytics: AnalyticsService
|
||||
|
||||
private var actionsSubject: PassthroughSubject<LoginScreenViewModelAction, Never> = .init()
|
||||
@@ -23,9 +24,11 @@ class LoginScreenViewModel: LoginScreenViewModelType, LoginScreenViewModelProtoc
|
||||
init(authenticationService: AuthenticationServiceProtocol,
|
||||
loginHint: String?,
|
||||
userIndicatorController: UserIndicatorControllerProtocol,
|
||||
appSettings: AppSettings,
|
||||
analytics: AnalyticsService) {
|
||||
self.authenticationService = authenticationService
|
||||
self.userIndicatorController = userIndicatorController
|
||||
self.appSettings = appSettings
|
||||
self.analytics = analytics
|
||||
|
||||
let username = switch loginHint {
|
||||
@@ -146,6 +149,16 @@ class LoginScreenViewModel: LoginScreenViewModelType, LoginScreenViewModelProtoc
|
||||
|
||||
// Clear out the invalid username to avoid an attempted login to matrix.org
|
||||
state.bindings.username = ""
|
||||
case .elementProRequired(let serverName):
|
||||
state.bindings.alertInfo = AlertInfo(id: .elementProAlert,
|
||||
title: L10n.screenChangeServerErrorElementProRequiredTitle,
|
||||
message: L10n.screenChangeServerErrorElementProRequiredMessage(serverName),
|
||||
primaryButton: .init(title: L10n.screenChangeServerErrorElementProRequiredActionIos) {
|
||||
UIApplication.shared.open(self.appSettings.elementProAppStoreURL)
|
||||
},
|
||||
secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil))
|
||||
// Clear out the invalid username to avoid an attempted login to matrix.org
|
||||
state.bindings.username = ""
|
||||
case .sessionTokenRefreshNotSupported:
|
||||
state.bindings.alertInfo = AlertInfo(id: .refreshTokenAlert,
|
||||
title: L10n.commonServerNotSupported,
|
||||
|
||||
@@ -159,6 +159,7 @@ struct LoginScreen_Previews: PreviewProvider, TestablePreview {
|
||||
let viewModel = LoginScreenViewModel(authenticationService: authenticationService,
|
||||
loginHint: nil,
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analytics: ServiceLocator.shared.analytics)
|
||||
|
||||
if withCredentials {
|
||||
|
||||
+1
@@ -40,6 +40,7 @@ final class ServerConfirmationScreenCoordinator: CoordinatorProtocol {
|
||||
viewModel = ServerConfirmationScreenViewModel(authenticationService: parameters.authenticationService,
|
||||
mode: mode,
|
||||
authenticationFlow: parameters.authenticationFlow,
|
||||
appSettings: parameters.appSettings,
|
||||
userIndicatorController: parameters.userIndicatorController)
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -94,6 +94,8 @@ enum ServerConfirmationScreenAlert: Hashable {
|
||||
case login
|
||||
/// An alert that informs the user that registration isn't supported.
|
||||
case registration
|
||||
/// An alert that informs the user that Element Pro should be used for a particular server.
|
||||
case elementProRequired(serverName: String)
|
||||
/// An unknown error has occurred.
|
||||
case unknownError
|
||||
}
|
||||
|
||||
+13
@@ -13,6 +13,7 @@ typealias ServerConfirmationScreenViewModelType = StateStoreViewModelV2<ServerCo
|
||||
class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType, ServerConfirmationScreenViewModelProtocol {
|
||||
let authenticationService: AuthenticationServiceProtocol
|
||||
let authenticationFlow: AuthenticationFlow
|
||||
let appSettings: AppSettings
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
|
||||
private var actionsSubject: PassthroughSubject<ServerConfirmationScreenViewModelAction, Never> = .init()
|
||||
@@ -24,9 +25,11 @@ class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType,
|
||||
init(authenticationService: AuthenticationServiceProtocol,
|
||||
mode: ServerConfirmationScreenMode,
|
||||
authenticationFlow: AuthenticationFlow,
|
||||
appSettings: AppSettings,
|
||||
userIndicatorController: UserIndicatorControllerProtocol) {
|
||||
self.authenticationService = authenticationService
|
||||
self.authenticationFlow = authenticationFlow
|
||||
self.appSettings = appSettings
|
||||
self.userIndicatorController = userIndicatorController
|
||||
|
||||
let pickerSelection: String? = switch mode {
|
||||
@@ -94,6 +97,8 @@ class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType,
|
||||
displayError(.login)
|
||||
case .registrationNotSupported:
|
||||
displayError(.registration)
|
||||
case .elementProRequired(let serverName):
|
||||
displayError(.elementProRequired(serverName: serverName))
|
||||
default:
|
||||
displayError(.unknownError)
|
||||
}
|
||||
@@ -184,6 +189,14 @@ class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType,
|
||||
state.bindings.alertInfo = AlertInfo(id: .registration,
|
||||
title: L10n.commonServerNotSupported,
|
||||
message: L10n.errorAccountCreationNotPossible)
|
||||
case .elementProRequired(let serverName):
|
||||
state.bindings.alertInfo = AlertInfo(id: .elementProRequired(serverName: serverName),
|
||||
title: L10n.screenChangeServerErrorElementProRequiredTitle,
|
||||
message: L10n.screenChangeServerErrorElementProRequiredMessage(serverName),
|
||||
primaryButton: .init(title: L10n.screenChangeServerErrorElementProRequiredActionIos) {
|
||||
UIApplication.shared.open(self.appSettings.elementProAppStoreURL)
|
||||
},
|
||||
secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil))
|
||||
case .unknownError:
|
||||
state.bindings.alertInfo = AlertInfo(id: .unknownError)
|
||||
}
|
||||
|
||||
+1
@@ -162,6 +162,7 @@ struct ServerConfirmationScreen_Previews: PreviewProvider, TestablePreview {
|
||||
ServerConfirmationScreenViewModel(authenticationService: AuthenticationService.mock,
|
||||
mode: mode,
|
||||
authenticationFlow: flow,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -12,6 +12,7 @@ struct ServerSelectionScreenCoordinatorParameters {
|
||||
/// The service used to authenticate the user.
|
||||
let authenticationService: AuthenticationServiceProtocol
|
||||
let authenticationFlow: AuthenticationFlow
|
||||
let appSettings: AppSettings
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
}
|
||||
|
||||
@@ -38,6 +39,7 @@ final class ServerSelectionScreenCoordinator: CoordinatorProtocol {
|
||||
self.parameters = parameters
|
||||
viewModel = ServerSelectionScreenViewModel(authenticationService: parameters.authenticationService,
|
||||
authenticationFlow: parameters.authenticationFlow,
|
||||
appSettings: parameters.appSettings,
|
||||
userIndicatorController: parameters.userIndicatorController)
|
||||
userIndicatorController = parameters.userIndicatorController
|
||||
}
|
||||
|
||||
+2
@@ -66,4 +66,6 @@ enum ServerSelectionScreenErrorType: Hashable {
|
||||
case loginAlert
|
||||
/// An alert that informs the user that registration isn't supported.
|
||||
case registrationAlert
|
||||
/// An alert that informs the user that Element Pro should be used for a particular server.
|
||||
case elementProAlert
|
||||
}
|
||||
|
||||
+11
@@ -13,6 +13,7 @@ typealias ServerSelectionScreenViewModelType = StateStoreViewModelV2<ServerSelec
|
||||
class ServerSelectionScreenViewModel: ServerSelectionScreenViewModelType, ServerSelectionScreenViewModelProtocol {
|
||||
private let authenticationService: AuthenticationServiceProtocol
|
||||
private let authenticationFlow: AuthenticationFlow
|
||||
private let appSettings: AppSettings
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
|
||||
private var actionsSubject: PassthroughSubject<ServerSelectionScreenViewModelAction, Never> = .init()
|
||||
@@ -23,9 +24,11 @@ class ServerSelectionScreenViewModel: ServerSelectionScreenViewModelType, Server
|
||||
|
||||
init(authenticationService: AuthenticationServiceProtocol,
|
||||
authenticationFlow: AuthenticationFlow,
|
||||
appSettings: AppSettings,
|
||||
userIndicatorController: UserIndicatorControllerProtocol) {
|
||||
self.authenticationService = authenticationService
|
||||
self.authenticationFlow = authenticationFlow
|
||||
self.appSettings = appSettings
|
||||
self.userIndicatorController = userIndicatorController
|
||||
|
||||
let bindings = ServerSelectionScreenBindings(homeserverAddress: authenticationService.homeserver.value.address)
|
||||
@@ -96,6 +99,14 @@ class ServerSelectionScreenViewModel: ServerSelectionScreenViewModelType, Server
|
||||
state.bindings.alertInfo = AlertInfo(id: .registrationAlert,
|
||||
title: L10n.commonServerNotSupported,
|
||||
message: L10n.errorAccountCreationNotPossible)
|
||||
case .elementProRequired(let serverName):
|
||||
state.bindings.alertInfo = AlertInfo(id: .elementProAlert,
|
||||
title: L10n.screenChangeServerErrorElementProRequiredTitle,
|
||||
message: L10n.screenChangeServerErrorElementProRequiredMessage(serverName),
|
||||
primaryButton: .init(title: L10n.screenChangeServerErrorElementProRequiredActionIos) {
|
||||
UIApplication.shared.open(self.appSettings.elementProAppStoreURL)
|
||||
},
|
||||
secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil))
|
||||
default:
|
||||
showFooterMessage(L10n.errorUnknown)
|
||||
}
|
||||
|
||||
+1
@@ -115,6 +115,7 @@ struct ServerSelection_Previews: PreviewProvider, TestablePreview {
|
||||
|
||||
let viewModel = ServerSelectionScreenViewModel(authenticationService: authenticationService,
|
||||
authenticationFlow: .login,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
viewModel.context.homeserverAddress = homeserverAddress
|
||||
if homeserverAddress == "thisisbad" {
|
||||
|
||||
@@ -52,6 +52,7 @@ class AuthenticationService: AuthenticationServiceProtocol {
|
||||
var homeserver = LoginHomeserver(address: homeserverAddress, loginMode: .unknown)
|
||||
|
||||
let client = try await makeClient(homeserverAddress: homeserverAddress)
|
||||
try await appHooks.elementWellKnownHook.validate(using: client).get()
|
||||
let loginDetails = await client.homeserverLoginDetails()
|
||||
|
||||
MXLog.info("Sliding sync: \(client.slidingSyncVersion())")
|
||||
@@ -81,6 +82,8 @@ class AuthenticationService: AuthenticationServiceProtocol {
|
||||
} catch ClientBuildError.SlidingSyncVersion(let error) {
|
||||
MXLog.info("User entered a homeserver that isn't configured for sliding sync: \(error)")
|
||||
return .failure(.slidingSyncNotAvailable)
|
||||
} catch ElementWellKnownError.elementProRequired(let serverName) {
|
||||
return .failure(.elementProRequired(serverName: serverName))
|
||||
} catch {
|
||||
MXLog.error("Failed configuring a server: \(error)")
|
||||
return .failure(.invalidHomeserverAddress)
|
||||
@@ -176,6 +179,7 @@ class AuthenticationService: AuthenticationServiceProtocol {
|
||||
|
||||
do {
|
||||
let client = try await makeClient(homeserverAddress: scannedServerName)
|
||||
try await appHooks.elementWellKnownHook.validate(using: client).get()
|
||||
try await client.loginWithQrCode(qrCodeData: qrData,
|
||||
oidcConfiguration: appSettings.oidcConfiguration.rustValue,
|
||||
progressListener: listener)
|
||||
@@ -184,6 +188,8 @@ class AuthenticationService: AuthenticationServiceProtocol {
|
||||
} catch let error as HumanQrLoginError {
|
||||
MXLog.error("QRCode login error: \(error)")
|
||||
return .failure(error.serviceError)
|
||||
} catch ElementWellKnownError.elementProRequired(let serverName) {
|
||||
return .failure(.elementProRequired(serverName: serverName))
|
||||
} catch {
|
||||
MXLog.error("QRCode login unknown error: \(error)")
|
||||
return .failure(.qrCodeError(.unknown))
|
||||
|
||||
@@ -30,6 +30,7 @@ enum AuthenticationServiceError: Error, Equatable {
|
||||
case slidingSyncNotAvailable
|
||||
case loginNotSupported
|
||||
case registrationNotSupported
|
||||
case elementProRequired(serverName: String)
|
||||
case accountDeactivated
|
||||
case failedLoggingIn
|
||||
case sessionTokenRefreshNotSupported
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// Copyright 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
extension ClientProtocol {
|
||||
func elementWellKnown() async -> Result<Data, ClientProxyError> {
|
||||
let serverNameURLString = if let userIDServerName = try? userIdServerName() {
|
||||
"https://\(userIDServerName)"
|
||||
} else {
|
||||
server() ?? homeserver()
|
||||
}
|
||||
|
||||
do {
|
||||
guard let url = URL(string: serverNameURLString)?.appending(path: "/.well-known/element/element.json") else {
|
||||
return .failure(.invalidServerName)
|
||||
}
|
||||
|
||||
let response = try await getUrl(url: url.absoluteString)
|
||||
|
||||
guard let data = response.data(using: .utf8) else {
|
||||
return .failure(.invalidResponse)
|
||||
}
|
||||
|
||||
return .success(data)
|
||||
} catch {
|
||||
return .failure(.sdkError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ enum ClientProxyError: Error {
|
||||
|
||||
case invalidMedia
|
||||
case invalidServerName
|
||||
case invalidResponse
|
||||
case failedUploadingMedia(ErrorKind)
|
||||
case roomPreviewIsPrivate
|
||||
case failedRetrievingUserIdentity
|
||||
|
||||
@@ -117,6 +117,7 @@ class MockScreen: Identifiable {
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
let coordinator = ServerSelectionScreenCoordinator(parameters: .init(authenticationService: AuthenticationService.mock,
|
||||
authenticationFlow: .login,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController))
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
return navigationStackCoordinator
|
||||
|
||||
+1
-1
Submodule Enterprise updated: 233f49aa20...245f823e92
@@ -143,6 +143,21 @@ class LoginScreenViewModelTests: XCTestCase {
|
||||
XCTAssertEqual(context.alertInfo?.id, .unknown, "An alert should be shown to the user.")
|
||||
}
|
||||
|
||||
func testElementProRequired() async throws {
|
||||
// Given the screen configured for matrix.org
|
||||
await setupViewModel()
|
||||
XCTAssertNil(context.alertInfo, "There shouldn't be an alert when the screen loads.")
|
||||
|
||||
// When entering a username for an unsupported homeserver.
|
||||
let deferred = deferFulfillment(context.observe(\.viewState.bindings.alertInfo)) { $0 != nil }
|
||||
context.username = "@bob:secure.gov"
|
||||
context.send(viewAction: .parseUsername)
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the view state should be updated to show an alert.
|
||||
XCTAssertEqual(context.alertInfo?.id, .elementProAlert, "An alert should be shown to the user.")
|
||||
}
|
||||
|
||||
func testLoginHint() async throws {
|
||||
await setupViewModel(loginHint: "")
|
||||
XCTAssertEqual(context.username, "")
|
||||
@@ -172,6 +187,7 @@ class LoginScreenViewModelTests: XCTestCase {
|
||||
viewModel = LoginScreenViewModel(authenticationService: service,
|
||||
loginHint: loginHint,
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analytics: ServiceLocator.shared.analytics)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,6 +196,23 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
||||
XCTAssertEqual(context.alertInfo?.id, .login)
|
||||
}
|
||||
|
||||
func testElementProRequired() async throws {
|
||||
// Given a view model for login using a service that hasn't been configured and the default server requires Element Pro.
|
||||
setupViewModel(authenticationFlow: .login, supportsOIDC: false, supportsOIDCCreatePrompt: false, supportsPasswordLogin: false, requiresElementPro: true)
|
||||
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
|
||||
XCTAssertNil(context.alertInfo)
|
||||
|
||||
// When continuing from the confirmation screen.
|
||||
let deferred = deferFulfillment(context.observe(\.alertInfo)) { $0 != nil }
|
||||
context.send(viewAction: .confirm)
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the configuration should fail with an alert telling the user to download Element Pro.
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(context.alertInfo?.id, .elementProRequired(serverName: "matrix.org"))
|
||||
}
|
||||
|
||||
// MARK: - Picker mode
|
||||
|
||||
func testPickerWithoutConfiguration() async throws {
|
||||
@@ -288,7 +305,8 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
||||
supportsOIDC: Bool = true,
|
||||
supportsOIDCCreatePrompt: Bool = true,
|
||||
supportsPasswordLogin: Bool = true,
|
||||
restrictedFlow: Bool = false) {
|
||||
restrictedFlow: Bool = false,
|
||||
requiresElementPro: Bool = false) {
|
||||
var mode = ServerConfirmationScreenMode.confirmation("matrix.org")
|
||||
if restrictedFlow {
|
||||
appSettings.override(accountProviders: ["matrix.org", "beta.matrix.org"],
|
||||
@@ -315,7 +333,8 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
||||
// Manually create a configuration as the default homeserver address setting is immutable.
|
||||
client = ClientSDKMock(configuration: .init(oidcLoginURL: supportsOIDC ? "https://account.matrix.org/authorize" : nil,
|
||||
supportsOIDCCreatePrompt: supportsOIDCCreatePrompt,
|
||||
supportsPasswordLogin: supportsPasswordLogin))
|
||||
supportsPasswordLogin: supportsPasswordLogin,
|
||||
elementWellKnown: requiresElementPro ? "{\"version\":1,\"enforce_element_pro\":true}" : nil))
|
||||
let configuration = AuthenticationClientFactoryMock.Configuration(homeserverClients: ["matrix.org": client])
|
||||
|
||||
clientFactory = AuthenticationClientFactoryMock(configuration: configuration)
|
||||
@@ -328,6 +347,7 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
||||
viewModel = ServerConfirmationScreenViewModel(authenticationService: service,
|
||||
mode: mode,
|
||||
authenticationFlow: authenticationFlow,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
|
||||
// Add a fake window in order for the OIDC flow to continue
|
||||
|
||||
@@ -87,6 +87,24 @@ class ServerSelectionScreenViewModelTests: XCTestCase {
|
||||
XCTAssertEqual(context.alertInfo?.id, .registrationAlert)
|
||||
}
|
||||
|
||||
func testElementProRequiredAlert() async throws {
|
||||
// Given a view model for login.
|
||||
setupViewModel(authenticationFlow: .login)
|
||||
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
|
||||
XCTAssertNil(context.alertInfo)
|
||||
|
||||
// When selecting a server that requires Element Pro
|
||||
context.homeserverAddress = "secure.gov"
|
||||
let deferred = deferFulfillment(context.observe(\.alertInfo)) { $0 != nil }
|
||||
context.send(viewAction: .confirm)
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then selection should fail with an alert telling the user to download Element Pro.
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(context.alertInfo?.id, .elementProAlert)
|
||||
}
|
||||
|
||||
func testInvalidServer() async throws {
|
||||
// Given a new instance of the view model.
|
||||
setupViewModel(authenticationFlow: .login)
|
||||
@@ -131,6 +149,7 @@ class ServerSelectionScreenViewModelTests: XCTestCase {
|
||||
|
||||
viewModel = ServerSelectionScreenViewModel(authenticationService: service,
|
||||
authenticationFlow: authenticationFlow,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user