diff --git a/CHANGES.rst b/CHANGES.rst index f53440bda..40f773e6f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,6 +9,7 @@ Improvements: * Clean up iOS version checking (#2190). * Key backup: Implement setup screen (#2198). * Key backup: Implement recover screen (#2196). + * Key backup: Add a dedicated section to settings (#2193). * Key backup: Implement setup reminder (#2211). Bug fix: diff --git a/Riot.xcodeproj/project.pbxproj b/Riot.xcodeproj/project.pbxproj index f8a9afc02..d67a06dfb 100644 --- a/Riot.xcodeproj/project.pbxproj +++ b/Riot.xcodeproj/project.pbxproj @@ -25,8 +25,14 @@ 3233F7461F3497E2006ACA81 /* JitsiMeet.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3233F7441F3497DA006ACA81 /* JitsiMeet.framework */; }; 3233F7471F3497E2006ACA81 /* JitsiMeet.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3233F7441F3497DA006ACA81 /* JitsiMeet.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 3275FD8C21A5A2C500B9C13D /* TermsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3275FD8B21A5A2C500B9C13D /* TermsView.swift */; }; + 3281BCF72201FA4200F4A383 /* UIControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3281BCF62201FA4200F4A383 /* UIControl.swift */; }; 3284A35120A07C210044F922 /* postMessageAPI.js in Resources */ = {isa = PBXBuildFile; fileRef = 3284A35020A07C210044F922 /* postMessageAPI.js */; }; 32B1FEDB21A46F2C00637127 /* TermsView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 32B1FEDA21A46F2C00637127 /* TermsView.xib */; }; + 32BF994F21FA29A400698084 /* SettingsKeyBackupViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32BF994E21FA29A400698084 /* SettingsKeyBackupViewModel.swift */; }; + 32BF995121FA29DC00698084 /* SettingsKeyBackupViewModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32BF995021FA29DC00698084 /* SettingsKeyBackupViewModelType.swift */; }; + 32BF995321FA2A1300698084 /* SettingsKeyBackupViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32BF995221FA2A1300698084 /* SettingsKeyBackupViewState.swift */; }; + 32BF995521FA2AB700698084 /* SettingsKeyBackupViewAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32BF995421FA2AB700698084 /* SettingsKeyBackupViewAction.swift */; }; + 32BF995721FB07A400698084 /* SettingsKeyBackupTableViewSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32BF995621FB07A400698084 /* SettingsKeyBackupTableViewSection.swift */; }; 358DB9429359F97520545D35 /* Pods_RiotPods_RiotShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F5856BA7A55E53C0AEAFC084 /* Pods_RiotPods_RiotShareExtension.framework */; }; 89C94E649229EA68AE787E9E /* Pods_RiotPods_Riot.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2510A69B4A681C1FEC36E848 /* Pods_RiotPods_Riot.framework */; }; 926FA53F1F4C132000F826C2 /* MXSession+Riot.m in Sources */ = {isa = PBXBuildFile; fileRef = 926FA53E1F4C132000F826C2 /* MXSession+Riot.m */; }; @@ -472,6 +478,7 @@ 3267EFB520E379FD00FF1CAA /* AUTHORS.rst */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = AUTHORS.rst; sourceTree = ""; }; 3267EFB620E379FD00FF1CAA /* README.rst */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = README.rst; sourceTree = ""; }; 3275FD8B21A5A2C500B9C13D /* TermsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsView.swift; sourceTree = ""; }; + 3281BCF62201FA4200F4A383 /* UIControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIControl.swift; sourceTree = ""; }; 3284A35020A07C210044F922 /* postMessageAPI.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = postMessageAPI.js; sourceTree = ""; }; 32B1FEDA21A46F2C00637127 /* TermsView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TermsView.xib; sourceTree = ""; }; 32BDC9A1211C2C870064AF51 /* zh_Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh_Hant; path = zh_Hant.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -480,6 +487,11 @@ 32BDC9A4211C34C90064AF51 /* sq */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sq; path = sq.lproj/InfoPlist.strings; sourceTree = ""; }; 32BDC9A5211C34C90064AF51 /* sq */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sq; path = sq.lproj/Localizable.strings; sourceTree = ""; }; 32BDC9A6211C34C90064AF51 /* sq */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sq; path = sq.lproj/Vector.strings; sourceTree = ""; }; + 32BF994E21FA29A400698084 /* SettingsKeyBackupViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsKeyBackupViewModel.swift; sourceTree = ""; }; + 32BF995021FA29DC00698084 /* SettingsKeyBackupViewModelType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsKeyBackupViewModelType.swift; sourceTree = ""; }; + 32BF995221FA2A1300698084 /* SettingsKeyBackupViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsKeyBackupViewState.swift; sourceTree = ""; }; + 32BF995421FA2AB700698084 /* SettingsKeyBackupViewAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsKeyBackupViewAction.swift; sourceTree = ""; }; + 32BF995621FB07A400698084 /* SettingsKeyBackupTableViewSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsKeyBackupTableViewSection.swift; sourceTree = ""; }; 32D7159E2146CC6F00DF59C9 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Vector.strings; sourceTree = ""; }; 32D7159F2146CC7F00DF59C9 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; 32D715A02146CC8800DF59C9 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -1166,6 +1178,18 @@ path = js; sourceTree = ""; }; + 32BF994D21FA1C6300698084 /* KeyBackup */ = { + isa = PBXGroup; + children = ( + 32BF994E21FA29A400698084 /* SettingsKeyBackupViewModel.swift */, + 32BF995021FA29DC00698084 /* SettingsKeyBackupViewModelType.swift */, + 32BF995221FA2A1300698084 /* SettingsKeyBackupViewState.swift */, + 32BF995421FA2AB700698084 /* SettingsKeyBackupViewAction.swift */, + 32BF995621FB07A400698084 /* SettingsKeyBackupTableViewSection.swift */, + ); + path = KeyBackup; + sourceTree = ""; + }; 5FC42FA41F5186AFFB6A2404 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -1624,6 +1648,7 @@ B1B5567F20EE6C4C00210D55 /* SettingsViewController.h */, B1B5567E20EE6C4C00210D55 /* SettingsViewController.m */, B1B5578120EF564900210D55 /* Views */, + 32BF994D21FA1C6300698084 /* KeyBackup */, B1B5567B20EE6C4C00210D55 /* Language */, B1B5567820EE6C4C00210D55 /* PhoneCountry */, B1B5568020EE6C4C00210D55 /* DeactivateAccount */, @@ -2826,6 +2851,7 @@ B1CA3A2821EF692B000D1D89 /* UIView.swift */, B140B4A121F87F7100E3F5FE /* OperationQueue.swift */, B1E5368821FB1E20001F3AFF /* UIButton.swift */, + 3281BCF62201FA4200F4A383 /* UIControl.swift */, ); path = Categories; sourceTree = ""; @@ -3451,6 +3477,7 @@ B1B5598520EFC3E000210D55 /* RageShakeManager.m in Sources */, B1B558D420EF768F00210D55 /* RoomOutgoingEncryptedTextMsgWithoutSenderInfoBubbleCell.m in Sources */, B169331420F3CAFC00746532 /* PublicRoomTableViewCell.m in Sources */, + 32BF995721FB07A400698084 /* SettingsKeyBackupTableViewSection.swift in Sources */, B1B558E120EF768F00210D55 /* RoomMembershipCollapsedBubbleCell.m in Sources */, B1B5571A20EE6C4D00210D55 /* SettingsViewController.m in Sources */, B1B5594720EF7BD000210D55 /* RoomCollectionViewCell.m in Sources */, @@ -3602,8 +3629,11 @@ B1B558D320EF768F00210D55 /* RoomOutgoingEncryptedTextMsgBubbleCell.m in Sources */, B1B5576F20EE702800210D55 /* IntegrationManagerViewController.m in Sources */, B1B557AC20EF5A6D00210D55 /* DeviceView.m in Sources */, + 3281BCF72201FA4200F4A383 /* UIControl.swift in Sources */, B16932EE20F3C3C900746532 /* FilesSearchCellData.m in Sources */, B1B558E520EF768F00210D55 /* RoomMembershipExpandedBubbleCell.m in Sources */, + 32BF995121FA29DC00698084 /* SettingsKeyBackupViewModelType.swift in Sources */, + 32BF995321FA2A1300698084 /* SettingsKeyBackupViewState.swift in Sources */, B1B5599420EFC5E400210D55 /* DecryptionFailureTracker.m in Sources */, F083BDF01E7009ED00A9B29C /* UIViewController+RiotSearch.m in Sources */, B139C22D21FF32F000BB68EC /* KeyBackupRecoverFromRecoveryKeyViewState.swift in Sources */, @@ -3611,6 +3641,7 @@ F083BDF91E7009ED00A9B29C /* RoomEmailInvitation.m in Sources */, B1B5572C20EE6C4D00210D55 /* RoomParticipantsViewController.m in Sources */, B1B558EE20EF768F00210D55 /* RoomOutgoingAttachmentBubbleCell.m in Sources */, + 32BF994F21FA29A400698084 /* SettingsKeyBackupViewModel.swift in Sources */, B1B5574920EE6C4D00210D55 /* RiotSplitViewController.m in Sources */, B1B5574E20EE6C4D00210D55 /* DirectoryServerPickerViewController.m in Sources */, B1B5575B20EE6C4D00210D55 /* HomeFilesSearchViewController.m in Sources */, @@ -3622,6 +3653,7 @@ B16932E720F3C37100746532 /* HomeMessagesSearchDataSource.m in Sources */, B1B558CE20EF768F00210D55 /* RoomOutgoingEncryptedAttachmentBubbleCell.m in Sources */, B1B5577D20EE84BF00210D55 /* CircleButton.m in Sources */, + 32BF995521FA2AB700698084 /* SettingsKeyBackupViewAction.swift in Sources */, B1B558FF20EF768F00210D55 /* RoomIncomingTextMsgBubbleCell.m in Sources */, B1098C0021ECFE65000DDA48 /* KeyBackupSetupPassphraseViewController.swift in Sources */, B1B5591020EF782800210D55 /* TableViewCellWithPhoneNumberTextField.m in Sources */, diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 8732cbf15..3f90b3a44 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -335,6 +335,7 @@ "settings_flair" = "Show flair where allowed"; "settings_devices" = "DEVICES"; "settings_cryptography" = "CRYPTOGRAPHY"; +"settings_key_backup" = "SECURE MESSAGE RECOVERY"; "settings_deactivate_account" = "DEACTIVATE ACCOUNT"; "settings_sign_out" = "Sign Out"; @@ -420,6 +421,31 @@ "settings_deactivate_my_account" = "Deactivate my account"; +"settings_key_backup_info_checking" = "Checking..."; +"settings_key_backup_info_none" = "Secure Message Recovery has not been set up."; +"settings_key_backup_info_version" = "Key Backup Version: %@"; +"settings_key_backup_info_algorithm" = "Algorithm: %@"; +"settings_key_backup_info_valid" = "Secure Message Recovery has been correctly set up for this device."; +"settings_key_backup_info_not_valid" = "Secure Message Recovery is not active on this device."; +"settings_key_backup_info_progress" = "Backing up %@ keys..."; +"settings_key_backup_info_progress_done" = "All keys have been backed up"; +"settings_key_backup_info_not_trusted_from_verifiable_device_fix_action" = "To use Secure Message Recovery on this device, verify %@ now."; +"settings_key_backup_info_not_trusted_fix_action" = "To use Secure Message Recovery on this device, provide your passphrase or recovery key now."; + +"settings_key_backup_info_trust_signature_unknown" = "Backup has a signature from device with ID: %@"; +"settings_key_backup_info_trust_signature_valid" = "Backup has a valid signature from this device"; +"settings_key_backup_info_trust_signature_valid_device_verified" = "Backup has a valid signature from %@"; +"settings_key_backup_info_trust_signature_valid_device_unverified" = "Backup has a signature from %@"; +"settings_key_backup_info_trust_signature_invalid_device_verified" = "Backup has an invalid signature from %@"; +"settings_key_backup_info_trust_signature_invalid_device_unverified" = "Backup has an invalid signature from %@"; + +"settings_key_backup_button_create" = "Set up Secure Message Recovery"; +"settings_key_backup_button_restore" = "Restore backup"; +"settings_key_backup_button_delete" = "Delete backup"; +"settings_key_backup_button_verify" = "Verify"; +"settings_key_backup_delete_confirmation_prompt_title" = "Delete Backup"; +"settings_key_backup_delete_confirmation_prompt_msg" = "Delete your backed up encryption keys from the server? You will no longer be able to use your recovery key to read encrypted message history"; + // Room Details "room_details_title" = "Room Details"; "room_details_people" = "Members"; diff --git a/Riot/Categories/UIControl.swift b/Riot/Categories/UIControl.swift new file mode 100644 index 000000000..a6323581a --- /dev/null +++ b/Riot/Categories/UIControl.swift @@ -0,0 +1,38 @@ +/* + Copyright 2019 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 UIKit + +// Source: https://stackoverflow.com/a/44917661 +class ClosureSleeve { + let closure: () -> () + + init(attachTo: AnyObject, closure: @escaping () -> ()) { + self.closure = closure + objc_setAssociatedObject(attachTo, "[\(arc4random())]", self, .OBJC_ASSOCIATION_RETAIN) + } + + @objc func invoke() { + closure() + } +} + +extension UIControl { + func vc_addAction(for controlEvents: UIControlEvents = .primaryActionTriggered, action: @escaping () -> ()) { + let sleeve = ClosureSleeve(attachTo: self, closure: action) + addTarget(sleeve, action: #selector(ClosureSleeve.invoke), for: controlEvents) + } +} diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 4603d84d3..2b3911452 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -1926,6 +1926,98 @@ internal enum VectorL10n { internal static var settingsIgnoredUsers: String { return VectorL10n.tr("Vector", "settings_ignored_users") } + /// SECURE MESSAGE RECOVERY + internal static var settingsKeyBackup: String { + return VectorL10n.tr("Vector", "settings_key_backup") + } + /// Set up Secure Message Recovery + internal static var settingsKeyBackupButtonCreate: String { + return VectorL10n.tr("Vector", "settings_key_backup_button_create") + } + /// Delete backup + internal static var settingsKeyBackupButtonDelete: String { + return VectorL10n.tr("Vector", "settings_key_backup_button_delete") + } + /// Restore backup + internal static var settingsKeyBackupButtonRestore: String { + return VectorL10n.tr("Vector", "settings_key_backup_button_restore") + } + /// Verify + internal static var settingsKeyBackupButtonVerify: String { + return VectorL10n.tr("Vector", "settings_key_backup_button_verify") + } + /// Delete your backed up encryption keys from the server? You will no longer be able to use your recovery key to read encrypted message history + internal static var settingsKeyBackupDeleteConfirmationPromptMsg: String { + return VectorL10n.tr("Vector", "settings_key_backup_delete_confirmation_prompt_msg") + } + /// Delete Backup + internal static var settingsKeyBackupDeleteConfirmationPromptTitle: String { + return VectorL10n.tr("Vector", "settings_key_backup_delete_confirmation_prompt_title") + } + /// Algorithm: %@ + internal static func settingsKeyBackupInfoAlgorithm(_ p1: String) -> String { + return VectorL10n.tr("Vector", "settings_key_backup_info_algorithm", p1) + } + /// Checking... + internal static var settingsKeyBackupInfoChecking: String { + return VectorL10n.tr("Vector", "settings_key_backup_info_checking") + } + /// Secure Message Recovery has not been set up. + internal static var settingsKeyBackupInfoNone: String { + return VectorL10n.tr("Vector", "settings_key_backup_info_none") + } + /// To use Secure Message Recovery on this device, provide your passphrase or recovery key now. + internal static var settingsKeyBackupInfoNotTrustedFixAction: String { + return VectorL10n.tr("Vector", "settings_key_backup_info_not_trusted_fix_action") + } + /// To use Secure Message Recovery on this device, verify %@ now. + internal static func settingsKeyBackupInfoNotTrustedFromVerifiableDeviceFixAction(_ p1: String) -> String { + return VectorL10n.tr("Vector", "settings_key_backup_info_not_trusted_from_verifiable_device_fix_action", p1) + } + /// Secure Message Recovery is not active on this device. + internal static var settingsKeyBackupInfoNotValid: String { + return VectorL10n.tr("Vector", "settings_key_backup_info_not_valid") + } + /// Backing up %@ keys... + internal static func settingsKeyBackupInfoProgress(_ p1: String) -> String { + return VectorL10n.tr("Vector", "settings_key_backup_info_progress", p1) + } + /// All keys have been backed up + internal static var settingsKeyBackupInfoProgressDone: String { + return VectorL10n.tr("Vector", "settings_key_backup_info_progress_done") + } + /// Backup has an invalid signature from %@ + internal static func settingsKeyBackupInfoTrustSignatureInvalidDeviceUnverified(_ p1: String) -> String { + return VectorL10n.tr("Vector", "settings_key_backup_info_trust_signature_invalid_device_unverified", p1) + } + /// Backup has an invalid signature from %@ + internal static func settingsKeyBackupInfoTrustSignatureInvalidDeviceVerified(_ p1: String) -> String { + return VectorL10n.tr("Vector", "settings_key_backup_info_trust_signature_invalid_device_verified", p1) + } + /// Backup has a signature from device with ID: %@ + internal static func settingsKeyBackupInfoTrustSignatureUnknown(_ p1: String) -> String { + return VectorL10n.tr("Vector", "settings_key_backup_info_trust_signature_unknown", p1) + } + /// Backup has a valid signature from this device + internal static var settingsKeyBackupInfoTrustSignatureValid: String { + return VectorL10n.tr("Vector", "settings_key_backup_info_trust_signature_valid") + } + /// Backup has a signature from %@ + internal static func settingsKeyBackupInfoTrustSignatureValidDeviceUnverified(_ p1: String) -> String { + return VectorL10n.tr("Vector", "settings_key_backup_info_trust_signature_valid_device_unverified", p1) + } + /// Backup has a valid signature from %@ + internal static func settingsKeyBackupInfoTrustSignatureValidDeviceVerified(_ p1: String) -> String { + return VectorL10n.tr("Vector", "settings_key_backup_info_trust_signature_valid_device_verified", p1) + } + /// Secure Message Recovery has been correctly set up for this device. + internal static var settingsKeyBackupInfoValid: String { + return VectorL10n.tr("Vector", "settings_key_backup_info_valid") + } + /// Key Backup Version: %@ + internal static func settingsKeyBackupInfoVersion(_ p1: String) -> String { + return VectorL10n.tr("Vector", "settings_key_backup_info_version", p1) + } /// LABS internal static var settingsLabs: String { return VectorL10n.tr("Vector", "settings_labs") diff --git a/Riot/Modules/KeyBackup/Setup/KeyBackupSetupCoordinatorBridgePresenter.swift b/Riot/Modules/KeyBackup/Setup/KeyBackupSetupCoordinatorBridgePresenter.swift index fc38b477e..6a0fdb48e 100644 --- a/Riot/Modules/KeyBackup/Setup/KeyBackupSetupCoordinatorBridgePresenter.swift +++ b/Riot/Modules/KeyBackup/Setup/KeyBackupSetupCoordinatorBridgePresenter.swift @@ -72,6 +72,6 @@ extension KeyBackupSetupCoordinatorBridgePresenter: KeyBackupSetupCoordinatorDel } func keyBackupSetupCoordinatorDidSetupRecoveryKey(_ keyBackupSetupCoordinator: KeyBackupSetupCoordinatorType) { - self.delegate?.keyBackupSetupCoordinatorBridgePresenterDelegateDidCancel(self) + self.delegate?.keyBackupSetupCoordinatorBridgePresenterDelegateDidSetupRecoveryKey(self) } } diff --git a/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseCoordinator.swift b/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseCoordinator.swift index 9b2d74f13..704c0b614 100644 --- a/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseCoordinator.swift +++ b/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseCoordinator.swift @@ -38,8 +38,7 @@ final class KeyBackupSetupPassphraseCoordinator: KeyBackupSetupPassphraseCoordin init(session: MXSession) { self.session = session - let keyBackup = MXKeyBackup(matrixSession: session) - let keyBackupSetupPassphraseViewModel = KeyBackupSetupPassphraseViewModel(keyBackup: keyBackup) + let keyBackupSetupPassphraseViewModel = KeyBackupSetupPassphraseViewModel(keyBackup: self.session.crypto.backup) let keyBackupSetupPassphraseViewController = KeyBackupSetupPassphraseViewController.instantiate(with: keyBackupSetupPassphraseViewModel) self.keyBackupSetupPassphraseViewModel = keyBackupSetupPassphraseViewModel self.keyBackupSetupPassphraseViewController = keyBackupSetupPassphraseViewController diff --git a/Riot/Modules/KeyBackup/Setup/RecoveryKey/KeyBackupSetupRecoveryKeyCoordinator.swift b/Riot/Modules/KeyBackup/Setup/RecoveryKey/KeyBackupSetupRecoveryKeyCoordinator.swift index fdc2104b2..6039a24dd 100644 --- a/Riot/Modules/KeyBackup/Setup/RecoveryKey/KeyBackupSetupRecoveryKeyCoordinator.swift +++ b/Riot/Modules/KeyBackup/Setup/RecoveryKey/KeyBackupSetupRecoveryKeyCoordinator.swift @@ -35,8 +35,7 @@ final class KeyBackupSetupRecoveryKeyCoordinator: KeyBackupSetupRecoveryKeyCoord // MARK: - Setup init(session: MXSession, megolmBackupCreationInfo: MXMegolmBackupCreationInfo) { - let keyBackup = MXKeyBackup(matrixSession: session) - let keyBackupSetupRecoveryKeyViewModel = KeyBackupSetupRecoveryKeyViewModel(keyBackup: keyBackup, megolmBackupCreationInfo: megolmBackupCreationInfo) + let keyBackupSetupRecoveryKeyViewModel = KeyBackupSetupRecoveryKeyViewModel(keyBackup: session.crypto.backup, megolmBackupCreationInfo: megolmBackupCreationInfo) let keyBackupSetupRecoveryKeyViewController = KeyBackupSetupRecoveryKeyViewController.instantiate(with: keyBackupSetupRecoveryKeyViewModel) self.keyBackupSetupRecoveryKeyViewModel = keyBackupSetupRecoveryKeyViewModel self.keyBackupSetupRecoveryKeyViewController = keyBackupSetupRecoveryKeyViewController diff --git a/Riot/Modules/Settings/KeyBackup/SettingsKeyBackupTableViewSection.swift b/Riot/Modules/Settings/KeyBackup/SettingsKeyBackupTableViewSection.swift new file mode 100644 index 000000000..3bed6ca45 --- /dev/null +++ b/Riot/Modules/Settings/KeyBackup/SettingsKeyBackupTableViewSection.swift @@ -0,0 +1,525 @@ +/* + Copyright 2019 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 UIKit + +@objc protocol SettingsKeyBackupTableViewSectionDelegate: class { + func settingsKeyBackupTableViewSectionDidUpdate(_ settingsKeyBackupTableViewSection: SettingsKeyBackupTableViewSection) + + func settingsKeyBackupTableViewSection(_ settingsKeyBackupTableViewSection: SettingsKeyBackupTableViewSection, textCellForRow: Int) -> MXKTableViewCellWithTextView + func settingsKeyBackupTableViewSection(_ settingsKeyBackupTableViewSection: SettingsKeyBackupTableViewSection, buttonCellForRow: Int) -> MXKTableViewCellWithButton + + + func settingsKeyBackupTableViewSectionShowKeyBackupSetup(_ settingsKeyBackupTableViewSection: SettingsKeyBackupTableViewSection) + func settingsKeyBackup(_ settingsKeyBackupTableViewSection: SettingsKeyBackupTableViewSection, showVerifyDevice deviceId:String) + func settingsKeyBackup(_ settingsKeyBackupTableViewSection: SettingsKeyBackupTableViewSection, showKeyBackupRecover keyBackupVersion:MXKeyBackupVersion) + func settingsKeyBackup(_ settingsKeyBackupTableViewSection: SettingsKeyBackupTableViewSection, showKeyBackupDeleteConfirm keyBackupVersion:MXKeyBackupVersion) + + func settingsKeyBackup(_ settingsKeyBackupTableViewSection: SettingsKeyBackupTableViewSection, showActivityIndicator show:Bool) + func settingsKeyBackup(_ settingsKeyBackupTableViewSection: SettingsKeyBackupTableViewSection, showError error:Error) +} + +@objc final class SettingsKeyBackupTableViewSection: NSObject { + + // MARK: - Properties + + @objc weak var delegate: SettingsKeyBackupTableViewSectionDelegate? + + // MARK: Private + + // This view class holds the model because the model is in pure Swift + // whereas this class can be used from objC + private var viewModel: SettingsKeyBackupViewModelType! + + // Need to know the state to make `cellForRow` deliver cells accordingly + private var viewState: SettingsKeyBackupViewState = .checkingBackup + + private var userDevice: MXDeviceInfo + + // MARK: - Public + + @objc init(withKeyBackup keyBackup: MXKeyBackup, userDevice: MXDeviceInfo) { + self.viewModel = SettingsKeyBackupViewModel(keyBackup: keyBackup) + self.userDevice = userDevice + super.init() + self.viewModel.viewDelegate = self + + self.viewModel.process(viewAction: .load) + } + + @objc func numberOfRows() -> Int { + var numberOfRows: Int + + switch self.viewState { + case .checkingBackup: + numberOfRows = self.numberOfCheckingBackupRows() + case .noBackup: + numberOfRows = self.numberOfNoBackupRows() + case .backup: + numberOfRows = self.numberOfBackupRows() + case .backupAndRunning: + numberOfRows = self.numberOfBackupAndRunningRows() + case .backupNotTrusted: + numberOfRows = self.numberOfBackupNotTrustedRows() + } + + return numberOfRows + } + + @objc func cellForRow(atRow row:Int) -> UITableViewCell { + var cell: UITableViewCell + + switch self.viewState { + case .checkingBackup: + cell = self.renderCheckingBackupCell(atRow:row) + + case .noBackup: + cell = self.renderNoBackupCell(atRow:row) + + case .backup(let keyBackupVersion, let keyBackupVersionTrust): + cell = self.renderBackupCell(atRow: row, + keyBackupVersion: keyBackupVersion, + keyBackupVersionTrust: keyBackupVersionTrust) + + case .backupAndRunning(let keyBackupVersion, let keyBackupVersionTrust, let backupProgress): + cell = self.renderBackupAndRunningCell(atRow:row, + keyBackupVersion: keyBackupVersion, + keyBackupVersionTrust: keyBackupVersionTrust, + backupProgress: backupProgress) + + case .backupNotTrusted(let keyBackupVersion, let keyBackupVersionTrust): + cell = self.renderBackupNotTrustedCell(atRow:row, + keyBackupVersion: keyBackupVersion, + keyBackupVersionTrust: keyBackupVersionTrust) + } + + return cell + } + + @objc func reload() { + self.viewModel.process(viewAction: .load) + } + + @objc func delete(keyBackupVersion: MXKeyBackupVersion) { + self.viewModel.process(viewAction: .delete(keyBackupVersion)) + } + + + // MARK: - Pseudo TableView datasource + + private func numberOfCheckingBackupRows() -> Int { + return 1 + } + + private func renderCheckingBackupCell(atRow row: Int) -> UITableViewCell { + guard let delegate = self.delegate else { + return UITableViewCell.init() + } + + let cell: MXKTableViewCellWithTextView = delegate.settingsKeyBackupTableViewSection(self, textCellForRow: row) + cell.mxkTextView.text = VectorL10n.settingsKeyBackupInfoChecking + + return cell + } + + + private func numberOfNoBackupRows() -> Int { + return 2 + } + + private func renderNoBackupCell(atRow row: Int) -> UITableViewCell { + guard let delegate = self.delegate else { + return UITableViewCell.init() + } + + var cell: UITableViewCell + switch row { + case 0: + let infoCell: MXKTableViewCellWithTextView = delegate.settingsKeyBackupTableViewSection(self, textCellForRow: row) + infoCell.mxkTextView.text = VectorL10n.settingsKeyBackupInfoNone + + cell = infoCell + + case 1: + cell = self.buttonCellForCreate(atRow: row) + + default: + cell = UITableViewCell.init() + } + + return cell + } + + + private func numberOfBackupRows() -> Int { + return 5 + } + + private func renderBackupCell(atRow row: Int, keyBackupVersion: MXKeyBackupVersion, keyBackupVersionTrust: MXKeyBackupVersionTrust) -> UITableViewCell { + guard let delegate = self.delegate else { + return UITableViewCell.init() + } + + var cell: UITableViewCell + switch row { + case 0: + let infoCell: MXKTableViewCellWithTextView = delegate.settingsKeyBackupTableViewSection(self, textCellForRow: row) + + let backupStatus = VectorL10n.settingsKeyBackupInfoValid + let uploadStatus = VectorL10n.settingsKeyBackupInfoProgressDone + + let strings = [backupStatus, uploadStatus] + infoCell.mxkTextView.text = strings.joined(separator: "\n") + + cell = infoCell + + case 1: + let infoCell: MXKTableViewCellWithTextView = delegate.settingsKeyBackupTableViewSection(self, textCellForRow: row) + + let version = VectorL10n.settingsKeyBackupInfoVersion(keyBackupVersion.version ?? "") + let algorithm = VectorL10n.settingsKeyBackupInfoAlgorithm(keyBackupVersion.algorithm) + + let strings = [version, algorithm] + infoCell.mxkTextView.text = strings.joined(separator: "\n") + + cell = infoCell + + case 2: + let infoCell: MXKTableViewCellWithTextView = delegate.settingsKeyBackupTableViewSection(self, textCellForRow: row) + + let backupTrust = self.stringForKeyBackupTrust(keyBackupVersionTrust); + infoCell.mxkTextView.text = backupTrust.joined(separator: "\n") + + cell = infoCell + + case 3: + cell = self.buttonCellForRestore(keyBackupVersion: keyBackupVersion, atRow: row) + + case 4: + cell = self.buttonCellForDelete(keyBackupVersion: keyBackupVersion, atRow: row) + + default: + cell = UITableViewCell.init() + } + + return cell + } + + + private func numberOfBackupAndRunningRows() -> Int { + return 5 + } + + private func renderBackupAndRunningCell(atRow row: Int, keyBackupVersion: MXKeyBackupVersion, keyBackupVersionTrust: MXKeyBackupVersionTrust, backupProgress: Progress) -> UITableViewCell { + guard let delegate = self.delegate else { + return UITableViewCell.init() + } + + var cell: UITableViewCell + switch row { + case 0: + let infoCell: MXKTableViewCellWithTextView = delegate.settingsKeyBackupTableViewSection(self, textCellForRow: 0) + + let remaining = backupProgress.totalUnitCount - backupProgress.completedUnitCount + + let backupStatus = VectorL10n.settingsKeyBackupInfoValid + let uploadStatus = VectorL10n.settingsKeyBackupInfoProgress(String(remaining)) + + let strings = [backupStatus, uploadStatus] + infoCell.mxkTextView.text = strings.joined(separator: "\n") + + cell = infoCell + + case 1: + let infoCell: MXKTableViewCellWithTextView = delegate.settingsKeyBackupTableViewSection(self, textCellForRow: row) + + let version = VectorL10n.settingsKeyBackupInfoVersion(keyBackupVersion.version ?? "") + let algorithm = VectorL10n.settingsKeyBackupInfoAlgorithm(keyBackupVersion.algorithm) + + let strings = [version, algorithm] + infoCell.mxkTextView.text = strings.joined(separator: "\n") + + cell = infoCell + + case 2: + let infoCell: MXKTableViewCellWithTextView = delegate.settingsKeyBackupTableViewSection(self, textCellForRow: row) + + let backupTrust = self.stringForKeyBackupTrust(keyBackupVersionTrust); + infoCell.mxkTextView.text = backupTrust.joined(separator: "\n") + + cell = infoCell + + case 3: + cell = self.buttonCellForRestore(keyBackupVersion: keyBackupVersion, atRow: row) + + case 4: + cell = self.buttonCellForDelete(keyBackupVersion: keyBackupVersion, atRow: row) + + default: + cell = UITableViewCell.init() + } + + return cell + } + + + private func numberOfBackupNotTrustedRows() -> Int { + return 6 + } + + private func renderBackupNotTrustedCell(atRow row: Int, keyBackupVersion: MXKeyBackupVersion, keyBackupVersionTrust: MXKeyBackupVersionTrust) -> UITableViewCell { + guard let delegate = self.delegate else { + return UITableViewCell.init() + } + + // Is the device that created the device verifiable? + // ie, is it known and already stored in crytpo store? + let lastNonVerifiedDevice = self.lastNonVerifiedDevice(keyBackupVersionTrust) + let lastUnVerifiableDevice = self.lastUnVerifiableDevice(keyBackupVersionTrust) + + var cell: UITableViewCell + switch row { + case 0: + let infoCell: MXKTableViewCellWithTextView = delegate.settingsKeyBackupTableViewSection(self, textCellForRow: row) + + let backupStatus = VectorL10n.settingsKeyBackupInfoNotValid + + var fixAction: [String] = [] + if let lastNonVerifiedDevice = lastNonVerifiedDevice { + let deviceName = lastNonVerifiedDevice.displayName ?? lastNonVerifiedDevice.deviceId ?? "" + fixAction = [VectorL10n.settingsKeyBackupInfoNotTrustedFromVerifiableDeviceFixAction(deviceName)] + } + else if lastUnVerifiableDevice != nil { + fixAction = [VectorL10n.settingsKeyBackupInfoNotTrustedFixAction] + } + + let strings = [backupStatus] + fixAction + infoCell.mxkTextView.text = strings.joined(separator: "\n") + + cell = infoCell + + case 1: + if let lastNonVerifiedDevice = lastNonVerifiedDevice { + cell = self.buttonCellForVerifyingDevice(lastNonVerifiedDevice.deviceId, atRow: row) + } + else if lastUnVerifiableDevice != nil { + cell = self.buttonCellForRestore(keyBackupVersion: keyBackupVersion, atRow: row, title: VectorL10n.settingsKeyBackupButtonVerify) + } + else { + cell = UITableViewCell.init() + } + + case 2: + let infoCell: MXKTableViewCellWithTextView = delegate.settingsKeyBackupTableViewSection(self, textCellForRow: row) + + let version = VectorL10n.settingsKeyBackupInfoVersion(keyBackupVersion.version ?? "") + let algorithm = VectorL10n.settingsKeyBackupInfoAlgorithm(keyBackupVersion.algorithm) + + let strings = [version, algorithm] + infoCell.mxkTextView.text = strings.joined(separator: "\n") + + cell = infoCell + + case 3: + let infoCell: MXKTableViewCellWithTextView = delegate.settingsKeyBackupTableViewSection(self, textCellForRow: row) + + let backupTrust = self.stringForKeyBackupTrust(keyBackupVersionTrust); + infoCell.mxkTextView.text = backupTrust.joined(separator: "\n") + + cell = infoCell + + case 4: + cell = self.buttonCellForRestore(keyBackupVersion: keyBackupVersion, atRow: row) + + case 5: + cell = self.buttonCellForDelete(keyBackupVersion: keyBackupVersion, atRow: row) + + default: + cell = UITableViewCell.init() + } + + return cell + } + + + // MARK: - Data Computing + + private func stringForKeyBackupTrust(_ keyBackupVersionTrust: MXKeyBackupVersionTrust) -> [String] { + + return keyBackupVersionTrust.signatures.map { (signature) -> String in + guard let device = signature.device else { + return VectorL10n.settingsKeyBackupInfoTrustSignatureUnknown(signature.deviceId) + } + + let displayName = device.displayName ?? device.deviceId ?? "" + + if device.fingerprint == self.userDevice.fingerprint { + return VectorL10n.settingsKeyBackupInfoTrustSignatureValid + } + else if signature.valid && (device.verified == MXDeviceVerified) { + return VectorL10n.settingsKeyBackupInfoTrustSignatureValidDeviceVerified(displayName) + } + else if signature.valid && (device.verified != MXDeviceVerified) { + return VectorL10n.settingsKeyBackupInfoTrustSignatureValidDeviceUnverified(displayName) + } + else if !signature.valid && (device.verified == MXDeviceVerified) { + return VectorL10n.settingsKeyBackupInfoTrustSignatureInvalidDeviceVerified(displayName) + } + else if !signature.valid && (device.verified != MXDeviceVerified) { + return VectorL10n.settingsKeyBackupInfoTrustSignatureInvalidDeviceUnverified(displayName) + } + + return ""; + } + } + + private func lastNonVerifiedDevice(_ keyBackupVersionTrust:MXKeyBackupVersionTrust) -> MXDeviceInfo? + { + var lastNonVerifiedDevice: MXDeviceInfo? + for signature in keyBackupVersionTrust.signatures.reversed() { + + guard let device = signature.device else { + continue + } + + if device.verified != MXDeviceVerified + { + lastNonVerifiedDevice = device + break + } + } + return lastNonVerifiedDevice + } + + private func lastUnVerifiableDevice(_ keyBackupVersionTrust:MXKeyBackupVersionTrust) -> String? + { + var lastUnVerifiableDevice: String? + for signature in keyBackupVersionTrust.signatures.reversed() { + + if signature.device == nil { + lastUnVerifiableDevice = signature.deviceId + break + } + } + return lastUnVerifiableDevice + } + + // MARK: - Button cells + + private func buttonCellForCreate(atRow row: Int) -> UITableViewCell { + + guard let delegate = self.delegate else { + return UITableViewCell.init() + } + + let cell:MXKTableViewCellWithButton = delegate.settingsKeyBackupTableViewSection(self, buttonCellForRow: row) + + let btnTitle = VectorL10n.settingsKeyBackupButtonCreate + cell.mxkButton.setTitle(btnTitle, for: .normal) + cell.mxkButton.setTitle(btnTitle, for: .highlighted) + + cell.mxkButton.vc_addAction { + self.viewModel.process(viewAction: .create) + } + + return cell + } + + private func buttonCellForVerifyingDevice(_ deviceId: String, atRow row: Int) -> UITableViewCell { + + guard let delegate = self.delegate else { + return UITableViewCell.init() + } + + let cell:MXKTableViewCellWithButton = delegate.settingsKeyBackupTableViewSection(self, buttonCellForRow: row) + + let btnTitle = VectorL10n.settingsKeyBackupButtonVerify + cell.mxkButton.setTitle(btnTitle, for: .normal) + cell.mxkButton.setTitle(btnTitle, for: .highlighted) + + cell.mxkButton.vc_addAction { + self.viewModel.process(viewAction: .verify(deviceId)) + } + + return cell + } + + private func buttonCellForRestore(keyBackupVersion: MXKeyBackupVersion, atRow row: Int, title: String = VectorL10n.settingsKeyBackupButtonRestore) -> UITableViewCell { + guard let delegate = self.delegate else { + return UITableViewCell.init() + } + + let cell:MXKTableViewCellWithButton = delegate.settingsKeyBackupTableViewSection(self, buttonCellForRow: row) + cell.mxkButton.setTitle(title, for: .normal) + cell.mxkButton.setTitle(title, for: .highlighted) + cell.mxkButton.vc_addAction { + self.viewModel.process(viewAction: .restore(keyBackupVersion)) + } + return cell + } + + private func buttonCellForDelete(keyBackupVersion: MXKeyBackupVersion, atRow row: Int) -> UITableViewCell { + guard let delegate = self.delegate else { + return UITableViewCell.init() + } + + let cell:MXKTableViewCellWithButton = delegate.settingsKeyBackupTableViewSection(self, buttonCellForRow: row) + let btnTitle = VectorL10n.settingsKeyBackupButtonDelete + cell.mxkButton.setTitle(btnTitle, for: .normal) + cell.mxkButton.setTitle(btnTitle, for: .highlighted) + cell.mxkButton.tintColor = ThemeService.shared().theme.warningColor + cell.mxkButton.vc_addAction { + self.viewModel.process(viewAction: .confirmDelete(keyBackupVersion)) + } + + return cell + } +} + + +// MARK: - KeyBackupSetupRecoveryKeyViewModelViewDelegate +extension SettingsKeyBackupTableViewSection: SettingsKeyBackupViewModelViewDelegate { + func settingsKeyBackupViewModel(_ viewModel: SettingsKeyBackupViewModelType, didUpdateViewState viewState: SettingsKeyBackupViewState) { + self.viewState = viewState + + // The tableview datasource will call `self.cellForRow()` + self.delegate?.settingsKeyBackupTableViewSectionDidUpdate(self) + } + + func settingsKeyBackupViewModel(_ viewModel: SettingsKeyBackupViewModelType, didUpdateNetworkRequestViewState networkRequestViewSate: SettingsKeyBackupNetworkRequestViewState) { + switch networkRequestViewSate { + case .loading: + self.delegate?.settingsKeyBackup(self, showActivityIndicator: true) + case .loaded: + self.delegate?.settingsKeyBackup(self, showActivityIndicator: false) + case .error(let error): + self.delegate?.settingsKeyBackup(self, showError: error) + } + } + + func settingsKeyBackupViewModelShowKeyBackupSetup(_ viewModel: SettingsKeyBackupViewModelType) { + self.delegate?.settingsKeyBackupTableViewSectionShowKeyBackupSetup(self) + } + func settingsKeyBackup(_ viewModel: SettingsKeyBackupViewModelType, showVerifyDevice deviceId: String) { + self.delegate?.settingsKeyBackup(self, showVerifyDevice: deviceId) + } + func settingsKeyBackup(_ viewModel: SettingsKeyBackupViewModelType, showKeyBackupRecover keyBackupVersion: MXKeyBackupVersion) { + self.delegate?.settingsKeyBackup(self, showKeyBackupRecover: keyBackupVersion) + } + func settingsKeyBackup(_ viewModel: SettingsKeyBackupViewModelType, showKeyBackupDeleteConfirm keyBackupVersion: MXKeyBackupVersion) { + self.delegate?.settingsKeyBackup(self, showKeyBackupDeleteConfirm: keyBackupVersion) + } +} diff --git a/Riot/Modules/Settings/KeyBackup/SettingsKeyBackupViewAction.swift b/Riot/Modules/Settings/KeyBackup/SettingsKeyBackupViewAction.swift new file mode 100644 index 000000000..f1748bf67 --- /dev/null +++ b/Riot/Modules/Settings/KeyBackup/SettingsKeyBackupViewAction.swift @@ -0,0 +1,26 @@ +/* + Copyright 2019 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 UIKit + +enum SettingsKeyBackupViewAction { + case load + case create + case verify(String) + case restore(MXKeyBackupVersion) + case confirmDelete(MXKeyBackupVersion) + case delete(MXKeyBackupVersion) +} diff --git a/Riot/Modules/Settings/KeyBackup/SettingsKeyBackupViewModel.swift b/Riot/Modules/Settings/KeyBackup/SettingsKeyBackupViewModel.swift new file mode 100644 index 000000000..ab80e90ab --- /dev/null +++ b/Riot/Modules/Settings/KeyBackup/SettingsKeyBackupViewModel.swift @@ -0,0 +1,153 @@ +/* + Copyright 2019 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 UIKit + +final class SettingsKeyBackupViewModel: SettingsKeyBackupViewModelType { + + // MARK: - Properties + weak var viewDelegate: SettingsKeyBackupViewModelViewDelegate? + + // MARK: Private + private let keyBackup: MXKeyBackup + + init(keyBackup: MXKeyBackup) { + self.keyBackup = keyBackup + self.registerKeyBackupVersionDidChangeStateNotification() + } + + private func registerKeyBackupVersionDidChangeStateNotification() { + NotificationCenter.default.addObserver(self, selector: #selector(keyBackupDidStateChange), name: NSNotification.Name.mxKeyBackupDidStateChange, object: self.keyBackup) + } + + @objc private func keyBackupDidStateChange() { + self.checkKeyBackupState() + } + + func process(viewAction: SettingsKeyBackupViewAction) { + guard let viewDelegate = self.viewDelegate else { + return + } + + switch viewAction { + case .load: + viewDelegate.settingsKeyBackupViewModel(self, didUpdateViewState: .checkingBackup) + self.checkKeyBackupState() + case .create: + viewDelegate.settingsKeyBackupViewModelShowKeyBackupSetup(self) + break + case .verify(let deviceId): + viewDelegate.settingsKeyBackup(self, showVerifyDevice: deviceId) + break + case .restore(let keyBackupVersion): + viewDelegate.settingsKeyBackup(self, showKeyBackupRecover: keyBackupVersion) + break + case .confirmDelete(let keyBackupVersion): + viewDelegate.settingsKeyBackup(self, showKeyBackupDeleteConfirm: keyBackupVersion) + break + case .delete(let keyBackupVersion): + self.deleteKeyBackupVersion(keyBackupVersion) + break + } + } + + // MARK: - Private + + private func checkKeyBackupState() { + + if let keyBackupVersion = self.keyBackup.keyBackupVersion { + + self.keyBackup.trust(for: keyBackupVersion, onComplete: { [weak self] (keyBackupVersionTrust) in + + guard let sself = self else { + return + } + + sself.computeState(withBackupVersionTrust:keyBackupVersionTrust) + }) + } + else { + computeState() + } + } + + private func computeState(withBackupVersionTrust keyBackupVersionTrust:MXKeyBackupVersionTrust? = nil) { + + var viewState: SettingsKeyBackupViewState? + switch self.keyBackup.state { + + case MXKeyBackupStateUnknown, + MXKeyBackupStateCheckingBackUpOnHomeserver: + viewState = .checkingBackup + + case MXKeyBackupStateDisabled, MXKeyBackupStateEnabling: + viewState = .noBackup + + case MXKeyBackupStateNotTrusted: + guard let keyBackupVersion = self.keyBackup.keyBackupVersion, let keyBackupVersionTrust = keyBackupVersionTrust else { + return + } + viewState = .backupNotTrusted(keyBackupVersion, keyBackupVersionTrust) + + case MXKeyBackupStateReadyToBackUp: + guard let keyBackupVersion = self.keyBackup.keyBackupVersion, let keyBackupVersionTrust = keyBackupVersionTrust else { + return + } + viewState = .backup(keyBackupVersion, keyBackupVersionTrust) + + case MXKeyBackupStateWillBackUp, MXKeyBackupStateBackingUp: + guard let keyBackupVersion = self.keyBackup.keyBackupVersion, let keyBackupVersionTrust = keyBackupVersionTrust else { + return + } + + // Get the backup progress before updating the state + self.keyBackup.backupProgress { [weak self] (progress) in + guard let sself = self else { + return + } + + sself.viewDelegate?.settingsKeyBackupViewModel(sself, didUpdateViewState: .backupAndRunning(keyBackupVersion, keyBackupVersionTrust, progress)) + } + default: + break + } + + if let vviewState = viewState { + self.viewDelegate?.settingsKeyBackupViewModel(self, didUpdateViewState: vviewState) + } + } + + private func deleteKeyBackupVersion(_ keyBackupVersion: MXKeyBackupVersion) { + guard let keyBackupVersionVersion = keyBackupVersion.version else { + return + } + + self.viewDelegate?.settingsKeyBackupViewModel(self, didUpdateNetworkRequestViewState: .loading) + + self.keyBackup.deleteVersion(keyBackupVersionVersion, success: { [weak self] () in + guard let sself = self else { + return + } + sself.viewDelegate?.settingsKeyBackupViewModel(sself, didUpdateNetworkRequestViewState: .loaded) + + }, failure: { [weak self] error in + guard let sself = self else { + return + } + sself.viewDelegate?.settingsKeyBackupViewModel(sself, didUpdateNetworkRequestViewState: .error(error)) + }) + } +} diff --git a/Riot/Modules/Settings/KeyBackup/SettingsKeyBackupViewModelType.swift b/Riot/Modules/Settings/KeyBackup/SettingsKeyBackupViewModelType.swift new file mode 100644 index 000000000..20c59ba94 --- /dev/null +++ b/Riot/Modules/Settings/KeyBackup/SettingsKeyBackupViewModelType.swift @@ -0,0 +1,34 @@ +/* + Copyright 2019 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 UIKit + +protocol SettingsKeyBackupViewModelViewDelegate: class { + func settingsKeyBackupViewModel(_ viewModel: SettingsKeyBackupViewModelType, didUpdateViewState viewState: SettingsKeyBackupViewState) + func settingsKeyBackupViewModel(_ viewModel: SettingsKeyBackupViewModelType, didUpdateNetworkRequestViewState networkRequestViewSate: SettingsKeyBackupNetworkRequestViewState) + + func settingsKeyBackupViewModelShowKeyBackupSetup(_ viewModel: SettingsKeyBackupViewModelType) + func settingsKeyBackup(_ viewModel: SettingsKeyBackupViewModelType, showVerifyDevice deviceId:String) + func settingsKeyBackup(_ viewModel: SettingsKeyBackupViewModelType, showKeyBackupRecover keyBackupVersion:MXKeyBackupVersion) + func settingsKeyBackup(_ viewModel: SettingsKeyBackupViewModelType, showKeyBackupDeleteConfirm keyBackupVersion:MXKeyBackupVersion) +} + +protocol SettingsKeyBackupViewModelType { + + var viewDelegate: SettingsKeyBackupViewModelViewDelegate? { get set } + + func process(viewAction: SettingsKeyBackupViewAction) +} diff --git a/Riot/Modules/Settings/KeyBackup/SettingsKeyBackupViewState.swift b/Riot/Modules/Settings/KeyBackup/SettingsKeyBackupViewState.swift new file mode 100644 index 000000000..9751e402e --- /dev/null +++ b/Riot/Modules/Settings/KeyBackup/SettingsKeyBackupViewState.swift @@ -0,0 +1,41 @@ +/* + Copyright 2019 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 UIKit + +/// SettingsKeyBackup view state +/// +/// - checkingBackup: Load current backup on the homeserver +/// - checkError: Fail to load current backup data +/// - noBackup: There is no backup on the homeserver +/// - backup: There is a valid backup on the homeserver. All keys have been backed up to it +/// - backupAndRunning: There is a valid backup on the homeserver. Keys are being sent to it +/// - backupButNotVerified: There is a backup on the homeserver but it has not been verified yet +enum SettingsKeyBackupViewState { + case checkingBackup + case noBackup + case backup(MXKeyBackupVersion, MXKeyBackupVersionTrust) + case backupAndRunning(MXKeyBackupVersion, MXKeyBackupVersionTrust, Progress) + case backupNotTrusted(MXKeyBackupVersion, MXKeyBackupVersionTrust) +} + +/// State representing a network request made by the module +/// Only SettingsKeyBackupViewAction.delete generates such states +enum SettingsKeyBackupNetworkRequestViewState { + case loading + case loaded + case error(Error) +} diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 18d7e3fb7..13c295a0e 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -48,6 +48,8 @@ #import "Riot-Swift.h" +#import "EncryptionInfoView.h" + NSString* const kSettingsViewControllerPhoneBookCountryCellId = @"kSettingsViewControllerPhoneBookCountryCellId"; enum @@ -63,8 +65,9 @@ enum SETTINGS_SECTION_OTHER_INDEX, SETTINGS_SECTION_LABS_INDEX, SETTINGS_SECTION_CRYPTOGRAPHY_INDEX, - SETTINGS_SECTION_FLAIR_INDEX, + SETTINGS_SECTION_KEYBACKUP_INDEX, SETTINGS_SECTION_DEVICES_INDEX, + SETTINGS_SECTION_FLAIR_INDEX, SETTINGS_SECTION_DEACTIVATE_ACCOUNT_INDEX, SETTINGS_SECTION_COUNT }; @@ -135,7 +138,11 @@ enum { typedef void (^blockSettingsViewController_onReadyToDestroy)(void); -@interface SettingsViewController () +@interface SettingsViewController () { // Current alert (if any). UIAlertController *currentAlert; @@ -227,6 +234,10 @@ typedef void (^blockSettingsViewController_onReadyToDestroy)(void); // The current pushed view controller UIViewController *pushedViewController; + + SettingsKeyBackupTableViewSection *keyBackupSection; + KeyBackupSetupCoordinatorBridgePresenter *keyBackupSetupCoordinatorBridgePresenter; + KeyBackupRecoverCoordinatorBridgePresenter *keyBackupRecoverCoordinatorBridgePresenter; } /** @@ -314,7 +325,19 @@ typedef void (^blockSettingsViewController_onReadyToDestroy)(void); { [self addMatrixSession:mxSession]; } - + + if (self.mainSession.crypto.backup) + { + MXDeviceInfo *deviceInfo = [self.mainSession.crypto.deviceList storedDevice:self.mainSession.matrixRestClient.credentials.userId + deviceId:self.mainSession.matrixRestClient.credentials.deviceId]; + + if (deviceInfo) + { + keyBackupSection = [[SettingsKeyBackupTableViewSection alloc] initWithKeyBackup:self.mainSession.crypto.backup userDevice:deviceInfo]; + keyBackupSection.delegate = self; + } + } + groupsDataSource = [[GroupsDataSource alloc] initWithMatrixSession:self.mainSession]; [groupsDataSource finalizeInitialization]; groupsDataSource.delegate = self; @@ -404,6 +427,9 @@ typedef void (^blockSettingsViewController_onReadyToDestroy)(void); [super destroy]; } + + keyBackupSetupCoordinatorBridgePresenter = nil; + keyBackupRecoverCoordinatorBridgePresenter = nil; } - (void)onMatrixSessionStateDidChange:(NSNotification *)notif @@ -1279,6 +1305,14 @@ typedef void (^blockSettingsViewController_onReadyToDestroy)(void); count = CRYPTOGRAPHY_COUNT; } } + else if (section == SETTINGS_SECTION_KEYBACKUP_INDEX) + { + // Check whether this section is visible. + if (self.mainSession.crypto) + { + count = keyBackupSection.numberOfRows; + } + } else if (section == SETTINGS_SECTION_DEACTIVATE_ACCOUNT_INDEX) { count = 1; @@ -2205,6 +2239,10 @@ typedef void (^blockSettingsViewController_onReadyToDestroy)(void); cell = exportKeysBtnCell; } } + else if (section == SETTINGS_SECTION_KEYBACKUP_INDEX) + { + cell = [keyBackupSection cellForRowAtRow:row]; + } else if (section == SETTINGS_SECTION_DEACTIVATE_ACCOUNT_INDEX) { MXKTableViewCellWithButton *deactivateAccountBtnCell = [tableView dequeueReusableCellWithIdentifier:[MXKTableViewCellWithButton defaultReuseIdentifier]]; @@ -2308,12 +2346,12 @@ typedef void (^blockSettingsViewController_onReadyToDestroy)(void); return NSLocalizedStringFromTable(@"settings_cryptography", @"Vector", nil); } } - else if (section == SETTINGS_SECTION_CRYPTOGRAPHY_INDEX) + else if (section == SETTINGS_SECTION_KEYBACKUP_INDEX) { // Check whether this section is visible if (self.mainSession.crypto) { - return NSLocalizedStringFromTable(@"settings_cryptography", @"Vector", nil); + return NSLocalizedStringFromTable(@"settings_key_backup", @"Vector", nil); } } else if (section == SETTINGS_SECTION_DEACTIVATE_ACCOUNT_INDEX) @@ -4206,7 +4244,6 @@ typedef void (^blockSettingsViewController_onReadyToDestroy)(void); - (void)deactivateAccountViewControllerDidDeactivateWithSuccess:(DeactivateAccountViewController *)deactivateAccountViewController { NSLog(@"[SettingsViewController] Deactivate account with success"); - [[AppDelegate theDelegate] logoutSendingRequestServer:NO completion:^(BOOL isLoggedOut) { NSLog(@"[SettingsViewController] Complete clear user data after account deactivation"); @@ -4218,4 +4255,204 @@ typedef void (^blockSettingsViewController_onReadyToDestroy)(void); [deactivateAccountViewController dismissViewControllerAnimated:YES completion:nil]; } +#pragma mark - SettingsKeyBackupTableViewSectionDelegate + +- (void)settingsKeyBackupTableViewSectionDidUpdate:(SettingsKeyBackupTableViewSection *)settingsKeyBackupTableViewSection +{ + [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:SETTINGS_SECTION_KEYBACKUP_INDEX] + withRowAnimation:UITableViewRowAnimationAutomatic]; +} + +- (MXKTableViewCellWithTextView *)settingsKeyBackupTableViewSection:(SettingsKeyBackupTableViewSection *)settingsKeyBackupTableViewSection textCellForRow:(NSInteger)textCellForRow +{ + return [self textViewCellForTableView:self.tableView atIndexPath:[NSIndexPath indexPathForRow:textCellForRow inSection:SETTINGS_SECTION_KEYBACKUP_INDEX]]; +} + +- (MXKTableViewCellWithButton *)settingsKeyBackupTableViewSection:(SettingsKeyBackupTableViewSection *)settingsKeyBackupTableViewSection buttonCellForRow:(NSInteger)buttonCellForRow +{ + MXKTableViewCellWithButton *cell = [self.tableView dequeueReusableCellWithIdentifier:[MXKTableViewCellWithButton defaultReuseIdentifier]]; + + if (!cell) + { + cell = [[MXKTableViewCellWithButton alloc] init]; + } + else + { + // Fix https://github.com/vector-im/riot-ios/issues/1354 + cell.mxkButton.titleLabel.text = nil; + } + + cell.mxkButton.titleLabel.font = [UIFont systemFontOfSize:17]; + [cell.mxkButton setTintColor:ThemeService.shared.theme.tintColor]; + + return cell; +} + +- (void)settingsKeyBackupTableViewSectionShowKeyBackupSetup:(SettingsKeyBackupTableViewSection *)settingsKeyBackupTableViewSection +{ + [self showKeyBackupSetup]; +} + +- (void)settingsKeyBackup:(SettingsKeyBackupTableViewSection *)settingsKeyBackupTableViewSection showVerifyDevice:(NSString *)deviceId +{ + MXDeviceInfo *deviceInfo = [self.mainSession.crypto.deviceList storedDevice:self.mainSession.myUser.userId deviceId:deviceId]; + [self showDeviceInfo:deviceInfo]; +} + +- (void)settingsKeyBackup:(SettingsKeyBackupTableViewSection *)settingsKeyBackupTableViewSection showKeyBackupRecover:(MXKeyBackupVersion *)keyBackupVersion +{ + [self showKeyBackupRecover:keyBackupVersion]; +} + +- (void)settingsKeyBackup:(SettingsKeyBackupTableViewSection *)settingsKeyBackupTableViewSection showKeyBackupDeleteConfirm:(MXKeyBackupVersion *)keyBackupVersion +{ + MXWeakify(self); + [currentAlert dismissViewControllerAnimated:NO completion:nil]; + + currentAlert = + [UIAlertController alertControllerWithTitle:NSLocalizedStringFromTable(@"settings_key_backup_delete_confirmation_prompt_title", @"Vector", nil) + message:NSLocalizedStringFromTable(@"settings_key_backup_delete_confirmation_prompt_msg", @"Vector", nil) + preferredStyle:UIAlertControllerStyleAlert]; + + [currentAlert addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"cancel"] + style:UIAlertActionStyleCancel + handler:^(UIAlertAction * action) { + MXStrongifyAndReturnIfNil(self); + self->currentAlert = nil; + }]]; + + [currentAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"remove", @"Vector", nil) + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { + MXStrongifyAndReturnIfNil(self); + self->currentAlert = nil; + + [self->keyBackupSection deleteWithKeyBackupVersion:keyBackupVersion]; + }]]; + + [currentAlert mxk_setAccessibilityIdentifier: @"SettingsVCDeleteKeyBackup"]; + [self presentViewController:currentAlert animated:YES completion:nil]; +} + +- (void)settingsKeyBackup:(SettingsKeyBackupTableViewSection *)settingsKeyBackupTableViewSection showActivityIndicator:(BOOL)show +{ + if (show) + { + [self startActivityIndicator]; + } + else + { + [self stopActivityIndicator]; + } +} + +- (void)settingsKeyBackup:(SettingsKeyBackupTableViewSection *)settingsKeyBackupTableViewSection showError:(NSError *)error +{ + [[AppDelegate theDelegate] showErrorAsAlert:error]; +} + +#pragma mark - MXKEncryptionInfoView + +- (void)showDeviceInfo:(MXDeviceInfo*)deviceInfo +{ + // Show it modally on the root view controller + // TODO: Improve it + UIViewController *rootViewController = [AppDelegate theDelegate].window.rootViewController; + if (rootViewController) + { + EncryptionInfoView *encryptionInfoView = [[EncryptionInfoView alloc] initWithDeviceInfo:deviceInfo andMatrixSession:self.mainSession]; + [encryptionInfoView onButtonPressed:encryptionInfoView.verifyButton]; + + encryptionInfoView.delegate = self; + + // Add shadow on added view + encryptionInfoView.layer.cornerRadius = 5; + encryptionInfoView.layer.shadowOffset = CGSizeMake(0, 1); + encryptionInfoView.layer.shadowOpacity = 0.5f; + + // Add the view and define edge constraints + [rootViewController.view addSubview:encryptionInfoView]; + + [rootViewController.view addConstraint:[NSLayoutConstraint constraintWithItem:encryptionInfoView + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:rootViewController.topLayoutGuide + attribute:NSLayoutAttributeBottom + multiplier:1.0f + constant:10.0f]]; + + [rootViewController.view addConstraint:[NSLayoutConstraint constraintWithItem:encryptionInfoView + attribute:NSLayoutAttributeBottom + relatedBy:NSLayoutRelationEqual + toItem:rootViewController.bottomLayoutGuide + attribute:NSLayoutAttributeTop + multiplier:1.0f + constant:-10.0f]]; + + [rootViewController.view addConstraint:[NSLayoutConstraint constraintWithItem:rootViewController.view + attribute:NSLayoutAttributeLeading + relatedBy:NSLayoutRelationEqual + toItem:encryptionInfoView + attribute:NSLayoutAttributeLeading + multiplier:1.0f + constant:-10.0f]]; + + [rootViewController.view addConstraint:[NSLayoutConstraint constraintWithItem:rootViewController.view + attribute:NSLayoutAttributeTrailing + relatedBy:NSLayoutRelationEqual + toItem:encryptionInfoView + attribute:NSLayoutAttributeTrailing + multiplier:1.0f + constant:10.0f]]; + [rootViewController.view setNeedsUpdateConstraints]; + } +} + +- (void)encryptionInfoView:(MXKEncryptionInfoView*)encryptionInfoView didDeviceInfoVerifiedChange:(MXDeviceInfo*)deviceInfo +{ + [keyBackupSection reload]; +} + +#pragma mark - KeyBackupRecoverCoordinatorBridgePresenter + +- (void)showKeyBackupSetup +{ + keyBackupSetupCoordinatorBridgePresenter = [[KeyBackupSetupCoordinatorBridgePresenter alloc] initWithSession:self.mainSession]; + + [keyBackupSetupCoordinatorBridgePresenter presentFrom:self animated:true]; + keyBackupSetupCoordinatorBridgePresenter.delegate = self; +} + +- (void)keyBackupSetupCoordinatorBridgePresenterDelegateDidCancel:(KeyBackupSetupCoordinatorBridgePresenter *)bridgePresenter { + [keyBackupSetupCoordinatorBridgePresenter dismissWithAnimated:true]; + keyBackupSetupCoordinatorBridgePresenter = nil; +} + +- (void)keyBackupSetupCoordinatorBridgePresenterDelegateDidSetupRecoveryKey:(KeyBackupSetupCoordinatorBridgePresenter *)bridgePresenter { + [keyBackupSetupCoordinatorBridgePresenter dismissWithAnimated:true]; + keyBackupSetupCoordinatorBridgePresenter = nil; + + [keyBackupSection reload]; +} + +#pragma mark - KeyBackupRecoverCoordinatorBridgePresenter + +- (void)showKeyBackupRecover:(MXKeyBackupVersion*)keyBackupVersion +{ + keyBackupRecoverCoordinatorBridgePresenter = [[KeyBackupRecoverCoordinatorBridgePresenter alloc] initWithSession:self.mainSession keyBackupVersion:keyBackupVersion]; + + [keyBackupRecoverCoordinatorBridgePresenter presentFrom:self animated:true]; + keyBackupRecoverCoordinatorBridgePresenter.delegate = self; +} + +- (void)keyBackupRecoverCoordinatorBridgePresenterDidCancel:(KeyBackupRecoverCoordinatorBridgePresenter *)bridgePresenter { + [keyBackupRecoverCoordinatorBridgePresenter dismissWithAnimated:true]; + keyBackupRecoverCoordinatorBridgePresenter = nil; +} + +- (void)keyBackupRecoverCoordinatorBridgePresenterDidRecover:(KeyBackupRecoverCoordinatorBridgePresenter *)bridgePresenter { + [keyBackupRecoverCoordinatorBridgePresenter dismissWithAnimated:true]; + keyBackupRecoverCoordinatorBridgePresenter = nil; +} + @end