mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +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 */; };
|
A4C29D373986AFE4559696D5 /* SecureBackupKeyBackupScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4525E8C0FBDD27D1ACE90952 /* SecureBackupKeyBackupScreenViewModelProtocol.swift */; };
|
||||||
A4E885358D7DD5A072A06824 /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = CCE5BF78B125320CBF3BB834 /* PostHog */; };
|
A4E885358D7DD5A072A06824 /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = CCE5BF78B125320CBF3BB834 /* PostHog */; };
|
||||||
A51C65E5A3C9F2464A91A380 /* AuthenticationClientBuilderFactoryMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0554FEA301486A8CFA475D5A /* AuthenticationClientBuilderFactoryMock.swift */; };
|
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 */; };
|
A583B70939707197B0B21DFC /* DeferredFulfillmentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEBB74427E24AF30CDB131B7 /* DeferredFulfillmentTests.swift */; };
|
||||||
A588572ED0EB18D947B32A5E /* SendInviteConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F276F31C1AEC19E52B951B62 /* SendInviteConfirmationView.swift */; };
|
A588572ED0EB18D947B32A5E /* SendInviteConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F276F31C1AEC19E52B951B62 /* SendInviteConfirmationView.swift */; };
|
||||||
A5B455D1A6DADF7476F7B417 /* EmojiProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BCCE3D12B0A9C6E559B5B5A /* EmojiProviderProtocol.swift */; };
|
A5B455D1A6DADF7476F7B417 /* EmojiProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BCCE3D12B0A9C6E559B5B5A /* EmojiProviderProtocol.swift */; };
|
||||||
A5B9EF45C7B8ACEB4954AE36 /* LoginScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9780389F8A53E4D26E23DD03 /* LoginScreenViewModelProtocol.swift */; };
|
A5B9EF45C7B8ACEB4954AE36 /* LoginScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9780389F8A53E4D26E23DD03 /* LoginScreenViewModelProtocol.swift */; };
|
||||||
A5D551E5691749066E0E0C44 /* RoomDetailsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 837B440C4705E4B899BCB899 /* RoomDetailsScreenViewModel.swift */; };
|
A5D551E5691749066E0E0C44 /* RoomDetailsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 837B440C4705E4B899BCB899 /* RoomDetailsScreenViewModel.swift */; };
|
||||||
A5FD8284744E2FECFC842FC1 /* TraceLogPack.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7149BDDE47F8AD104E644E2 /* TraceLogPack.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 */; };
|
A64B52D9F73F9A6B95AF24FE /* UserDetailsEditScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CD503F5E0938FE53C7C6E7 /* UserDetailsEditScreenCoordinator.swift */; };
|
||||||
A6B83EB78F025D21B6EBA90C /* CompoundIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 044E501B8331B339874D1B96 /* CompoundIcon.swift */; };
|
A6B83EB78F025D21B6EBA90C /* CompoundIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 044E501B8331B339874D1B96 /* CompoundIcon.swift */; };
|
||||||
A6D4C5EEA85A6A0ABA1559D6 /* RoomDetailsEditScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16D09C79746BDCD9173EB3A7 /* RoomDetailsEditScreenModels.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
077D7C3BE199B6E5DDEC07EC /* AppCoordinatorStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinatorStateMachine.swift; sourceTree = "<group>"; };
|
||||||
@@ -3271,6 +3275,7 @@
|
|||||||
3865AD7B7249C939D7C69C33 /* CertificateValidatorHook.swift */,
|
3865AD7B7249C939D7C69C33 /* CertificateValidatorHook.swift */,
|
||||||
7AC0CD1CAFD3F8B057F9AEA5 /* ClientBuilderHook.swift */,
|
7AC0CD1CAFD3F8B057F9AEA5 /* ClientBuilderHook.swift */,
|
||||||
8B89D6C760E8CAE29CA28FB1 /* CompoundHook.swift */,
|
8B89D6C760E8CAE29CA28FB1 /* CompoundHook.swift */,
|
||||||
|
0142FAE169CC4619D8A30262 /* ElementWellKnownHook.swift */,
|
||||||
B343C5255FB408DDE853CFDF /* RoomScreenHook.swift */,
|
B343C5255FB408DDE853CFDF /* RoomScreenHook.swift */,
|
||||||
);
|
);
|
||||||
path = Hooks;
|
path = Hooks;
|
||||||
@@ -4674,6 +4679,7 @@
|
|||||||
8039515BAA53B7C3275AC64A /* Client */ = {
|
8039515BAA53B7C3275AC64A /* Client */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
0724EBDFE8BB4C9E5547C57D /* Client.swift */,
|
||||||
18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */,
|
18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */,
|
||||||
6033779EB37259F27F938937 /* ClientProxyProtocol.swift */,
|
6033779EB37259F27F938937 /* ClientProxyProtocol.swift */,
|
||||||
);
|
);
|
||||||
@@ -7261,6 +7267,7 @@
|
|||||||
E14E469CD97550D0FC58F3CA /* CancellableTask.swift in Sources */,
|
E14E469CD97550D0FC58F3CA /* CancellableTask.swift in Sources */,
|
||||||
DF8F1211F2B0B56F0FCCA5C2 /* CertificateValidatorHook.swift in Sources */,
|
DF8F1211F2B0B56F0FCCA5C2 /* CertificateValidatorHook.swift in Sources */,
|
||||||
D885B783B95AD7832D4EF5DD /* CharacterSet.swift in Sources */,
|
D885B783B95AD7832D4EF5DD /* CharacterSet.swift in Sources */,
|
||||||
|
A52090A4FE0DB826578DFC03 /* Client.swift in Sources */,
|
||||||
C80E06ED97CE52704A46C148 /* ClientBuilder.swift in Sources */,
|
C80E06ED97CE52704A46C148 /* ClientBuilder.swift in Sources */,
|
||||||
87CEA3E07B602705BC2D2A20 /* ClientBuilderHook.swift in Sources */,
|
87CEA3E07B602705BC2D2A20 /* ClientBuilderHook.swift in Sources */,
|
||||||
1950A80CD198BED283DFC2CE /* ClientProxy.swift in Sources */,
|
1950A80CD198BED283DFC2CE /* ClientProxy.swift in Sources */,
|
||||||
@@ -7336,6 +7343,7 @@
|
|||||||
07CC13C5729C24255348CBBD /* ElementCallWidgetDriver.swift in Sources */,
|
07CC13C5729C24255348CBBD /* ElementCallWidgetDriver.swift in Sources */,
|
||||||
370AF5BFCD4384DD455479B6 /* ElementCallWidgetDriverProtocol.swift in Sources */,
|
370AF5BFCD4384DD455479B6 /* ElementCallWidgetDriverProtocol.swift in Sources */,
|
||||||
A87DC550659C5176AC1829DE /* ElementTextFieldStyle.swift in Sources */,
|
A87DC550659C5176AC1829DE /* ElementTextFieldStyle.swift in Sources */,
|
||||||
|
A63DA8A6D229889273A53D99 /* ElementWellKnownHook.swift in Sources */,
|
||||||
7C1A7B594B2F8143F0DD0005 /* ElementXAttributeScope.swift in Sources */,
|
7C1A7B594B2F8143F0DD0005 /* ElementXAttributeScope.swift in Sources */,
|
||||||
3A08584ECDD4A4541DBF21F8 /* EmojiLoaderProtocol.swift in Sources */,
|
3A08584ECDD4A4541DBF21F8 /* EmojiLoaderProtocol.swift in Sources */,
|
||||||
340D39DB87F3800D53A6A621 /* EmojiPickerScreen.swift in Sources */,
|
340D39DB87F3800D53A6A621 /* EmojiPickerScreen.swift in Sources */,
|
||||||
|
|||||||
@@ -659,6 +659,9 @@
|
|||||||
"screen_change_account_provider_other" = "Other";
|
"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_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_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_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_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$@.";
|
"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
|
certificateValidatorHook = hook
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private(set) var elementWellKnownHook: ElementWellKnownHookProtocol = DefaultElementWellKnownHook()
|
||||||
|
func registerElementWellKnownHook(_ hook: ElementWellKnownHookProtocol) {
|
||||||
|
elementWellKnownHook = hook
|
||||||
|
}
|
||||||
|
|
||||||
private(set) var roomScreenHook: RoomScreenHookProtocol = DefaultRoomScreenHook()
|
private(set) var roomScreenHook: RoomScreenHookProtocol = DefaultRoomScreenHook()
|
||||||
func registerRoomScreenHook(_ hook: RoomScreenHookProtocol) {
|
func registerRoomScreenHook(_ hook: RoomScreenHookProtocol) {
|
||||||
roomScreenHook = hook
|
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"]
|
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.
|
/// The domain that account provisioning links will be hosted on - used for handling the links.
|
||||||
private(set) var accountProvisioningHost = "mobile.element.io"
|
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))
|
@UserPreference(key: UserDefaultsKeys.appAppearance, defaultValue: .system, storageType: .userDefaults(store))
|
||||||
var appAppearance: AppAppearance
|
var appAppearance: AppAppearance
|
||||||
|
|||||||
@@ -363,6 +363,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
|
|
||||||
let parameters = ServerSelectionScreenCoordinatorParameters(authenticationService: authenticationService,
|
let parameters = ServerSelectionScreenCoordinatorParameters(authenticationService: authenticationService,
|
||||||
authenticationFlow: authenticationFlow,
|
authenticationFlow: authenticationFlow,
|
||||||
|
appSettings: appSettings,
|
||||||
userIndicatorController: userIndicatorController)
|
userIndicatorController: userIndicatorController)
|
||||||
let coordinator = ServerSelectionScreenCoordinator(parameters: parameters)
|
let coordinator = ServerSelectionScreenCoordinator(parameters: parameters)
|
||||||
|
|
||||||
@@ -408,6 +409,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
let parameters = LoginScreenCoordinatorParameters(authenticationService: authenticationService,
|
let parameters = LoginScreenCoordinatorParameters(authenticationService: authenticationService,
|
||||||
loginHint: loginHint,
|
loginHint: loginHint,
|
||||||
userIndicatorController: userIndicatorController,
|
userIndicatorController: userIndicatorController,
|
||||||
|
appSettings: appSettings,
|
||||||
analytics: analytics)
|
analytics: analytics)
|
||||||
let coordinator = LoginScreenCoordinator(parameters: parameters)
|
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") }
|
internal static var screenChangeAccountProviderSubtitle: String { return L10n.tr("Localizable", "screen_change_account_provider_subtitle") }
|
||||||
/// Change account provider
|
/// Change account provider
|
||||||
internal static var screenChangeAccountProviderTitle: String { return L10n.tr("Localizable", "screen_change_account_provider_title") }
|
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.
|
/// 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") }
|
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:
|
/// Server isn't available due to an issue in the .well-known file:
|
||||||
|
|||||||
@@ -25,11 +25,18 @@ extension AuthenticationClientFactoryMock {
|
|||||||
supportsOIDCCreatePrompt: false,
|
supportsOIDCCreatePrompt: false,
|
||||||
supportsPasswordLogin: false)),
|
supportsPasswordLogin: false)),
|
||||||
"server.net": ClientSDKMock(configuration: .init(serverAddress: "server.net",
|
"server.net": ClientSDKMock(configuration: .init(serverAddress: "server.net",
|
||||||
homeserverURL: "https://matrix.example.com",
|
homeserverURL: "https://matrix.server.net",
|
||||||
slidingSyncVersion: .native,
|
slidingSyncVersion: .native,
|
||||||
oidcLoginURL: nil,
|
oidcLoginURL: nil,
|
||||||
supportsOIDCCreatePrompt: false,
|
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 oidcLoginURL: String? = "https://account.matrix.org/authorize"
|
||||||
var supportsOIDCCreatePrompt = true
|
var supportsOIDCCreatePrompt = true
|
||||||
var supportsPasswordLogin = true
|
var supportsPasswordLogin = true
|
||||||
|
var elementWellKnown: String?
|
||||||
var validCredentials = (username: "alice", password: "12345678")
|
var validCredentials = (username: "alice", password: "12345678")
|
||||||
|
|
||||||
// MARK: Session
|
// MARK: Session
|
||||||
@@ -51,6 +52,14 @@ extension ClientSDKMock {
|
|||||||
|
|
||||||
userIdReturnValue = configuration.userID
|
userIdReturnValue = configuration.userID
|
||||||
sessionReturnValue = configuration.session
|
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.
|
/// An optional hint that can be used to pre-fill the form.
|
||||||
let loginHint: String?
|
let loginHint: String?
|
||||||
let userIndicatorController: UserIndicatorControllerProtocol
|
let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
|
let appSettings: AppSettings
|
||||||
let analytics: AnalyticsService
|
let analytics: AnalyticsService
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,6 +47,7 @@ final class LoginScreenCoordinator: CoordinatorProtocol {
|
|||||||
viewModel = LoginScreenViewModel(authenticationService: parameters.authenticationService,
|
viewModel = LoginScreenViewModel(authenticationService: parameters.authenticationService,
|
||||||
loginHint: parameters.loginHint,
|
loginHint: parameters.loginHint,
|
||||||
userIndicatorController: parameters.userIndicatorController,
|
userIndicatorController: parameters.userIndicatorController,
|
||||||
|
appSettings: parameters.appSettings,
|
||||||
analytics: parameters.analytics)
|
analytics: parameters.analytics)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ enum LoginScreenErrorType: Hashable {
|
|||||||
case invalidWellKnownAlert(String)
|
case invalidWellKnownAlert(String)
|
||||||
/// An alert that allows the user to learn about sliding sync.
|
/// An alert that allows the user to learn about sliding sync.
|
||||||
case slidingSyncAlert
|
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.
|
/// An alert that informs the user that login failed due to a refresh token being returned.
|
||||||
case refreshTokenAlert
|
case refreshTokenAlert
|
||||||
/// The response from the homeserver was unexpected.
|
/// The response from the homeserver was unexpected.
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ typealias LoginScreenViewModelType = StateStoreViewModelV2<LoginScreenViewState,
|
|||||||
class LoginScreenViewModel: LoginScreenViewModelType, LoginScreenViewModelProtocol {
|
class LoginScreenViewModel: LoginScreenViewModelType, LoginScreenViewModelProtocol {
|
||||||
private let authenticationService: AuthenticationServiceProtocol
|
private let authenticationService: AuthenticationServiceProtocol
|
||||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
|
private let appSettings: AppSettings
|
||||||
private let analytics: AnalyticsService
|
private let analytics: AnalyticsService
|
||||||
|
|
||||||
private var actionsSubject: PassthroughSubject<LoginScreenViewModelAction, Never> = .init()
|
private var actionsSubject: PassthroughSubject<LoginScreenViewModelAction, Never> = .init()
|
||||||
@@ -23,9 +24,11 @@ class LoginScreenViewModel: LoginScreenViewModelType, LoginScreenViewModelProtoc
|
|||||||
init(authenticationService: AuthenticationServiceProtocol,
|
init(authenticationService: AuthenticationServiceProtocol,
|
||||||
loginHint: String?,
|
loginHint: String?,
|
||||||
userIndicatorController: UserIndicatorControllerProtocol,
|
userIndicatorController: UserIndicatorControllerProtocol,
|
||||||
|
appSettings: AppSettings,
|
||||||
analytics: AnalyticsService) {
|
analytics: AnalyticsService) {
|
||||||
self.authenticationService = authenticationService
|
self.authenticationService = authenticationService
|
||||||
self.userIndicatorController = userIndicatorController
|
self.userIndicatorController = userIndicatorController
|
||||||
|
self.appSettings = appSettings
|
||||||
self.analytics = analytics
|
self.analytics = analytics
|
||||||
|
|
||||||
let username = switch loginHint {
|
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
|
// Clear out the invalid username to avoid an attempted login to matrix.org
|
||||||
state.bindings.username = ""
|
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:
|
case .sessionTokenRefreshNotSupported:
|
||||||
state.bindings.alertInfo = AlertInfo(id: .refreshTokenAlert,
|
state.bindings.alertInfo = AlertInfo(id: .refreshTokenAlert,
|
||||||
title: L10n.commonServerNotSupported,
|
title: L10n.commonServerNotSupported,
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ struct LoginScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
let viewModel = LoginScreenViewModel(authenticationService: authenticationService,
|
let viewModel = LoginScreenViewModel(authenticationService: authenticationService,
|
||||||
loginHint: nil,
|
loginHint: nil,
|
||||||
userIndicatorController: UserIndicatorControllerMock(),
|
userIndicatorController: UserIndicatorControllerMock(),
|
||||||
|
appSettings: ServiceLocator.shared.settings,
|
||||||
analytics: ServiceLocator.shared.analytics)
|
analytics: ServiceLocator.shared.analytics)
|
||||||
|
|
||||||
if withCredentials {
|
if withCredentials {
|
||||||
|
|||||||
+1
@@ -40,6 +40,7 @@ final class ServerConfirmationScreenCoordinator: CoordinatorProtocol {
|
|||||||
viewModel = ServerConfirmationScreenViewModel(authenticationService: parameters.authenticationService,
|
viewModel = ServerConfirmationScreenViewModel(authenticationService: parameters.authenticationService,
|
||||||
mode: mode,
|
mode: mode,
|
||||||
authenticationFlow: parameters.authenticationFlow,
|
authenticationFlow: parameters.authenticationFlow,
|
||||||
|
appSettings: parameters.appSettings,
|
||||||
userIndicatorController: parameters.userIndicatorController)
|
userIndicatorController: parameters.userIndicatorController)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
@@ -94,6 +94,8 @@ enum ServerConfirmationScreenAlert: Hashable {
|
|||||||
case login
|
case login
|
||||||
/// An alert that informs the user that registration isn't supported.
|
/// An alert that informs the user that registration isn't supported.
|
||||||
case registration
|
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.
|
/// An unknown error has occurred.
|
||||||
case unknownError
|
case unknownError
|
||||||
}
|
}
|
||||||
|
|||||||
+13
@@ -13,6 +13,7 @@ typealias ServerConfirmationScreenViewModelType = StateStoreViewModelV2<ServerCo
|
|||||||
class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType, ServerConfirmationScreenViewModelProtocol {
|
class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType, ServerConfirmationScreenViewModelProtocol {
|
||||||
let authenticationService: AuthenticationServiceProtocol
|
let authenticationService: AuthenticationServiceProtocol
|
||||||
let authenticationFlow: AuthenticationFlow
|
let authenticationFlow: AuthenticationFlow
|
||||||
|
let appSettings: AppSettings
|
||||||
let userIndicatorController: UserIndicatorControllerProtocol
|
let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
|
|
||||||
private var actionsSubject: PassthroughSubject<ServerConfirmationScreenViewModelAction, Never> = .init()
|
private var actionsSubject: PassthroughSubject<ServerConfirmationScreenViewModelAction, Never> = .init()
|
||||||
@@ -24,9 +25,11 @@ class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType,
|
|||||||
init(authenticationService: AuthenticationServiceProtocol,
|
init(authenticationService: AuthenticationServiceProtocol,
|
||||||
mode: ServerConfirmationScreenMode,
|
mode: ServerConfirmationScreenMode,
|
||||||
authenticationFlow: AuthenticationFlow,
|
authenticationFlow: AuthenticationFlow,
|
||||||
|
appSettings: AppSettings,
|
||||||
userIndicatorController: UserIndicatorControllerProtocol) {
|
userIndicatorController: UserIndicatorControllerProtocol) {
|
||||||
self.authenticationService = authenticationService
|
self.authenticationService = authenticationService
|
||||||
self.authenticationFlow = authenticationFlow
|
self.authenticationFlow = authenticationFlow
|
||||||
|
self.appSettings = appSettings
|
||||||
self.userIndicatorController = userIndicatorController
|
self.userIndicatorController = userIndicatorController
|
||||||
|
|
||||||
let pickerSelection: String? = switch mode {
|
let pickerSelection: String? = switch mode {
|
||||||
@@ -94,6 +97,8 @@ class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType,
|
|||||||
displayError(.login)
|
displayError(.login)
|
||||||
case .registrationNotSupported:
|
case .registrationNotSupported:
|
||||||
displayError(.registration)
|
displayError(.registration)
|
||||||
|
case .elementProRequired(let serverName):
|
||||||
|
displayError(.elementProRequired(serverName: serverName))
|
||||||
default:
|
default:
|
||||||
displayError(.unknownError)
|
displayError(.unknownError)
|
||||||
}
|
}
|
||||||
@@ -184,6 +189,14 @@ class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType,
|
|||||||
state.bindings.alertInfo = AlertInfo(id: .registration,
|
state.bindings.alertInfo = AlertInfo(id: .registration,
|
||||||
title: L10n.commonServerNotSupported,
|
title: L10n.commonServerNotSupported,
|
||||||
message: L10n.errorAccountCreationNotPossible)
|
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:
|
case .unknownError:
|
||||||
state.bindings.alertInfo = AlertInfo(id: .unknownError)
|
state.bindings.alertInfo = AlertInfo(id: .unknownError)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -162,6 +162,7 @@ struct ServerConfirmationScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
ServerConfirmationScreenViewModel(authenticationService: AuthenticationService.mock,
|
ServerConfirmationScreenViewModel(authenticationService: AuthenticationService.mock,
|
||||||
mode: mode,
|
mode: mode,
|
||||||
authenticationFlow: flow,
|
authenticationFlow: flow,
|
||||||
|
appSettings: ServiceLocator.shared.settings,
|
||||||
userIndicatorController: UserIndicatorControllerMock())
|
userIndicatorController: UserIndicatorControllerMock())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -12,6 +12,7 @@ struct ServerSelectionScreenCoordinatorParameters {
|
|||||||
/// The service used to authenticate the user.
|
/// The service used to authenticate the user.
|
||||||
let authenticationService: AuthenticationServiceProtocol
|
let authenticationService: AuthenticationServiceProtocol
|
||||||
let authenticationFlow: AuthenticationFlow
|
let authenticationFlow: AuthenticationFlow
|
||||||
|
let appSettings: AppSettings
|
||||||
let userIndicatorController: UserIndicatorControllerProtocol
|
let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,6 +39,7 @@ final class ServerSelectionScreenCoordinator: CoordinatorProtocol {
|
|||||||
self.parameters = parameters
|
self.parameters = parameters
|
||||||
viewModel = ServerSelectionScreenViewModel(authenticationService: parameters.authenticationService,
|
viewModel = ServerSelectionScreenViewModel(authenticationService: parameters.authenticationService,
|
||||||
authenticationFlow: parameters.authenticationFlow,
|
authenticationFlow: parameters.authenticationFlow,
|
||||||
|
appSettings: parameters.appSettings,
|
||||||
userIndicatorController: parameters.userIndicatorController)
|
userIndicatorController: parameters.userIndicatorController)
|
||||||
userIndicatorController = parameters.userIndicatorController
|
userIndicatorController = parameters.userIndicatorController
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -66,4 +66,6 @@ enum ServerSelectionScreenErrorType: Hashable {
|
|||||||
case loginAlert
|
case loginAlert
|
||||||
/// An alert that informs the user that registration isn't supported.
|
/// An alert that informs the user that registration isn't supported.
|
||||||
case registrationAlert
|
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 {
|
class ServerSelectionScreenViewModel: ServerSelectionScreenViewModelType, ServerSelectionScreenViewModelProtocol {
|
||||||
private let authenticationService: AuthenticationServiceProtocol
|
private let authenticationService: AuthenticationServiceProtocol
|
||||||
private let authenticationFlow: AuthenticationFlow
|
private let authenticationFlow: AuthenticationFlow
|
||||||
|
private let appSettings: AppSettings
|
||||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
|
|
||||||
private var actionsSubject: PassthroughSubject<ServerSelectionScreenViewModelAction, Never> = .init()
|
private var actionsSubject: PassthroughSubject<ServerSelectionScreenViewModelAction, Never> = .init()
|
||||||
@@ -23,9 +24,11 @@ class ServerSelectionScreenViewModel: ServerSelectionScreenViewModelType, Server
|
|||||||
|
|
||||||
init(authenticationService: AuthenticationServiceProtocol,
|
init(authenticationService: AuthenticationServiceProtocol,
|
||||||
authenticationFlow: AuthenticationFlow,
|
authenticationFlow: AuthenticationFlow,
|
||||||
|
appSettings: AppSettings,
|
||||||
userIndicatorController: UserIndicatorControllerProtocol) {
|
userIndicatorController: UserIndicatorControllerProtocol) {
|
||||||
self.authenticationService = authenticationService
|
self.authenticationService = authenticationService
|
||||||
self.authenticationFlow = authenticationFlow
|
self.authenticationFlow = authenticationFlow
|
||||||
|
self.appSettings = appSettings
|
||||||
self.userIndicatorController = userIndicatorController
|
self.userIndicatorController = userIndicatorController
|
||||||
|
|
||||||
let bindings = ServerSelectionScreenBindings(homeserverAddress: authenticationService.homeserver.value.address)
|
let bindings = ServerSelectionScreenBindings(homeserverAddress: authenticationService.homeserver.value.address)
|
||||||
@@ -96,6 +99,14 @@ class ServerSelectionScreenViewModel: ServerSelectionScreenViewModelType, Server
|
|||||||
state.bindings.alertInfo = AlertInfo(id: .registrationAlert,
|
state.bindings.alertInfo = AlertInfo(id: .registrationAlert,
|
||||||
title: L10n.commonServerNotSupported,
|
title: L10n.commonServerNotSupported,
|
||||||
message: L10n.errorAccountCreationNotPossible)
|
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:
|
default:
|
||||||
showFooterMessage(L10n.errorUnknown)
|
showFooterMessage(L10n.errorUnknown)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -115,6 +115,7 @@ struct ServerSelection_Previews: PreviewProvider, TestablePreview {
|
|||||||
|
|
||||||
let viewModel = ServerSelectionScreenViewModel(authenticationService: authenticationService,
|
let viewModel = ServerSelectionScreenViewModel(authenticationService: authenticationService,
|
||||||
authenticationFlow: .login,
|
authenticationFlow: .login,
|
||||||
|
appSettings: ServiceLocator.shared.settings,
|
||||||
userIndicatorController: UserIndicatorControllerMock())
|
userIndicatorController: UserIndicatorControllerMock())
|
||||||
viewModel.context.homeserverAddress = homeserverAddress
|
viewModel.context.homeserverAddress = homeserverAddress
|
||||||
if homeserverAddress == "thisisbad" {
|
if homeserverAddress == "thisisbad" {
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ class AuthenticationService: AuthenticationServiceProtocol {
|
|||||||
var homeserver = LoginHomeserver(address: homeserverAddress, loginMode: .unknown)
|
var homeserver = LoginHomeserver(address: homeserverAddress, loginMode: .unknown)
|
||||||
|
|
||||||
let client = try await makeClient(homeserverAddress: homeserverAddress)
|
let client = try await makeClient(homeserverAddress: homeserverAddress)
|
||||||
|
try await appHooks.elementWellKnownHook.validate(using: client).get()
|
||||||
let loginDetails = await client.homeserverLoginDetails()
|
let loginDetails = await client.homeserverLoginDetails()
|
||||||
|
|
||||||
MXLog.info("Sliding sync: \(client.slidingSyncVersion())")
|
MXLog.info("Sliding sync: \(client.slidingSyncVersion())")
|
||||||
@@ -81,6 +82,8 @@ class AuthenticationService: AuthenticationServiceProtocol {
|
|||||||
} catch ClientBuildError.SlidingSyncVersion(let error) {
|
} catch ClientBuildError.SlidingSyncVersion(let error) {
|
||||||
MXLog.info("User entered a homeserver that isn't configured for sliding sync: \(error)")
|
MXLog.info("User entered a homeserver that isn't configured for sliding sync: \(error)")
|
||||||
return .failure(.slidingSyncNotAvailable)
|
return .failure(.slidingSyncNotAvailable)
|
||||||
|
} catch ElementWellKnownError.elementProRequired(let serverName) {
|
||||||
|
return .failure(.elementProRequired(serverName: serverName))
|
||||||
} catch {
|
} catch {
|
||||||
MXLog.error("Failed configuring a server: \(error)")
|
MXLog.error("Failed configuring a server: \(error)")
|
||||||
return .failure(.invalidHomeserverAddress)
|
return .failure(.invalidHomeserverAddress)
|
||||||
@@ -176,6 +179,7 @@ class AuthenticationService: AuthenticationServiceProtocol {
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
let client = try await makeClient(homeserverAddress: scannedServerName)
|
let client = try await makeClient(homeserverAddress: scannedServerName)
|
||||||
|
try await appHooks.elementWellKnownHook.validate(using: client).get()
|
||||||
try await client.loginWithQrCode(qrCodeData: qrData,
|
try await client.loginWithQrCode(qrCodeData: qrData,
|
||||||
oidcConfiguration: appSettings.oidcConfiguration.rustValue,
|
oidcConfiguration: appSettings.oidcConfiguration.rustValue,
|
||||||
progressListener: listener)
|
progressListener: listener)
|
||||||
@@ -184,6 +188,8 @@ class AuthenticationService: AuthenticationServiceProtocol {
|
|||||||
} catch let error as HumanQrLoginError {
|
} catch let error as HumanQrLoginError {
|
||||||
MXLog.error("QRCode login error: \(error)")
|
MXLog.error("QRCode login error: \(error)")
|
||||||
return .failure(error.serviceError)
|
return .failure(error.serviceError)
|
||||||
|
} catch ElementWellKnownError.elementProRequired(let serverName) {
|
||||||
|
return .failure(.elementProRequired(serverName: serverName))
|
||||||
} catch {
|
} catch {
|
||||||
MXLog.error("QRCode login unknown error: \(error)")
|
MXLog.error("QRCode login unknown error: \(error)")
|
||||||
return .failure(.qrCodeError(.unknown))
|
return .failure(.qrCodeError(.unknown))
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ enum AuthenticationServiceError: Error, Equatable {
|
|||||||
case slidingSyncNotAvailable
|
case slidingSyncNotAvailable
|
||||||
case loginNotSupported
|
case loginNotSupported
|
||||||
case registrationNotSupported
|
case registrationNotSupported
|
||||||
|
case elementProRequired(serverName: String)
|
||||||
case accountDeactivated
|
case accountDeactivated
|
||||||
case failedLoggingIn
|
case failedLoggingIn
|
||||||
case sessionTokenRefreshNotSupported
|
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 invalidMedia
|
||||||
case invalidServerName
|
case invalidServerName
|
||||||
|
case invalidResponse
|
||||||
case failedUploadingMedia(ErrorKind)
|
case failedUploadingMedia(ErrorKind)
|
||||||
case roomPreviewIsPrivate
|
case roomPreviewIsPrivate
|
||||||
case failedRetrievingUserIdentity
|
case failedRetrievingUserIdentity
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ class MockScreen: Identifiable {
|
|||||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||||
let coordinator = ServerSelectionScreenCoordinator(parameters: .init(authenticationService: AuthenticationService.mock,
|
let coordinator = ServerSelectionScreenCoordinator(parameters: .init(authenticationService: AuthenticationService.mock,
|
||||||
authenticationFlow: .login,
|
authenticationFlow: .login,
|
||||||
|
appSettings: ServiceLocator.shared.settings,
|
||||||
userIndicatorController: ServiceLocator.shared.userIndicatorController))
|
userIndicatorController: ServiceLocator.shared.userIndicatorController))
|
||||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||||
return navigationStackCoordinator
|
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.")
|
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 {
|
func testLoginHint() async throws {
|
||||||
await setupViewModel(loginHint: "")
|
await setupViewModel(loginHint: "")
|
||||||
XCTAssertEqual(context.username, "")
|
XCTAssertEqual(context.username, "")
|
||||||
@@ -172,6 +187,7 @@ class LoginScreenViewModelTests: XCTestCase {
|
|||||||
viewModel = LoginScreenViewModel(authenticationService: service,
|
viewModel = LoginScreenViewModel(authenticationService: service,
|
||||||
loginHint: loginHint,
|
loginHint: loginHint,
|
||||||
userIndicatorController: UserIndicatorControllerMock(),
|
userIndicatorController: UserIndicatorControllerMock(),
|
||||||
|
appSettings: ServiceLocator.shared.settings,
|
||||||
analytics: ServiceLocator.shared.analytics)
|
analytics: ServiceLocator.shared.analytics)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -196,6 +196,23 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
|||||||
XCTAssertEqual(context.alertInfo?.id, .login)
|
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
|
// MARK: - Picker mode
|
||||||
|
|
||||||
func testPickerWithoutConfiguration() async throws {
|
func testPickerWithoutConfiguration() async throws {
|
||||||
@@ -288,7 +305,8 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
|||||||
supportsOIDC: Bool = true,
|
supportsOIDC: Bool = true,
|
||||||
supportsOIDCCreatePrompt: Bool = true,
|
supportsOIDCCreatePrompt: Bool = true,
|
||||||
supportsPasswordLogin: Bool = true,
|
supportsPasswordLogin: Bool = true,
|
||||||
restrictedFlow: Bool = false) {
|
restrictedFlow: Bool = false,
|
||||||
|
requiresElementPro: Bool = false) {
|
||||||
var mode = ServerConfirmationScreenMode.confirmation("matrix.org")
|
var mode = ServerConfirmationScreenMode.confirmation("matrix.org")
|
||||||
if restrictedFlow {
|
if restrictedFlow {
|
||||||
appSettings.override(accountProviders: ["matrix.org", "beta.matrix.org"],
|
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.
|
// Manually create a configuration as the default homeserver address setting is immutable.
|
||||||
client = ClientSDKMock(configuration: .init(oidcLoginURL: supportsOIDC ? "https://account.matrix.org/authorize" : nil,
|
client = ClientSDKMock(configuration: .init(oidcLoginURL: supportsOIDC ? "https://account.matrix.org/authorize" : nil,
|
||||||
supportsOIDCCreatePrompt: supportsOIDCCreatePrompt,
|
supportsOIDCCreatePrompt: supportsOIDCCreatePrompt,
|
||||||
supportsPasswordLogin: supportsPasswordLogin))
|
supportsPasswordLogin: supportsPasswordLogin,
|
||||||
|
elementWellKnown: requiresElementPro ? "{\"version\":1,\"enforce_element_pro\":true}" : nil))
|
||||||
let configuration = AuthenticationClientFactoryMock.Configuration(homeserverClients: ["matrix.org": client])
|
let configuration = AuthenticationClientFactoryMock.Configuration(homeserverClients: ["matrix.org": client])
|
||||||
|
|
||||||
clientFactory = AuthenticationClientFactoryMock(configuration: configuration)
|
clientFactory = AuthenticationClientFactoryMock(configuration: configuration)
|
||||||
@@ -328,6 +347,7 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
|||||||
viewModel = ServerConfirmationScreenViewModel(authenticationService: service,
|
viewModel = ServerConfirmationScreenViewModel(authenticationService: service,
|
||||||
mode: mode,
|
mode: mode,
|
||||||
authenticationFlow: authenticationFlow,
|
authenticationFlow: authenticationFlow,
|
||||||
|
appSettings: ServiceLocator.shared.settings,
|
||||||
userIndicatorController: UserIndicatorControllerMock())
|
userIndicatorController: UserIndicatorControllerMock())
|
||||||
|
|
||||||
// Add a fake window in order for the OIDC flow to continue
|
// Add a fake window in order for the OIDC flow to continue
|
||||||
|
|||||||
@@ -87,6 +87,24 @@ class ServerSelectionScreenViewModelTests: XCTestCase {
|
|||||||
XCTAssertEqual(context.alertInfo?.id, .registrationAlert)
|
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 {
|
func testInvalidServer() async throws {
|
||||||
// Given a new instance of the view model.
|
// Given a new instance of the view model.
|
||||||
setupViewModel(authenticationFlow: .login)
|
setupViewModel(authenticationFlow: .login)
|
||||||
@@ -131,6 +149,7 @@ class ServerSelectionScreenViewModelTests: XCTestCase {
|
|||||||
|
|
||||||
viewModel = ServerSelectionScreenViewModel(authenticationService: service,
|
viewModel = ServerSelectionScreenViewModel(authenticationService: service,
|
||||||
authenticationFlow: authenticationFlow,
|
authenticationFlow: authenticationFlow,
|
||||||
|
appSettings: ServiceLocator.shared.settings,
|
||||||
userIndicatorController: UserIndicatorControllerMock())
|
userIndicatorController: UserIndicatorControllerMock())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user