From 8e26718d0bc7ee9729107b8c89e71e354ae2cbdb Mon Sep 17 00:00:00 2001 From: Doug <6060466+pixlwave@users.noreply.github.com> Date: Mon, 4 Nov 2024 14:22:50 +0000 Subject: [PATCH] Encryption Flow Coordinators. (#3471) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Manage the secure backup screens with flow coordinators. * Add UI tests for the EncryptionSettingsFlowCoordinator. * Realise that the settings flow can't reset anymore and remove the sub-flow 🤦‍♂️ * Add UI tests for the EncryptionResetFlowCoordinator. --- ElementX.xcodeproj/project.pbxproj | 24 +++ .../EncryptionResetFlowCoordinator.swift | 158 ++++++++++++++ .../EncryptionSettingsFlowCoordinator.swift | 198 ++++++++++++++++++ .../OnboardingFlowCoordinator.swift | 44 ++-- .../SettingsFlowCoordinator.swift | 28 +-- ElementX/Sources/Mocks/ClientProxyMock.swift | 10 +- .../SDK/IdentityResetHandleSDKMock.swift | 22 ++ .../Mocks/SecureBackupControllerMock.swift | 48 +++++ .../Other/AccessibilityIdentifiers.swift | 31 +++ ...yptionResetPasswordScreenCoordinator.swift | 21 +- .../EncryptionResetPasswordScreenModels.swift | 13 +- ...cryptionResetPasswordScreenViewModel.swift | 11 +- .../View/EncryptionResetPasswordScreen.swift | 10 +- .../EncryptionResetScreenCoordinator.swift | 27 +-- .../EncryptionResetScreenModels.swift | 3 +- .../EncryptionResetScreenViewModel.swift | 16 +- ...cryptionResetScreenViewModelProtocol.swift | 1 - .../View/EncryptionResetScreen.swift | 1 + .../View/SecureBackupKeyBackupScreen.swift | 1 + ...reBackupRecoveryKeyScreenCoordinator.swift | 32 +-- .../SecureBackupRecoveryKeyScreenModels.swift | 2 - ...cureBackupRecoveryKeyScreenViewModel.swift | 8 +- .../View/SecureBackupRecoveryKeyScreen.swift | 5 + .../SecureBackupScreenCoordinator.swift | 92 +------- .../SecureBackupScreenModels.swift | 4 +- .../SecureBackupScreenViewModel.swift | 4 +- .../View/SecureBackupScreen.swift | 20 +- .../UITests/UITestsAppCoordinator.swift | 34 +++ .../UITests/UITestsScreenIdentifier.swift | 3 + UITests/Sources/EncryptionResetUITests.swift | 37 ++++ .../Sources/EncryptionSettingsUITests.swift | 80 +++++++ ...nReset-0-iPad-10th-generation-en-GB.UI.png | 3 + .../encryptionReset-0-iPhone-16-en-GB.UI.png | 3 + ...nReset-1-iPad-10th-generation-en-GB.UI.png | 3 + .../encryptionReset-1-iPhone-16-en-GB.UI.png | 3 + ...nReset-2-iPad-10th-generation-en-GB.UI.png | 3 + .../encryptionReset-2-iPhone-16-en-GB.UI.png | 3 + ...ttings-0-iPad-10th-generation-en-GB.UI.png | 3 + ...ncryptionSettings-0-iPhone-16-en-GB.UI.png | 3 + ...ttings-1-iPad-10th-generation-en-GB.UI.png | 3 + ...ncryptionSettings-1-iPhone-16-en-GB.UI.png | 3 + ...ttings-2-iPad-10th-generation-en-GB.UI.png | 3 + ...ncryptionSettings-2-iPhone-16-en-GB.UI.png | 3 + ...ttings-3-iPad-10th-generation-en-GB.UI.png | 3 + ...ncryptionSettings-3-iPhone-16-en-GB.UI.png | 3 + ...ttings-4-iPad-10th-generation-en-GB.UI.png | 3 + ...ncryptionSettings-4-iPhone-16-en-GB.UI.png | 3 + ...ttings-5-iPad-10th-generation-en-GB.UI.png | 3 + ...ncryptionSettings-5-iPhone-16-en-GB.UI.png | 3 + ...ttings-6-iPad-10th-generation-en-GB.UI.png | 3 + ...ncryptionSettings-6-iPhone-16-en-GB.UI.png | 3 + 51 files changed, 822 insertions(+), 226 deletions(-) create mode 100644 ElementX/Sources/FlowCoordinators/EncryptionResetFlowCoordinator.swift create mode 100644 ElementX/Sources/FlowCoordinators/EncryptionSettingsFlowCoordinator.swift create mode 100644 ElementX/Sources/Mocks/SDK/IdentityResetHandleSDKMock.swift create mode 100644 ElementX/Sources/Mocks/SecureBackupControllerMock.swift create mode 100644 UITests/Sources/EncryptionResetUITests.swift create mode 100644 UITests/Sources/EncryptionSettingsUITests.swift create mode 100644 UITests/Sources/__Snapshots__/Application/encryptionReset-0-iPad-10th-generation-en-GB.UI.png create mode 100644 UITests/Sources/__Snapshots__/Application/encryptionReset-0-iPhone-16-en-GB.UI.png create mode 100644 UITests/Sources/__Snapshots__/Application/encryptionReset-1-iPad-10th-generation-en-GB.UI.png create mode 100644 UITests/Sources/__Snapshots__/Application/encryptionReset-1-iPhone-16-en-GB.UI.png create mode 100644 UITests/Sources/__Snapshots__/Application/encryptionReset-2-iPad-10th-generation-en-GB.UI.png create mode 100644 UITests/Sources/__Snapshots__/Application/encryptionReset-2-iPhone-16-en-GB.UI.png create mode 100644 UITests/Sources/__Snapshots__/Application/encryptionSettings-0-iPad-10th-generation-en-GB.UI.png create mode 100644 UITests/Sources/__Snapshots__/Application/encryptionSettings-0-iPhone-16-en-GB.UI.png create mode 100644 UITests/Sources/__Snapshots__/Application/encryptionSettings-1-iPad-10th-generation-en-GB.UI.png create mode 100644 UITests/Sources/__Snapshots__/Application/encryptionSettings-1-iPhone-16-en-GB.UI.png create mode 100644 UITests/Sources/__Snapshots__/Application/encryptionSettings-2-iPad-10th-generation-en-GB.UI.png create mode 100644 UITests/Sources/__Snapshots__/Application/encryptionSettings-2-iPhone-16-en-GB.UI.png create mode 100644 UITests/Sources/__Snapshots__/Application/encryptionSettings-3-iPad-10th-generation-en-GB.UI.png create mode 100644 UITests/Sources/__Snapshots__/Application/encryptionSettings-3-iPhone-16-en-GB.UI.png create mode 100644 UITests/Sources/__Snapshots__/Application/encryptionSettings-4-iPad-10th-generation-en-GB.UI.png create mode 100644 UITests/Sources/__Snapshots__/Application/encryptionSettings-4-iPhone-16-en-GB.UI.png create mode 100644 UITests/Sources/__Snapshots__/Application/encryptionSettings-5-iPad-10th-generation-en-GB.UI.png create mode 100644 UITests/Sources/__Snapshots__/Application/encryptionSettings-5-iPhone-16-en-GB.UI.png create mode 100644 UITests/Sources/__Snapshots__/Application/encryptionSettings-6-iPad-10th-generation-en-GB.UI.png create mode 100644 UITests/Sources/__Snapshots__/Application/encryptionSettings-6-iPhone-16-en-GB.UI.png diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index bab5fb81f7..0c81278f56 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -158,6 +158,7 @@ 21F29351EDD7B2A5534EE862 /* SecureBackupKeyBackupScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD558A898847C179E4B7A237 /* SecureBackupKeyBackupScreen.swift */; }; 22882C710BC99EC34A5024A0 /* UITestsScreenIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CEBE5EA91E8691EDF364EC2 /* UITestsScreenIdentifier.swift */; }; 22C5483D01EEB290B8339817 /* HomeScreenInviteCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8FC33C3F6BF597E095CE9FA /* HomeScreenInviteCell.swift */; }; + 230981086F0199F913434D6B /* EncryptionSettingsUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF8E5D4C95974B96A18C80E /* EncryptionSettingsUITests.swift */; }; 2335D1AB954C151FD8779F45 /* RoomPermissionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0096BC5DA86AF6B6E5742AC /* RoomPermissionsTests.swift */; }; 23701DE32ACD6FD40AA992C3 /* MediaUploadingPreprocessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE203026B9AD3DB412439866 /* MediaUploadingPreprocessorTests.swift */; }; 237FC70AA257B935F53316BA /* SessionVerificationControllerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C55D7E514F9DE4E3D72FDCAD /* SessionVerificationControllerProxy.swift */; }; @@ -205,6 +206,7 @@ 2C5E832434EE94E21AB3B238 /* EmojiPickerScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3EAE3E9D5EF4A6D5D9C6CFD /* EmojiPickerScreenViewModel.swift */; }; 2CA61BB208CD82EBDB58CD13 /* VideoRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED0CBEAB5F796BEFBAF7BB6A /* VideoRoomTimelineView.swift */; }; 2CA6ABBC9A88EB89EA52FCCB /* ConfettiScene.scn in Resources */ = {isa = PBXBuildFile; fileRef = B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */; }; + 2D0E3983288E2D35613AD681 /* SecureBackupControllerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AB29A2D95D3469B5F016655 /* SecureBackupControllerMock.swift */; }; 2D2D8A53B35BE8D8A01449C6 /* PinnedEventsBannerStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FA38E813BE14149F173F461 /* PinnedEventsBannerStateTests.swift */; }; 2DA27D78560D5F79B917E163 /* AudioConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E44E35AA87F49503E7B3BF6E /* AudioConverter.swift */; }; 2DD9D0FE7CB5CFC80D071451 /* AppLockScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3E67E09FE5A35D73818C39 /* AppLockScreenModels.swift */; }; @@ -353,6 +355,7 @@ 4CE638FD837ED72CD98AD9A9 /* AppHooks.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E4AB573FAEBB7B853DD04C /* AppHooks.swift */; }; 4D0F4385B7DDB68C66C78857 /* FormattedBodyText.swift in Sources */ = {isa = PBXBuildFile; fileRef = C258C9C815272911A5B132C3 /* FormattedBodyText.swift */; }; 4D23D41B8109E010304050F8 /* QRCodeLoginScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA551A98778CEE7366838CE2 /* QRCodeLoginScreenCoordinator.swift */; }; + 4D2B54233C7B2C04B4ABE55A /* EncryptionSettingsFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECB836DD8BE31931F51B8AC9 /* EncryptionSettingsFlowCoordinator.swift */; }; 4D4D236F0BBCDC4D2CBCCBB5 /* RoomChangePermissionsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = C729D95CB4588D4D9AAC3DFA /* RoomChangePermissionsScreenModels.swift */; }; 4DAEE2468669848B6C9F55B4 /* TimelineReadReceiptsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33035418BB35754232985871 /* TimelineReadReceiptsView.swift */; }; 4DEEFB73181C3B023DB42686 /* NetworkMonitorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1575947B7A6FE08C57FE5EE4 /* NetworkMonitorProtocol.swift */; }; @@ -448,6 +451,7 @@ 64D05250CEDE8B604119F6E6 /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 981663D961C94270FA035FD0 /* Alert.swift */; }; 64E541F88F35BD126C4AFCA1 /* AppLockScreenPINKeypad.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38345442415E07A931197C55 /* AppLockScreenPINKeypad.swift */; }; 64EE9D2CF7AD02EE53983CE1 /* FileRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F75EF13F49DD2204E760910 /* FileRoomTimelineView.swift */; }; + 64F8590F4BEE4DA231F97D83 /* EncryptionResetFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A07B011547B201A836C03052 /* EncryptionResetFlowCoordinator.swift */; }; 651341E67C3514F9811A1EC1 /* LoginScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05F598B1B346DAF223651C91 /* LoginScreenCoordinator.swift */; }; 652ACCF104A8CEF30788963C /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1423AB065857FA546444DB15 /* NotificationManager.swift */; }; 6530865EB9A8C0F0AF0216DA /* ServerSelectionScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9501D11B4258DFA33BA3B40F /* ServerSelectionScreenModels.swift */; }; @@ -917,6 +921,7 @@ C9BE065FA7D4E77E4C61CB69 /* MapLibreModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = B81B6170DB690013CEB646F4 /* MapLibreModels.swift */; }; C9F5B48D15B9BCAE1F8D564E /* RoomNotificationModeProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1511766C534367700C8DD75 /* RoomNotificationModeProxy.swift */; }; CA12AE0DCD57D49CD96C699A /* WaveformCursorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB9EABCA9348DFA27439A809 /* WaveformCursorView.swift */; }; + CACD1352927336F01FC76612 /* EncryptionResetUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE4C76F31A382B8E4DD07583 /* EncryptionResetUITests.swift */; }; CB137BFB3E083C33E398A6CB /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 020597E28A4BC8E1BE8EDF6E /* KeychainAccess */; }; CB498F4E27AA0545DCEF0F6F /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 36B7FC232711031AA2B0D188 /* DTCoreText */; }; CB6956565D858C523E3E3B16 /* ComposerDraftServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7E37072597F67C4DD8CC2DB /* ComposerDraftServiceProtocol.swift */; }; @@ -1149,6 +1154,7 @@ FF34BF2AF731340AF9414A18 /* SwipeRightAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4552D3466B1453F287223ADA /* SwipeRightAction.swift */; }; FF7E8ECC8E7E1D1851517536 /* PollFormScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 347D708104CCEF771428C9A3 /* PollFormScreenViewModelTests.swift */; }; FF9C06BBF6AC6F1CFFBEBFFC /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 90791B9C739C716A40E1B230 /* target.yml */; }; + FFD52DCDA6962055A363CC8F /* IdentityResetHandleSDKMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EFB1D29B0870AFB6A56E9B8 /* IdentityResetHandleSDKMock.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1475,6 +1481,7 @@ 3BAC027034248429A438886B /* AppMediatorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMediatorMock.swift; sourceTree = ""; }; 3BC1B7CB061C9865B2B91B56 /* QRCodeLoginScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeLoginScreenViewModel.swift; sourceTree = ""; }; 3BDCCD2F6B405C14B9BCE94E /* JoinRoomScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenCoordinator.swift; sourceTree = ""; }; + 3BF8E5D4C95974B96A18C80E /* EncryptionSettingsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionSettingsUITests.swift; sourceTree = ""; }; 3C368CAB3063EF275357ECD4 /* LoginScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenViewModel.swift; sourceTree = ""; }; 3C3E67E09FE5A35D73818C39 /* AppLockScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenModels.swift; sourceTree = ""; }; 3CCD41CD67DB5DA0D436BFE9 /* VoiceMessageRoomPlaybackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRoomPlaybackView.swift; sourceTree = ""; }; @@ -1553,6 +1560,7 @@ 49E751D7EDB6043238111D90 /* UNNotificationRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UNNotificationRequest.swift; sourceTree = ""; }; 4A2B5274C1D3D2999D643786 /* EncryptionResetPasswordScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetPasswordScreenViewModelProtocol.swift; sourceTree = ""; }; 4A5B4CD611DE7E94F5BA87B2 /* AppLockTimerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockTimerTests.swift; sourceTree = ""; }; + 4AB29A2D95D3469B5F016655 /* SecureBackupControllerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupControllerMock.swift; sourceTree = ""; }; 4B2D4EEBE8C098BBADD10939 /* SecureBackupKeyBackupScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupKeyBackupScreenCoordinator.swift; sourceTree = ""; }; 4B41FABA2B0AEF4389986495 /* LoginMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginMode.swift; sourceTree = ""; }; 4BD371B60E07A5324B9507EF /* AnalyticsSettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenCoordinator.swift; sourceTree = ""; }; @@ -1632,6 +1640,7 @@ 5E75948AA1FE1D1A7809931F /* AuthenticationServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceProtocol.swift; sourceTree = ""; }; 5E9CBF577B9711CFBB4FA40D /* VoiceMessageRecordingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRecordingView.swift; sourceTree = ""; }; 5EB2CAA266B921D128C35710 /* LegalInformationScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenCoordinator.swift; sourceTree = ""; }; + 5EFB1D29B0870AFB6A56E9B8 /* IdentityResetHandleSDKMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityResetHandleSDKMock.swift; sourceTree = ""; }; 5F12E996BFBEB43815189ABF /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = uk; path = uk.lproj/Localizable.stringsdict; sourceTree = ""; }; 5F4134FEFE4EB55759017408 /* UserSessionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionProtocol.swift; sourceTree = ""; }; 5FACD034DB52525A3CEF2BDF /* SessionVerificationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreen.swift; sourceTree = ""; }; @@ -1906,6 +1915,7 @@ A019A12C866D64CF072024B9 /* AppLockSetupPINScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupPINScreenViewModel.swift; sourceTree = ""; }; A02D1A490944BF01A37586E1 /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/SAS.strings; sourceTree = ""; }; A057F2FDC14866C3026A89A4 /* NotificationManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManagerProtocol.swift; sourceTree = ""; }; + A07B011547B201A836C03052 /* EncryptionResetFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetFlowCoordinator.swift; sourceTree = ""; }; A103580EBA06155B1343EF16 /* HomeScreenKnockedCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenKnockedCell.swift; sourceTree = ""; }; A12D3B1BCF920880CA8BBB6B /* UserIndicatorControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorControllerProtocol.swift; sourceTree = ""; }; A130A2251A15A7AACC84FD37 /* RoomPollsHistoryScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -2035,6 +2045,7 @@ BC8AA23D4F37CC26564F63C5 /* LayoutMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutMocks.swift; sourceTree = ""; }; BCDA016D05107DED3B9495CB /* TimelineItemDebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemDebugView.swift; sourceTree = ""; }; BE148A4FFEE853C5A281500C /* UNNotificationContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UNNotificationContent.swift; sourceTree = ""; }; + BE4C76F31A382B8E4DD07583 /* EncryptionResetUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetUITests.swift; sourceTree = ""; }; BE78CAD0B964C66FD06EF83E /* DeactivateAccountScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeactivateAccountScreenModels.swift; sourceTree = ""; }; BE89A8BD65CCE3FCC925CA14 /* TimelineItemReplyDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemReplyDetails.swift; sourceTree = ""; }; BE9BBB18FB27F09032AD8769 /* NotificationPermissionsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPermissionsScreenViewModel.swift; sourceTree = ""; }; @@ -2244,6 +2255,7 @@ EBEB8D9F4940E161B18FE4BC /* UITestsNotificationCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsNotificationCenter.swift; sourceTree = ""; }; EC589E641AE46EFB2962534D /* RoomMemberDetailsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsViewModelTests.swift; sourceTree = ""; }; EC5D7DA665E1F5F509C994C7 /* ScaledOffsetModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScaledOffsetModifier.swift; sourceTree = ""; }; + ECB836DD8BE31931F51B8AC9 /* EncryptionSettingsFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionSettingsFlowCoordinator.swift; sourceTree = ""; }; ECD5FCBA169B6A82F501CA1B /* AnalyticsSettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenViewModelProtocol.swift; sourceTree = ""; }; ECF79FB25E2D4BD6F50CE7C9 /* RoomMembersListScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenViewModel.swift; sourceTree = ""; }; ED003DF1B7CF40E7073A2280 /* TracingConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingConfiguration.swift; sourceTree = ""; }; @@ -2935,6 +2947,7 @@ FC83F47D2173B7538AA72E0E /* RoomSummaryProviderMock.swift */, D479DF730528153665E5782E /* RoomTimelineControllerFactoryMock.swift */, F74532E01B317C56C1BE8FA8 /* RoomTimelineProviderMock.swift */, + 4AB29A2D95D3469B5F016655 /* SecureBackupControllerMock.swift */, 248649EBA5BC33DB93698734 /* SessionVerificationControllerProxyMock.swift */, 7893780A1FD6E3F38B3E9049 /* UserIndicatorControllerMock.swift */, AAD01F7FC2BBAC7351948595 /* UserProfile+Mock.swift */, @@ -3531,6 +3544,8 @@ 0DBB08A95EFA668F2CF27211 /* AppLockSetupFlowCoordinator.swift */, A9B069D7772DDF6513E0F1B8 /* AuthenticationFlowCoordinator.swift */, 7367B3B9A8CAF902220F31D1 /* BugReportFlowCoordinator.swift */, + A07B011547B201A836C03052 /* EncryptionResetFlowCoordinator.swift */, + ECB836DD8BE31931F51B8AC9 /* EncryptionSettingsFlowCoordinator.swift */, C3285BD95B564CA2A948E511 /* OnboardingFlowCoordinator.swift */, A54AAF72E821B4084B7E4298 /* PinnedEventsTimelineFlowCoordinator.swift */, 9A008E57D52B07B78DFAD1BB /* RoomFlowCoordinator.swift */, @@ -4444,6 +4459,8 @@ 295E28C3B9EAADF519BF2F44 /* AuthenticationFlowCoordinatorUITests.swift */, C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */, F8CEB4634C0DD7779C4AB504 /* CreateRoomScreenUITests.swift */, + BE4C76F31A382B8E4DD07583 /* EncryptionResetUITests.swift */, + 3BF8E5D4C95974B96A18C80E /* EncryptionSettingsUITests.swift */, 3368395F06AA180138E185B6 /* PollFormScreenUITests.swift */, C5B7A755E985FA14469E86B2 /* RoomMembersListScreenUITests.swift */, 45571C2EBD98ED7E0CEA7AF7 /* RoomRolesAndPermissionsUITests.swift */, @@ -5287,6 +5304,7 @@ isa = PBXGroup; children = ( 8EAF4A49F3ACD8BB8B0D2371 /* ClientSDKMock.swift */, + 5EFB1D29B0870AFB6A56E9B8 /* IdentityResetHandleSDKMock.swift */, E8DE9D0D480D087D0F676B52 /* UserIdentitySDKMock.swift */, ); path = SDK; @@ -6470,6 +6488,7 @@ 03CDCA6243F89B194E3FAD17 /* EncryptionAuthenticity.swift in Sources */, FBD402E3170EB1ED0D1AA672 /* EncryptionKeyProvider.swift in Sources */, 46A6DB0F78FB399BD59E2D41 /* EncryptionKeyProviderProtocol.swift in Sources */, + 64F8590F4BEE4DA231F97D83 /* EncryptionResetFlowCoordinator.swift in Sources */, 0C6DF318E9C8F6461E6ABDE7 /* EncryptionResetPasswordScreen.swift in Sources */, 36926D795D6D19177C7812F8 /* EncryptionResetPasswordScreenCoordinator.swift in Sources */, B1B255CE0E4306DD6E09D936 /* EncryptionResetPasswordScreenModels.swift in Sources */, @@ -6480,6 +6499,7 @@ 97BAEDD9054FB5F233EE928B /* EncryptionResetScreenModels.swift in Sources */, EC3320639828BED8B3E5F2C6 /* EncryptionResetScreenViewModel.swift in Sources */, A0868BDE84D2140A885BE3C9 /* EncryptionResetScreenViewModelProtocol.swift in Sources */, + 4D2B54233C7B2C04B4ABE55A /* EncryptionSettingsFlowCoordinator.swift in Sources */, 50539366B408780B232C1910 /* EstimatedWaveformView.swift in Sources */, F78BAD28482A467287A9A5A3 /* EventBasedMessageTimelineItemProtocol.swift in Sources */, 02D8DF8EB7537EB4E9019DDB /* EventBasedTimelineItemProtocol.swift in Sources */, @@ -6527,6 +6547,7 @@ D22345698F6548C1EE960940 /* IdentityConfirmedScreenModels.swift in Sources */, 01681E8B20AD6F0D237F2DC1 /* IdentityConfirmedScreenViewModel.swift in Sources */, AADE7C2497A7B55D8BED7BD6 /* IdentityConfirmedScreenViewModelProtocol.swift in Sources */, + FFD52DCDA6962055A363CC8F /* IdentityResetHandleSDKMock.swift in Sources */, BA31448FBD9697F8CB9A83CD /* ImageCache.swift in Sources */, 7CD16990BA843BE9ED639129 /* ImageRoomTimelineItem.swift in Sources */, B796A25F282C0A340D1B9C12 /* ImageRoomTimelineItemContent.swift in Sources */, @@ -6868,6 +6889,7 @@ 67160204A8D362BB7D4AD259 /* Search.swift in Sources */, 339BC18777912E1989F2F17D /* Section.swift in Sources */, F833D5B5BE6707F961FA88DB /* SecureBackupController.swift in Sources */, + 2D0E3983288E2D35613AD681 /* SecureBackupControllerMock.swift in Sources */, 0C88044649BAEE6C49BFC43A /* SecureBackupControllerProtocol.swift in Sources */, 21F29351EDD7B2A5534EE862 /* SecureBackupKeyBackupScreen.swift in Sources */, 5BC6C4ADFE7F2A795ECDE130 /* SecureBackupKeyBackupScreenCoordinator.swift in Sources */, @@ -7092,6 +7114,8 @@ 7756C4E90CABE6F14F7920A0 /* BugReportUITests.swift in Sources */, 94D0F36A87E596A93C0C178A /* Bundle.swift in Sources */, 9F19096BFA629C0AC282B1E4 /* CreateRoomScreenUITests.swift in Sources */, + CACD1352927336F01FC76612 /* EncryptionResetUITests.swift in Sources */, + 230981086F0199F913434D6B /* EncryptionSettingsUITests.swift in Sources */, 0CF81807BE5FBFC9E2BBCECF /* PollFormScreenUITests.swift in Sources */, 44121202B4A260C98BF615A7 /* RoomMembersListScreenUITests.swift in Sources */, D29E046C1E3045E0346C479D /* RoomRolesAndPermissionsUITests.swift in Sources */, diff --git a/ElementX/Sources/FlowCoordinators/EncryptionResetFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/EncryptionResetFlowCoordinator.swift new file mode 100644 index 0000000000..aea6264c49 --- /dev/null +++ b/ElementX/Sources/FlowCoordinators/EncryptionResetFlowCoordinator.swift @@ -0,0 +1,158 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Combine +import Foundation +import SwiftState + +enum EncryptionResetFlowCoordinatorAction: Equatable { + /// The flow is complete. + case resetComplete + /// The flow was cancelled. + case cancel +} + +struct EncryptionResetFlowCoordinatorParameters { + let userSession: UserSessionProtocol + let userIndicatorController: UserIndicatorControllerProtocol + let navigationStackCoordinator: NavigationStackCoordinator + let windowManger: WindowManagerProtocol +} + +class EncryptionResetFlowCoordinator: FlowCoordinatorProtocol { + private let userSession: UserSessionProtocol + private let userIndicatorController: UserIndicatorControllerProtocol + + private let navigationStackCoordinator: NavigationStackCoordinator + private let windowManager: WindowManagerProtocol + + enum State: StateType { + /// The state machine hasn't started. + case initial + /// The root screen for this flow. + case encryptionResetScreen + /// Confirming the user's password to continue. + case confirmingPassword + } + + enum Event: EventType { + /// The flow is being started. + case start + + /// The user needs to confirm their password to reset. + case confirmPassword + /// The user confirmed their password. + case finishedConfirmingPassword + } + + private let stateMachine: StateMachine + private var cancellables: Set = [] + + private let actionsSubject: PassthroughSubject = .init() + var actionsPublisher: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(parameters: EncryptionResetFlowCoordinatorParameters) { + userSession = parameters.userSession + userIndicatorController = parameters.userIndicatorController + navigationStackCoordinator = parameters.navigationStackCoordinator + windowManager = parameters.windowManger + + stateMachine = .init(state: .initial) + configureStateMachine() + } + + func start() { + stateMachine.tryEvent(.start) + } + + func handleAppRoute(_ appRoute: AppRoute, animated: Bool) { + // There aren't any routes to this screen, so always clear the stack. + clearRoute(animated: animated) + } + + func clearRoute(animated: Bool) { + // As we push screens on top of an existing stack, popping to root wouldn't be safe. + switch stateMachine.state { + case .initial: + break + case .encryptionResetScreen: + navigationStackCoordinator.pop(animated: animated) + case .confirmingPassword: + navigationStackCoordinator.pop(animated: animated) // Password screen. + navigationStackCoordinator.pop(animated: animated) // EncryptionReset screen. + } + } + + // MARK: - Private + + private func configureStateMachine() { + stateMachine.addRoutes(event: .start, transitions: [.initial => .encryptionResetScreen]) { [weak self] _ in + self?.presentEncryptionResetScreen() + } + + stateMachine.addRoutes(event: .confirmPassword, transitions: [.encryptionResetScreen => .confirmingPassword]) { [weak self] context in + guard let passwordPublisher = context.userInfo as? PassthroughSubject else { fatalError("Expected a publisher in the userInfo.") } + self?.presentPasswordScreen(passwordPublisher: passwordPublisher) + } + stateMachine.addRoutes(event: .finishedConfirmingPassword, transitions: [.confirmingPassword => .encryptionResetScreen]) + + stateMachine.addErrorHandler { context in + fatalError("Unexpected transition: \(context)") + } + } + + private func presentEncryptionResetScreen() { + let coordinator = EncryptionResetScreenCoordinator(parameters: .init(clientProxy: userSession.clientProxy, + userIndicatorController: userIndicatorController)) + + coordinator.actionsPublisher.sink { [weak self] action in + guard let self else { return } + + switch action { + case .requestOIDCAuthorisation(let url): + presentOIDCAuthorization(for: url) + case .requestPassword(let passwordPublisher): + stateMachine.tryEvent(.confirmPassword, userInfo: passwordPublisher) + case .cancel: + actionsSubject.send(.cancel) + case .resetFinished: + actionsSubject.send(.resetComplete) + } + } + .store(in: &cancellables) + + navigationStackCoordinator.setRootCoordinator(coordinator) + } + + private func presentPasswordScreen(passwordPublisher: PassthroughSubject) { + let coordinator = EncryptionResetPasswordScreenCoordinator(parameters: .init(passwordPublisher: passwordPublisher)) + + coordinator.actionsPublisher.sink { [weak self] action in + guard let self else { return } + + switch action { + case .passwordEntered: + navigationStackCoordinator.pop() + } + } + .store(in: &cancellables) + + navigationStackCoordinator.push(coordinator) { [stateMachine] in + stateMachine.tryEvent(.finishedConfirmingPassword) + } + } + + private var accountSettingsPresenter: OIDCAccountSettingsPresenter? + private func presentOIDCAuthorization(for 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() + } +} diff --git a/ElementX/Sources/FlowCoordinators/EncryptionSettingsFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/EncryptionSettingsFlowCoordinator.swift new file mode 100644 index 0000000000..af620ef40b --- /dev/null +++ b/ElementX/Sources/FlowCoordinators/EncryptionSettingsFlowCoordinator.swift @@ -0,0 +1,198 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Combine +import Foundation +import SwiftState + +enum EncryptionSettingsFlowCoordinatorAction: Equatable { + /// The flow is complete. + case complete +} + +struct EncryptionSettingsFlowCoordinatorParameters { + let userSession: UserSessionProtocol + let appSettings: AppSettings + let userIndicatorController: UserIndicatorControllerProtocol + let navigationStackCoordinator: NavigationStackCoordinator +} + +class EncryptionSettingsFlowCoordinator: FlowCoordinatorProtocol { + private let userSession: UserSessionProtocol + private let appSettings: AppSettings + private let userIndicatorController: UserIndicatorControllerProtocol + private let navigationStackCoordinator: NavigationStackCoordinator + + // periphery:ignore - retaining purpose + private var encryptionResetFlowCoordinator: EncryptionResetFlowCoordinator? + + enum State: StateType { + /// The state machine hasn't started. + case initial + /// The root screen for this flow. + case secureBackupScreen + /// The user is managing their recovery key. + case recoveryKeyScreen + /// The user is disabling key backups. + case keyBackupScreen + } + + enum Event: EventType { + /// The flow is being started. + case start + + /// The user would like to manage their recovery key. + case manageRecoveryKey + /// The user finished managing their recovery key. + case finishedManagingRecoveryKey + + /// The user doesn't want to use key backup any more. + case disableKeyBackup + /// The key backup screen was dismissed. + case finishedDisablingKeyBackup + } + + private let stateMachine: StateMachine + private var cancellables: Set = [] + + private let actionsSubject: PassthroughSubject = .init() + var actionsPublisher: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(parameters: EncryptionSettingsFlowCoordinatorParameters) { + userSession = parameters.userSession + appSettings = parameters.appSettings + userIndicatorController = parameters.userIndicatorController + navigationStackCoordinator = parameters.navigationStackCoordinator + + stateMachine = .init(state: .initial) + configureStateMachine() + } + + func start() { + stateMachine.tryEvent(.start) + } + + func handleAppRoute(_ appRoute: AppRoute, animated: Bool) { + switch appRoute { + case .roomList, .room, .roomAlias, .childRoom, .childRoomAlias, + .roomDetails, .roomMemberDetails, .userProfile, + .event, .eventOnRoomAlias, .childEvent, .childEventOnRoomAlias, + .call, .genericCallLink, .settings: + // These routes aren't in this flow so clear the entire stack. + clearRoute(animated: animated) + case .chatBackupSettings: + popToRootScreen(animated: animated) + } + } + + func clearRoute(animated: Bool) { + let fromState = stateMachine.state + popToRootScreen(animated: animated) + guard fromState != .initial else { return } + navigationStackCoordinator.pop(animated: animated) // SecureBackup screen. + } + + func popToRootScreen(animated: Bool) { + // As we push screens on top of an existing stack, a literal pop to root wouldn't be safe. + switch stateMachine.state { + case .initial, .secureBackupScreen: + break + case .recoveryKeyScreen: + navigationStackCoordinator.setSheetCoordinator(nil, animated: animated) // RecoveryKey screen. + case .keyBackupScreen: + navigationStackCoordinator.setSheetCoordinator(nil, animated: animated) // KeyBackup screen. + } + } + + // MARK: - Private + + private func configureStateMachine() { + stateMachine.addRoutes(event: .start, transitions: [.initial => .secureBackupScreen]) { [weak self] _ in + self?.presentSecureBackupScreen() + } + + stateMachine.addRoutes(event: .manageRecoveryKey, transitions: [.secureBackupScreen => .recoveryKeyScreen]) { [weak self] _ in + self?.presentRecoveryKeyScreen() + } + stateMachine.addRoutes(event: .finishedManagingRecoveryKey, transitions: [.recoveryKeyScreen => .secureBackupScreen]) + + stateMachine.addRoutes(event: .disableKeyBackup, transitions: [.secureBackupScreen => .keyBackupScreen]) { [weak self] _ in + self?.presentKeyBackupScreen() + } + stateMachine.addRoutes(event: .finishedDisablingKeyBackup, transitions: [.keyBackupScreen => .secureBackupScreen]) + + stateMachine.addErrorHandler { context in + fatalError("Unexpected transition: \(context)") + } + } + + private func presentSecureBackupScreen(animated: Bool = true) { + let coordinator = SecureBackupScreenCoordinator(parameters: .init(appSettings: appSettings, + clientProxy: userSession.clientProxy, + userIndicatorController: userIndicatorController)) + coordinator.actions.sink { [weak self] action in + guard let self else { return } + + switch action { + case .manageRecoveryKey: + stateMachine.tryEvent(.manageRecoveryKey) + case .disableKeyBackup: + stateMachine.tryEvent(.disableKeyBackup) + } + } + .store(in: &cancellables) + + navigationStackCoordinator.push(coordinator, animated: animated) { [weak self] in + self?.actionsSubject.send(.complete) + } + } + + private func presentRecoveryKeyScreen() { + let sheetNavigationStackCoordinator = NavigationStackCoordinator() + let coordinator = SecureBackupRecoveryKeyScreenCoordinator(parameters: .init(secureBackupController: userSession.clientProxy.secureBackupController, + userIndicatorController: userIndicatorController, + isModallyPresented: true)) + + coordinator.actions.sink { [weak self] action in + guard let self else { return } + switch action { + case .complete: + navigationStackCoordinator.setSheetCoordinator(nil) + } + } + .store(in: &cancellables) + + sheetNavigationStackCoordinator.setRootCoordinator(coordinator, animated: true) + + navigationStackCoordinator.setSheetCoordinator(sheetNavigationStackCoordinator) { [stateMachine] in + stateMachine.tryEvent(.finishedManagingRecoveryKey) + } + } + + private func presentKeyBackupScreen() { + let sheetNavigationStackCoordinator = NavigationStackCoordinator() + + let coordinator = SecureBackupKeyBackupScreenCoordinator(parameters: .init(secureBackupController: userSession.clientProxy.secureBackupController, + userIndicatorController: userIndicatorController)) + + coordinator.actions.sink { [weak self] action in + switch action { + case .done: + self?.navigationStackCoordinator.setSheetCoordinator(nil) + } + } + .store(in: &cancellables) + + sheetNavigationStackCoordinator.setRootCoordinator(coordinator, animated: true) + + navigationStackCoordinator.setSheetCoordinator(sheetNavigationStackCoordinator) { [stateMachine] in + stateMachine.tryEvent(.finishedDisablingKeyBackup) + } + } +} diff --git a/ElementX/Sources/FlowCoordinators/OnboardingFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/OnboardingFlowCoordinator.swift index 48428c2a8a..cea5f952c5 100644 --- a/ElementX/Sources/FlowCoordinators/OnboardingFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/OnboardingFlowCoordinator.swift @@ -46,6 +46,8 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol { // periphery: ignore - used to store the coordinator to avoid deallocation private var appLockFlowCoordinator: AppLockSetupFlowCoordinator? + // periphery: ignore - used to store the coordinator to avoid deallocation + private var encryptionResetFlowCoordinator: EncryptionResetFlowCoordinator? private let actionsSubject: PassthroughSubject = .init() var actions: AnyPublisher { @@ -251,7 +253,7 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol { appSettings.hasRunIdentityConfirmationOnboarding = true stateMachine.tryEvent(.nextSkippingIdentityConfimed) case .reset: - presentEncryptionResetScreen() + startEncryptionResetFlow() case .logout: actionsSubject.send(.logout) } @@ -295,12 +297,8 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol { guard let self else { return } switch action { - case .recoveryFixed: + case .complete: break // Moving to next state is Handled by the global session verification listener - case .resetEncryption: - presentEncryptionResetScreen() - default: - MXLog.error("Unexpected recovery action: \(action)") } } .store(in: &cancellables) @@ -308,31 +306,31 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol { presentCoordinator(coordinator) } - private func presentEncryptionResetScreen() { + private func startEncryptionResetFlow() { let resetNavigationStackCoordinator = NavigationStackCoordinator() - - let coordinator = EncryptionResetScreenCoordinator(parameters: .init(clientProxy: userSession.clientProxy, - navigationStackCoordinator: resetNavigationStackCoordinator, - userIndicatorController: userIndicatorController)) + let coordinator = EncryptionResetFlowCoordinator(parameters: .init(userSession: userSession, + userIndicatorController: userIndicatorController, + navigationStackCoordinator: resetNavigationStackCoordinator, + windowManger: windowManager)) coordinator.actionsPublisher.sink { [weak self] action in guard let self else { return } - switch action { - case .cancel: - navigationStackCoordinator.setSheetCoordinator(nil) - case .requestOIDCAuthorisation(let url): - presentOIDCAuthorisationScreen(url: url) - case .resetFinished: + case .resetComplete: // Moving to next state is handled by the global session verification listener navigationStackCoordinator.setSheetCoordinator(nil) + case .cancel: + navigationStackCoordinator.setSheetCoordinator(nil) } } .store(in: &cancellables) - resetNavigationStackCoordinator.setRootCoordinator(coordinator) + encryptionResetFlowCoordinator = coordinator + coordinator.start() - navigationStackCoordinator.setSheetCoordinator(resetNavigationStackCoordinator) + navigationStackCoordinator.setSheetCoordinator(resetNavigationStackCoordinator) { [weak self] in + self?.encryptionResetFlowCoordinator = nil + } } private func presentIdentityConfirmedScreen() { @@ -411,12 +409,4 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol { navigationStackCoordinator.push(coordinator, dismissalCallback: dismissalCallback) } } - - 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() - } } diff --git a/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift index 3ae3d31cfa..3ff63c7bbe 100644 --- a/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift @@ -39,9 +39,10 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol { // periphery:ignore - retaining purpose private var appLockSetupFlowCoordinator: AppLockSetupFlowCoordinator? - // periphery:ignore - retaining purpose private var bugReportFlowCoordinator: BugReportFlowCoordinator? + // periphery:ignore - retaining purpose + private var encryptionSettingsFlowCoordinator: EncryptionSettingsFlowCoordinator? private let actionsSubject: PassthroughSubject = .init() var actions: AnyPublisher { @@ -68,7 +69,7 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol { // The navigation stack doesn't like it if the root and the push happen // on the same loop run DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { - self.presentSecureBackupScreen(animated: animated) + self.startEncryptionSettingsFlow(animated: animated) } default: break @@ -102,7 +103,7 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol { self.actionsSubject.send(.runLogoutFlow) } case .secureBackup: - presentSecureBackupScreen(animated: true) + startEncryptionSettingsFlow(animated: true) case .userDetails: presentUserDetailsEditScreen() case let .manageAccount(url): @@ -145,21 +146,22 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol { actionsSubject.send(.presentedSettings) } - private func presentSecureBackupScreen(animated: Bool) { - let coordinator = SecureBackupScreenCoordinator(parameters: .init(appSettings: parameters.appSettings, - clientProxy: parameters.userSession.clientProxy, - navigationStackCoordinator: navigationStackCoordinator, - userIndicatorController: parameters.userIndicatorController)) - - coordinator.actions.sink { [weak self] action in + private func startEncryptionSettingsFlow(animated: Bool) { + let coordinator = EncryptionSettingsFlowCoordinator(parameters: .init(userSession: parameters.userSession, + appSettings: parameters.appSettings, + userIndicatorController: parameters.userIndicatorController, + navigationStackCoordinator: navigationStackCoordinator)) + coordinator.actionsPublisher.sink { [weak self] action in switch action { - case .requestOIDCAuthorisation(let url): - self?.presentAccountManagementURL(url) + case .complete: + // The flow coordinator tidies up the stack, no need to do anything. + self?.encryptionSettingsFlowCoordinator = nil } } .store(in: &cancellables) - navigationStackCoordinator.push(coordinator, animated: animated) + encryptionSettingsFlowCoordinator = coordinator + coordinator.start() } private func presentUserDetailsEditScreen() { diff --git a/ElementX/Sources/Mocks/ClientProxyMock.swift b/ElementX/Sources/Mocks/ClientProxyMock.swift index 8cc4962f89..b5b52018bd 100644 --- a/ElementX/Sources/Mocks/ClientProxyMock.swift +++ b/ElementX/Sources/Mocks/ClientProxyMock.swift @@ -13,6 +13,8 @@ struct ClientProxyMockConfiguration { var deviceID: String? var roomSummaryProvider: RoomSummaryProviderProtocol? = RoomSummaryProviderMock(.init()) var roomDirectorySearchProxy: RoomDirectorySearchProxyProtocol? + + var recoveryState: SecureBackupRecoveryState = .enabled } enum ClientProxyMockError: Error { @@ -78,12 +80,8 @@ extension ClientProxyMock { loadMediaThumbnailForSourceWidthHeightThrowableError = ClientProxyError.sdkError(ClientProxyMockError.generic) loadMediaFileForSourceFilenameThrowableError = ClientProxyError.sdkError(ClientProxyMockError.generic) - secureBackupController = { - let secureBackupController = SecureBackupControllerMock() - secureBackupController.underlyingRecoveryState = .init(CurrentValueSubject(.enabled)) - secureBackupController.underlyingKeyBackupState = .init(CurrentValueSubject(.enabled)) - return secureBackupController - }() + secureBackupController = SecureBackupControllerMock(.init(recoveryState: configuration.recoveryState)) + resetIdentityReturnValue = .success(IdentityResetHandleSDKMock(.init())) roomForIdentifierClosure = { [weak self] identifier in guard let room = self?.roomSummaryProvider?.roomListPublisher.value.first(where: { $0.id == identifier }) else { diff --git a/ElementX/Sources/Mocks/SDK/IdentityResetHandleSDKMock.swift b/ElementX/Sources/Mocks/SDK/IdentityResetHandleSDKMock.swift new file mode 100644 index 0000000000..7f4808b2e1 --- /dev/null +++ b/ElementX/Sources/Mocks/SDK/IdentityResetHandleSDKMock.swift @@ -0,0 +1,22 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Foundation +import MatrixRustSDK + +extension IdentityResetHandleSDKMock { + struct Configuration { } + + convenience init(_ configuration: Configuration) { + self.init() + + authTypeReturnValue = .uiaa + resetAuthClosure = { _ in + try await Task.sleep(for: .seconds(60)) + } + } +} diff --git a/ElementX/Sources/Mocks/SecureBackupControllerMock.swift b/ElementX/Sources/Mocks/SecureBackupControllerMock.swift new file mode 100644 index 0000000000..a1c5e0554d --- /dev/null +++ b/ElementX/Sources/Mocks/SecureBackupControllerMock.swift @@ -0,0 +1,48 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Combine +import Foundation + +extension SecureBackupControllerMock { + struct Configuration { + var recoveryState: SecureBackupRecoveryState = .enabled + var keyBackupState: SecureBackupKeyBackupState = .enabled + } + + convenience init(_ configuration: Configuration) { + self.init() + + let recoveryStateSubject = CurrentValueSubject(configuration.recoveryState) + underlyingRecoveryState = .init(recoveryStateSubject) + + let keyBackupStateSubject = CurrentValueSubject(configuration.keyBackupState) + underlyingKeyBackupState = .init(keyBackupStateSubject) + + disableClosure = { + recoveryStateSubject.send(.disabled) + keyBackupStateSubject.send(.unknown) + return .success(()) + } + + enableClosure = { + recoveryStateSubject.send(.disabled) + keyBackupStateSubject.send(.enabled) + return .success(()) + } + + generateRecoveryKeyClosure = { + recoveryStateSubject.send(.enabled) + return .success("a1B2 C3d4 E5F6 g7H8 i9J0 K1l2 M3n4 O5p6 Q7R8 s9T0 U1v2 W3X4") + } + + confirmRecoveryKeyClosure = { _ in + recoveryStateSubject.send(.enabled) + return .success(()) + } + } +} diff --git a/ElementX/Sources/Other/AccessibilityIdentifiers.swift b/ElementX/Sources/Other/AccessibilityIdentifiers.swift index 63eb3aa73f..0c6c95aab4 100644 --- a/ElementX/Sources/Other/AccessibilityIdentifiers.swift +++ b/ElementX/Sources/Other/AccessibilityIdentifiers.swift @@ -16,6 +16,8 @@ enum A11yIdentifiers { static let appLockSetupSettingsScreen = AppLockSetupSettingsScreen() static let bugReportScreen = BugReportScreen() static let changeServerScreen = ChangeServer() + static let encryptionResetScreen = EncryptionResetScreen() + static let encryptionResetPasswordScreen = EncryptionResetPasswordScreen() static let homeScreen = HomeScreen() static let loginScreen = LoginScreen() static let authenticationStartScreen = AuthenticationStartScreen() @@ -24,6 +26,9 @@ enum A11yIdentifiers { static let roomDetailsScreen = RoomDetailsScreen() static let roomNotificationSettingsScreen = RoomNotificationSettingsScreen() static let roomRolesAndPermissionsScreen = RoomRolesAndPermissionsScreen() + static let secureBackupScreen = SecureBackupScreen() + static let secureBackupKeyBackupScreen = SecureBackupKeyBackupScreen() + static let secureBackupRecoveryKeyScreen = SecureBackupRecoveryKeyScreen() static let serverConfirmationScreen = ServerConfirmationScreen() static let sessionVerificationScreen = SessionVerificationScreen() static let settingsScreen = SettingsScreen() @@ -81,6 +86,15 @@ enum A11yIdentifiers { let dismiss = "change_server-dismiss" } + struct EncryptionResetScreen { + let continueReset = "encryption_reset-continue_reset" + } + + struct EncryptionResetPasswordScreen { + let passwordField = "encryption_reset_password-password_field" + let submit = "encryption_reset_password-submit" + } + struct HomeScreen { let userAvatar = "home_screen-user_avatar" let recoveryKeyConfirmationBannerContinue = "home_screen-recovery_key_confirmation_continue" @@ -178,6 +192,23 @@ enum A11yIdentifiers { let memberModeration = "room_roles_and_permissions-member_moderation" } + struct SecureBackupScreen { + let keyStorage = "secure_backup-key_storage" + let recoveryKey = "secure_backup-recovery_key" + } + + struct SecureBackupKeyBackupScreen { + let deleteKeyStorage = "secure_backup_key_backup-delete_key_storage" + } + + struct SecureBackupRecoveryKeyScreen { + let generateRecoveryKey = "secure_backup_recovery_key-generate_recovery_key" + let copyRecoveryKey = "secure_backup_recovery_key-copy_recovery_key" + let done = "secure_backup_recovery_key-done" + let recoveryKeyField = "secure_backup_recovery_key-recovery_key_field" + let confirm = "secure_backup_recovery_key-confirm" + } + struct ServerConfirmationScreen { let `continue` = "server_confirmation-continue" let changeServer = "server_confirmation-change_server" diff --git a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/EncryptionResetPasswordScreenCoordinator.swift b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/EncryptionResetPasswordScreenCoordinator.swift index 3c2a969208..5a6003db20 100644 --- a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/EncryptionResetPasswordScreenCoordinator.swift +++ b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/EncryptionResetPasswordScreenCoordinator.swift @@ -8,17 +8,12 @@ import Combine import SwiftUI -struct EncryptionResetPasswordScreenCoordinatorParameters { } +struct EncryptionResetPasswordScreenCoordinatorParameters { + let passwordPublisher: PassthroughSubject +} -enum EncryptionResetPasswordScreenCoordinatorAction: CustomStringConvertible { - case resetIdentity(String) - - var description: String { - switch self { - case .resetIdentity: - "resetIdentity" - } - } +enum EncryptionResetPasswordScreenCoordinatorAction { + case passwordEntered } final class EncryptionResetPasswordScreenCoordinator: CoordinatorProtocol { @@ -35,7 +30,7 @@ final class EncryptionResetPasswordScreenCoordinator: CoordinatorProtocol { init(parameters: EncryptionResetPasswordScreenCoordinatorParameters) { self.parameters = parameters - viewModel = EncryptionResetPasswordScreenViewModel() + viewModel = EncryptionResetPasswordScreenViewModel(passwordPublisher: parameters.passwordPublisher) } func start() { @@ -44,8 +39,8 @@ final class EncryptionResetPasswordScreenCoordinator: CoordinatorProtocol { guard let self else { return } switch action { - case .resetIdentity(let password): - self.actionsSubject.send(.resetIdentity(password)) + case .passwordEntered: + self.actionsSubject.send(.passwordEntered) } } .store(in: &cancellables) diff --git a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/EncryptionResetPasswordScreenModels.swift b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/EncryptionResetPasswordScreenModels.swift index d228ab4037..d226d29657 100644 --- a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/EncryptionResetPasswordScreenModels.swift +++ b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/EncryptionResetPasswordScreenModels.swift @@ -7,15 +7,8 @@ import Foundation -enum EncryptionResetPasswordScreenViewModelAction: CustomStringConvertible { - case resetIdentity(String) - - var description: String { - switch self { - case .resetIdentity: - "resetIdentity" - } - } +enum EncryptionResetPasswordScreenViewModelAction { + case passwordEntered } struct EncryptionResetPasswordScreenViewState: BindableState { @@ -28,5 +21,5 @@ struct EncryptionResetPasswordScreenViewStateBindings { } enum EncryptionResetPasswordScreenViewAction { - case resetIdentity + case submit } diff --git a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/EncryptionResetPasswordScreenViewModel.swift b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/EncryptionResetPasswordScreenViewModel.swift index ed73d1924f..fd01cfc95b 100644 --- a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/EncryptionResetPasswordScreenViewModel.swift +++ b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/EncryptionResetPasswordScreenViewModel.swift @@ -11,12 +11,16 @@ import SwiftUI typealias EncryptionResetPasswordScreenViewModelType = StateStoreViewModel class EncryptionResetPasswordScreenViewModel: EncryptionResetPasswordScreenViewModelType, EncryptionResetPasswordScreenViewModelProtocol { + private let passwordPublisher: PassthroughSubject + private let actionsSubject: PassthroughSubject = .init() var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - init() { + init(passwordPublisher: PassthroughSubject) { + self.passwordPublisher = passwordPublisher + super.init(initialViewState: .init(bindings: .init(password: ""))) } @@ -26,8 +30,9 @@ class EncryptionResetPasswordScreenViewModel: EncryptionResetPasswordScreenViewM MXLog.info("View model: received view action: \(viewAction)") switch viewAction { - case .resetIdentity: - actionsSubject.send(.resetIdentity(state.bindings.password)) + case .submit: + passwordPublisher.send(state.bindings.password) + actionsSubject.send(.passwordEntered) } } } diff --git a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/View/EncryptionResetPasswordScreen.swift b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/View/EncryptionResetPasswordScreen.swift index 03681a81d4..df14c8d134 100644 --- a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/View/EncryptionResetPasswordScreen.swift +++ b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/View/EncryptionResetPasswordScreen.swift @@ -5,6 +5,7 @@ // Please see LICENSE in the repository root for full details. // +import Combine import Compound import SwiftUI @@ -32,9 +33,10 @@ struct EncryptionResetPasswordScreen: View { .padding(16) } bottomContent: { Button(L10n.actionResetIdentity, role: .destructive) { - context.send(viewAction: .resetIdentity) + context.send(viewAction: .submit) } .buttonStyle(.compound(.primary)) + .accessibilityIdentifier(A11yIdentifiers.encryptionResetPasswordScreen.submit) } .background() .backgroundStyle(.compound.bgCanvasDefault) @@ -58,8 +60,9 @@ struct EncryptionResetPasswordScreen: View { .focused($textFieldFocus) .submitLabel(.done) .onSubmit { - context.send(viewAction: .resetIdentity) + context.send(viewAction: .submit) } + .accessibilityIdentifier(A11yIdentifiers.encryptionResetPasswordScreen.passwordField) } } } @@ -67,7 +70,8 @@ struct EncryptionResetPasswordScreen: View { // MARK: - Previews struct EncryptionResetPasswordScreen_Previews: PreviewProvider, TestablePreview { - static let viewModel = EncryptionResetPasswordScreenViewModel() + static let passwordPublisher = PassthroughSubject() + static let viewModel = EncryptionResetPasswordScreenViewModel(passwordPublisher: passwordPublisher) static var previews: some View { NavigationStack { EncryptionResetPasswordScreen(context: viewModel.context) diff --git a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenCoordinator.swift b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenCoordinator.swift index b2f7edeeab..446a5ab606 100644 --- a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenCoordinator.swift +++ b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenCoordinator.swift @@ -9,14 +9,14 @@ import Combine import SwiftUI enum EncryptionResetScreenCoordinatorAction { - case cancel case requestOIDCAuthorisation(URL) + case requestPassword(passwordPublisher: PassthroughSubject) case resetFinished + case cancel } struct EncryptionResetScreenCoordinatorParameters { let clientProxy: ClientProxyProtocol - let navigationStackCoordinator: NavigationStackCoordinator let userIndicatorController: UserIndicatorControllerProtocol } @@ -43,10 +43,10 @@ final class EncryptionResetScreenCoordinator: CoordinatorProtocol { guard let self else { return } switch action { - case .requestPassword: - presentPasswordScreen() case .requestOIDCAuthorisation(let url): self.actionsSubject.send(.requestOIDCAuthorisation(url)) + case .requestPassword(let passwordPublisher): + self.actionsSubject.send(.requestPassword(passwordPublisher: passwordPublisher)) case .resetFinished: self.actionsSubject.send(.resetFinished) case .cancel: @@ -63,23 +63,4 @@ final class EncryptionResetScreenCoordinator: CoordinatorProtocol { 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) - } } diff --git a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenModels.swift b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenModels.swift index 77ca459153..d85984d452 100644 --- a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenModels.swift +++ b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenModels.swift @@ -5,10 +5,11 @@ // Please see LICENSE in the repository root for full details. // +import Combine import Foundation enum EncryptionResetScreenViewModelAction { - case requestPassword + case requestPassword(passwordPublisher: PassthroughSubject) case requestOIDCAuthorisation(url: URL) case resetFinished case cancel diff --git a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenViewModel.swift b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenViewModel.swift index ebdfda67bc..a81c2f112c 100644 --- a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenViewModel.swift +++ b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenViewModel.swift @@ -21,6 +21,7 @@ class EncryptionResetScreenViewModel: EncryptionResetScreenViewModelType, Encryp } private var identityResetHandle: IdentityResetHandle? + private var passwordCancellable: AnyCancellable? init(clientProxy: ClientProxyProtocol, userIndicatorController: UserIndicatorControllerProtocol) { self.clientProxy = clientProxy @@ -46,12 +47,6 @@ class EncryptionResetScreenViewModel: EncryptionResetScreenViewModelType, Encryp } } - func continueResetFlowWith(password: String) { - Task { - await resetWith(password: password) - } - } - func stop() { Task { await identityResetHandle?.cancel() @@ -80,7 +75,14 @@ class EncryptionResetScreenViewModel: EncryptionResetScreenViewModelType, Encryp switch handle.authType() { case .uiaa: - actionsSubject.send(.requestPassword) + let passwordPublisher = PassthroughSubject() + passwordCancellable = passwordPublisher.sink { [weak self] password in + guard let self else { return } + passwordCancellable = nil + Task { await self.resetWith(password: password) } + } + + actionsSubject.send(.requestPassword(passwordPublisher: passwordPublisher)) case .oidc(let oidcInfo): guard let url = URL(string: oidcInfo.approvalUrl) else { fatalError("Invalid URL received through identity reset handle: \(oidcInfo.approvalUrl)") diff --git a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenViewModelProtocol.swift b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenViewModelProtocol.swift index 2ebf141d12..92b48a87d4 100644 --- a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenViewModelProtocol.swift +++ b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenViewModelProtocol.swift @@ -12,6 +12,5 @@ protocol EncryptionResetScreenViewModelProtocol { var actionsPublisher: AnyPublisher { get } var context: EncryptionResetScreenViewModelType.Context { get } - func continueResetFlowWith(password: String) func stop() } diff --git a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/View/EncryptionResetScreen.swift b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/View/EncryptionResetScreen.swift index 0bcf831683..e79f243b1e 100644 --- a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/View/EncryptionResetScreen.swift +++ b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/View/EncryptionResetScreen.swift @@ -19,6 +19,7 @@ struct EncryptionResetScreen: View { context.send(viewAction: .reset) } .buttonStyle(.compound(.primary)) + .accessibilityIdentifier(A11yIdentifiers.encryptionResetScreen.continueReset) } .background() .backgroundStyle(.compound.bgSubtleSecondary) diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/View/SecureBackupKeyBackupScreen.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/View/SecureBackupKeyBackupScreen.swift index 2f7923c01b..4a0de09b95 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/View/SecureBackupKeyBackupScreen.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/View/SecureBackupKeyBackupScreen.swift @@ -22,6 +22,7 @@ struct SecureBackupKeyBackupScreen: View { Text(L10n.screenChatBackupKeyBackupActionDisable) } .buttonStyle(.compound(.primary)) + .accessibilityIdentifier(A11yIdentifiers.secureBackupKeyBackupScreen.deleteKeyStorage) } .background() .backgroundStyle(.compound.bgCanvasDefault) diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenCoordinator.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenCoordinator.swift index 0713b3405c..38788a0028 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenCoordinator.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenCoordinator.swift @@ -15,23 +15,22 @@ struct SecureBackupRecoveryKeyScreenCoordinatorParameters { } enum SecureBackupRecoveryKeyScreenCoordinatorAction { - case cancel - case recoverySetUp - case recoveryChanged - case recoveryFixed - case resetEncryption + case complete } final class SecureBackupRecoveryKeyScreenCoordinator: CoordinatorProtocol { + private let parameters: SecureBackupRecoveryKeyScreenCoordinatorParameters private var viewModel: SecureBackupRecoveryKeyScreenViewModelProtocol - private let actionsSubject: PassthroughSubject = .init() + private var cancellables = Set() + private let actionsSubject: PassthroughSubject = .init() var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } init(parameters: SecureBackupRecoveryKeyScreenCoordinatorParameters) { + self.parameters = parameters viewModel = SecureBackupRecoveryKeyScreenViewModel(secureBackupController: parameters.secureBackupController, userIndicatorController: parameters.userIndicatorController, isModallyPresented: parameters.isModallyPresented) @@ -44,20 +43,19 @@ final class SecureBackupRecoveryKeyScreenCoordinator: CoordinatorProtocol { guard let self else { return } switch action { case .cancel: - self.actionsSubject.send(.cancel) + self.actionsSubject.send(.complete) case .done(let mode): switch mode { case .setupRecovery: - self.actionsSubject.send(.recoverySetUp) + showSuccessIndicator(title: L10n.screenRecoveryKeySetupSuccess) case .changeRecovery: - self.actionsSubject.send(.recoveryChanged) + showSuccessIndicator(title: L10n.screenRecoveryKeyChangeSuccess) case .fixRecovery: - self.actionsSubject.send(.recoveryFixed) + showSuccessIndicator(title: L10n.screenRecoveryKeyConfirmSuccess) case .unknown: fatalError() } - case .resetEncryption: - self.actionsSubject.send(.resetEncryption) + self.actionsSubject.send(.complete) } } .store(in: &cancellables) @@ -66,4 +64,14 @@ final class SecureBackupRecoveryKeyScreenCoordinator: CoordinatorProtocol { func toPresentable() -> AnyView { AnyView(SecureBackupRecoveryKeyScreen(context: viewModel.context)) } + + // MARK: - Private + + private func showSuccessIndicator(title: String) { + parameters.userIndicatorController.submitIndicator(.init(id: .init(), + type: .modal(progress: .none, interactiveDismissDisabled: false, allowsInteraction: false), + title: title, + iconName: "checkmark", + persistent: false)) + } } diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenModels.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenModels.swift index f04790123c..066577b9df 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenModels.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenModels.swift @@ -10,7 +10,6 @@ import Foundation enum SecureBackupRecoveryKeyScreenViewModelAction { case done(mode: SecureBackupRecoveryKeyScreenViewMode) case cancel - case resetEncryption } enum SecureBackupRecoveryKeyScreenViewMode { @@ -82,7 +81,6 @@ enum SecureBackupRecoveryKeyScreenViewAction { case copyKey case keySaved case confirmKey - case resetEncryption case done case cancel } diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenViewModel.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenViewModel.swift index 84c31fb8c7..0c3ca4ac1e 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenViewModel.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenViewModel.swift @@ -78,13 +78,11 @@ class SecureBackupRecoveryKeyScreenViewModel: SecureBackupRecoveryKeyScreenViewM state.bindings.alertInfo = .init(id: .init(), title: L10n.screenRecoveryKeySetupConfirmationTitle, message: L10n.screenRecoveryKeySetupConfirmationDescription, - primaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil), - secondaryButton: .init(title: L10n.actionContinue, action: { [weak self] in + primaryButton: .init(title: L10n.actionContinue) { [weak self] in guard let self else { return } actionsSubject.send(.done(mode: context.viewState.mode)) - })) - case .resetEncryption: - actionsSubject.send(.resetEncryption) + }, + secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil)) } } diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/View/SecureBackupRecoveryKeyScreen.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/View/SecureBackupRecoveryKeyScreen.swift index db36934f90..acd39bd180 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/View/SecureBackupRecoveryKeyScreen.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/View/SecureBackupRecoveryKeyScreen.swift @@ -89,6 +89,7 @@ struct SecureBackupRecoveryKeyScreen: View { } .buttonStyle(.compound(.primary)) .disabled(context.confirmationRecoveryKey.isEmpty) + .accessibilityIdentifier(A11yIdentifiers.secureBackupRecoveryKeyScreen.confirm) } } @@ -111,6 +112,7 @@ struct SecureBackupRecoveryKeyScreen: View { } .buttonStyle(.compound(.primary)) .disabled(context.viewState.recoveryKey == nil || context.viewState.doneButtonEnabled == false) + .accessibilityIdentifier(A11yIdentifiers.secureBackupRecoveryKeyScreen.done) } } @@ -140,6 +142,7 @@ struct SecureBackupRecoveryKeyScreen: View { } .font(.compound.bodyLGSemibold) .padding(.vertical, 11) + .accessibilityIdentifier(A11yIdentifiers.secureBackupRecoveryKeyScreen.generateRecoveryKey) } else { HStack(spacing: 8) { ProgressView() @@ -163,6 +166,7 @@ struct SecureBackupRecoveryKeyScreen: View { } .tint(.compound.iconSecondary) .accessibilityLabel(L10n.actionCopy) + .accessibilityIdentifier(A11yIdentifiers.secureBackupRecoveryKeyScreen.copyRecoveryKey) } } } @@ -204,6 +208,7 @@ struct SecureBackupRecoveryKeyScreen: View { .onSubmit { context.send(viewAction: .confirmKey) } + .accessibilityIdentifier(A11yIdentifiers.secureBackupRecoveryKeyScreen.recoveryKeyField) if let subtitle = context.viewState.recoveryKeySubtitle { Text(subtitle) diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenCoordinator.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenCoordinator.swift index 6605642e8f..52710b9344 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenCoordinator.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenCoordinator.swift @@ -11,12 +11,12 @@ import SwiftUI struct SecureBackupScreenCoordinatorParameters { let appSettings: AppSettings let clientProxy: ClientProxyProtocol - weak var navigationStackCoordinator: NavigationStackCoordinator? let userIndicatorController: UserIndicatorControllerProtocol } enum SecureBackupScreenCoordinatorAction { - case requestOIDCAuthorisation(URL) + case manageRecoveryKey + case disableKeyBackup } final class SecureBackupScreenCoordinator: CoordinatorProtocol { @@ -43,53 +43,10 @@ final class SecureBackupScreenCoordinator: CoordinatorProtocol { guard let self else { return } switch action { - case .recoveryKey: - let recoveryNavigationStackCoordinator = NavigationStackCoordinator() - - let recoveryKeyCoordinator = SecureBackupRecoveryKeyScreenCoordinator(parameters: .init(secureBackupController: parameters.clientProxy.secureBackupController, - userIndicatorController: parameters.userIndicatorController, - isModallyPresented: true)) - - recoveryKeyCoordinator.actions.sink { [weak self] action in - guard let self else { return } - switch action { - case .cancel: - parameters.navigationStackCoordinator?.setSheetCoordinator(nil) - case .recoverySetUp: - showSuccessIndicator(title: L10n.screenRecoveryKeySetupSuccess) - parameters.navigationStackCoordinator?.setSheetCoordinator(nil) - case .recoveryChanged: - showSuccessIndicator(title: L10n.screenRecoveryKeyChangeSuccess) - parameters.navigationStackCoordinator?.setSheetCoordinator(nil) - case .recoveryFixed: - showSuccessIndicator(title: L10n.screenRecoveryKeyConfirmSuccess) - parameters.navigationStackCoordinator?.setSheetCoordinator(nil) - case .resetEncryption: - showEncryptionReset(recoveryNavigationStackCoordinator: recoveryNavigationStackCoordinator) - } - } - .store(in: &cancellables) - - recoveryNavigationStackCoordinator.setRootCoordinator(recoveryKeyCoordinator, animated: true) - - parameters.navigationStackCoordinator?.setSheetCoordinator(recoveryNavigationStackCoordinator) - case .keyBackup: - let navigationStackCoordinator = NavigationStackCoordinator() - - let keyBackupCoordinator = SecureBackupKeyBackupScreenCoordinator(parameters: .init(secureBackupController: parameters.clientProxy.secureBackupController, - userIndicatorController: parameters.userIndicatorController)) - - keyBackupCoordinator.actions.sink { [weak self] action in - switch action { - case .done: - self?.parameters.navigationStackCoordinator?.setSheetCoordinator(nil) - } - } - .store(in: &cancellables) - - navigationStackCoordinator.setRootCoordinator(keyBackupCoordinator, animated: true) - - parameters.navigationStackCoordinator?.setSheetCoordinator(navigationStackCoordinator) + case .manageRecoveryKey: + actionsSubject.send(.manageRecoveryKey) + case .disableKeyBackup: + actionsSubject.send(.disableKeyBackup) } } .store(in: &cancellables) @@ -98,41 +55,4 @@ final class SecureBackupScreenCoordinator: CoordinatorProtocol { func toPresentable() -> AnyView { AnyView(SecureBackupScreen(context: viewModel.context)) } - - // MARK: - Private - - private func showSuccessIndicator(title: String) { - parameters.userIndicatorController.submitIndicator(.init(id: .init(), - type: .modal(progress: .none, interactiveDismissDisabled: false, allowsInteraction: false), - title: title, - iconName: "checkmark", - persistent: false)) - } - - 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: - 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) - - resetNavigationStackCoordinator.setRootCoordinator(coordinator) - - recoveryNavigationStackCoordinator.setSheetCoordinator(resetNavigationStackCoordinator) - } } diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenModels.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenModels.swift index e0cf773231..85925e073f 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenModels.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenModels.swift @@ -8,8 +8,8 @@ import Foundation enum SecureBackupScreenViewModelAction { - case recoveryKey - case keyBackup + case manageRecoveryKey + case disableKeyBackup } struct SecureBackupScreenViewState: BindableState { diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenViewModel.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenViewModel.swift index 6e3cf495ef..c929f5319a 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenViewModel.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenViewModel.swift @@ -48,7 +48,7 @@ class SecureBackupScreenViewModel: SecureBackupScreenViewModelType, SecureBackup override func process(viewAction: SecureBackupScreenViewAction) { switch viewAction { case .recoveryKey: - actionsSubject.send(.recoveryKey) + actionsSubject.send(.manageRecoveryKey) case .keyStorageToggled(let enable): let keyBackupState = secureBackupController.keyBackupState.value switch (keyBackupState, enable) { @@ -57,7 +57,7 @@ class SecureBackupScreenViewModel: SecureBackupScreenViewModelType, SecureBackup enableBackup() case (.enabled, false): state.bindings.keyStorageEnabled = keyBackupState.keyStorageToggleState // Reset the toggle in case the user cancels - actionsSubject.send(.keyBackup) + actionsSubject.send(.disableKeyBackup) default: break } diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/View/SecureBackupScreen.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/View/SecureBackupScreen.swift index dbdb8a2317..a0aaa17181 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/View/SecureBackupScreen.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/View/SecureBackupScreen.swift @@ -53,7 +53,13 @@ struct SecureBackupScreen: View { .accessibilityElement(children: .combine) }) - keyStorageToggle + ListRow(label: .plain(title: L10n.screenChatBackupKeyStorageToggleTitle, + description: context.viewState.keyStorageToggleDescription), + kind: .toggle($context.keyStorageEnabled)) + .onChange(of: context.keyStorageEnabled) { _, newValue in + context.send(viewAction: .keyStorageToggled(newValue)) + } + .accessibilityIdentifier(A11yIdentifiers.secureBackupScreen.keyStorage) } } @@ -67,15 +73,6 @@ struct SecureBackupScreen: View { return description } - private var keyStorageToggle: some View { - ListRow(label: .plain(title: L10n.screenChatBackupKeyStorageToggleTitle, - description: context.viewState.keyStorageToggleDescription), - kind: .toggle($context.keyStorageEnabled)) - .onChange(of: context.keyStorageEnabled) { _, newValue in - context.send(viewAction: .keyStorageToggled(newValue)) - } - } - private var recoveryKeySection: some View { Section { switch context.viewState.recoveryState { @@ -85,6 +82,7 @@ struct SecureBackupScreen: View { icon: \.key, iconAlignment: .top), kind: .navigationLink { context.send(viewAction: .recoveryKey) }) + .accessibilityIdentifier(A11yIdentifiers.secureBackupScreen.recoveryKey) case .disabled: ListRow(label: .default(title: L10n.screenChatBackupRecoveryActionSetup, description: L10n.screenChatBackupRecoveryActionChangeDescription, @@ -92,10 +90,12 @@ struct SecureBackupScreen: View { iconAlignment: .top), details: .icon(BadgeView(size: 10)), kind: .navigationLink { context.send(viewAction: .recoveryKey) }) + .accessibilityIdentifier(A11yIdentifiers.secureBackupScreen.recoveryKey) case .incomplete: ListRow(label: .plain(title: L10n.screenChatBackupRecoveryActionConfirm), details: .icon(BadgeView(size: 10)), kind: .navigationLink { context.send(viewAction: .recoveryKey) }) + .accessibilityIdentifier(A11yIdentifiers.secureBackupScreen.recoveryKey) default: ListRow(label: .plain(title: L10n.commonLoading), details: .isWaiting(true), kind: .label) } diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index 01cf66c47e..01bea2b808 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -630,6 +630,40 @@ class MockScreen: Identifiable { let navigationStackCoordinator = NavigationStackCoordinator() let coordinator = PollFormScreenCoordinator(parameters: .init(mode: .new)) navigationStackCoordinator.setRootCoordinator(coordinator) + return navigationStackCoordinator + case .encryptionSettings, .encryptionSettingsOutOfSync: + let recoveryState: SecureBackupRecoveryState = id == .encryptionSettings ? .enabled : .incomplete + let clientProxy = ClientProxyMock(.init(userID: "@mock:client.com", recoveryState: recoveryState)) + let userSession = UserSessionMock(.init(clientProxy: clientProxy)) + + let navigationStackCoordinator = NavigationStackCoordinator() + navigationStackCoordinator.setRootCoordinator(BlankFormCoordinator()) + + let coordinator = EncryptionSettingsFlowCoordinator(parameters: .init(userSession: userSession, + appSettings: ServiceLocator.shared.settings, + userIndicatorController: UserIndicatorControllerMock(), + navigationStackCoordinator: navigationStackCoordinator)) + retainedState.append(coordinator) + coordinator.start() + + return navigationStackCoordinator + case .encryptionReset: + let recoveryState: SecureBackupRecoveryState = id == .encryptionSettings ? .enabled : .incomplete + let clientProxy = ClientProxyMock(.init(userID: "@mock:client.com", recoveryState: recoveryState)) + let userSession = UserSessionMock(.init(clientProxy: clientProxy)) + + let userIndicatorController = UserIndicatorController() + userIndicatorController.window = windowManager.overlayWindow + let navigationStackCoordinator = NavigationStackCoordinator() + + let coordinator = EncryptionResetFlowCoordinator(parameters: .init(userSession: userSession, + userIndicatorController: userIndicatorController, + navigationStackCoordinator: navigationStackCoordinator, + windowManger: windowManager)) + + retainedState.append(coordinator) + coordinator.start() + return navigationStackCoordinator case .autoUpdatingTimeline: let appSettings: AppSettings = ServiceLocator.shared.settings diff --git a/ElementX/Sources/UITests/UITestsScreenIdentifier.swift b/ElementX/Sources/UITests/UITestsScreenIdentifier.swift index b725b2060a..74764ee47e 100644 --- a/ElementX/Sources/UITests/UITestsScreenIdentifier.swift +++ b/ElementX/Sources/UITests/UITestsScreenIdentifier.swift @@ -20,6 +20,9 @@ enum UITestsScreenIdentifier: String { case createPoll case createRoom case createRoomNoUsers + case encryptionSettings + case encryptionSettingsOutOfSync + case encryptionReset case roomLayoutBottom case roomLayoutMiddle case roomLayoutTop diff --git a/UITests/Sources/EncryptionResetUITests.swift b/UITests/Sources/EncryptionResetUITests.swift new file mode 100644 index 0000000000..5bc5bea00d --- /dev/null +++ b/UITests/Sources/EncryptionResetUITests.swift @@ -0,0 +1,37 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import XCTest + +@MainActor +class EncryptionResetUITests: XCTestCase { + var app: XCUIApplication! + + @MainActor enum Step { + static let resetScreen = 0 + static let passwordScreen = 1 + static let resetingEncryption = 2 + } + + func testPasswordFlow() async throws { + app = Application.launch(.encryptionReset) + + // Starting with the root screen. + try await app.assertScreenshot(.encryptionReset, step: Step.resetScreen) + + // Confirm the intent to reset. + app.buttons[A11yIdentifiers.encryptionResetScreen.continueReset].tap() + app.buttons[A11yIdentifiers.alertInfo.primaryButton].tap() + try await app.assertScreenshot(.encryptionReset, step: Step.passwordScreen) + + // Enter the password and submit. + let passwordField = app.secureTextFields[A11yIdentifiers.encryptionResetPasswordScreen.passwordField] + passwordField.clearAndTypeText("supersecurepassword", app: app) + app.buttons[A11yIdentifiers.encryptionResetPasswordScreen.submit].tap() + try await app.assertScreenshot(.encryptionReset, step: Step.resetingEncryption) + } +} diff --git a/UITests/Sources/EncryptionSettingsUITests.swift b/UITests/Sources/EncryptionSettingsUITests.swift new file mode 100644 index 0000000000..9b22a6d437 --- /dev/null +++ b/UITests/Sources/EncryptionSettingsUITests.swift @@ -0,0 +1,80 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import XCTest + +@MainActor +class EncryptionSettingsUITests: XCTestCase { + var app: XCUIApplication! + + @MainActor enum Step { + static let secureBackupScreenSetUp = 0 + static let keyBackupScreen = 1 + static let secureBackupScreenDisabled = 2 + static let setUpRecovery = 3 + static let changeRecovery = 4 + + static let secureBackupScreenOutOfSync = 5 + static let confirmRecovery = 6 + } + + func testFlow() async throws { + app = Application.launch(.encryptionSettings) + + // Starting with key storage and recovery enabled. + try await app.assertScreenshot(.encryptionSettings, step: Step.secureBackupScreenSetUp) + + // Toggle key storage off. + app.switches[A11yIdentifiers.secureBackupScreen.keyStorage].tap() + try await app.assertScreenshot(.encryptionSettings, step: Step.keyBackupScreen) + + // Confirm deletion of keys. + app.buttons[A11yIdentifiers.secureBackupKeyBackupScreen.deleteKeyStorage].tap() + app.buttons[A11yIdentifiers.alertInfo.primaryButton].tap() + try await app.assertScreenshot(.encryptionSettings, step: Step.secureBackupScreenDisabled) + + // Toggle key storage back on and set up recovery. + app.switches[A11yIdentifiers.secureBackupScreen.keyStorage].tap() + app.buttons[A11yIdentifiers.secureBackupScreen.recoveryKey].tap() + try await app.assertScreenshot(.encryptionSettings, step: Step.setUpRecovery) + + // Generate and copy a new recovery key. + app.buttons[A11yIdentifiers.secureBackupRecoveryKeyScreen.generateRecoveryKey].tap() + app.buttons[A11yIdentifiers.secureBackupRecoveryKeyScreen.copyRecoveryKey].tap() + app.buttons[A11yIdentifiers.secureBackupRecoveryKeyScreen.done].tap() + app.buttons[A11yIdentifiers.alertInfo.primaryButton].tap() + try await app.assertScreenshot(.encryptionSettings, step: Step.secureBackupScreenSetUp) + + // Change the recovery key. + app.buttons[A11yIdentifiers.secureBackupScreen.recoveryKey].tap() + try await app.assertScreenshot(.encryptionSettings, step: Step.changeRecovery) + + // Generate and copy the updated recovery key. + app.buttons[A11yIdentifiers.secureBackupRecoveryKeyScreen.generateRecoveryKey].tap() + app.buttons[A11yIdentifiers.secureBackupRecoveryKeyScreen.copyRecoveryKey].tap() + app.buttons[A11yIdentifiers.secureBackupRecoveryKeyScreen.done].tap() + app.buttons[A11yIdentifiers.alertInfo.primaryButton].tap() + try await app.assertScreenshot(.encryptionSettings, step: Step.secureBackupScreenSetUp) + } + + func testOutOfSyncFlow() async throws { + app = Application.launch(.encryptionSettingsOutOfSync) + + // Starting with key storage and recovery enabled. + try await app.assertScreenshot(.encryptionSettings, step: Step.secureBackupScreenOutOfSync) + + // Confirm the recovery key. + app.buttons[A11yIdentifiers.secureBackupScreen.recoveryKey].tap() + try await app.assertScreenshot(.encryptionSettings, step: Step.confirmRecovery) + + // Enter the recovery key and submit. + let recoveryKeyField = app.secureTextFields[A11yIdentifiers.secureBackupRecoveryKeyScreen.recoveryKeyField] + recoveryKeyField.clearAndTypeText("sUpe RSec rEtR Ecov ERYk Ey12", app: app) + app.buttons[A11yIdentifiers.secureBackupRecoveryKeyScreen.confirm].tap() + try await app.assertScreenshot(.encryptionSettings, step: Step.secureBackupScreenSetUp) + } +} diff --git a/UITests/Sources/__Snapshots__/Application/encryptionReset-0-iPad-10th-generation-en-GB.UI.png b/UITests/Sources/__Snapshots__/Application/encryptionReset-0-iPad-10th-generation-en-GB.UI.png new file mode 100644 index 0000000000..88c37812eb --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/encryptionReset-0-iPad-10th-generation-en-GB.UI.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83ebdd64a97d8cef02516cb5be9d5191077c8b3be41252e828a4b83d45004b6e +size 133247 diff --git a/UITests/Sources/__Snapshots__/Application/encryptionReset-0-iPhone-16-en-GB.UI.png b/UITests/Sources/__Snapshots__/Application/encryptionReset-0-iPhone-16-en-GB.UI.png new file mode 100644 index 0000000000..70d68529b1 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/encryptionReset-0-iPhone-16-en-GB.UI.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e9dfc381ac69b5e415c4bdf05175ceb6df08cec2db54f3f4b02172eda1f6dd64 +size 162470 diff --git a/UITests/Sources/__Snapshots__/Application/encryptionReset-1-iPad-10th-generation-en-GB.UI.png b/UITests/Sources/__Snapshots__/Application/encryptionReset-1-iPad-10th-generation-en-GB.UI.png new file mode 100644 index 0000000000..c06d19dde7 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/encryptionReset-1-iPad-10th-generation-en-GB.UI.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9720f0cd45534c9699eef06f8a2afd1504d5b51a8773b2b76585002b23d67b77 +size 92574 diff --git a/UITests/Sources/__Snapshots__/Application/encryptionReset-1-iPhone-16-en-GB.UI.png b/UITests/Sources/__Snapshots__/Application/encryptionReset-1-iPhone-16-en-GB.UI.png new file mode 100644 index 0000000000..5b9cf6bc88 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/encryptionReset-1-iPhone-16-en-GB.UI.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7170468705d17c9ba1c6062524d2ccc4cbbd54fd45f1056e53921b20fb6756f9 +size 98637 diff --git a/UITests/Sources/__Snapshots__/Application/encryptionReset-2-iPad-10th-generation-en-GB.UI.png b/UITests/Sources/__Snapshots__/Application/encryptionReset-2-iPad-10th-generation-en-GB.UI.png new file mode 100644 index 0000000000..c1f55d7253 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/encryptionReset-2-iPad-10th-generation-en-GB.UI.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26acd80deba0bdd9babf133b0b015b6f21942fa02983b921da28925a92573f8e +size 140798 diff --git a/UITests/Sources/__Snapshots__/Application/encryptionReset-2-iPhone-16-en-GB.UI.png b/UITests/Sources/__Snapshots__/Application/encryptionReset-2-iPhone-16-en-GB.UI.png new file mode 100644 index 0000000000..544b27d1cf --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/encryptionReset-2-iPhone-16-en-GB.UI.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3618590fa6e84095976811f53bc283123654470c9aa15babfaec79cc16f19e88 +size 177016 diff --git a/UITests/Sources/__Snapshots__/Application/encryptionSettings-0-iPad-10th-generation-en-GB.UI.png b/UITests/Sources/__Snapshots__/Application/encryptionSettings-0-iPad-10th-generation-en-GB.UI.png new file mode 100644 index 0000000000..12ec2feb44 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/encryptionSettings-0-iPad-10th-generation-en-GB.UI.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:28d3ae3eb6b7ed45c41e1381c052a080906ff767aad74562e09b34b432ba1f06 +size 119892 diff --git a/UITests/Sources/__Snapshots__/Application/encryptionSettings-0-iPhone-16-en-GB.UI.png b/UITests/Sources/__Snapshots__/Application/encryptionSettings-0-iPhone-16-en-GB.UI.png new file mode 100644 index 0000000000..72cb8d5020 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/encryptionSettings-0-iPhone-16-en-GB.UI.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9c7753ecb9a032c14ac1b53ec94dfaf055da89c3d053b0dd095b15b291129730 +size 141912 diff --git a/UITests/Sources/__Snapshots__/Application/encryptionSettings-1-iPad-10th-generation-en-GB.UI.png b/UITests/Sources/__Snapshots__/Application/encryptionSettings-1-iPad-10th-generation-en-GB.UI.png new file mode 100644 index 0000000000..17fb74a505 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/encryptionSettings-1-iPad-10th-generation-en-GB.UI.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:46a5294fe4bf36583b6a7f58602e2ea00761e6027d74e9c982a99f912bf28c26 +size 232922 diff --git a/UITests/Sources/__Snapshots__/Application/encryptionSettings-1-iPhone-16-en-GB.UI.png b/UITests/Sources/__Snapshots__/Application/encryptionSettings-1-iPhone-16-en-GB.UI.png new file mode 100644 index 0000000000..0e8b945fda --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/encryptionSettings-1-iPhone-16-en-GB.UI.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a6dc3f0216fa3281196281e997a5cceba862c8427ad352674b860b9d6c16ca02 +size 157003 diff --git a/UITests/Sources/__Snapshots__/Application/encryptionSettings-2-iPad-10th-generation-en-GB.UI.png b/UITests/Sources/__Snapshots__/Application/encryptionSettings-2-iPad-10th-generation-en-GB.UI.png new file mode 100644 index 0000000000..0beffeb38d --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/encryptionSettings-2-iPad-10th-generation-en-GB.UI.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3fbf90b3889e39f75abb39bd5e92ec60dfe18968c85b5ba9c56addf0c4579e6d +size 103188 diff --git a/UITests/Sources/__Snapshots__/Application/encryptionSettings-2-iPhone-16-en-GB.UI.png b/UITests/Sources/__Snapshots__/Application/encryptionSettings-2-iPhone-16-en-GB.UI.png new file mode 100644 index 0000000000..cd9a3e7e26 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/encryptionSettings-2-iPhone-16-en-GB.UI.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9f4752e15127fd336156f09cd998b96931b1a04f85b3ada3c1cd043d2461195 +size 116493 diff --git a/UITests/Sources/__Snapshots__/Application/encryptionSettings-3-iPad-10th-generation-en-GB.UI.png b/UITests/Sources/__Snapshots__/Application/encryptionSettings-3-iPad-10th-generation-en-GB.UI.png new file mode 100644 index 0000000000..b4c4af1248 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/encryptionSettings-3-iPad-10th-generation-en-GB.UI.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9690d135c26b3bef7c9e1ecd25fe5a45605bf1838f5f3d22f34ef4cc47cf59dc +size 211322 diff --git a/UITests/Sources/__Snapshots__/Application/encryptionSettings-3-iPhone-16-en-GB.UI.png b/UITests/Sources/__Snapshots__/Application/encryptionSettings-3-iPhone-16-en-GB.UI.png new file mode 100644 index 0000000000..f6f18d408e --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/encryptionSettings-3-iPhone-16-en-GB.UI.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a3f5e7ccbc806ce6ef88e465be9bb879ef31fd3675822368c93126d66563e07b +size 121844 diff --git a/UITests/Sources/__Snapshots__/Application/encryptionSettings-4-iPad-10th-generation-en-GB.UI.png b/UITests/Sources/__Snapshots__/Application/encryptionSettings-4-iPad-10th-generation-en-GB.UI.png new file mode 100644 index 0000000000..7c91791239 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/encryptionSettings-4-iPad-10th-generation-en-GB.UI.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1bfdba161a6e974df12efd09a48cd905d8162c828c2e6af99eaed74a8631891b +size 210352 diff --git a/UITests/Sources/__Snapshots__/Application/encryptionSettings-4-iPhone-16-en-GB.UI.png b/UITests/Sources/__Snapshots__/Application/encryptionSettings-4-iPhone-16-en-GB.UI.png new file mode 100644 index 0000000000..b89307d812 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/encryptionSettings-4-iPhone-16-en-GB.UI.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db98e2b1dc6dc2c4f55aa46e783366c69e5ff4521cd0179be0028f47f0387688 +size 119868 diff --git a/UITests/Sources/__Snapshots__/Application/encryptionSettings-5-iPad-10th-generation-en-GB.UI.png b/UITests/Sources/__Snapshots__/Application/encryptionSettings-5-iPad-10th-generation-en-GB.UI.png new file mode 100644 index 0000000000..681c5958c4 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/encryptionSettings-5-iPad-10th-generation-en-GB.UI.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:59b95216f4ee18fa2b5c35fa237caf58ac7a976ef70c2a52c6a95ada9966c9c1 +size 76515 diff --git a/UITests/Sources/__Snapshots__/Application/encryptionSettings-5-iPhone-16-en-GB.UI.png b/UITests/Sources/__Snapshots__/Application/encryptionSettings-5-iPhone-16-en-GB.UI.png new file mode 100644 index 0000000000..41ff76b1db --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/encryptionSettings-5-iPhone-16-en-GB.UI.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:745a30111d340511f2be87af6df4ce85ec00baa7d1b085b1082c922337c59ca2 +size 72736 diff --git a/UITests/Sources/__Snapshots__/Application/encryptionSettings-6-iPad-10th-generation-en-GB.UI.png b/UITests/Sources/__Snapshots__/Application/encryptionSettings-6-iPad-10th-generation-en-GB.UI.png new file mode 100644 index 0000000000..ed948b2911 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/encryptionSettings-6-iPad-10th-generation-en-GB.UI.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca24039f2eacc6517cb7131cfba469fc3db152e7fa28e63307047b170344c17b +size 165666 diff --git a/UITests/Sources/__Snapshots__/Application/encryptionSettings-6-iPhone-16-en-GB.UI.png b/UITests/Sources/__Snapshots__/Application/encryptionSettings-6-iPhone-16-en-GB.UI.png new file mode 100644 index 0000000000..4e47441159 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/encryptionSettings-6-iPhone-16-en-GB.UI.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8b12eda65d7c9b472d156e7a964d6bf962810e881d5310bf04932a05a74a2443 +size 100696