mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-05 11:58:32 +00:00
Crypto identity reset (#3107)
* Add EncryptionReset password entry screen * Expose client proxy identity reset method * Update the IdentityConfirmationScreen to the latest designs * Replace the old recovery key reset screen with a new EncryptionReset one * Fixes #3102 - Add support for resetting the user's whole crypto identity * Address review comments * Bump the SDK to v1.0.30
This commit is contained in:
@@ -26,6 +26,7 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol {
|
||||
private let notificationManager: NotificationManagerProtocol
|
||||
private let rootNavigationStackCoordinator: NavigationStackCoordinator
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
private let windowManager: WindowManagerProtocol
|
||||
private let isNewLogin: Bool
|
||||
|
||||
private var navigationStackCoordinator: NavigationStackCoordinator!
|
||||
@@ -57,6 +58,7 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol {
|
||||
notificationManager: NotificationManagerProtocol,
|
||||
navigationStackCoordinator: NavigationStackCoordinator,
|
||||
userIndicatorController: UserIndicatorControllerProtocol,
|
||||
windowManager: WindowManagerProtocol,
|
||||
isNewLogin: Bool) {
|
||||
self.userSession = userSession
|
||||
self.appLockService = appLockService
|
||||
@@ -64,6 +66,7 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol {
|
||||
self.appSettings = appSettings
|
||||
self.notificationManager = notificationManager
|
||||
self.userIndicatorController = userIndicatorController
|
||||
self.windowManager = windowManager
|
||||
self.isNewLogin = isNewLogin
|
||||
|
||||
rootNavigationStackCoordinator = navigationStackCoordinator
|
||||
@@ -215,7 +218,7 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol {
|
||||
appSettings.hasRunIdentityConfirmationOnboarding = true
|
||||
stateMachine.tryEvent(.next)
|
||||
case .reset:
|
||||
presentResetRecoveryKeyScreen()
|
||||
presentEncryptionResetScreen()
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
@@ -262,8 +265,8 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol {
|
||||
case .recoveryFixed:
|
||||
appSettings.hasRunIdentityConfirmationOnboarding = true
|
||||
stateMachine.tryEvent(.next)
|
||||
case .showResetKeyInfo:
|
||||
presentResetRecoveryKeyScreen()
|
||||
case .resetEncryption:
|
||||
presentEncryptionResetScreen()
|
||||
default:
|
||||
MXLog.error("Unexpected recovery action: \(action)")
|
||||
}
|
||||
@@ -273,16 +276,32 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol {
|
||||
presentCoordinator(coordinator)
|
||||
}
|
||||
|
||||
private func presentResetRecoveryKeyScreen() {
|
||||
let coordinator = ResetRecoveryKeyScreenCoordinator()
|
||||
private func presentEncryptionResetScreen() {
|
||||
let resetNavigationStackCoordinator = NavigationStackCoordinator()
|
||||
|
||||
let coordinator = EncryptionResetScreenCoordinator(parameters: .init(clientProxy: userSession.clientProxy,
|
||||
navigationStackCoordinator: resetNavigationStackCoordinator,
|
||||
userIndicatorController: userIndicatorController))
|
||||
|
||||
coordinator.actionsPublisher.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .cancel:
|
||||
self?.navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
case .requestOIDCAuthorisation(let url):
|
||||
presentOIDCAuthorisationScreen(url: url)
|
||||
case .resetFinished:
|
||||
appSettings.hasRunIdentityConfirmationOnboarding = true
|
||||
stateMachine.tryEvent(.next)
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
navigationStackCoordinator.setSheetCoordinator(coordinator)
|
||||
|
||||
resetNavigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
|
||||
navigationStackCoordinator.setSheetCoordinator(resetNavigationStackCoordinator)
|
||||
}
|
||||
|
||||
private func presentIdentityConfirmedScreen() {
|
||||
@@ -361,4 +380,12 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol {
|
||||
navigationStackCoordinator.push(coordinator)
|
||||
}
|
||||
}
|
||||
|
||||
private var accountSettingsPresenter: OIDCAccountSettingsPresenter?
|
||||
private func presentOIDCAuthorisationScreen(url: URL) {
|
||||
// Note to anyone in the future if you come back here to make this open in Safari instead of a WAS.
|
||||
// As of iOS 16, there is an issue on the simulator with accessing the cookie but it works on a device. 🤷♂️
|
||||
accountSettingsPresenter = OIDCAccountSettingsPresenter(accountURL: url, presentationAnchor: windowManager.mainWindow)
|
||||
accountSettingsPresenter?.start()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,10 +153,18 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
private func presentSecureBackupScreen(animated: Bool) {
|
||||
let coordinator = SecureBackupScreenCoordinator(parameters: .init(appSettings: parameters.appSettings,
|
||||
secureBackupController: parameters.userSession.clientProxy.secureBackupController,
|
||||
clientProxy: parameters.userSession.clientProxy,
|
||||
navigationStackCoordinator: navigationStackCoordinator,
|
||||
userIndicatorController: parameters.userIndicatorController))
|
||||
|
||||
coordinator.actions.sink { [weak self] action in
|
||||
switch action {
|
||||
case .requestOIDCAuthorisation(let url):
|
||||
self?.presentAccountManagementURL(url)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
navigationStackCoordinator.push(coordinator, animated: animated)
|
||||
}
|
||||
|
||||
|
||||
@@ -115,6 +115,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
notificationManager: notificationManager,
|
||||
navigationStackCoordinator: detailNavigationStackCoordinator,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
windowManager: appMediator.windowManager,
|
||||
isNewLogin: isNewLogin)
|
||||
|
||||
setupStateMachine()
|
||||
|
||||
@@ -196,6 +196,8 @@ internal enum L10n {
|
||||
internal static var actionReportContent: String { return L10n.tr("Localizable", "action_report_content") }
|
||||
/// Reset
|
||||
internal static var actionReset: String { return L10n.tr("Localizable", "action_reset") }
|
||||
/// Reset identity
|
||||
internal static var actionResetIdentity: String { return L10n.tr("Localizable", "action_reset_identity") }
|
||||
/// Retry
|
||||
internal static var actionRetry: String { return L10n.tr("Localizable", "action_retry") }
|
||||
/// Retry decryption
|
||||
@@ -1045,12 +1047,30 @@ internal enum L10n {
|
||||
internal static var screenEditProfileTitle: String { return L10n.tr("Localizable", "screen_edit_profile_title") }
|
||||
/// Updating profile…
|
||||
internal static var screenEditProfileUpdatingDetails: String { return L10n.tr("Localizable", "screen_edit_profile_updating_details") }
|
||||
/// Your account details, contacts, preferences, and chat list will be kept
|
||||
internal static var screenEncryptionResetBullet1: String { return L10n.tr("Localizable", "screen_encryption_reset_bullet_1") }
|
||||
/// You will lose your existing message history
|
||||
internal static var screenEncryptionResetBullet2: String { return L10n.tr("Localizable", "screen_encryption_reset_bullet_2") }
|
||||
/// You will need to verify all your existing devices and contacts again
|
||||
internal static var screenEncryptionResetBullet3: String { return L10n.tr("Localizable", "screen_encryption_reset_bullet_3") }
|
||||
/// Only reset your identity if you don’t have access to another signed-in device and you’ve lost your recovery key.
|
||||
internal static var screenEncryptionResetFooter: String { return L10n.tr("Localizable", "screen_encryption_reset_footer") }
|
||||
/// If you’re not signed in to any other devices and you’ve lost your recovery key, then you’ll need to reset your identity to continue using the app.
|
||||
internal static var screenEncryptionResetSubtitle: String { return L10n.tr("Localizable", "screen_encryption_reset_subtitle") }
|
||||
/// Reset your identity in case you can’t confirm another way
|
||||
internal static var screenEncryptionResetTitle: String { return L10n.tr("Localizable", "screen_encryption_reset_title") }
|
||||
/// Can't confirm?
|
||||
internal static var screenIdentityConfirmationCannotConfirm: String { return L10n.tr("Localizable", "screen_identity_confirmation_cannot_confirm") }
|
||||
/// Create a new recovery key
|
||||
internal static var screenIdentityConfirmationCreateNewRecoveryKey: String { return L10n.tr("Localizable", "screen_identity_confirmation_create_new_recovery_key") }
|
||||
/// Verify this device to set up secure messaging.
|
||||
internal static var screenIdentityConfirmationSubtitle: String { return L10n.tr("Localizable", "screen_identity_confirmation_subtitle") }
|
||||
/// Confirm that it's you
|
||||
internal static var screenIdentityConfirmationTitle: String { return L10n.tr("Localizable", "screen_identity_confirmation_title") }
|
||||
/// Use another device
|
||||
internal static var screenIdentityConfirmationUseAnotherDevice: String { return L10n.tr("Localizable", "screen_identity_confirmation_use_another_device") }
|
||||
/// Use recovery key
|
||||
internal static var screenIdentityConfirmationUseRecoveryKey: String { return L10n.tr("Localizable", "screen_identity_confirmation_use_recovery_key") }
|
||||
/// Now you can read or send messages securely, and anyone you chat with can also trust this device.
|
||||
internal static var screenIdentityConfirmedSubtitle: String { return L10n.tr("Localizable", "screen_identity_confirmed_subtitle") }
|
||||
/// Device verified
|
||||
@@ -1385,6 +1405,18 @@ internal enum L10n {
|
||||
internal static var screenReportContentExplanation: String { return L10n.tr("Localizable", "screen_report_content_explanation") }
|
||||
/// Reason for reporting this content
|
||||
internal static var screenReportContentHint: String { return L10n.tr("Localizable", "screen_report_content_hint") }
|
||||
/// Yes, reset now
|
||||
internal static var screenResetEncryptionConfirmationAlertAction: String { return L10n.tr("Localizable", "screen_reset_encryption_confirmation_alert_action") }
|
||||
/// This process is irreversible.
|
||||
internal static var screenResetEncryptionConfirmationAlertSubtitle: String { return L10n.tr("Localizable", "screen_reset_encryption_confirmation_alert_subtitle") }
|
||||
/// Are you sure you want to reset your encryption?
|
||||
internal static var screenResetEncryptionConfirmationAlertTitle: String { return L10n.tr("Localizable", "screen_reset_encryption_confirmation_alert_title") }
|
||||
/// Enter…
|
||||
internal static var screenResetEncryptionPasswordPlaceholder: String { return L10n.tr("Localizable", "screen_reset_encryption_password_placeholder") }
|
||||
/// Confirm that you want to reset your encryption.
|
||||
internal static var screenResetEncryptionPasswordSubtitle: String { return L10n.tr("Localizable", "screen_reset_encryption_password_subtitle") }
|
||||
/// Enter your account password to continue
|
||||
internal static var screenResetEncryptionPasswordTitle: String { return L10n.tr("Localizable", "screen_reset_encryption_password_title") }
|
||||
/// Failed to resolve room alias.
|
||||
internal static var screenRoomAliasResolverResolveAliasFailure: String { return L10n.tr("Localizable", "screen_room_alias_resolver_resolve_alias_failure") }
|
||||
/// Camera
|
||||
|
||||
@@ -4095,6 +4095,70 @@ class ClientProxyMock: ClientProxyProtocol {
|
||||
return curve25519Base64ReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - resetIdentity
|
||||
|
||||
var resetIdentityUnderlyingCallsCount = 0
|
||||
var resetIdentityCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return resetIdentityUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = resetIdentityUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
resetIdentityUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
resetIdentityUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var resetIdentityCalled: Bool {
|
||||
return resetIdentityCallsCount > 0
|
||||
}
|
||||
|
||||
var resetIdentityUnderlyingReturnValue: Result<IdentityResetHandle?, ClientProxyError>!
|
||||
var resetIdentityReturnValue: Result<IdentityResetHandle?, ClientProxyError>! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return resetIdentityUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: Result<IdentityResetHandle?, ClientProxyError>? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = resetIdentityUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
resetIdentityUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
resetIdentityUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var resetIdentityClosure: (() async -> Result<IdentityResetHandle?, ClientProxyError>)?
|
||||
|
||||
func resetIdentity() async -> Result<IdentityResetHandle?, ClientProxyError> {
|
||||
resetIdentityCallsCount += 1
|
||||
if let resetIdentityClosure = resetIdentityClosure {
|
||||
return await resetIdentityClosure()
|
||||
} else {
|
||||
return resetIdentityReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - loadMediaContentForSource
|
||||
|
||||
var loadMediaContentForSourceThrowableError: Error?
|
||||
|
||||
@@ -5915,6 +5915,75 @@ open class EncryptionSDKMock: MatrixRustSDK.Encryption {
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - resetIdentity
|
||||
|
||||
open var resetIdentityThrowableError: Error?
|
||||
var resetIdentityUnderlyingCallsCount = 0
|
||||
open var resetIdentityCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return resetIdentityUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = resetIdentityUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
resetIdentityUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
resetIdentityUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
open var resetIdentityCalled: Bool {
|
||||
return resetIdentityCallsCount > 0
|
||||
}
|
||||
|
||||
var resetIdentityUnderlyingReturnValue: IdentityResetHandle?
|
||||
open var resetIdentityReturnValue: IdentityResetHandle? {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return resetIdentityUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: IdentityResetHandle?? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = resetIdentityUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
resetIdentityUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
resetIdentityUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
open var resetIdentityClosure: (() async throws -> IdentityResetHandle?)?
|
||||
|
||||
open override func resetIdentity() async throws -> IdentityResetHandle? {
|
||||
if let error = resetIdentityThrowableError {
|
||||
throw error
|
||||
}
|
||||
resetIdentityCallsCount += 1
|
||||
if let resetIdentityClosure = resetIdentityClosure {
|
||||
return try await resetIdentityClosure()
|
||||
} else {
|
||||
return resetIdentityReturnValue
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - resetRecoveryKey
|
||||
|
||||
open var resetRecoveryKeyThrowableError: Error?
|
||||
@@ -7595,6 +7664,128 @@ open class HomeserverLoginDetailsSDKMock: MatrixRustSDK.HomeserverLoginDetails {
|
||||
}
|
||||
}
|
||||
}
|
||||
open class IdentityResetHandleSDKMock: MatrixRustSDK.IdentityResetHandle {
|
||||
init() {
|
||||
super.init(noPointer: .init())
|
||||
}
|
||||
|
||||
public required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) {
|
||||
fatalError("init(unsafeFromRawPointer:) has not been implemented")
|
||||
}
|
||||
|
||||
fileprivate var pointer: UnsafeMutableRawPointer!
|
||||
|
||||
//MARK: - authType
|
||||
|
||||
var authTypeUnderlyingCallsCount = 0
|
||||
open var authTypeCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return authTypeUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = authTypeUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
authTypeUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
authTypeUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
open var authTypeCalled: Bool {
|
||||
return authTypeCallsCount > 0
|
||||
}
|
||||
|
||||
var authTypeUnderlyingReturnValue: CrossSigningResetAuthType!
|
||||
open var authTypeReturnValue: CrossSigningResetAuthType! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return authTypeUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: CrossSigningResetAuthType? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = authTypeUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
authTypeUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
authTypeUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
open var authTypeClosure: (() -> CrossSigningResetAuthType)?
|
||||
|
||||
open override func authType() -> CrossSigningResetAuthType {
|
||||
authTypeCallsCount += 1
|
||||
if let authTypeClosure = authTypeClosure {
|
||||
return authTypeClosure()
|
||||
} else {
|
||||
return authTypeReturnValue
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - reset
|
||||
|
||||
open var resetAuthThrowableError: Error?
|
||||
var resetAuthUnderlyingCallsCount = 0
|
||||
open var resetAuthCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return resetAuthUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = resetAuthUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
resetAuthUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
resetAuthUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
open var resetAuthCalled: Bool {
|
||||
return resetAuthCallsCount > 0
|
||||
}
|
||||
open var resetAuthReceivedAuth: AuthData?
|
||||
open var resetAuthReceivedInvocations: [AuthData?] = []
|
||||
open var resetAuthClosure: ((AuthData?) async throws -> Void)?
|
||||
|
||||
open override func reset(auth: AuthData?) async throws {
|
||||
if let error = resetAuthThrowableError {
|
||||
throw error
|
||||
}
|
||||
resetAuthCallsCount += 1
|
||||
resetAuthReceivedAuth = auth
|
||||
DispatchQueue.main.async {
|
||||
self.resetAuthReceivedInvocations.append(auth)
|
||||
}
|
||||
try await resetAuthClosure?(auth)
|
||||
}
|
||||
}
|
||||
open class MediaFileHandleSDKMock: MatrixRustSDK.MediaFileHandle {
|
||||
init() {
|
||||
super.init(noPointer: .init())
|
||||
|
||||
@@ -58,12 +58,11 @@ struct RoundedLabelItem<Icon: View>: View {
|
||||
|
||||
private struct CheckmarkLabelStyle: LabelStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 16) {
|
||||
HStack(alignment: .top, spacing: 16) {
|
||||
configuration.icon
|
||||
.font(.compound.bodyLGSemibold)
|
||||
configuration.title
|
||||
.font(.compound.bodyMDSemibold)
|
||||
}
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
}
|
||||
}
|
||||
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
// periphery:ignore:all - this is just a encryptionResetPassword remove this comment once generating the final file
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
struct EncryptionResetPasswordScreenCoordinatorParameters { }
|
||||
|
||||
enum EncryptionResetPasswordScreenCoordinatorAction: CustomStringConvertible {
|
||||
case resetIdentity(String)
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .resetIdentity:
|
||||
"resetIdentity"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class EncryptionResetPasswordScreenCoordinator: CoordinatorProtocol {
|
||||
private let parameters: EncryptionResetPasswordScreenCoordinatorParameters
|
||||
private let viewModel: EncryptionResetPasswordScreenViewModelProtocol
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private let actionsSubject: PassthroughSubject<EncryptionResetPasswordScreenCoordinatorAction, Never> = .init()
|
||||
var actionsPublisher: AnyPublisher<EncryptionResetPasswordScreenCoordinatorAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(parameters: EncryptionResetPasswordScreenCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
viewModel = EncryptionResetPasswordScreenViewModel()
|
||||
}
|
||||
|
||||
func start() {
|
||||
viewModel.actionsPublisher.sink { [weak self] action in
|
||||
MXLog.info("Coordinator: received view model action: \(action)")
|
||||
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .resetIdentity(let password):
|
||||
self.actionsSubject.send(.resetIdentity(password))
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(EncryptionResetPasswordScreen(context: viewModel.context))
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum EncryptionResetPasswordScreenViewModelAction: CustomStringConvertible {
|
||||
case resetIdentity(String)
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .resetIdentity:
|
||||
"resetIdentity"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct EncryptionResetPasswordScreenViewState: BindableState {
|
||||
var bindings: EncryptionResetPasswordScreenViewStateBindings
|
||||
}
|
||||
|
||||
struct EncryptionResetPasswordScreenViewStateBindings {
|
||||
var password: String
|
||||
var alertInfo: AlertInfo<UUID>?
|
||||
}
|
||||
|
||||
enum EncryptionResetPasswordScreenViewAction {
|
||||
case resetIdentity
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
typealias EncryptionResetPasswordScreenViewModelType = StateStoreViewModel<EncryptionResetPasswordScreenViewState, EncryptionResetPasswordScreenViewAction>
|
||||
|
||||
class EncryptionResetPasswordScreenViewModel: EncryptionResetPasswordScreenViewModelType, EncryptionResetPasswordScreenViewModelProtocol {
|
||||
private let actionsSubject: PassthroughSubject<EncryptionResetPasswordScreenViewModelAction, Never> = .init()
|
||||
var actionsPublisher: AnyPublisher<EncryptionResetPasswordScreenViewModelAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init(initialViewState: .init(bindings: .init(password: "")))
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: EncryptionResetPasswordScreenViewAction) {
|
||||
MXLog.info("View model: received view action: \(viewAction)")
|
||||
|
||||
switch viewAction {
|
||||
case .resetIdentity:
|
||||
actionsSubject.send(.resetIdentity(state.bindings.password))
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -17,7 +17,7 @@
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
protocol ResetRecoveryKeyScreenViewModelProtocol {
|
||||
var actionsPublisher: AnyPublisher<ResetRecoveryKeyScreenViewModelAction, Never> { get }
|
||||
var context: ResetRecoveryKeyScreenViewModelType.Context { get }
|
||||
protocol EncryptionResetPasswordScreenViewModelProtocol {
|
||||
var actionsPublisher: AnyPublisher<EncryptionResetPasswordScreenViewModelAction, Never> { get }
|
||||
var context: EncryptionResetPasswordScreenViewModelType.Context { get }
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Compound
|
||||
import SwiftUI
|
||||
|
||||
struct EncryptionResetPasswordScreen: View {
|
||||
@ObservedObject var context: EncryptionResetPasswordScreenViewModel.Context
|
||||
@FocusState private var textFieldFocus
|
||||
|
||||
var body: some View {
|
||||
FullscreenDialog {
|
||||
VStack(spacing: 16) {
|
||||
HeroImage(icon: \.lockSolid)
|
||||
|
||||
Text(L10n.screenResetEncryptionPasswordTitle)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
.font(.compound.headingMDBold)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Text(L10n.screenResetEncryptionPasswordSubtitle)
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
.font(.compound.bodyMD)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
passwordSection
|
||||
}
|
||||
.padding(16)
|
||||
} bottomContent: {
|
||||
Button(L10n.actionResetIdentity, role: .destructive) {
|
||||
context.send(viewAction: .resetIdentity)
|
||||
}
|
||||
.buttonStyle(.compound(.primary))
|
||||
}
|
||||
.backgroundStyle(.compound.bgCanvasDefault)
|
||||
.interactiveDismissDisabled()
|
||||
.onAppear { textFieldFocus = true }
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var passwordSection: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(L10n.commonPassword)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
.font(.compound.bodySMSemibold)
|
||||
|
||||
SecureField(L10n.screenResetEncryptionPasswordPlaceholder, text: $context.password)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(Color.compound.bgSubtleSecondaryLevel0)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
.focused($textFieldFocus)
|
||||
.submitLabel(.done)
|
||||
.onSubmit {
|
||||
context.send(viewAction: .resetIdentity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct EncryptionResetPasswordScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = EncryptionResetPasswordScreenViewModel()
|
||||
static var previews: some View {
|
||||
NavigationStack {
|
||||
EncryptionResetPasswordScreen(context: viewModel.context)
|
||||
}
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
enum EncryptionResetScreenCoordinatorAction {
|
||||
case cancel
|
||||
case requestOIDCAuthorisation(URL)
|
||||
case resetFinished
|
||||
}
|
||||
|
||||
struct EncryptionResetScreenCoordinatorParameters {
|
||||
let clientProxy: ClientProxyProtocol
|
||||
let navigationStackCoordinator: NavigationStackCoordinator
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
}
|
||||
|
||||
final class EncryptionResetScreenCoordinator: CoordinatorProtocol {
|
||||
private let parameters: EncryptionResetScreenCoordinatorParameters
|
||||
private let viewModel: EncryptionResetScreenViewModelProtocol
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private let actionsSubject: PassthroughSubject<EncryptionResetScreenCoordinatorAction, Never> = .init()
|
||||
var actionsPublisher: AnyPublisher<EncryptionResetScreenCoordinatorAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(parameters: EncryptionResetScreenCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
viewModel = EncryptionResetScreenViewModel(clientProxy: parameters.clientProxy,
|
||||
userIndicatorController: parameters.userIndicatorController)
|
||||
}
|
||||
|
||||
func start() {
|
||||
viewModel.actionsPublisher.sink { [weak self] action in
|
||||
MXLog.info("Coordinator: received view model action: \(action)")
|
||||
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .requestPassword:
|
||||
presentPasswordScreen()
|
||||
case .requestOIDCAuthorisation(let url):
|
||||
self.actionsSubject.send(.requestOIDCAuthorisation(url))
|
||||
case .resetFinished:
|
||||
self.actionsSubject.send(.resetFinished)
|
||||
case .cancel:
|
||||
self.actionsSubject.send(.cancel)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(EncryptionResetScreen(context: viewModel.context))
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func presentPasswordScreen() {
|
||||
let coordinator = EncryptionResetPasswordScreenCoordinator(parameters: .init())
|
||||
|
||||
coordinator.actionsPublisher.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .resetIdentity(let password):
|
||||
viewModel.continueResetFlowWith(password: password)
|
||||
parameters.navigationStackCoordinator.pop()
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
parameters.navigationStackCoordinator.push(coordinator)
|
||||
}
|
||||
}
|
||||
+13
-3
@@ -16,11 +16,14 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
enum ResetRecoveryKeyScreenViewModelAction {
|
||||
enum EncryptionResetScreenViewModelAction {
|
||||
case requestPassword
|
||||
case requestOIDCAuthorisation(url: URL)
|
||||
case resetFinished
|
||||
case cancel
|
||||
}
|
||||
|
||||
struct ResetRecoveryKeyScreenViewState: BindableState {
|
||||
struct EncryptionResetScreenViewState: BindableState {
|
||||
private let listItem3AttributedText = {
|
||||
let boldPlaceholder = "{bold}"
|
||||
var finalString = AttributedString(L10n.screenCreateNewRecoveryKeyListItem3(boldPlaceholder))
|
||||
@@ -39,8 +42,15 @@ struct ResetRecoveryKeyScreenViewState: BindableState {
|
||||
AttributedString(L10n.screenCreateNewRecoveryKeyListItem5)
|
||||
]
|
||||
}
|
||||
|
||||
var bindings: EncryptionResetScreenViewStateBindings
|
||||
}
|
||||
|
||||
enum ResetRecoveryKeyScreenViewAction {
|
||||
struct EncryptionResetScreenViewStateBindings {
|
||||
var alertInfo: AlertInfo<UUID>?
|
||||
}
|
||||
|
||||
enum EncryptionResetScreenViewAction {
|
||||
case reset
|
||||
case cancel
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import MatrixRustSDK
|
||||
import SwiftUI
|
||||
|
||||
typealias EncryptionResetScreenViewModelType = StateStoreViewModel<EncryptionResetScreenViewState, EncryptionResetScreenViewAction>
|
||||
|
||||
class EncryptionResetScreenViewModel: EncryptionResetScreenViewModelType, EncryptionResetScreenViewModelProtocol {
|
||||
private let clientProxy: ClientProxyProtocol
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
|
||||
private let actionsSubject: PassthroughSubject<EncryptionResetScreenViewModelAction, Never> = .init()
|
||||
var actionsPublisher: AnyPublisher<EncryptionResetScreenViewModelAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
private var identityResetHandle: IdentityResetHandle?
|
||||
|
||||
init(clientProxy: ClientProxyProtocol, userIndicatorController: UserIndicatorControllerProtocol) {
|
||||
self.clientProxy = clientProxy
|
||||
self.userIndicatorController = userIndicatorController
|
||||
|
||||
super.init(initialViewState: EncryptionResetScreenViewState(bindings: .init()))
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: EncryptionResetScreenViewAction) {
|
||||
switch viewAction {
|
||||
case .reset:
|
||||
state.bindings.alertInfo = .init(id: UUID(),
|
||||
title: L10n.screenResetEncryptionConfirmationAlertTitle,
|
||||
message: L10n.screenResetEncryptionConfirmationAlertSubtitle,
|
||||
primaryButton: .init(title: L10n.screenResetEncryptionConfirmationAlertAction, role: .destructive, action: { [weak self] in
|
||||
guard let self else { return }
|
||||
Task { await self.startResetFlow() }
|
||||
}))
|
||||
case .cancel:
|
||||
actionsSubject.send(.cancel)
|
||||
}
|
||||
}
|
||||
|
||||
func continueResetFlowWith(password: String) {
|
||||
Task {
|
||||
await resetWith(password: password)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func startResetFlow() async {
|
||||
showLoadingIndicator()
|
||||
|
||||
defer {
|
||||
hideLoadingIndicator()
|
||||
}
|
||||
|
||||
switch await clientProxy.resetIdentity() {
|
||||
case .success(let handle):
|
||||
// If the handle is missing then interactive authentication wasn't
|
||||
// necessary and the reset proceeded as normal
|
||||
guard let handle else {
|
||||
actionsSubject.send(.resetFinished)
|
||||
return
|
||||
}
|
||||
|
||||
identityResetHandle = handle
|
||||
|
||||
switch handle.authType() {
|
||||
case .uiaa:
|
||||
actionsSubject.send(.requestPassword)
|
||||
case .oidc(let oidcInfo):
|
||||
guard let url = URL(string: oidcInfo.approvalUrl) else {
|
||||
fatalError("Invalid URL received through identity reset handle: \(oidcInfo.approvalUrl)")
|
||||
}
|
||||
|
||||
hideLoadingIndicator()
|
||||
|
||||
actionsSubject.send(.requestOIDCAuthorisation(url: url))
|
||||
|
||||
await resetWithOIDCAuthorisation()
|
||||
}
|
||||
case .failure(let error):
|
||||
MXLog.error("Failed resetting encryption with error \(error)")
|
||||
showErrorToast()
|
||||
}
|
||||
}
|
||||
|
||||
func resetWith(password: String) async {
|
||||
guard let identityResetHandle else {
|
||||
fatalError("Requested reset flow continuation without a stored handle")
|
||||
}
|
||||
|
||||
showLoadingIndicator()
|
||||
|
||||
defer {
|
||||
hideLoadingIndicator()
|
||||
}
|
||||
|
||||
do {
|
||||
try await identityResetHandle.reset(auth: .password(passwordDetails: .init(identifier: clientProxy.userID, password: password)))
|
||||
actionsSubject.send(.resetFinished)
|
||||
} catch {
|
||||
MXLog.error("Failed resetting encryption with error \(error)")
|
||||
showErrorToast()
|
||||
}
|
||||
}
|
||||
|
||||
private func resetWithOIDCAuthorisation() async {
|
||||
guard let identityResetHandle else {
|
||||
fatalError("Requested reset flow continuation without a stored handle")
|
||||
}
|
||||
|
||||
do {
|
||||
try await identityResetHandle.reset(auth: nil)
|
||||
actionsSubject.send(.resetFinished)
|
||||
} catch {
|
||||
MXLog.error("Failed resetting encryption with error \(error)")
|
||||
showErrorToast()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Toasts and loading indicators
|
||||
|
||||
private static let loadingIndicatorIdentifier = "\(EncryptionResetScreenViewModel.self)-Loading"
|
||||
|
||||
private func showLoadingIndicator() {
|
||||
userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
|
||||
type: .modal,
|
||||
title: L10n.commonLoading,
|
||||
persistent: true))
|
||||
}
|
||||
|
||||
private func hideLoadingIndicator() {
|
||||
userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier)
|
||||
}
|
||||
|
||||
private func showErrorToast() {
|
||||
userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown))
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
protocol EncryptionResetScreenViewModelProtocol {
|
||||
var actionsPublisher: AnyPublisher<EncryptionResetScreenViewModelAction, Never> { get }
|
||||
var context: EncryptionResetScreenViewModelType.Context { get }
|
||||
|
||||
func continueResetFlowWith(password: String)
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Compound
|
||||
import SwiftUI
|
||||
|
||||
struct EncryptionResetScreen: View {
|
||||
@ObservedObject var context: EncryptionResetScreenViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
FullscreenDialog {
|
||||
mainContent
|
||||
} bottomContent: {
|
||||
Button(L10n.actionContinue, role: .destructive) {
|
||||
context.send(viewAction: .reset)
|
||||
}
|
||||
.buttonStyle(.compound(.primary))
|
||||
}
|
||||
.background()
|
||||
.backgroundStyle(.compound.bgSubtleSecondary)
|
||||
.interactiveDismissDisabled()
|
||||
.toolbar { toolbar }
|
||||
.toolbar(.visible, for: .navigationBar)
|
||||
.alert(item: $context.alertInfo)
|
||||
}
|
||||
|
||||
/// The main content of the screen that is shown inside the scroll view.
|
||||
private var mainContent: some View {
|
||||
VStack(spacing: 24) {
|
||||
header
|
||||
checkmarkList
|
||||
footer
|
||||
}
|
||||
}
|
||||
|
||||
private var header: some View {
|
||||
VStack(spacing: 8) {
|
||||
HeroImage(icon: \.error, style: .critical)
|
||||
.padding(.bottom, 8)
|
||||
|
||||
Text(L10n.screenEncryptionResetTitle)
|
||||
.font(.compound.headingMDBold)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
|
||||
Text(L10n.screenEncryptionResetSubtitle)
|
||||
.font(.compound.bodyMD)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
}
|
||||
}
|
||||
|
||||
private var footer: some View {
|
||||
Text(L10n.screenEncryptionResetFooter)
|
||||
.font(.compound.bodyMDSemibold)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
}
|
||||
|
||||
/// The list of re-assurances about analytics.
|
||||
private var checkmarkList: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
checkMarkItem(title: L10n.screenEncryptionResetBullet1, position: .top, positive: true)
|
||||
checkMarkItem(title: L10n.screenEncryptionResetBullet2, position: .middle, positive: false)
|
||||
checkMarkItem(title: L10n.screenEncryptionResetBullet3, position: .bottom, positive: false)
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.frame(maxWidth: .infinity)
|
||||
.environment(\.backgroundStyle, AnyShapeStyle(.compound.bgSubtleSecondary))
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func checkMarkItem(title: String, position: ListPosition, positive: Bool) -> some View {
|
||||
RoundedLabelItem(title: title, listPosition: position) {
|
||||
if positive {
|
||||
CompoundIcon(\.check)
|
||||
.foregroundColor(.compound.iconAccentPrimary)
|
||||
} else {
|
||||
CompoundIcon(\.close)
|
||||
.foregroundColor(.compound.iconCriticalPrimary)
|
||||
}
|
||||
}
|
||||
.backgroundStyle(.compound.bgCanvasDefault)
|
||||
}
|
||||
|
||||
@ToolbarContentBuilder
|
||||
private var toolbar: some ToolbarContent {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button(L10n.actionCancel) {
|
||||
context.send(viewAction: .cancel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct EncryptionResetScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = EncryptionResetScreenViewModel(clientProxy: ClientProxyMock(.init()),
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
static var previews: some View {
|
||||
NavigationStack {
|
||||
EncryptionResetScreen(context: viewModel.context)
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-9
@@ -14,6 +14,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Compound
|
||||
import SwiftUI
|
||||
|
||||
/// A prompt that asks the user whether they would like to enable Analytics or not.
|
||||
@@ -58,14 +59,6 @@ struct AnalyticsPromptScreen: View {
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var checkMark: some View {
|
||||
Image(systemName: "checkmark.circle")
|
||||
.symbolVariant(.fill)
|
||||
.symbolRenderingMode(.palette)
|
||||
.foregroundStyle(Color.compound.iconAccentTertiary, Color.compound.textOnSolidPrimary)
|
||||
}
|
||||
|
||||
/// The list of re-assurances about analytics.
|
||||
private var checkmarkList: some View {
|
||||
@@ -82,7 +75,8 @@ struct AnalyticsPromptScreen: View {
|
||||
@ViewBuilder
|
||||
private func checkMarkItem(title: String, position: ListPosition) -> some View {
|
||||
RoundedLabelItem(title: title, listPosition: position) {
|
||||
checkMark
|
||||
CompoundIcon(\.checkCircle, size: .small, relativeTo: .body)
|
||||
.foregroundColor(.compound.iconAccentPrimary)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+11
-8
@@ -70,21 +70,21 @@ struct IdentityConfirmationScreen: View {
|
||||
|
||||
@ViewBuilder
|
||||
private var actionButtons: some View {
|
||||
VStack(spacing: 32) {
|
||||
VStack(spacing: 16) {
|
||||
if context.viewState.availableActions.contains(.interactiveVerification) {
|
||||
Button(L10n.actionStartVerification) {
|
||||
Button(L10n.screenIdentityConfirmationUseAnotherDevice) {
|
||||
context.send(viewAction: .otherDevice)
|
||||
}
|
||||
.buttonStyle(.compound(.primary))
|
||||
|
||||
if context.viewState.availableActions.contains(.recovery) {
|
||||
Button(L10n.screenSessionVerificationEnterRecoveryKey) {
|
||||
Button(L10n.screenIdentityConfirmationUseRecoveryKey) {
|
||||
context.send(viewAction: .recoveryKey)
|
||||
}
|
||||
.buttonStyle(.compound(.plain))
|
||||
.buttonStyle(.compound(.secondary))
|
||||
}
|
||||
} else if context.viewState.availableActions.contains(.recovery) {
|
||||
Button(L10n.screenSessionVerificationEnterRecoveryKey) {
|
||||
Button(L10n.screenIdentityConfirmationUseRecoveryKey) {
|
||||
context.send(viewAction: .recoveryKey)
|
||||
}
|
||||
.buttonStyle(.compound(.primary))
|
||||
@@ -97,10 +97,11 @@ struct IdentityConfirmationScreen: View {
|
||||
.buttonStyle(.compound(.plain))
|
||||
}
|
||||
|
||||
Button(L10n.screenRecoveryKeyConfirmLostRecoveryKey, role: .destructive) {
|
||||
Button(L10n.screenIdentityConfirmationCannotConfirm) {
|
||||
context.send(viewAction: .reset)
|
||||
}
|
||||
.buttonStyle(.compound(.plain))
|
||||
.padding(.vertical, 14)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,11 +113,13 @@ struct IdentityConfirmationScreen_Previews: PreviewProvider, TestablePreview {
|
||||
NavigationStack {
|
||||
IdentityConfirmationScreen(context: viewModel.context)
|
||||
}
|
||||
.snapshot(delay: 0.25)
|
||||
}
|
||||
|
||||
private static var viewModel: IdentityConfirmationScreenViewModel {
|
||||
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: "@user:example.com",
|
||||
roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded([])))))))
|
||||
let clientProxy = ClientProxyMock(.init())
|
||||
let userSession = UserSessionMock(.init(clientProxy: clientProxy))
|
||||
userSession.sessionSecurityStatePublisher = CurrentValuePublisher<SessionSecurityState, Never>(.init(verificationState: .unverified, recoveryState: .enabled))
|
||||
|
||||
return IdentityConfirmationScreenViewModel(userSession: userSession,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
|
||||
-56
@@ -1,56 +0,0 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
// periphery:ignore:all - this is just a resetKey remove this comment once generating the final file
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
enum ResetRecoveryKeyScreenCoordinatorAction {
|
||||
case cancel
|
||||
}
|
||||
|
||||
final class ResetRecoveryKeyScreenCoordinator: CoordinatorProtocol {
|
||||
private let viewModel: ResetRecoveryKeyScreenViewModelProtocol
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private let actionsSubject: PassthroughSubject<ResetRecoveryKeyScreenCoordinatorAction, Never> = .init()
|
||||
var actionsPublisher: AnyPublisher<ResetRecoveryKeyScreenCoordinatorAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init() {
|
||||
viewModel = ResetRecoveryKeyScreenViewModel()
|
||||
}
|
||||
|
||||
func start() {
|
||||
viewModel.actionsPublisher.sink { [weak self] action in
|
||||
MXLog.info("Coordinator: received view model action: \(action)")
|
||||
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .cancel:
|
||||
self.actionsSubject.send(.cancel)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(ResetRecoveryKeyScreen(context: viewModel.context))
|
||||
}
|
||||
}
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
typealias ResetRecoveryKeyScreenViewModelType = StateStoreViewModel<ResetRecoveryKeyScreenViewState, ResetRecoveryKeyScreenViewAction>
|
||||
|
||||
class ResetRecoveryKeyScreenViewModel: ResetRecoveryKeyScreenViewModelType, ResetRecoveryKeyScreenViewModelProtocol {
|
||||
private let actionsSubject: PassthroughSubject<ResetRecoveryKeyScreenViewModelAction, Never> = .init()
|
||||
var actionsPublisher: AnyPublisher<ResetRecoveryKeyScreenViewModelAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init(initialViewState: ResetRecoveryKeyScreenViewState())
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: ResetRecoveryKeyScreenViewAction) {
|
||||
switch viewAction {
|
||||
case .cancel:
|
||||
// We might also need to display first an alert and do a logOut API call in some cases
|
||||
actionsSubject.send(.cancel)
|
||||
}
|
||||
}
|
||||
}
|
||||
-75
@@ -1,75 +0,0 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Compound
|
||||
import SwiftUI
|
||||
|
||||
struct ResetRecoveryKeyScreen: View {
|
||||
@ObservedObject var context: ResetRecoveryKeyScreenViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
FullscreenDialog {
|
||||
mainContent
|
||||
} bottomContent: {
|
||||
EmptyView()
|
||||
}
|
||||
.toolbar { toolbar }
|
||||
.toolbar(.visible, for: .navigationBar)
|
||||
.background()
|
||||
.backgroundStyle(.compound.bgSubtleSecondary)
|
||||
.interactiveDismissDisabled()
|
||||
}
|
||||
}
|
||||
|
||||
private var mainContent: some View {
|
||||
VStack(spacing: 40) {
|
||||
header
|
||||
SFNumberedListView(items: context.viewState.listItems)
|
||||
}
|
||||
}
|
||||
|
||||
private var header: some View {
|
||||
VStack(spacing: 16) {
|
||||
HeroImage(icon: \.computer, style: .subtle)
|
||||
|
||||
Text(L10n.screenCreateNewRecoveryKeyTitle)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
.font(.compound.headingMDBold)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
}
|
||||
|
||||
@ToolbarContentBuilder
|
||||
private var toolbar: some ToolbarContent {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button(L10n.actionCancel) {
|
||||
context.send(viewAction: .cancel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct ResetRecoveryKeyScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = ResetRecoveryKeyScreenViewModel()
|
||||
static var previews: some View {
|
||||
NavigationStack {
|
||||
ResetRecoveryKeyScreen(context: viewModel.context)
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -28,7 +28,7 @@ enum SecureBackupRecoveryKeyScreenCoordinatorAction {
|
||||
case recoverySetUp
|
||||
case recoveryChanged
|
||||
case recoveryFixed
|
||||
case showResetKeyInfo
|
||||
case resetEncryption
|
||||
}
|
||||
|
||||
final class SecureBackupRecoveryKeyScreenCoordinator: CoordinatorProtocol {
|
||||
@@ -65,8 +65,8 @@ final class SecureBackupRecoveryKeyScreenCoordinator: CoordinatorProtocol {
|
||||
case .unknown:
|
||||
fatalError()
|
||||
}
|
||||
case .showResetKeyInfo:
|
||||
self.actionsSubject.send(.showResetKeyInfo)
|
||||
case .resetEncryption:
|
||||
self.actionsSubject.send(.resetEncryption)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
+2
-2
@@ -19,7 +19,7 @@ import Foundation
|
||||
enum SecureBackupRecoveryKeyScreenViewModelAction {
|
||||
case done(mode: SecureBackupRecoveryKeyScreenViewMode)
|
||||
case cancel
|
||||
case showResetKeyInfo
|
||||
case resetEncryption
|
||||
}
|
||||
|
||||
enum SecureBackupRecoveryKeyScreenViewMode {
|
||||
@@ -90,7 +90,7 @@ enum SecureBackupRecoveryKeyScreenViewAction {
|
||||
case copyKey
|
||||
case keySaved
|
||||
case confirmKey
|
||||
case resetKey
|
||||
case resetEncryption
|
||||
case done
|
||||
case cancel
|
||||
}
|
||||
|
||||
+22
-9
@@ -40,13 +40,12 @@ class SecureBackupRecoveryKeyScreenViewModel: SecureBackupRecoveryKeyScreenViewM
|
||||
|
||||
secureBackupController.recoveryState
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveValue: { [weak userIndicatorController] state in
|
||||
let loadingIndicatorIdentifier = "SecureBackupRecoveryKeyScreenLoading"
|
||||
.sink(receiveValue: { [weak self] state in
|
||||
switch state {
|
||||
case .settingUp:
|
||||
userIndicatorController?.submitIndicator(.init(id: loadingIndicatorIdentifier, type: .modal, title: L10n.commonLoading, persistent: true))
|
||||
self?.showLoadingIndicator()
|
||||
default:
|
||||
userIndicatorController?.retractIndicatorWithId(loadingIndicatorIdentifier)
|
||||
self?.hideLoadingIndicator()
|
||||
}
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
@@ -67,6 +66,8 @@ class SecureBackupRecoveryKeyScreenViewModel: SecureBackupRecoveryKeyScreenViewM
|
||||
MXLog.error("Failed generating recovery key with error: \(error)")
|
||||
state.bindings.alertInfo = .init(id: .init())
|
||||
}
|
||||
|
||||
hideLoadingIndicator()
|
||||
}
|
||||
case .copyKey:
|
||||
UIPasteboard.general.string = state.recoveryKey
|
||||
@@ -76,8 +77,7 @@ class SecureBackupRecoveryKeyScreenViewModel: SecureBackupRecoveryKeyScreenViewM
|
||||
state.doneButtonEnabled = true
|
||||
case .confirmKey:
|
||||
Task {
|
||||
let loadingIndicatorIdentifier = "SecureBackupRecoveryKeyScreen"
|
||||
userIndicatorController.submitIndicator(.init(id: loadingIndicatorIdentifier, type: .modal, title: L10n.commonLoading, persistent: true))
|
||||
showLoadingIndicator()
|
||||
|
||||
switch await secureBackupController.confirmRecoveryKey(state.bindings.confirmationRecoveryKey) {
|
||||
case .success:
|
||||
@@ -87,7 +87,7 @@ class SecureBackupRecoveryKeyScreenViewModel: SecureBackupRecoveryKeyScreenViewM
|
||||
state.bindings.alertInfo = .init(id: .init())
|
||||
}
|
||||
|
||||
userIndicatorController.retractIndicatorWithId(loadingIndicatorIdentifier)
|
||||
hideLoadingIndicator()
|
||||
}
|
||||
case .cancel:
|
||||
actionsSubject.send(.cancel)
|
||||
@@ -100,10 +100,23 @@ class SecureBackupRecoveryKeyScreenViewModel: SecureBackupRecoveryKeyScreenViewM
|
||||
guard let self else { return }
|
||||
actionsSubject.send(.done(mode: context.viewState.mode))
|
||||
}))
|
||||
case .resetKey:
|
||||
actionsSubject.send(.showResetKeyInfo)
|
||||
case .resetEncryption:
|
||||
actionsSubject.send(.resetEncryption)
|
||||
}
|
||||
}
|
||||
|
||||
private static let loadingIndicatorIdentifier = "\(SecureBackupRecoveryKeyScreenViewModel.self)-Loading"
|
||||
|
||||
private func showLoadingIndicator() {
|
||||
userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
|
||||
type: .modal,
|
||||
title: L10n.commonLoading,
|
||||
persistent: true))
|
||||
}
|
||||
|
||||
private func hideLoadingIndicator() {
|
||||
userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier)
|
||||
}
|
||||
}
|
||||
|
||||
extension SecureBackupRecoveryState {
|
||||
|
||||
+1
-4
@@ -101,7 +101,7 @@ struct SecureBackupRecoveryKeyScreen: View {
|
||||
.disabled(context.confirmationRecoveryKey.isEmpty)
|
||||
|
||||
Button {
|
||||
context.send(viewAction: .resetKey)
|
||||
context.send(viewAction: .resetEncryption)
|
||||
} label: {
|
||||
Text(L10n.screenIdentityConfirmationCreateNewRecoveryKey)
|
||||
.padding(.vertical, 14)
|
||||
@@ -205,9 +205,6 @@ struct SecureBackupRecoveryKeyScreen: View {
|
||||
.font(.compound.bodySMSemibold)
|
||||
|
||||
SecureField(L10n.screenRecoveryKeyConfirmKeyPlaceholder, text: $context.confirmationRecoveryKey)
|
||||
.textContentType(.password) // Not ideal but stops random suggestions
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(Color.compound.bgSubtleSecondaryLevel0)
|
||||
|
||||
+39
-14
@@ -19,20 +19,30 @@ import SwiftUI
|
||||
|
||||
struct SecureBackupScreenCoordinatorParameters {
|
||||
let appSettings: AppSettings
|
||||
let secureBackupController: SecureBackupControllerProtocol
|
||||
let clientProxy: ClientProxyProtocol
|
||||
weak var navigationStackCoordinator: NavigationStackCoordinator?
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
}
|
||||
|
||||
enum SecureBackupScreenCoordinatorAction {
|
||||
case requestOIDCAuthorisation(URL)
|
||||
}
|
||||
|
||||
final class SecureBackupScreenCoordinator: CoordinatorProtocol {
|
||||
private let parameters: SecureBackupScreenCoordinatorParameters
|
||||
private var viewModel: SecureBackupScreenViewModelProtocol
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private let actionsSubject: PassthroughSubject<SecureBackupScreenCoordinatorAction, Never> = .init()
|
||||
var actions: AnyPublisher<SecureBackupScreenCoordinatorAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(parameters: SecureBackupScreenCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
viewModel = SecureBackupScreenViewModel(secureBackupController: parameters.secureBackupController,
|
||||
viewModel = SecureBackupScreenViewModel(secureBackupController: parameters.clientProxy.secureBackupController,
|
||||
userIndicatorController: parameters.userIndicatorController,
|
||||
chatBackupDetailsURL: parameters.appSettings.chatBackupDetailsURL)
|
||||
}
|
||||
@@ -43,9 +53,9 @@ final class SecureBackupScreenCoordinator: CoordinatorProtocol {
|
||||
|
||||
switch action {
|
||||
case .recoveryKey:
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
let recoveryNavigationStackCoordinator = NavigationStackCoordinator()
|
||||
|
||||
let recoveryKeyCoordinator = SecureBackupRecoveryKeyScreenCoordinator(parameters: .init(secureBackupController: parameters.secureBackupController,
|
||||
let recoveryKeyCoordinator = SecureBackupRecoveryKeyScreenCoordinator(parameters: .init(secureBackupController: parameters.clientProxy.secureBackupController,
|
||||
userIndicatorController: parameters.userIndicatorController,
|
||||
isModallyPresented: true))
|
||||
|
||||
@@ -63,19 +73,19 @@ final class SecureBackupScreenCoordinator: CoordinatorProtocol {
|
||||
case .recoveryFixed:
|
||||
showSuccessIndicator(title: L10n.screenRecoveryKeyConfirmSuccess)
|
||||
parameters.navigationStackCoordinator?.setSheetCoordinator(nil)
|
||||
case .showResetKeyInfo:
|
||||
showResetRecoveryKeyScreen(navigationStackCoordinator: navigationStackCoordinator)
|
||||
case .resetEncryption:
|
||||
showEncryptionReset(recoveryNavigationStackCoordinator: recoveryNavigationStackCoordinator)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
navigationStackCoordinator.setRootCoordinator(recoveryKeyCoordinator, animated: true)
|
||||
recoveryNavigationStackCoordinator.setRootCoordinator(recoveryKeyCoordinator, animated: true)
|
||||
|
||||
parameters.navigationStackCoordinator?.setSheetCoordinator(navigationStackCoordinator)
|
||||
parameters.navigationStackCoordinator?.setSheetCoordinator(recoveryNavigationStackCoordinator)
|
||||
case .keyBackup:
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
|
||||
let keyBackupCoordinator = SecureBackupKeyBackupScreenCoordinator(parameters: .init(secureBackupController: parameters.secureBackupController,
|
||||
let keyBackupCoordinator = SecureBackupKeyBackupScreenCoordinator(parameters: .init(secureBackupController: parameters.clientProxy.secureBackupController,
|
||||
userIndicatorController: parameters.userIndicatorController))
|
||||
|
||||
keyBackupCoordinator.actions.sink { [weak self] action in
|
||||
@@ -108,15 +118,30 @@ final class SecureBackupScreenCoordinator: CoordinatorProtocol {
|
||||
persistent: false))
|
||||
}
|
||||
|
||||
private func showResetRecoveryKeyScreen(navigationStackCoordinator: NavigationStackCoordinator) {
|
||||
let coordinator = ResetRecoveryKeyScreenCoordinator()
|
||||
coordinator.actionsPublisher.sink { action in
|
||||
private func showEncryptionReset(recoveryNavigationStackCoordinator: NavigationStackCoordinator) {
|
||||
let resetNavigationStackCoordinator = NavigationStackCoordinator()
|
||||
|
||||
let coordinator = EncryptionResetScreenCoordinator(parameters: .init(clientProxy: parameters.clientProxy,
|
||||
navigationStackCoordinator: resetNavigationStackCoordinator,
|
||||
userIndicatorController: parameters.userIndicatorController))
|
||||
|
||||
coordinator.actionsPublisher.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .cancel:
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
recoveryNavigationStackCoordinator.setSheetCoordinator(nil)
|
||||
case .requestOIDCAuthorisation(let url):
|
||||
actionsSubject.send(.requestOIDCAuthorisation(url))
|
||||
case .resetFinished:
|
||||
parameters.navigationStackCoordinator?.setSheetCoordinator(nil) // Dismiss the recovery screen
|
||||
recoveryNavigationStackCoordinator.setSheetCoordinator(nil)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
navigationStackCoordinator.setSheetCoordinator(coordinator)
|
||||
|
||||
resetNavigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
|
||||
recoveryNavigationStackCoordinator.setSheetCoordinator(resetNavigationStackCoordinator)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,12 +42,12 @@ final class SettingsScreenCoordinator: CoordinatorProtocol {
|
||||
private var viewModel: SettingsScreenViewModelProtocol
|
||||
|
||||
private let actionsSubject: PassthroughSubject<SettingsScreenCoordinatorAction, Never> = .init()
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
var actions: AnyPublisher<SettingsScreenCoordinatorAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(parameters: SettingsScreenCoordinatorParameters) {
|
||||
|
||||
@@ -883,7 +883,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Encryption
|
||||
// MARK: - Crypto
|
||||
|
||||
func ed25519Base64() async -> String? {
|
||||
await client.encryption().ed25519Key()
|
||||
@@ -892,6 +892,14 @@ class ClientProxy: ClientProxyProtocol {
|
||||
func curve25519Base64() async -> String? {
|
||||
await client.encryption().curve25519Key()
|
||||
}
|
||||
|
||||
func resetIdentity() async -> Result<IdentityResetHandle?, ClientProxyError> {
|
||||
do {
|
||||
return try await .success(client.encryption().resetIdentity())
|
||||
} catch {
|
||||
return .failure(.sdkError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ClientProxy: MediaLoaderProtocol {
|
||||
|
||||
@@ -190,8 +190,10 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
|
||||
|
||||
func recentConversationCounterparts() async -> [UserProfileProxy]
|
||||
|
||||
// MARK: - Encryption Info
|
||||
// MARK: - Crypto
|
||||
|
||||
func ed25519Base64() async -> String?
|
||||
func curve25519Base64() async -> String?
|
||||
|
||||
func resetIdentity() async -> Result<IdentityResetHandle?, ClientProxyError>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user