From 1fe4244fe0d2b98b5b9a7e4bbc5ab33a900ab357 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 19 Oct 2023 15:34:10 +0300 Subject: [PATCH] #1899, #1900, #1901 - Implement chat backup setting screens --- ElementX.xcodeproj/project.pbxproj | 168 +++++++++++++- .../xcshareddata/swiftpm/Package.resolved | 2 +- .../images/secure-backup/Contents.json | 6 + .../secure-backup-icon.imageset/Contents.json | 16 ++ .../secure-backup-icon.svg | 3 + .../secure-backup-off.imageset/Contents.json | 16 ++ .../secure-backup-off.svg | 3 + .../secure-backup-on.imageset/Contents.json | 16 ++ .../secure-backup-on.svg | 3 + .../en.lproj/Localizable.strings | 49 +++- .../Sources/Application/AppSettings.swift | 6 + .../UserSessionFlowCoordinator.swift | 1 + ElementX/Sources/Generated/Assets.swift | 3 + ElementX/Sources/Generated/Strings.swift | 104 ++++++++- .../Mocks/Generated/GeneratedMocks.swift | 85 +++++++ .../Other/AccessibilityIdentifiers.swift | 1 + .../Other/SwiftUI/Views/BadgeView.swift | 87 +++++++ .../SwiftUI/Views/HeroImage.swift} | 10 +- .../View/AnalyticsPromptScreen.swift | 2 +- .../AppLockScreen/View/AppLockScreen.swift | 2 +- .../LoginScreen/View/LoginScreen.swift | 2 +- .../View/ServerConfirmationScreen.swift | 2 +- .../View/ServerSelectionScreen.swift | 2 +- .../Screens/HomeScreen/HomeScreenModels.swift | 2 + .../HomeScreen/HomeScreenViewModel.swift | 15 +- .../View/HomeScreenUserMenuButton.swift | 29 ++- .../RoomDetailsEditScreenViewModel.swift | 2 +- ...cureBackupKeyBackupScreenCoordinator.swift | 62 +++++ .../SecureBackupKeyBackupScreenModels.swift | 41 ++++ ...SecureBackupKeyBackupScreenViewModel.swift | 107 +++++++++ ...ckupKeyBackupScreenViewModelProtocol.swift | 23 ++ .../View/SecureBackupKeyBackupScreen.swift | 117 ++++++++++ ...reBackupRecoveryKeyScreenCoordinator.swift | 74 ++++++ .../SecureBackupRecoveryKeyScreenModels.swift | 84 +++++++ ...cureBackupRecoveryKeyScreenViewModel.swift | 111 +++++++++ ...upRecoveryKeyScreenViewModelProtocol.swift | 23 ++ .../View/SecureBackupRecoveryKeyScreen.swift | 213 ++++++++++++++++++ .../SecureBackupScreenCoordinator.swift | 116 ++++++++++ .../SecureBackupScreenModels.swift | 38 ++++ .../SecureBackupScreenViewModel.swift | 85 +++++++ .../SecureBackupScreenViewModelProtocol.swift | 23 ++ .../View/SecureBackupScreen.swift | 168 ++++++++++++++ .../View/SessionVerificationScreen.swift | 2 +- .../DeveloperOptionsScreenModels.swift | 1 + .../View/DeveloperOptionsScreen.swift | 21 +- .../SettingsScreenCoordinator.swift | 19 +- .../SettingsScreen/SettingsScreenModels.swift | 6 +- .../SettingsScreenViewModel.swift | 27 ++- .../SettingsScreen/View/SettingsScreen.swift | 32 ++- .../Sources/Services/Client/ClientProxy.swift | 4 + .../Services/Client/ClientProxyProtocol.swift | 2 + .../Services/Client/MockClientProxy.swift | 7 + .../SecureBackup/SecureBackupController.swift | 79 +++++++ .../SecureBackupControllerProtocol.swift | 65 ++++++ .../UITests/UITestsAppCoordinator.swift | 1 + .../SecureBackupKeyBackupScreenUITests.swift | 21 ++ ...SecureBackupRecoveryKeyScreenUITests.swift | 21 ++ .../Sources/SecureBackupScreenUITests.swift | 21 ++ ...eBackupKeyBackupScreenViewModelTests.swift | 22 ++ ...ackupRecoveryKeyScreenViewModelTests.swift | 22 ++ .../SecureBackupScreenViewModelTests.swift | 22 ++ ...onIconImage.1.png => test_heroImage.1.png} | 0 ...est_secureBackupKeyBackupScreen.Set-up.png | 3 + ...cureBackupRecoveryKeyScreen.Incomplete.png | 3 + ...cureBackupRecoveryKeyScreen.Not-set-up.png | 3 + ...t_secureBackupRecoveryKeyScreen.Set-up.png | 3 + .../test_secureBackupScreen.Both-setup.png | 3 + ...secureBackupScreen.Key-backup-disabled.png | 3 + ...cureBackupScreen.Only-key-backup-setup.png | 3 + ...secureBackupScreen.Recovery-incomplete.png | 3 + 70 files changed, 2293 insertions(+), 48 deletions(-) create mode 100644 ElementX/Resources/Assets.xcassets/images/secure-backup/Contents.json create mode 100644 ElementX/Resources/Assets.xcassets/images/secure-backup/secure-backup-icon.imageset/Contents.json create mode 100644 ElementX/Resources/Assets.xcassets/images/secure-backup/secure-backup-icon.imageset/secure-backup-icon.svg create mode 100644 ElementX/Resources/Assets.xcassets/images/secure-backup/secure-backup-off.imageset/Contents.json create mode 100644 ElementX/Resources/Assets.xcassets/images/secure-backup/secure-backup-off.imageset/secure-backup-off.svg create mode 100644 ElementX/Resources/Assets.xcassets/images/secure-backup/secure-backup-on.imageset/Contents.json create mode 100644 ElementX/Resources/Assets.xcassets/images/secure-backup/secure-backup-on.imageset/secure-backup-on.svg create mode 100644 ElementX/Sources/Other/SwiftUI/Views/BadgeView.swift rename ElementX/Sources/{Screens/Authentication/AuthenticationIconImage.swift => Other/SwiftUI/Views/HeroImage.swift} (78%) create mode 100644 ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/SecureBackupKeyBackupScreenCoordinator.swift create mode 100644 ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/SecureBackupKeyBackupScreenModels.swift create mode 100644 ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/SecureBackupKeyBackupScreenViewModel.swift create mode 100644 ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/SecureBackupKeyBackupScreenViewModelProtocol.swift create mode 100644 ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/View/SecureBackupKeyBackupScreen.swift create mode 100644 ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenCoordinator.swift create mode 100644 ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenModels.swift create mode 100644 ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenViewModel.swift create mode 100644 ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenViewModelProtocol.swift create mode 100644 ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/View/SecureBackupRecoveryKeyScreen.swift create mode 100644 ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenCoordinator.swift create mode 100644 ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenModels.swift create mode 100644 ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenViewModel.swift create mode 100644 ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenViewModelProtocol.swift create mode 100644 ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/View/SecureBackupScreen.swift create mode 100644 ElementX/Sources/Services/SecureBackup/SecureBackupController.swift create mode 100644 ElementX/Sources/Services/SecureBackup/SecureBackupControllerProtocol.swift create mode 100644 UITests/Sources/SecureBackupKeyBackupScreenUITests.swift create mode 100644 UITests/Sources/SecureBackupRecoveryKeyScreenUITests.swift create mode 100644 UITests/Sources/SecureBackupScreenUITests.swift create mode 100644 UnitTests/Sources/SecureBackupKeyBackupScreenViewModelTests.swift create mode 100644 UnitTests/Sources/SecureBackupRecoveryKeyScreenViewModelTests.swift create mode 100644 UnitTests/Sources/SecureBackupScreenViewModelTests.swift rename UnitTests/__Snapshots__/PreviewTests/{test_authenticationIconImage.1.png => test_heroImage.1.png} (100%) create mode 100644 UnitTests/__Snapshots__/PreviewTests/test_secureBackupKeyBackupScreen.Set-up.png create mode 100644 UnitTests/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen.Incomplete.png create mode 100644 UnitTests/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen.Not-set-up.png create mode 100644 UnitTests/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen.Set-up.png create mode 100644 UnitTests/__Snapshots__/PreviewTests/test_secureBackupScreen.Both-setup.png create mode 100644 UnitTests/__Snapshots__/PreviewTests/test_secureBackupScreen.Key-backup-disabled.png create mode 100644 UnitTests/__Snapshots__/PreviewTests/test_secureBackupScreen.Only-key-backup-setup.png create mode 100644 UnitTests/__Snapshots__/PreviewTests/test_secureBackupScreen.Recovery-incomplete.png diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 44c9e9459..b0388195c 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -26,6 +26,7 @@ 05EC896A4B9AF4A56670C0BB /* SessionVerificationUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D4777F0142E330A75C46FE4 /* SessionVerificationUITests.swift */; }; 066A1E9B94723EE9F3038044 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47EBB5D698CE9A25BB553A2D /* Strings.swift */; }; 06AA515C7053FD7E17A5CF81 /* RoomNotificationSettingsScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66901977F6469D03C333DF32 /* RoomNotificationSettingsScreenUITests.swift */; }; + 06B31F84CE52A7A7C271267C /* SecureBackupRecoveryKeyScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FF08D0BD7D0B4B6877AB7D /* SecureBackupRecoveryKeyScreenViewModelTests.swift */; }; 06B55882911B4BF5B14E9851 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227AC5D71A4CE43512062243 /* URL.swift */; }; 06D3942496E9E0E655F14D21 /* NotificationManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A057F2FDC14866C3026A89A4 /* NotificationManagerProtocol.swift */; }; 06F8EDF52E33A2D36BCC1161 /* AppLockScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D6F88FE35A0979D2821E06 /* AppLockScreen.swift */; }; @@ -53,6 +54,7 @@ 0C47AE2CA7929CB3B0E2D793 /* ServerSelectionScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0685156EB62D7E243F097CFC /* ServerSelectionScreenViewModelProtocol.swift */; }; 0C58A846F61949B1D545D661 /* NoticeRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 421E716C521F96D24ECE69B3 /* NoticeRoomTimelineItem.swift */; }; 0C797CD650DFD2876BEC5173 /* CollapsibleReactionLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F7C6DDBB5D12F6EF6A3D6E1 /* CollapsibleReactionLayout.swift */; }; + 0C88044649BAEE6C49BFC43A /* SecureBackupControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C21A715237F2B6D6E80998C /* SecureBackupControllerProtocol.swift */; }; 0C932A5158C1D0604DFC5750 /* ComposerToolbarViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA29952595B804DA221A0C1D /* ComposerToolbarViewModelTests.swift */; }; 0DC815CA24E1BD7F408F37D3 /* CollapsibleTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7C4EA55DA62F9D0F984A2AE /* CollapsibleTimelineItem.swift */; }; 0DCDF49AB95F75BFC8B1879C /* SwipeToReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E45C3DC740D3AB9A47FD32 /* SwipeToReplyView.swift */; }; @@ -86,6 +88,7 @@ 1702981A8085BE4FB0EC001B /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = D33116993D54FADC0C721C1F /* Application.swift */; }; 172E6E9A612ADCF10A62CF13 /* BugReportServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A68BCE6438873D2661D93D0 /* BugReportServiceProtocol.swift */; }; 1772AFA97DDA51CF1B293A78 /* RoomAttachmentPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E6A9B9DFEE964962C179DE3 /* RoomAttachmentPicker.swift */; }; + 1795EA6A6C4942CAE0459DF0 /* SecureBackupKeyBackupScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82B612853BFB68373249777B /* SecureBackupKeyBackupScreenViewModel.swift */; }; 17BC15DA08A52587466698C5 /* RoomMessageEventStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80E815FF3CC5E5A355E3A25E /* RoomMessageEventStringBuilder.swift */; }; 1830E5431DB426E2F3660D58 /* NotificationSettingsEditScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46F52419AEEDA2C006CB7181 /* NotificationSettingsEditScreenUITests.swift */; }; 18867F4F1C8991EEC56EA932 /* UTType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 897DF5E9A70CE05A632FC8AF /* UTType.swift */; }; @@ -98,6 +101,7 @@ 1B2DADC008EE211AF1DA5292 /* NotificationManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30ED584467DB380E3CEFB1DB /* NotificationManagerTests.swift */; }; 1B4B3E847BF944DB2C1C217F /* BackgroundTaskServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE73D571D4F9C36DD45255A /* BackgroundTaskServiceProtocol.swift */; }; 1B88BB631F7FC45A213BB554 /* TimelineItemSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55AEEF8142DF1B59DB40FB93 /* TimelineItemSender.swift */; }; + 1B8E30B35BF8F541C1318F19 /* SecureBackupScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40316EFFEAC7B206EE9A55AE /* SecureBackupScreenViewModelTests.swift */; }; 1BA04D05EBC6646958B1BE60 /* PlaceholderScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF34A2FD6797535C95AC918D /* PlaceholderScreenCoordinator.swift */; }; 1C409A26A99F0371C47AFA51 /* UserDiscoveryServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F615A00DB223FF3280204D2 /* UserDiscoveryServiceProtocol.swift */; }; 1C8BC70A18060677E295A846 /* ShareToMapsAppActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4481799F455B3DA243BDA2AC /* ShareToMapsAppActivity.swift */; }; @@ -116,6 +120,7 @@ 2185C1F6724C78FFF355D6FA /* WelcomeScreenScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AB10FA6570DD08B3966C159 /* WelcomeScreenScreenUITests.swift */; }; 21AFEFB8CEFE56A3811A1F5B /* VoiceMessageCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 283974987DA7EC61D2AB57D9 /* VoiceMessageCacheTests.swift */; }; 21BF2B7CEDFE3CA67C5355AD /* test_image.png in Resources */ = {isa = PBXBuildFile; fileRef = C733D11B421CFE3A657EF230 /* test_image.png */; }; + 21F29351EDD7B2A5534EE862 /* SecureBackupKeyBackupScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD558A898847C179E4B7A237 /* SecureBackupKeyBackupScreen.swift */; }; 22882C710BC99EC34A5024A0 /* UITestsScreenIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CEBE5EA91E8691EDF364EC2 /* UITestsScreenIdentifier.swift */; }; 234E2C782981003971ABE96E /* PermalinkBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F754E66A8970963B15B2A41E /* PermalinkBuilder.swift */; }; 2352C541AF857241489756FF /* MockRoomSummaryProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F7D42E66E939B709C1EC390 /* MockRoomSummaryProvider.swift */; }; @@ -135,6 +140,7 @@ 275EDE8849A2AC1D9309ED7C /* TemplateScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B43456E73F8A2D52B69B9FB9 /* TemplateScreenViewModel.swift */; }; 2797C9D9BA642370F1C85D78 /* Untranslated.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = F75DF9500D69A3AAF8339E69 /* Untranslated.stringsdict */; }; 27E9263DA75E266690A37EB1 /* PermalinkBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB31A32C93D94930B253FBF /* PermalinkBuilderTests.swift */; }; + 27F015B0D5436633B5B3C8C3 /* SecureBackupRecoveryKeyScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7061BE2C0BF427C38AEDEF5E /* SecureBackupRecoveryKeyScreenViewModel.swift */; }; 281BED345D59A9A6A99E9D98 /* UNNotificationContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE148A4FFEE853C5A281500C /* UNNotificationContent.swift */; }; 282A5F3375DDC774AE09B0C3 /* TracingConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1734A445A58ED855B977A0A8 /* TracingConfigurationTests.swift */; }; 2835FD52F3F618D07F799B3D /* Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7310D8DFE01AF45F0689C3AA /* Publisher.swift */; }; @@ -309,6 +315,7 @@ 59F940FCBE6BC343AECEF75E /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2245243369B99216C7D84E /* ImageCache.swift */; }; 5B2D1210B40570D87B11BD3B /* ThreadDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CA3F8E905DF50BF22ECC18F /* ThreadDecorator.swift */; }; 5B6E5AD224509E6C0B520D6E /* RoomMemberDetailsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DDF49CEBC0DFC59C308335F /* RoomMemberDetailsScreenViewModelProtocol.swift */; }; + 5BC6C4ADFE7F2A795ECDE130 /* SecureBackupKeyBackupScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B2D4EEBE8C098BBADD10939 /* SecureBackupKeyBackupScreenCoordinator.swift */; }; 5C02841B2A86327B2C377682 /* NotificationConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C830A64609CBD152F06E0457 /* NotificationConstants.swift */; }; 5C164551F7D26E24F09083D3 /* StaticLocationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C616D90B1E2F033CAA325439 /* StaticLocationScreenViewModelProtocol.swift */; }; 5C8AFBF168A41E20835F3B86 /* LoginScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB34B0C74CD242FED9DD069 /* LoginScreenUITests.swift */; }; @@ -408,6 +415,7 @@ 763D69741D58D2B650BC1FC9 /* CallScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F37FA1A5D55633E1942B153B /* CallScreenCoordinator.swift */; }; 764AFCC225B044CF5F9B41E5 /* PaginationIndicatorRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42EEA67A6796BDC2761619C5 /* PaginationIndicatorRoomTimelineView.swift */; }; 767D366C40F1311CFA333763 /* PillContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86376BEE425704AEE197CA54 /* PillContext.swift */; }; + 7691233E3572A9173FD96CB3 /* SecureBackupKeyBackupScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E88534A39781D76487D59DF /* SecureBackupKeyBackupScreenViewModelTests.swift */; }; 76BA28216FBAF83B2D86A027 /* InvitesScreenCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA2A71915C1F075E403F559C /* InvitesScreenCell.swift */; }; 7708976CEE6AFB5CFAEFBA68 /* PillTextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CF1EE0AA78470C674554262 /* PillTextAttachment.swift */; }; 7719778A682FDAC21445E9C8 /* OnboardingLogo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B0D7955FFB19B584594844B /* OnboardingLogo.swift */; }; @@ -428,6 +436,7 @@ 7AEC56ADEFC5A7198A17412F /* InviteUsersScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB35E2DB4EFE8E6F3959629 /* InviteUsersScreenUITests.swift */; }; 7B5DAB915357BE596529BF25 /* MapTilerStaticMapProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20872C3887F835958CE2F1D0 /* MapTilerStaticMapProtocol.swift */; }; 7BB31E67648CF32D2AB5E502 /* RoomScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CE3C90E487B255B735D73C8 /* RoomScreenViewModel.swift */; }; + 7BF368A78E6D9AFD222F25AF /* SecureBackupScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE094FCB6387D268C436161 /* SecureBackupScreenViewModel.swift */; }; 7C1A7B594B2F8143F0DD0005 /* ElementXAttributeScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = C024C151639C4E1B91FCC68B /* ElementXAttributeScope.swift */; }; 7C384A8E54A4B60A14CDE8E5 /* WaitlistScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12F1E7F9C2BE8BB751037826 /* WaitlistScreenCoordinator.swift */; }; 7C6376192F578E0BA801BFEC /* AnalyticsSettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C64A14EE89928207E3B42B /* AnalyticsSettingsScreenModels.swift */; }; @@ -489,6 +498,7 @@ 899359A4D1147601F6C4E364 /* PillConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB8D34E94AB07128DB73D6C7 /* PillConstants.swift */; }; 899793EFC63DF93C3E0141E7 /* RoomMemberDetailsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FA60F848D1C14F873F9621A /* RoomMemberDetailsScreenCoordinator.swift */; }; 8A0BD60CA4A6004DB06B5403 /* MediaUploadingPreprocessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669F35C505ACE1110589F875 /* MediaUploadingPreprocessor.swift */; }; + 8AA84EF202F2EFC8453A97BD /* SecureBackupRecoveryKeyScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 645E027C112740573D27765C /* SecureBackupRecoveryKeyScreenModels.swift */; }; 8AB8ED1051216546CB35FA0E /* UserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5E9C044BEB7C70B1378E91 /* UserSession.swift */; }; 8AC256AF0EC54658321C9241 /* LegalInformationScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5725BC6C63604CB769145B /* LegalInformationScreenViewModelTests.swift */; }; 8B1D5CE017EEC734CF5FE130 /* Encodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 260004737C573A56FA01E86E /* Encodable.swift */; }; @@ -498,6 +508,7 @@ 8C050A8012E6078BEAEF5BC8 /* PillTextAttachmentData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 913C8E13B8B602C7B6C0C4AE /* PillTextAttachmentData.swift */; }; 8C1A5ECAF895D4CAF8C4D461 /* AppActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F21ED7205048668BEB44A38 /* AppActivityView.swift */; }; 8C27BEB00B903D953F31F962 /* VoiceMessageRecordingButtonTooltipView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF449205DF1E9817115245C4 /* VoiceMessageRecordingButtonTooltipView.swift */; }; + 8C42B5B1642D189C362A5EDF /* SecureBackupScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91831D7042EADD0CC2B5EC36 /* SecureBackupScreenUITests.swift */; }; 8C454500B8073E1201F801A9 /* MXLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = A34A814CBD56230BC74FFCF4 /* MXLogger.swift */; }; 8C706DA7EAC0974CA2F8F1CD /* MentionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15748C254911E3654C93B0ED /* MentionBuilder.swift */; }; 8CC12086CBF91A7E10CDC205 /* HomeScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653265D006E708E4E51AD64 /* HomeScreenCoordinator.swift */; }; @@ -589,6 +600,8 @@ A439B456D0761D6541745CC3 /* NSRegularExpresion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95BAC0F6C9644336E9567EE6 /* NSRegularExpresion.swift */; }; A440D4BC02088482EC633A88 /* KeychainControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5E94DCFEE803E5ABAE8ACCE /* KeychainControllerProtocol.swift */; }; A494741843F087881299ACF0 /* RestorationToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3558A15CFB934F9229301527 /* RestorationToken.swift */; }; + A4B0BAD62A12ED76BD611B79 /* BadgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FA515B3B0D61EF1E907D2D /* BadgeView.swift */; }; + A4C29D373986AFE4559696D5 /* SecureBackupKeyBackupScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4525E8C0FBDD27D1ACE90952 /* SecureBackupKeyBackupScreenViewModelProtocol.swift */; }; A4E885358D7DD5A072A06824 /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = CCE5BF78B125320CBF3BB834 /* PostHog */; }; A5B9EF45C7B8ACEB4954AE36 /* LoginScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9780389F8A53E4D26E23DD03 /* LoginScreenViewModelProtocol.swift */; }; A5C5C18671EDD2747AC16D2D /* OnboardingScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C1CEBA9BCF5D2AD1884FA /* OnboardingScreenViewModel.swift */; }; @@ -598,6 +611,7 @@ A6D4C5EEA85A6A0ABA1559D6 /* RoomDetailsEditScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16D09C79746BDCD9173EB3A7 /* RoomDetailsEditScreenModels.swift */; }; A6DEC1ADEC8FEEC206A0FA37 /* AttributedStringBuilderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72F37B5DA798C9AE436F2C2C /* AttributedStringBuilderProtocol.swift */; }; A722F426FD81FC67706BB1E0 /* CustomLayoutLabelStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42236480CF0431535EBE8387 /* CustomLayoutLabelStyle.swift */; }; + A743841F91B62B0E56217B04 /* SecureBackupKeyBackupScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DCB219D7B7B0299358FF81 /* SecureBackupKeyBackupScreenUITests.swift */; }; A74438ED16F8683A4B793E6A /* AnalyticsSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BCE3FAF40932AC7C7639AC4 /* AnalyticsSettingsScreenViewModel.swift */; }; A7BEE8216B4B12BE4C0F2C3F /* AppLockSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892EF45CCC5D2BF0FD1F770C /* AppLockSettingsScreenViewModel.swift */; }; A7D48E44D485B143AADDB77D /* Strings+Untranslated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */; }; @@ -618,6 +632,7 @@ ABF3FAB234AD3565B214309B /* TimelineSenderAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BC588051E6572A1AF51D738 /* TimelineSenderAvatarView.swift */; }; AC69B6DF15FC451AB2945036 /* UserSessionStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEBA759D1347CFFB3D84ED1F /* UserSessionStoreProtocol.swift */; }; AC7AA215D60FBC307F984028 /* Consumable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 127A57D053CE8C87B5EFB089 /* Consumable.swift */; }; + AC90434798E7894370E80E66 /* SecureBackupScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D79BB714D28C9F588DD69353 /* SecureBackupScreenViewModelProtocol.swift */; }; ACF094CF3BF02DBFA6DFDE60 /* AuthenticationCoordinatorUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D2D0A6F1ABC99D29462FB84 /* AuthenticationCoordinatorUITests.swift */; }; AD2A81B65A9F6163012086F1 /* MXLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 111B698739E3410E2CDB7144 /* MXLog.swift */; }; AD55E245FE686D7DB4C86406 /* RoomTimelineItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90F2F8998E5632668B0AD848 /* RoomTimelineItemView.swift */; }; @@ -630,11 +645,11 @@ AF8BFA37791E1756EE243E08 /* SettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8F0ED874DF8C9A51B0AB6F /* SettingsScreenCoordinator.swift */; }; AFA1F2543DFF7B45DF68ACD6 /* CompletionSuggestionModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 170BF6F7923A5C3792442F27 /* CompletionSuggestionModels.swift */; }; AFC518DCC38B821537EBF549 /* CreatePollScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ADC55DFF46083BC957E0019 /* CreatePollScreenModels.swift */; }; - B037C365CF8A58A0D149A2DB /* AuthenticationIconImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97755C01C3971474EFAD5367 /* AuthenticationIconImage.swift */; }; B064D42BA087649ACAE462E8 /* SoftLogoutUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55F30E764BED111C81739844 /* SoftLogoutUITests.swift */; }; B09DC6E3D0EE87C4D4ABFAB3 /* EncryptedHistoryRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0140615D2232612C813FD6C /* EncryptedHistoryRoomTimelineItem.swift */; }; B0CB16349B96262AA65A04AF /* Version in Frameworks */ = {isa = PBXBuildFile; productRef = A05AF81DDD14AD58CB0E1B9B /* Version */; }; B1069F361E604D5436AE9FFD /* StaticLocationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B06663F7858E45882E63471 /* StaticLocationScreen.swift */; }; + B1387648C6F71F1B98244803 /* SecureBackupRecoveryKeyScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 596AA8843AC1A234F3387767 /* SecureBackupRecoveryKeyScreenCoordinator.swift */; }; B14BC354E56616B6B7D9A3D7 /* NotificationServiceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A1AD6389A4659AF0CEAE62 /* NotificationServiceExtension.swift */; }; B22D857D1E8FCA6DD74A58E3 /* UserSessionScreenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F899D02CF26EA7675EEBE74C /* UserSessionScreenTests.swift */; }; B245583C63F8F90357B87FAE /* KZFileWatchers in Frameworks */ = {isa = PBXBuildFile; productRef = A2AE110B053B55E38F8D10C7 /* KZFileWatchers */; }; @@ -663,6 +678,7 @@ B6EC2148FA5443C9289BEEBA /* MediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17EFA1D3D09FC2F9C5E1CB2 /* MediaProvider.swift */; }; B717A820BE02C6FE2CB53F6E /* WaitlistScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B697816AF93DA06EC58C5D70 /* WaitlistScreenViewModelProtocol.swift */; }; B721125D17A0BA86794F29FB /* MockServerSelectionScreenState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E057FB1F07A5C201C89061 /* MockServerSelectionScreenState.swift */; }; + B7888FC1E1DEF816D175C8D6 /* SecureBackupKeyBackupScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD72A9B720D75DBE60AC299F /* SecureBackupKeyBackupScreenModels.swift */; }; B796A25F282C0A340D1B9C12 /* ImageRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B5EDCD05D50BA9B815C66C /* ImageRoomTimelineItemContent.swift */; }; B7C9E07F4F9CCC8DD7156A20 /* CallScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28146817C61423CACCF942F5 /* CallScreenModels.swift */; }; B828C600A54B2EE20871A451 /* PerformanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD700E035C85738EE4B97129 /* PerformanceTests.swift */; }; @@ -765,6 +781,7 @@ D43F0503EF2CBC55272538FE /* SDKGeneratedMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2F079B5DBD0D85FEA687AAE /* SDKGeneratedMocks.swift */; }; D46C33F8B61B55F0C8C2D15F /* LocationRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B2AC540DE619B36832A5DB5 /* LocationRoomTimelineItem.swift */; }; D4ACF3276F5D0DA28D4028C9 /* AnalyticsPromptScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8196D64EB9CF2AF1F43E4ED1 /* AnalyticsPromptScreenViewModelProtocol.swift */; }; + D4D5595C4A2A702CFF4E94FF /* HeroImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EC2F1622C5BBABED6012E12 /* HeroImage.swift */; }; D53B80EF02C1062E68659EDD /* ReportContentViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086C19086DD16E9B38E25954 /* ReportContentViewModelTests.swift */; }; D55AF9B5B55FEED04771A461 /* RoomFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A008E57D52B07B78DFAD1BB /* RoomFlowCoordinator.swift */; }; D5681C80D8281560AACE0035 /* Label.swift in Sources */ = {isa = PBXBuildFile; fileRef = 045253F9967A535EE5B16691 /* Label.swift */; }; @@ -784,6 +801,7 @@ D9473FC9B077A6EDB7A12001 /* LocationRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 772334731A8BF8E6D90B194D /* LocationRoomTimelineView.swift */; }; D98B5EE8C4F5A2CE84687AE8 /* UTType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 897DF5E9A70CE05A632FC8AF /* UTType.swift */; }; D9F80CE61BF8FF627FDB0543 /* LoadableImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352359663A0E52BA20761EE /* LoadableImage.swift */; }; + DA7E867F5EAFF8E20B2EE3B6 /* SecureBackupScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3D16709ADD4F4BCC710B1E /* SecureBackupScreenModels.swift */; }; DB079D1929B5A5F52D207C83 /* RoomDetailsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 466C71A0FED9BFF287613C82 /* RoomDetailsScreenModels.swift */; }; DC08ADC41E792086A340A8B3 /* AccessibilityIdentifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04BB8DDE245ED86C489BA983 /* AccessibilityIdentifiers.swift */; }; DC1BB5EE5F4D9B6A1F98A77A /* WelcomeScreenScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC2E8E1B20BB2EA07B0B61E /* WelcomeScreenScreenViewModel.swift */; }; @@ -823,9 +841,11 @@ E67418DACEDBC29E988E6ACD /* message.caf in Resources */ = {isa = PBXBuildFile; fileRef = ED482057AE39D5C6D9C5F3D8 /* message.caf */; }; E75CE800B3E64D0F7F8E228D /* TemplateScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08E9043618AE5B0BF7B07E1 /* TemplateScreenViewModelTests.swift */; }; E77469C5CD7F7F58C0AC9752 /* test_pdf.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 3FFDA99C98BE05F43A92343B /* test_pdf.pdf */; }; + E77FE06B165A38BF1735509F /* SecureBackupScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF73F49E6B6683F7E2D26F0 /* SecureBackupScreenCoordinator.swift */; }; E78D429F18071545BF661A52 /* RoomDetailsEditScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A3E77399BD262D301451BF2 /* RoomDetailsEditScreenCoordinator.swift */; }; E794AB6ABE1FF5AF0573FEA1 /* BlurHashEncode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9332DFE9642F0A46ECA0497B /* BlurHashEncode.swift */; }; E79D79CDAFE8BEBCC3AECA54 /* AppLockScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08283301736A6FE9D558B2CB /* AppLockScreenViewModelProtocol.swift */; }; + E84ADFE9696936C18C2424B5 /* SecureBackupScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A00BB9CD12CF6AC98D5485 /* SecureBackupScreen.swift */; }; E89536FC8C0E4B79E9842A78 /* RoomTimelineControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C0197EAE9D45A662B8847B6 /* RoomTimelineControllerProtocol.swift */; }; E9347F56CF0683208F4D9249 /* RoomNotificationSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81A9B5225D0881CEFA2CF7C9 /* RoomNotificationSettingsScreenViewModel.swift */; }; E9560744F7B0292E20ECE5F2 /* RoomDetailsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E8A1E8EE094F570573B6E8 /* RoomDetailsScreenViewModelProtocol.swift */; }; @@ -858,6 +878,7 @@ EF7924005216B8189898F370 /* BackgroundTaskProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA028DCD4157F9A1F999827 /* BackgroundTaskProtocol.swift */; }; EF890DEF0479E66548F2BA23 /* AppLockTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490BEADEFB2D6B7C9F618AE8 /* AppLockTimer.swift */; }; F05516474DB42369FD976CEF /* AppLockScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 349C633291427A0F29C28C54 /* AppLockScreenUITests.swift */; }; + F0570F1ECD70C4C851FB2052 /* SecureBackupRecoveryKeyScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93E7304F5ECB4CB11CB10E60 /* SecureBackupRecoveryKeyScreenViewModelProtocol.swift */; }; F06CE9132855E81EBB6DDC32 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 940C605265DD82DA0C655E23 /* Kingfisher */; }; F07D88421A9BC4D03D4A5055 /* VideoRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F348B5F2C12F9D4F4B4D3884 /* VideoRoomTimelineItem.swift */; }; F0A26CD502C3A5868353B0FA /* ServerConfirmationScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24DEE0682C95F897B6C7CB0D /* ServerConfirmationScreenViewModel.swift */; }; @@ -887,6 +908,7 @@ F777C6FEE7D106136E2ED2B2 /* MessageForwardingScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F6E6EDC4BBF962B2ED595A4 /* MessageForwardingScreenViewModelTests.swift */; }; F78BAD28482A467287A9A5A3 /* EventBasedMessageTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0900BBF0A5D5D775E917C70 /* EventBasedMessageTimelineItemProtocol.swift */; }; F7BC744FFA7FE248FAE7F570 /* UserIndicatorToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F57C8022B8A871A1DCD1750A /* UserIndicatorToastView.swift */; }; + F833D5B5BE6707F961FA88DB /* SecureBackupController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A1119E9C63AE530252640D2 /* SecureBackupController.swift */; }; F86102DC2C68BBBB0521BAAE /* SoftLogoutScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BB385E148DE55C85C0A02D6 /* SoftLogoutScreenModels.swift */; }; F8E725D42023ECA091349245 /* AudioRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57EAAF82432B0B53881CF826 /* AudioRoomTimelineItem.swift */; }; F94000E3D91B11C527DA8807 /* UserProfileCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 923485F85E1D765EF9D20E88 /* UserProfileCell.swift */; }; @@ -895,6 +917,7 @@ FA2BBAE9FC5E2E9F960C0980 /* NavigationCoordinators.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F28602AC7AC881AED37EBA /* NavigationCoordinators.swift */; }; FA4296218444C48BC890F46B /* RoomMemberDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B35311C7FED04B0E1B80C2 /* RoomMemberDetails.swift */; }; FA5A7E32B1920FCB4EEDC1BA /* RoomDetailsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6493AC9979CEB1410302BFE3 /* RoomDetailsScreenCoordinator.swift */; }; + FA71CD334F2D2289BEF0D749 /* SecureBackupRecoveryKeyScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A2FCA3D0F239B9E911B966B /* SecureBackupRecoveryKeyScreen.swift */; }; FA9C427FFB11B1AA2DCC5602 /* RoomProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */; }; FB0A9D06FC9122E37992D962 /* LayoutDirection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C14D83B2B7CD5501A0089EFC /* LayoutDirection.swift */; }; FB53CD9B74A15B3B94F9F788 /* CreateRoomModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B849D2FF2CC12BA411A1651 /* CreateRoomModels.swift */; }; @@ -903,6 +926,7 @@ FBCCF1EA25A071324FCD8544 /* TimelineItemDebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7023EB4F3B7C7D1FBA68638B /* TimelineItemDebugView.swift */; }; FBF09B6C900415800DDF2A21 /* EmojiProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C113E0CB7E15E9765B1817A /* EmojiProvider.swift */; }; FC10228E73323BDC09526F97 /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 9573B94B1C86C6DF751AF3FD /* SwiftState */; }; + FC4F6BA083A64840B38CE269 /* SecureBackupRecoveryKeyScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBA358C79F0DCBC4FA14A88 /* SecureBackupRecoveryKeyScreenUITests.swift */; }; FCD3F2B82CAB29A07887A127 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 2B43F2AF7456567FE37270A7 /* KeychainAccess */; }; FCDA202B246F75BA28E10C5F /* MapTilerAuthorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = E062C1750EFC8627DE4CAB8E /* MapTilerAuthorization.swift */; }; FD29471C72872F8B7580E3E1 /* KeychainControllerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39C0D861FC397AC34BCF089E /* KeychainControllerMock.swift */; }; @@ -1059,6 +1083,7 @@ 1B6E30BB748F3F480F077969 /* RoomMemberDetailsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreenModels.swift; sourceTree = ""; }; 1B8E176484A89BAC389D4076 /* RoomMembersListScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreen.swift; sourceTree = ""; }; 1B927CF5EF7FCCDA5EDC474B /* NotificationItemProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItemProxyProtocol.swift; sourceTree = ""; }; + 1C21A715237F2B6D6E80998C /* SecureBackupControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupControllerProtocol.swift; sourceTree = ""; }; 1CC575D1895FA62591451A93 /* RoomMemberDetailsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreen.swift; sourceTree = ""; }; 1D56469A9EE0CFA2B7BA9760 /* SessionVerificationControllerProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationControllerProxyProtocol.swift; sourceTree = ""; }; 1D67E616BCA82D8A1258D488 /* NetworkMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitor.swift; sourceTree = ""; }; @@ -1115,6 +1140,7 @@ 2CEBCB9676FCD1D0F13188DD /* StringTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringTests.swift; sourceTree = ""; }; 2D0946F77B696176E062D037 /* RoomMembersListScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenModels.swift; sourceTree = ""; }; 2D505843AB66822EB91F0DF0 /* TimelineItemProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemProxy.swift; sourceTree = ""; }; + 2E88534A39781D76487D59DF /* SecureBackupKeyBackupScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupKeyBackupScreenViewModelTests.swift; sourceTree = ""; }; 2EFE1922F39398ABFB36DF3F /* RoomDetailsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsViewModelTests.swift; sourceTree = ""; }; 2F36C5D9B37E50915ECBD3EE /* RoomMemberProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberProxy.swift; sourceTree = ""; }; 2FD0E68C42CA7DDCD4CAD68D /* MentionSuggestionItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionSuggestionItemView.swift; sourceTree = ""; }; @@ -1173,6 +1199,7 @@ 3F684BDD23ECEADB3053BA5A /* DeveloperOptionsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreenUITests.swift; sourceTree = ""; }; 3FFDA99C98BE05F43A92343B /* test_pdf.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = test_pdf.pdf; sourceTree = ""; }; 40076C770A5FB83325252973 /* VoiceMessageMediaManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageMediaManager.swift; sourceTree = ""; }; + 40316EFFEAC7B206EE9A55AE /* SecureBackupScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupScreenViewModelTests.swift; sourceTree = ""; }; 40B21E611DADDEF00307E7AC /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; 4132F882A984ED971338EE9D /* ReportContentScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportContentScreenUITests.swift; sourceTree = ""; }; 4151163F666ED94FD959475A /* NotificationName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationName.swift; sourceTree = ""; }; @@ -1187,6 +1214,7 @@ 43A84EE187D0C772E18A4E39 /* VoiceMessageCacheProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageCacheProtocol.swift; sourceTree = ""; }; 4481799F455B3DA243BDA2AC /* ShareToMapsAppActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareToMapsAppActivity.swift; sourceTree = ""; }; 450E04B2A976CC4C8CC1807C /* EmoteRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItem.swift; sourceTree = ""; }; + 4525E8C0FBDD27D1ACE90952 /* SecureBackupKeyBackupScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupKeyBackupScreenViewModelProtocol.swift; sourceTree = ""; }; 4549FCB53F43DB0B278374BC /* TemplateScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreen.swift; sourceTree = ""; }; 4552D3466B1453F287223ADA /* SwipeRightAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeRightAction.swift; sourceTree = ""; }; 45CDF9A107BFE6C79B58D6B5 /* RoomMembersListScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -1214,6 +1242,7 @@ 4A5B4CD611DE7E94F5BA87B2 /* AppLockTimerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockTimerTests.swift; sourceTree = ""; }; 4AB7D7DAAAF662DED9D02379 /* MockMediaLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMediaLoader.swift; sourceTree = ""; }; 4ADC55DFF46083BC957E0019 /* CreatePollScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePollScreenModels.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 = ""; }; 4B5046BB295AEAFA6FB81655 /* SessionVerificationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenModels.swift; sourceTree = ""; }; 4BD371B60E07A5324B9507EF /* AnalyticsSettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenCoordinator.swift; sourceTree = ""; }; @@ -1259,8 +1288,12 @@ 584A61D9C459FAFEF038A7C0 /* Section.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Section.swift; sourceTree = ""; }; 58C2527813FDAE23E72A9063 /* AnalyticsSettingsScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenViewModelTests.swift; sourceTree = ""; }; 58D295F0081084F38DB20893 /* RoomNotificationSettingsScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsScreenViewModelTests.swift; sourceTree = ""; }; + 58DCB219D7B7B0299358FF81 /* SecureBackupKeyBackupScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupKeyBackupScreenUITests.swift; sourceTree = ""; }; 592A35163B0749C66BFD6186 /* MapLibreStaticMapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapLibreStaticMapView.swift; sourceTree = ""; }; + 596AA8843AC1A234F3387767 /* SecureBackupRecoveryKeyScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupRecoveryKeyScreenCoordinator.swift; sourceTree = ""; }; 59846FA04E1DBBFDD8829C2A /* MessageForwardingScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenUITests.swift; sourceTree = ""; }; + 5A1119E9C63AE530252640D2 /* SecureBackupController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupController.swift; sourceTree = ""; }; + 5A2FCA3D0F239B9E911B966B /* SecureBackupRecoveryKeyScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupRecoveryKeyScreen.swift; sourceTree = ""; }; 5A37E2FACFD041CE466223CD /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 5AEA0B743847CFA5B3C38EE4 /* RoomMembersListScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenCoordinator.swift; sourceTree = ""; }; 5B0D7955FFB19B584594844B /* OnboardingLogo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingLogo.swift; sourceTree = ""; }; @@ -1285,6 +1318,7 @@ 6390A6DC140CA3D6865A66FF /* SeparatorRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorRoomTimelineView.swift; sourceTree = ""; }; 63E1FF2DA52B1DE7CAEC5422 /* AnalyticsPromptScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptScreenViewModel.swift; sourceTree = ""; }; 63E8A1E8EE094F570573B6E8 /* RoomDetailsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreenViewModelProtocol.swift; sourceTree = ""; }; + 645E027C112740573D27765C /* SecureBackupRecoveryKeyScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupRecoveryKeyScreenModels.swift; sourceTree = ""; }; 648DD1C10E4957CB791FE0B8 /* OverridableAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverridableAvatarImage.swift; sourceTree = ""; }; 6493AC9979CEB1410302BFE3 /* RoomDetailsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreenCoordinator.swift; sourceTree = ""; }; 64F49FB9EE2913234F06CE68 /* MediaPickerScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPickerScreenCoordinator.swift; sourceTree = ""; }; @@ -1328,6 +1362,7 @@ 6FB31A32C93D94930B253FBF /* PermalinkBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermalinkBuilderTests.swift; sourceTree = ""; }; 6FC5015B9634698BDB8701AF /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = it; path = it.lproj/Localizable.stringsdict; sourceTree = ""; }; 7023EB4F3B7C7D1FBA68638B /* TimelineItemDebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemDebugView.swift; sourceTree = ""; }; + 7061BE2C0BF427C38AEDEF5E /* SecureBackupRecoveryKeyScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupRecoveryKeyScreenViewModel.swift; sourceTree = ""; }; 70C86696AC9521F8ED88FBEB /* MediaUploadPreviewScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreen.swift; sourceTree = ""; }; 7101698791B321A76F552804 /* WelcomeScreenScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeScreenScreenViewModelProtocol.swift; sourceTree = ""; }; 713B48DBF65DE4B0DD445D66 /* ReportContentScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportContentScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -1355,9 +1390,11 @@ 79A1D75C7C52CD14A327CC90 /* ProgressMaskModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressMaskModifier.swift; sourceTree = ""; }; 7A5D2323D7B6BF4913EB7EED /* landscape_test_image.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = landscape_test_image.jpg; sourceTree = ""; }; 7AB7ED3A898B07976F3AA90F /* BugReportViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportViewModelTests.swift; sourceTree = ""; }; + 7AE094FCB6387D268C436161 /* SecureBackupScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupScreenViewModel.swift; sourceTree = ""; }; 7B04BD3874D736127A8156B8 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; 7B19B2BCC779ED934E0BBC2A /* AudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayer.swift; sourceTree = ""; }; 7B25F959A434BB9923A3223F /* ExpiringTaskRunner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpiringTaskRunner.swift; sourceTree = ""; }; + 7B3D16709ADD4F4BCC710B1E /* SecureBackupScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupScreenModels.swift; sourceTree = ""; }; 7B849D2FF2CC12BA411A1651 /* CreateRoomModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomModels.swift; sourceTree = ""; }; 7B9FCA1CFD07B8CF9BD21266 /* FlowCoordinatorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowCoordinatorProtocol.swift; sourceTree = ""; }; 7CA3F8E905DF50BF22ECC18F /* ThreadDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadDecorator.swift; sourceTree = ""; }; @@ -1366,6 +1403,7 @@ 7DDBF99755A9008CF8C8499E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 7DDF49CEBC0DFC59C308335F /* RoomMemberDetailsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreenViewModelProtocol.swift; sourceTree = ""; }; 7E492690C8B27A892C194CC4 /* AdvancedSettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsScreenCoordinator.swift; sourceTree = ""; }; + 7EC2F1622C5BBABED6012E12 /* HeroImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeroImage.swift; sourceTree = ""; }; 7F615A00DB223FF3280204D2 /* UserDiscoveryServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoveryServiceProtocol.swift; sourceTree = ""; }; 7FB2253D36E81E045E1CB432 /* Duration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Duration.swift; sourceTree = ""; }; 80C4927D09099497233E9980 /* WaitlistScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreen.swift; sourceTree = ""; }; @@ -1376,11 +1414,13 @@ 81B17B1F29448D1B9049B11C /* ReportContentScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportContentScreenViewModel.swift; sourceTree = ""; }; 81B17DB1BC3B0C62AF84D230 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 8296D6FB451E25CEC0767BBA /* RoomNotificationSettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsScreenCoordinator.swift; sourceTree = ""; }; + 82B612853BFB68373249777B /* SecureBackupKeyBackupScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupKeyBackupScreenViewModel.swift; sourceTree = ""; }; 837B440C4705E4B899BCB899 /* RoomDetailsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreenViewModel.swift; sourceTree = ""; }; 839E2C35DF3F9C7B54C3CE49 /* RoundedCornerShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedCornerShape.swift; sourceTree = ""; }; 840E86A67DB2C92C09771EAD /* AnalyticsPromptScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptScreenModels.swift; sourceTree = ""; }; 845DDBDE5A0887E73D38B826 /* InviteUsersViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersViewModelTests.swift; sourceTree = ""; }; 84816E0D2F34E368BF64FA60 /* SessionVerificationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreen.swift; sourceTree = ""; }; + 84A00BB9CD12CF6AC98D5485 /* SecureBackupScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupScreen.swift; sourceTree = ""; }; 84A87D0471D438A233C2CF4A /* RoomMemberDetailsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreenViewModel.swift; sourceTree = ""; }; 84B7A28A6606D58D1E38C55A /* ExpiringTaskRunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpiringTaskRunnerTests.swift; sourceTree = ""; }; 85149F56BA333619900E2410 /* UserDetailsEditScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDetailsEditScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -1426,6 +1466,7 @@ 90A55430639712CFACA34F43 /* TextRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineItem.swift; sourceTree = ""; }; 90F2F8998E5632668B0AD848 /* RoomTimelineItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemView.swift; sourceTree = ""; }; 913C8E13B8B602C7B6C0C4AE /* PillTextAttachmentData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillTextAttachmentData.swift; sourceTree = ""; }; + 91831D7042EADD0CC2B5EC36 /* SecureBackupScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupScreenUITests.swift; sourceTree = ""; }; 91CF6F7D08228D16BA69B63B /* zh-Hant-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant-TW"; path = "zh-Hant-TW.lproj/Localizable.strings"; sourceTree = ""; }; 923485F85E1D765EF9D20E88 /* UserProfileCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileCell.swift; sourceTree = ""; }; 92390F9FA98255440A6BF5F8 /* OIDCAuthenticationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OIDCAuthenticationPresenter.swift; sourceTree = ""; }; @@ -1436,13 +1477,13 @@ 9349F590E35CE514A71E6764 /* LoginHomeserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginHomeserver.swift; sourceTree = ""; }; 935C2FB18EFB8EEE96B26330 /* CreateRoomFlowParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomFlowParameters.swift; sourceTree = ""; }; 93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenViewModelTests.swift; sourceTree = ""; }; + 93E7304F5ECB4CB11CB10E60 /* SecureBackupRecoveryKeyScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupRecoveryKeyScreenViewModelProtocol.swift; sourceTree = ""; }; 94BCC8A9C73C1F838122C645 /* TimelineItemPlainStylerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemPlainStylerView.swift; sourceTree = ""; }; 94D670124FC3E84F23A62CCF /* APNSPayload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APNSPayload.swift; sourceTree = ""; }; 9501D11B4258DFA33BA3B40F /* ServerSelectionScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenModels.swift; sourceTree = ""; }; 95A1CCDEE545CB6453B084BF /* FormButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormButtonStyles.swift; sourceTree = ""; }; 95BAC0F6C9644336E9567EE6 /* NSRegularExpresion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSRegularExpresion.swift; sourceTree = ""; }; 96C4762F8D6112E43117DB2F /* CustomStringConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomStringConvertible.swift; sourceTree = ""; }; - 97755C01C3971474EFAD5367 /* AuthenticationIconImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationIconImage.swift; sourceTree = ""; }; 9780389F8A53E4D26E23DD03 /* LoginScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenViewModelProtocol.swift; sourceTree = ""; }; 97CE98208321C4D66E363612 /* ShimmerModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShimmerModifier.swift; sourceTree = ""; }; 981663D961C94270FA035FD0 /* Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alert.swift; sourceTree = ""; }; @@ -1508,7 +1549,9 @@ AC9104846487244648D32C6D /* AudioPlayerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerProtocol.swift; sourceTree = ""; }; ACCC1874C122E2BBE648B8F5 /* LegalInformationScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenUITests.swift; sourceTree = ""; }; AD378D580A41E42560C60E9C /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Localizable.strings; sourceTree = ""; }; + AD558A898847C179E4B7A237 /* SecureBackupKeyBackupScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupKeyBackupScreen.swift; sourceTree = ""; }; AD6B522BD637845AB9570B10 /* RoomNotificationSettingsProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsProxy.swift; sourceTree = ""; }; + AD72A9B720D75DBE60AC299F /* SecureBackupKeyBackupScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupKeyBackupScreenModels.swift; sourceTree = ""; }; AD9AD6AE5FC868962F090740 /* CallScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallScreenViewModelProtocol.swift; sourceTree = ""; }; AD9CB3B9DFA353AB2B7CD9F8 /* NotificationSettingsEditScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenCoordinator.swift; sourceTree = ""; }; ADB35E2DB4EFE8E6F3959629 /* InviteUsersScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenUITests.swift; sourceTree = ""; }; @@ -1580,11 +1623,13 @@ C08E9043618AE5B0BF7B07E1 /* TemplateScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenViewModelTests.swift; sourceTree = ""; }; C0900BBF0A5D5D775E917C70 /* EventBasedMessageTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBasedMessageTimelineItemProtocol.swift; sourceTree = ""; }; C0FEA560929DD73FFEF8C3DF /* HomeScreenEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenEmptyStateView.swift; sourceTree = ""; }; + C0FF08D0BD7D0B4B6877AB7D /* SecureBackupRecoveryKeyScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupRecoveryKeyScreenViewModelTests.swift; sourceTree = ""; }; C14D83B2B7CD5501A0089EFC /* LayoutDirection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutDirection.swift; sourceTree = ""; }; C1511766C534367700C8DD75 /* RoomNotificationModeProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationModeProxy.swift; sourceTree = ""; }; C15E0017717EAE3A1D02D005 /* StaticLocationScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticLocationScreenCoordinator.swift; sourceTree = ""; }; C18CC37B97E77838609CFFE7 /* AdvancedSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsScreen.swift; sourceTree = ""; }; C1D737F4672021D0A7D218CD /* OIDCAccountSettingsPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OIDCAccountSettingsPresenter.swift; sourceTree = ""; }; + C1FA515B3B0D61EF1E907D2D /* BadgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeView.swift; sourceTree = ""; }; C23B3FAD8B23C421BC0D1B1E /* MapTilerGeoCodingServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerGeoCodingServiceProtocol.swift; sourceTree = ""; }; C2886615BEBAE33A0AA4D5F8 /* RoomScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenModels.swift; sourceTree = ""; }; C2E9B841EE4878283ECDB554 /* InviteUsersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreen.swift; sourceTree = ""; }; @@ -1670,6 +1715,7 @@ D6CA5F386C7701C129398945 /* AuthenticationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationCoordinator.swift; sourceTree = ""; }; D6DC38E64A5ED3FDB201029A /* BugReportService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportService.swift; sourceTree = ""; }; D77B3D4950F1707E66E4A45A /* AnalyticsConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsConfiguration.swift; sourceTree = ""; }; + D79BB714D28C9F588DD69353 /* SecureBackupScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupScreenViewModelProtocol.swift; sourceTree = ""; }; D7BB243B26D54EF1A0C422C0 /* NotificationContentBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContentBuilder.swift; sourceTree = ""; }; D7BEB970F500BFB248443FA1 /* BloomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloomView.swift; sourceTree = ""; }; D8E057FB1F07A5C201C89061 /* MockServerSelectionScreenState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockServerSelectionScreenState.swift; sourceTree = ""; }; @@ -1781,6 +1827,8 @@ FCE93F0CBF0D96B77111C413 /* AppLockFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockFlowCoordinator.swift; sourceTree = ""; }; FD1275D9CE0FFBA6E8E85426 /* UserIndicatorController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorController.swift; sourceTree = ""; }; FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainControllerTests.swift; sourceTree = ""; }; + FDBA358C79F0DCBC4FA14A88 /* SecureBackupRecoveryKeyScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupRecoveryKeyScreenUITests.swift; sourceTree = ""; }; + FDF73F49E6B6683F7E2D26F0 /* SecureBackupScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupScreenCoordinator.swift; sourceTree = ""; }; FE87C931165F5E201CACBB87 /* MediaPlayerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayerProtocol.swift; sourceTree = ""; }; FEC2E8E1B20BB2EA07B0B61E /* WelcomeScreenScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeScreenScreenViewModel.swift; sourceTree = ""; }; FEFEEE93B82937B2E86F92EB /* AnalyticsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsScreen.swift; sourceTree = ""; }; @@ -1993,6 +2041,7 @@ 114DC16B28140F885FD833E2 /* NotificationSettings */, 40E6246F03D1FE377BC5D963 /* Room */, 07900E9BFFD109F91B35B4C5 /* RoomMember */, + BDCEF7C3BF6D09F5611CFC8B /* SecureBackup */, 82D5AD3EAE3A5C1068A44A88 /* Session */, 5329E48968EB951235E83DAE /* SessionVerification */, FCDF06BDB123505F0334B4F9 /* Timeline */, @@ -2141,6 +2190,14 @@ path = Replies; sourceTree = ""; }; + 1E53A2E18B59B82EE3D8C23C /* View */ = { + isa = PBXGroup; + children = ( + 5A2FCA3D0F239B9E911B966B /* SecureBackupRecoveryKeyScreen.swift */, + ); + path = View; + sourceTree = ""; + }; 23605DD08620BE6558242469 /* MediaUploadPreviewScreen */ = { isa = PBXGroup; children = ( @@ -2163,6 +2220,16 @@ path = SupportingFiles; sourceTree = ""; }; + 2565414373E6F68005966B8E /* SecureBackup */ = { + isa = PBXGroup; + children = ( + B1FD4FD6CEB987AE274AEEE5 /* SecureBackupKeyBackupScreen */, + 6E8F16377AD462BBD4951271 /* SecureBackupRecoveryKeyScreen */, + 3B4C46F36A42B42C4EB14933 /* SecureBackupScreen */, + ); + path = SecureBackup; + sourceTree = ""; + }; 26C16326BCCCED74A85A0F48 /* View */ = { isa = PBXGroup; children = ( @@ -2301,8 +2368,10 @@ children = ( 8F21ED7205048668BEB44A38 /* AppActivityView.swift */, CC743C7A85E3171BCBF0A653 /* AvatarHeaderView.swift */, + C1FA515B3B0D61EF1E907D2D /* BadgeView.swift */, 8CC23C63849452BC86EA2852 /* ButtonStyle.swift */, 0960A7F5C1B0B6679BDF26F9 /* ElementToggleStyle.swift */, + 7EC2F1622C5BBABED6012E12 /* HeroImage.swift */, B590BD4507D4F0A377FDE01A /* LoadableAvatarImage.swift */, C352359663A0E52BA20761EE /* LoadableImage.swift */, FFECCE59967018204876D0A5 /* LocationMarkerView.swift */, @@ -2437,6 +2506,18 @@ path = AppLockScreen; sourceTree = ""; }; + 3B4C46F36A42B42C4EB14933 /* SecureBackupScreen */ = { + isa = PBXGroup; + children = ( + FDF73F49E6B6683F7E2D26F0 /* SecureBackupScreenCoordinator.swift */, + 7B3D16709ADD4F4BCC710B1E /* SecureBackupScreenModels.swift */, + 7AE094FCB6387D268C436161 /* SecureBackupScreenViewModel.swift */, + D79BB714D28C9F588DD69353 /* SecureBackupScreenViewModelProtocol.swift */, + 7478D8764F916822CD6E10AB /* View */, + ); + path = SecureBackupScreen; + sourceTree = ""; + }; 3D22B0A4FC9008F7E353D0EA /* View */ = { isa = PBXGroup; children = ( @@ -2876,6 +2957,18 @@ path = Notification; sourceTree = ""; }; + 6E8F16377AD462BBD4951271 /* SecureBackupRecoveryKeyScreen */ = { + isa = PBXGroup; + children = ( + 596AA8843AC1A234F3387767 /* SecureBackupRecoveryKeyScreenCoordinator.swift */, + 645E027C112740573D27765C /* SecureBackupRecoveryKeyScreenModels.swift */, + 7061BE2C0BF427C38AEDEF5E /* SecureBackupRecoveryKeyScreenViewModel.swift */, + 93E7304F5ECB4CB11CB10E60 /* SecureBackupRecoveryKeyScreenViewModelProtocol.swift */, + 1E53A2E18B59B82EE3D8C23C /* View */, + ); + path = SecureBackupRecoveryKeyScreen; + sourceTree = ""; + }; 6EE5E2BBFBC7947CFE789B4D /* Manager */ = { isa = PBXGroup; children = ( @@ -2999,6 +3092,9 @@ 58D295F0081084F38DB20893 /* RoomNotificationSettingsScreenViewModelTests.swift */, 93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */, AEEAFB646E583655652C3D04 /* RoomStateEventStringBuilderTests.swift */, + 2E88534A39781D76487D59DF /* SecureBackupKeyBackupScreenViewModelTests.swift */, + C0FF08D0BD7D0B4B6877AB7D /* SecureBackupRecoveryKeyScreenViewModelTests.swift */, + 40316EFFEAC7B206EE9A55AE /* SecureBackupScreenViewModelTests.swift */, 277C20CDD5B64510401B6D0D /* ServerConfigurationScreenViewStateTests.swift */, F08776C48FFB47CACF64ED10 /* ServerConfirmationScreenViewModelTests.swift */, EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */, @@ -3038,6 +3134,14 @@ path = Analytics; sourceTree = ""; }; + 7478D8764F916822CD6E10AB /* View */ = { + isa = PBXGroup; + children = ( + 84A00BB9CD12CF6AC98D5485 /* SecureBackupScreen.swift */, + ); + path = View; + sourceTree = ""; + }; 7563BA5BBB57C5520C067859 /* AvancedOptionsScreen */ = { isa = PBXGroup; children = ( @@ -3452,6 +3556,9 @@ C5B7A755E985FA14469E86B2 /* RoomMembersListScreenUITests.swift */, 66901977F6469D03C333DF32 /* RoomNotificationSettingsScreenUITests.swift */, 086B997409328F091EBA43CE /* RoomScreenUITests.swift */, + 58DCB219D7B7B0299358FF81 /* SecureBackupKeyBackupScreenUITests.swift */, + FDBA358C79F0DCBC4FA14A88 /* SecureBackupRecoveryKeyScreenUITests.swift */, + 91831D7042EADD0CC2B5EC36 /* SecureBackupScreenUITests.swift */, DE846DDA83BFD7EC5C03760B /* ServerConfirmationScreenUITests.swift */, 054F469E433864CC6FE6EE8E /* ServerSelectionUITests.swift */, 6D4777F0142E330A75C46FE4 /* SessionVerificationUITests.swift */, @@ -3482,6 +3589,14 @@ path = TimelineItems; sourceTree = ""; }; + 96111C0A11B801B19C42DCBE /* View */ = { + isa = PBXGroup; + children = ( + AD558A898847C179E4B7A237 /* SecureBackupKeyBackupScreen.swift */, + ); + path = View; + sourceTree = ""; + }; 9613851C68D8C01EABFB3569 /* AppLock */ = { isa = PBXGroup; children = ( @@ -3717,6 +3832,18 @@ path = Other; sourceTree = ""; }; + B1FD4FD6CEB987AE274AEEE5 /* SecureBackupKeyBackupScreen */ = { + isa = PBXGroup; + children = ( + 4B2D4EEBE8C098BBADD10939 /* SecureBackupKeyBackupScreenCoordinator.swift */, + AD72A9B720D75DBE60AC299F /* SecureBackupKeyBackupScreenModels.swift */, + 82B612853BFB68373249777B /* SecureBackupKeyBackupScreenViewModel.swift */, + 4525E8C0FBDD27D1ACE90952 /* SecureBackupKeyBackupScreenViewModelProtocol.swift */, + 96111C0A11B801B19C42DCBE /* View */, + ); + path = SecureBackupKeyBackupScreen; + sourceTree = ""; + }; B23135B06B044CB811139D2F /* Generated */ = { isa = PBXGroup; children = ( @@ -3828,6 +3955,15 @@ path = ServerConfirmationScreen; sourceTree = ""; }; + BDCEF7C3BF6D09F5611CFC8B /* SecureBackup */ = { + isa = PBXGroup; + children = ( + 5A1119E9C63AE530252640D2 /* SecureBackupController.swift */, + 1C21A715237F2B6D6E80998C /* SecureBackupControllerProtocol.swift */, + ); + path = SecureBackup; + sourceTree = ""; + }; BE7641A284D3E81DC96943E3 /* View */ = { isa = PBXGroup; children = ( @@ -4125,6 +4261,7 @@ D4DB8163C10389C069458252 /* RoomMemberListScreen */, 0210F4932B59277E2EEEF7BC /* RoomNotificationSettingsScreen */, 679E9837ECA8D6776079D16E /* RoomScreen */, + 2565414373E6F68005966B8E /* SecureBackup */, 3153FCA3F4B0E88B16D99D12 /* SessionVerificationScreen */, 70B74A432C241E56A7ACE610 /* Settings */, EC4545C7E37E8294D3FE6800 /* StartChatScreen */, @@ -4193,7 +4330,6 @@ isa = PBXGroup; children = ( D6CA5F386C7701C129398945 /* AuthenticationCoordinator.swift */, - 97755C01C3971474EFAD5367 /* AuthenticationIconImage.swift */, CCACD75595C40EACD6AD4A74 /* AuthenticationTextFieldStyle.swift */, 92390F9FA98255440A6BF5F8 /* OIDCAuthenticationPresenter.swift */, 9E6D88E8AFFBF2C1D589C0FA /* UIConstants.swift */, @@ -4871,6 +5007,9 @@ E49F74BD93230BDEFFE5EA51 /* RoomNotificationSettingsScreenViewModelTests.swift in Sources */, 46562110EE202E580A5FFD9C /* RoomScreenViewModelTests.swift in Sources */, CC0D088F505F33A20DC5590F /* RoomStateEventStringBuilderTests.swift in Sources */, + 7691233E3572A9173FD96CB3 /* SecureBackupKeyBackupScreenViewModelTests.swift in Sources */, + 06B31F84CE52A7A7C271267C /* SecureBackupRecoveryKeyScreenViewModelTests.swift in Sources */, + 1B8E30B35BF8F541C1318F19 /* SecureBackupScreenViewModelTests.swift in Sources */, 53A55748D5F587C9061F98BF /* ServerConfigurationScreenViewStateTests.swift in Sources */, 89658A44C9FC19B58FD1C226 /* ServerConfirmationScreenViewModelTests.swift in Sources */, 93875ADD456142D20823ED24 /* ServerSelectionViewModelTests.swift in Sources */, @@ -4981,7 +5120,6 @@ 88F348E2CB14FF71CBBB665D /* AudioRoomTimelineItemContent.swift in Sources */, E62EC30B39354A391E32A126 /* AudioRoomTimelineView.swift in Sources */, EA65360A0EC026DD83AC0CF5 /* AuthenticationCoordinator.swift in Sources */, - B037C365CF8A58A0D149A2DB /* AuthenticationIconImage.swift in Sources */, 7F08F4BC1312075E2B5EAEFA /* AuthenticationServiceProxy.swift in Sources */, 64FF5CB4E35971255872E1BB /* AuthenticationServiceProxyProtocol.swift in Sources */, 6146996D5C4DDD5DA816FC87 /* AuthenticationTextFieldStyle.swift in Sources */, @@ -4989,6 +5127,7 @@ D876EC0FED3B6D46C806912A /* AvatarSize.swift in Sources */, E0A4DCA633D174EB43AD599F /* BackgroundTaskProtocol.swift in Sources */, 6D046D653DA28ADF1E6E59A4 /* BackgroundTaskServiceProtocol.swift in Sources */, + A4B0BAD62A12ED76BD611B79 /* BadgeView.swift in Sources */, 38546A6010A2CF240EC9AF73 /* BindableState.swift in Sources */, 5EE1D4E316D66943E97FDCF2 /* BloomView.swift in Sources */, B6DF6B6FA8734B70F9BF261E /* BlurHashDecode.swift in Sources */, @@ -5099,6 +5238,7 @@ F18CA61A58C77C84F551B8E7 /* GeneratedMocks.swift in Sources */, B53D292A5CA61E371C4CD785 /* GenericCallLinkCoordinator.swift in Sources */, 4295E5F850897710A51AE114 /* GeoURI.swift in Sources */, + D4D5595C4A2A702CFF4E94FF /* HeroImage.swift in Sources */, 964B9D2EC38C488C360CE0C9 /* HomeScreen.swift in Sources */, 8CC12086CBF91A7E10CDC205 /* HomeScreenCoordinator.swift in Sources */, 77BB228AEA861E50FFD6A228 /* HomeScreenEmptyStateView.swift in Sources */, @@ -5366,6 +5506,23 @@ 0BFA67AFD757EE2BA569836A /* ScrollViewAdapter.swift in Sources */, 67160204A8D362BB7D4AD259 /* Search.swift in Sources */, 339BC18777912E1989F2F17D /* Section.swift in Sources */, + F833D5B5BE6707F961FA88DB /* SecureBackupController.swift in Sources */, + 0C88044649BAEE6C49BFC43A /* SecureBackupControllerProtocol.swift in Sources */, + 21F29351EDD7B2A5534EE862 /* SecureBackupKeyBackupScreen.swift in Sources */, + 5BC6C4ADFE7F2A795ECDE130 /* SecureBackupKeyBackupScreenCoordinator.swift in Sources */, + B7888FC1E1DEF816D175C8D6 /* SecureBackupKeyBackupScreenModels.swift in Sources */, + 1795EA6A6C4942CAE0459DF0 /* SecureBackupKeyBackupScreenViewModel.swift in Sources */, + A4C29D373986AFE4559696D5 /* SecureBackupKeyBackupScreenViewModelProtocol.swift in Sources */, + FA71CD334F2D2289BEF0D749 /* SecureBackupRecoveryKeyScreen.swift in Sources */, + B1387648C6F71F1B98244803 /* SecureBackupRecoveryKeyScreenCoordinator.swift in Sources */, + 8AA84EF202F2EFC8453A97BD /* SecureBackupRecoveryKeyScreenModels.swift in Sources */, + 27F015B0D5436633B5B3C8C3 /* SecureBackupRecoveryKeyScreenViewModel.swift in Sources */, + F0570F1ECD70C4C851FB2052 /* SecureBackupRecoveryKeyScreenViewModelProtocol.swift in Sources */, + E84ADFE9696936C18C2424B5 /* SecureBackupScreen.swift in Sources */, + E77FE06B165A38BF1735509F /* SecureBackupScreenCoordinator.swift in Sources */, + DA7E867F5EAFF8E20B2EE3B6 /* SecureBackupScreenModels.swift in Sources */, + 7BF368A78E6D9AFD222F25AF /* SecureBackupScreenViewModel.swift in Sources */, + AC90434798E7894370E80E66 /* SecureBackupScreenViewModelProtocol.swift in Sources */, 14E99D27628B1A6F0CB46FEA /* SeparatorRoomTimelineItem.swift in Sources */, 49F2E7DD8CAACE09CEECE3E6 /* SeparatorRoomTimelineView.swift in Sources */, 2E8C6672D0EE7D5B1BEDB8E2 /* ServerConfirmationScreen.swift in Sources */, @@ -5568,6 +5725,9 @@ 44121202B4A260C98BF615A7 /* RoomMembersListScreenUITests.swift in Sources */, 06AA515C7053FD7E17A5CF81 /* RoomNotificationSettingsScreenUITests.swift in Sources */, 2F1CF90A3460C153154427F0 /* RoomScreenUITests.swift in Sources */, + A743841F91B62B0E56217B04 /* SecureBackupKeyBackupScreenUITests.swift in Sources */, + FC4F6BA083A64840B38CE269 /* SecureBackupRecoveryKeyScreenUITests.swift in Sources */, + 8C42B5B1642D189C362A5EDF /* SecureBackupScreenUITests.swift in Sources */, A1D4033881320C9EB88196E6 /* ServerConfirmationScreenUITests.swift in Sources */, 77FACC29F98FE2E65BBB6A5F /* ServerSelectionUITests.swift in Sources */, 05EC896A4B9AF4A56670C0BB /* SessionVerificationUITests.swift in Sources */, diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index b8e00efd4..cbd98051a 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -262,7 +262,7 @@ { "identity" : "swiftui-introspect", "kind" : "remoteSourceControl", - "location" : "https://github.com/siteline/SwiftUI-Introspect", + "location" : "https://github.com/siteline/SwiftUI-Introspect.git", "state" : { "revision" : "b94da693e57eaf79d16464b8b7c90d09cba4e290", "version" : "0.9.2" diff --git a/ElementX/Resources/Assets.xcassets/images/secure-backup/Contents.json b/ElementX/Resources/Assets.xcassets/images/secure-backup/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/ElementX/Resources/Assets.xcassets/images/secure-backup/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ElementX/Resources/Assets.xcassets/images/secure-backup/secure-backup-icon.imageset/Contents.json b/ElementX/Resources/Assets.xcassets/images/secure-backup/secure-backup-icon.imageset/Contents.json new file mode 100644 index 000000000..74023a195 --- /dev/null +++ b/ElementX/Resources/Assets.xcassets/images/secure-backup/secure-backup-icon.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "secure-backup-icon.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/ElementX/Resources/Assets.xcassets/images/secure-backup/secure-backup-icon.imageset/secure-backup-icon.svg b/ElementX/Resources/Assets.xcassets/images/secure-backup/secure-backup-icon.imageset/secure-backup-icon.svg new file mode 100644 index 000000000..88dc86847 --- /dev/null +++ b/ElementX/Resources/Assets.xcassets/images/secure-backup/secure-backup-icon.imageset/secure-backup-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/ElementX/Resources/Assets.xcassets/images/secure-backup/secure-backup-off.imageset/Contents.json b/ElementX/Resources/Assets.xcassets/images/secure-backup/secure-backup-off.imageset/Contents.json new file mode 100644 index 000000000..4d670b9a6 --- /dev/null +++ b/ElementX/Resources/Assets.xcassets/images/secure-backup/secure-backup-off.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "secure-backup-off.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/ElementX/Resources/Assets.xcassets/images/secure-backup/secure-backup-off.imageset/secure-backup-off.svg b/ElementX/Resources/Assets.xcassets/images/secure-backup/secure-backup-off.imageset/secure-backup-off.svg new file mode 100644 index 000000000..e6e398d3f --- /dev/null +++ b/ElementX/Resources/Assets.xcassets/images/secure-backup/secure-backup-off.imageset/secure-backup-off.svg @@ -0,0 +1,3 @@ + + + diff --git a/ElementX/Resources/Assets.xcassets/images/secure-backup/secure-backup-on.imageset/Contents.json b/ElementX/Resources/Assets.xcassets/images/secure-backup/secure-backup-on.imageset/Contents.json new file mode 100644 index 000000000..975c4f5da --- /dev/null +++ b/ElementX/Resources/Assets.xcassets/images/secure-backup/secure-backup-on.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "secure-backup-on.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/ElementX/Resources/Assets.xcassets/images/secure-backup/secure-backup-on.imageset/secure-backup-on.svg b/ElementX/Resources/Assets.xcassets/images/secure-backup/secure-backup-on.imageset/secure-backup-on.svg new file mode 100644 index 000000000..51c360122 --- /dev/null +++ b/ElementX/Resources/Assets.xcassets/images/secure-backup/secure-backup-on.imageset/secure-backup-on.svg @@ -0,0 +1,3 @@ + + + diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index e8052b1e4..158770485 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -68,6 +68,8 @@ "action_share" = "Share"; "action_share_link" = "Share link"; "action_sign_in_again" = "Sign in again"; +"action_signout" = "Sign out"; +"action_signout_anyway" = "Sign out anyway"; "action_skip" = "Skip"; "action_start" = "Start"; "action_start_chat" = "Start chat"; @@ -83,6 +85,7 @@ "common_analytics" = "Analytics"; "common_audio" = "Audio"; "common_bubbles" = "Bubbles"; +"common_chat_backup" = "Chat backup"; "common_copyright" = "Copyright"; "common_creating_room" = "Creating room…"; "common_current_user_left_room" = "Left room"; @@ -119,6 +122,7 @@ "common_privacy_policy" = "Privacy policy"; "common_reaction" = "Reaction"; "common_reactions" = "Reactions"; +"common_recovery_key" = "Recovery key"; "common_refreshing" = "Refreshing…"; "common_replying_to" = "Replying to %1$@"; "common_report_a_bug" = "Report a bug"; @@ -128,7 +132,6 @@ "common_room_name_placeholder" = "e.g. your project name"; "common_search_for_someone" = "Search for someone"; "common_search_results" = "Search results"; -"common_secure_backup" = "Secure backup"; "common_security" = "Security"; "common_sending" = "Sending…"; "common_server_not_supported" = "Server not supported"; @@ -234,6 +237,9 @@ "room_timeline_beginning_of_room" = "This is the beginning of %1$@."; "room_timeline_beginning_of_room_no_name" = "This is the beginning of this conversation."; "room_timeline_read_marker_title" = "New"; +"screen_advanced_settings_element_call_base_url" = "Custom Element Call base URL"; +"screen_advanced_settings_element_call_base_url_description" = "Set a custom base URL for Element Call."; +"screen_advanced_settings_element_call_base_url_validation_error" = "Invalid URL, please make sure you include the protocol (http/https) and the correct address."; "screen_account_provider_change" = "Change account provider"; "screen_account_provider_form_hint" = "Homeserver address"; "screen_account_provider_form_notice" = "Enter a search term or a domain address."; @@ -275,6 +281,15 @@ "screen_change_server_form_notice" = "You can only connect to an existing server that supports sliding sync. Your homeserver admin will need to configure it. %1$@"; "screen_change_server_subtitle" = "What is the address of your server?"; "screen_change_server_title" = "Select your server"; +"screen_chat_backup_key_backup_action_disable" = "Turn off backup"; +"screen_chat_backup_key_backup_action_enable" = "Turn on backup"; +"screen_chat_backup_key_backup_description" = "Backup ensures that you don't lose your message history. %1$@."; +"screen_chat_backup_key_backup_title" = "Backup"; +"screen_chat_backup_recovery_action_change" = "Change recovery key"; +"screen_chat_backup_recovery_action_confirm" = "Confirm recovery key"; +"screen_chat_backup_recovery_action_confirm_description" = "Your chat backup is currently out of sync."; +"screen_chat_backup_recovery_action_setup" = "Set up recovery"; +"screen_chat_backup_recovery_action_setup_description" = "Get access to your encrypted messages if you lose all your devices or are signed out of %1$@ everywhere."; "screen_create_poll_add_option_btn" = "Add option"; "screen_create_poll_anonymous_desc" = "Show results only after poll ends"; "screen_create_poll_anonymous_headline" = "Hide votes"; @@ -306,6 +321,13 @@ "screen_invites_decline_direct_chat_title" = "Decline chat"; "screen_invites_empty_list" = "No Invites"; "screen_invites_invited_you" = "%1$@ (%2$@) invited you"; +"screen_key_backup_disable_confirmation_action_turn_off" = "Turn off"; +"screen_key_backup_disable_confirmation_description" = "You will lose your encrypted messages if you are signed out of all devices."; +"screen_key_backup_disable_confirmation_title" = "Are you sure you want to turn off backup?"; +"screen_key_backup_disable_description" = "Turning off backup will remove your current encryption key backup and turn off other security features. In this case, you will:"; +"screen_key_backup_disable_description_point_1" = "Not have encrypted message history on new devices"; +"screen_key_backup_disable_description_point_2" = "Lose access to your encrypted messages if you are signed out of %1$@ everywhere"; +"screen_key_backup_disable_title" = "Are you sure you want to turn off backup?"; "screen_login_error_deactivated_account" = "This account has been deactivated."; "screen_login_error_invalid_credentials" = "Incorrect username and/or password"; "screen_login_error_invalid_user_id" = "This is not a valid user identifier. Expected format: ‘@user:homeserver.org’"; @@ -349,6 +371,27 @@ "screen_onboarding_welcome_message" = "Welcome to the fastest Element ever. Supercharged for speed and simplicity."; "screen_onboarding_welcome_subtitle" = "Welcome to %1$@. Supercharged, for speed and simplicity."; "screen_onboarding_welcome_title" = "Be in your element"; +"screen_recovery_key_change_description" = "Get a new recovery key if you've lost your existing one. After changing your recovery key, your old one will no longer work."; +"screen_recovery_key_change_generate_key" = "Generate a new recovery key"; +"screen_recovery_key_change_generate_key_description" = "Make sure you can store your recovery key somewhere safe"; +"screen_recovery_key_change_success" = "Recovery key changed"; +"screen_recovery_key_change_title" = "Change recovery key?"; +"screen_recovery_key_confirm_description" = "Enter your recovery key to confirm access to your chat backup."; +"screen_recovery_key_confirm_key_description" = "Enter the 48 character code."; +"screen_recovery_key_confirm_key_placeholder" = "Enter..."; +"screen_recovery_key_confirm_success" = "Recovery key confirmed"; +"screen_recovery_key_confirm_title" = "Confirm your recovery key"; +"screen_recovery_key_save_action" = "Save recovery key"; +"screen_recovery_key_save_description" = "Write down your recovery key somewhere safe or save it in a password manager."; +"screen_recovery_key_save_key_description" = "Tap to copy recovery key"; +"screen_recovery_key_save_title" = "Save your recovery key"; +"screen_recovery_key_setup_confirmation_description" = "You will not be able to access your new recovery key after this step."; +"screen_recovery_key_setup_confirmation_title" = "Have you saved your recovery key?"; +"screen_recovery_key_setup_description" = "Your chat backup is protected by a recovery key. If you need a new recovery key after setup you can recreate by selecting ‘Change recovery key’."; +"screen_recovery_key_setup_generate_key" = "Generate your recovery key"; +"screen_recovery_key_setup_generate_key_description" = "Make sure you can store your recovery key somewhere safe"; +"screen_recovery_key_setup_success" = "Recovery setup successful"; +"screen_recovery_key_setup_title" = "Set up recovery"; "screen_report_content_block_user_hint" = "Check if you want to hide all current and future messages from this user"; "screen_room_attachment_source_camera" = "Camera"; "screen_room_attachment_source_camera_photo" = "Take photo"; @@ -445,9 +488,13 @@ "screen_signed_out_reason_3" = "Your server’s administrator has invalidated your access"; "screen_signed_out_subtitle" = "You might have been signed out for one of the reasons listed below. Please sign in again to continue using %@."; "screen_signed_out_title" = "You’re signed out"; +"screen_signout_backing_up_subtitle" = "Please wait for this to complete before signing out."; +"screen_signout_backing_up_title" = "Your keys are still being backed up"; "screen_signout_confirmation_dialog_content" = "Are you sure you want to sign out?"; "screen_signout_confirmation_dialog_title" = "Sign out"; "screen_signout_in_progress_dialog_content" = "Signing out…"; +"screen_signout_last_session_subtitle" = "You are about to sign out of your last session. If you sign out now, you might lose access to your encrypted messages."; +"screen_signout_last_session_title" = "Have you saved your recovery key?"; "screen_start_chat_error_starting_chat" = "An error occurred when trying to start a chat"; "screen_view_location_title" = "Location"; "screen_waitlist_message" = "There's a high demand for %1$@ on %2$@ at the moment. Come back to the app in a few days and try again.\n\nThanks for your patience!"; diff --git a/ElementX/Sources/Application/AppSettings.swift b/ElementX/Sources/Application/AppSettings.swift index c94c63943..0fb15fb2b 100644 --- a/ElementX/Sources/Application/AppSettings.swift +++ b/ElementX/Sources/Application/AppSettings.swift @@ -44,6 +44,7 @@ final class AppSettings { case mentionsEnabled case appLockFlowEnabled case elementCallEnabled + case chatBackupEnabled } private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier @@ -116,6 +117,8 @@ final class AppSettings { let privacyURL: URL = "https://element.io/privacy" /// An email address that should be used for support requests. let supportEmailAddress = "support@element.io" + // A URL where users can go read more about the chat backup. + let chatBackupDetailsURL: URL = "https://element.io/help#encryption" // MARK: - Security @@ -276,4 +279,7 @@ final class AppSettings { @UserPreference(key: UserDefaultsKeys.elementCallEnabled, defaultValue: false, storageType: .userDefaults(store)) var elementCallEnabled + + @UserPreference(key: UserDefaultsKeys.chatBackupEnabled, defaultValue: false, storageType: .userDefaults(store)) + var chatBackupEnabled } diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift index a71ef5000..aaf4dbb18 100644 --- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift @@ -334,6 +334,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { appLockService: appLockService, bugReportService: bugReportService, notificationSettings: userSession.clientProxy.notificationSettings, + secureBackupController: userSession.clientProxy.secureBackupController, appSettings: appSettings) let settingsScreenCoordinator = SettingsScreenCoordinator(parameters: parameters) diff --git a/ElementX/Sources/Generated/Assets.swift b/ElementX/Sources/Generated/Assets.swift index 30e66ccb0..5e264c8fb 100644 --- a/ElementX/Sources/Generated/Assets.swift +++ b/ElementX/Sources/Generated/Assets.swift @@ -69,6 +69,9 @@ internal enum Asset { internal static let mediaPause = ImageAsset(name: "images/media-pause") internal static let mediaPlay = ImageAsset(name: "images/media-play") internal static let microphone = ImageAsset(name: "images/microphone") + internal static let secureBackupIcon = ImageAsset(name: "images/secure-backup-icon") + internal static let secureBackupOff = ImageAsset(name: "images/secure-backup-off") + internal static let secureBackupOn = ImageAsset(name: "images/secure-backup-on") internal static let addReaction = ImageAsset(name: "images/add-reaction") internal static let copy = ImageAsset(name: "images/copy") internal static let editOutline = ImageAsset(name: "images/edit-outline") diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index b1d1f33ce..9f9631653 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -152,6 +152,10 @@ public enum L10n { public static var actionShareLink: String { return L10n.tr("Localizable", "action_share_link") } /// Sign in again public static var actionSignInAgain: String { return L10n.tr("Localizable", "action_sign_in_again") } + /// Sign out + public static var actionSignout: String { return L10n.tr("Localizable", "action_signout") } + /// Sign out anyway + public static var actionSignoutAnyway: String { return L10n.tr("Localizable", "action_signout_anyway") } /// Skip public static var actionSkip: String { return L10n.tr("Localizable", "action_skip") } /// Start @@ -180,6 +184,8 @@ public enum L10n { public static var commonAudio: String { return L10n.tr("Localizable", "common_audio") } /// Bubbles public static var commonBubbles: String { return L10n.tr("Localizable", "common_bubbles") } + /// Chat backup + public static var commonChatBackup: String { return L10n.tr("Localizable", "common_chat_backup") } /// Copyright public static var commonCopyright: String { return L10n.tr("Localizable", "common_copyright") } /// Creating room… @@ -272,6 +278,8 @@ public enum L10n { public static var commonReaction: String { return L10n.tr("Localizable", "common_reaction") } /// Reactions public static var commonReactions: String { return L10n.tr("Localizable", "common_reactions") } + /// Recovery key + public static var commonRecoveryKey: String { return L10n.tr("Localizable", "common_recovery_key") } /// Refreshing… public static var commonRefreshing: String { return L10n.tr("Localizable", "common_refreshing") } /// Replying to %1$@ @@ -292,8 +300,6 @@ public enum L10n { public static var commonSearchForSomeone: String { return L10n.tr("Localizable", "common_search_for_someone") } /// Search results public static var commonSearchResults: String { return L10n.tr("Localizable", "common_search_results") } - /// Secure backup - public static var commonSecureBackup: String { return L10n.tr("Localizable", "common_secure_backup") } /// Security public static var commonSecurity: String { return L10n.tr("Localizable", "common_security") } /// Sending… @@ -592,6 +598,12 @@ public enum L10n { public static var screenAdvancedSettingsDeveloperMode: String { return L10n.tr("Localizable", "screen_advanced_settings_developer_mode") } /// Enable to have access to features and functionality for developers. public static var screenAdvancedSettingsDeveloperModeDescription: String { return L10n.tr("Localizable", "screen_advanced_settings_developer_mode_description") } + /// Custom Element Call base URL + public static var screenAdvancedSettingsElementCallBaseUrl: String { return L10n.tr("Localizable", "screen_advanced_settings_element_call_base_url") } + /// Set a custom base URL for Element Call. + public static var screenAdvancedSettingsElementCallBaseUrlDescription: String { return L10n.tr("Localizable", "screen_advanced_settings_element_call_base_url_description") } + /// Invalid URL, please make sure you include the protocol (http/https) and the correct address. + public static var screenAdvancedSettingsElementCallBaseUrlValidationError: String { return L10n.tr("Localizable", "screen_advanced_settings_element_call_base_url_validation_error") } /// Disable the rich text editor to type Markdown manually. public static var screenAdvancedSettingsRichTextEditorDescription: String { return L10n.tr("Localizable", "screen_advanced_settings_rich_text_editor_description") } /// We won't record or profile any personal data @@ -670,6 +682,28 @@ public enum L10n { public static var screenChangeServerSubtitle: String { return L10n.tr("Localizable", "screen_change_server_subtitle") } /// Select your server public static var screenChangeServerTitle: String { return L10n.tr("Localizable", "screen_change_server_title") } + /// Turn off backup + public static var screenChatBackupKeyBackupActionDisable: String { return L10n.tr("Localizable", "screen_chat_backup_key_backup_action_disable") } + /// Turn on backup + public static var screenChatBackupKeyBackupActionEnable: String { return L10n.tr("Localizable", "screen_chat_backup_key_backup_action_enable") } + /// Backup ensures that you don't lose your message history. %1$@. + public static func screenChatBackupKeyBackupDescription(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_chat_backup_key_backup_description", String(describing: p1)) + } + /// Backup + public static var screenChatBackupKeyBackupTitle: String { return L10n.tr("Localizable", "screen_chat_backup_key_backup_title") } + /// Change recovery key + public static var screenChatBackupRecoveryActionChange: String { return L10n.tr("Localizable", "screen_chat_backup_recovery_action_change") } + /// Confirm recovery key + public static var screenChatBackupRecoveryActionConfirm: String { return L10n.tr("Localizable", "screen_chat_backup_recovery_action_confirm") } + /// Your chat backup is currently out of sync. + public static var screenChatBackupRecoveryActionConfirmDescription: String { return L10n.tr("Localizable", "screen_chat_backup_recovery_action_confirm_description") } + /// Set up recovery + public static var screenChatBackupRecoveryActionSetup: String { return L10n.tr("Localizable", "screen_chat_backup_recovery_action_setup") } + /// Get access to your encrypted messages if you lose all your devices or are signed out of %1$@ everywhere. + public static func screenChatBackupRecoveryActionSetupDescription(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_chat_backup_recovery_action_setup_description", String(describing: p1)) + } /// Add option public static var screenCreatePollAddOptionBtn: String { return L10n.tr("Localizable", "screen_create_poll_add_option_btn") } /// Show results only after poll ends @@ -754,6 +788,22 @@ public enum L10n { public static func screenInvitesInvitedYou(_ p1: Any, _ p2: Any) -> String { return L10n.tr("Localizable", "screen_invites_invited_you", String(describing: p1), String(describing: p2)) } + /// Turn off + public static var screenKeyBackupDisableConfirmationActionTurnOff: String { return L10n.tr("Localizable", "screen_key_backup_disable_confirmation_action_turn_off") } + /// You will lose your encrypted messages if you are signed out of all devices. + public static var screenKeyBackupDisableConfirmationDescription: String { return L10n.tr("Localizable", "screen_key_backup_disable_confirmation_description") } + /// Are you sure you want to turn off backup? + public static var screenKeyBackupDisableConfirmationTitle: String { return L10n.tr("Localizable", "screen_key_backup_disable_confirmation_title") } + /// Turning off backup will remove your current encryption key backup and turn off other security features. In this case, you will: + public static var screenKeyBackupDisableDescription: String { return L10n.tr("Localizable", "screen_key_backup_disable_description") } + /// Not have encrypted message history on new devices + public static var screenKeyBackupDisableDescriptionPoint1: String { return L10n.tr("Localizable", "screen_key_backup_disable_description_point_1") } + /// Lose access to your encrypted messages if you are signed out of %1$@ everywhere + public static func screenKeyBackupDisableDescriptionPoint2(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_key_backup_disable_description_point_2", String(describing: p1)) + } + /// Are you sure you want to turn off backup? + public static var screenKeyBackupDisableTitle: String { return L10n.tr("Localizable", "screen_key_backup_disable_title") } /// This account has been deactivated. public static var screenLoginErrorDeactivatedAccount: String { return L10n.tr("Localizable", "screen_login_error_deactivated_account") } /// Incorrect username and/or password @@ -850,6 +900,48 @@ public enum L10n { } /// Be in your element public static var screenOnboardingWelcomeTitle: String { return L10n.tr("Localizable", "screen_onboarding_welcome_title") } + /// Get a new recovery key if you've lost your existing one. After changing your recovery key, your old one will no longer work. + public static var screenRecoveryKeyChangeDescription: String { return L10n.tr("Localizable", "screen_recovery_key_change_description") } + /// Generate a new recovery key + public static var screenRecoveryKeyChangeGenerateKey: String { return L10n.tr("Localizable", "screen_recovery_key_change_generate_key") } + /// Make sure you can store your recovery key somewhere safe + public static var screenRecoveryKeyChangeGenerateKeyDescription: String { return L10n.tr("Localizable", "screen_recovery_key_change_generate_key_description") } + /// Recovery key changed + public static var screenRecoveryKeyChangeSuccess: String { return L10n.tr("Localizable", "screen_recovery_key_change_success") } + /// Change recovery key? + public static var screenRecoveryKeyChangeTitle: String { return L10n.tr("Localizable", "screen_recovery_key_change_title") } + /// Enter your recovery key to confirm access to your chat backup. + public static var screenRecoveryKeyConfirmDescription: String { return L10n.tr("Localizable", "screen_recovery_key_confirm_description") } + /// Enter the 48 character code. + public static var screenRecoveryKeyConfirmKeyDescription: String { return L10n.tr("Localizable", "screen_recovery_key_confirm_key_description") } + /// Enter... + public static var screenRecoveryKeyConfirmKeyPlaceholder: String { return L10n.tr("Localizable", "screen_recovery_key_confirm_key_placeholder") } + /// Recovery key confirmed + public static var screenRecoveryKeyConfirmSuccess: String { return L10n.tr("Localizable", "screen_recovery_key_confirm_success") } + /// Confirm your recovery key + public static var screenRecoveryKeyConfirmTitle: String { return L10n.tr("Localizable", "screen_recovery_key_confirm_title") } + /// Save recovery key + public static var screenRecoveryKeySaveAction: String { return L10n.tr("Localizable", "screen_recovery_key_save_action") } + /// Write down your recovery key somewhere safe or save it in a password manager. + public static var screenRecoveryKeySaveDescription: String { return L10n.tr("Localizable", "screen_recovery_key_save_description") } + /// Tap to copy recovery key + public static var screenRecoveryKeySaveKeyDescription: String { return L10n.tr("Localizable", "screen_recovery_key_save_key_description") } + /// Save your recovery key + public static var screenRecoveryKeySaveTitle: String { return L10n.tr("Localizable", "screen_recovery_key_save_title") } + /// You will not be able to access your new recovery key after this step. + public static var screenRecoveryKeySetupConfirmationDescription: String { return L10n.tr("Localizable", "screen_recovery_key_setup_confirmation_description") } + /// Have you saved your recovery key? + public static var screenRecoveryKeySetupConfirmationTitle: String { return L10n.tr("Localizable", "screen_recovery_key_setup_confirmation_title") } + /// Your chat backup is protected by a recovery key. If you need a new recovery key after setup you can recreate by selecting ‘Change recovery key’. + public static var screenRecoveryKeySetupDescription: String { return L10n.tr("Localizable", "screen_recovery_key_setup_description") } + /// Generate your recovery key + public static var screenRecoveryKeySetupGenerateKey: String { return L10n.tr("Localizable", "screen_recovery_key_setup_generate_key") } + /// Make sure you can store your recovery key somewhere safe + public static var screenRecoveryKeySetupGenerateKeyDescription: String { return L10n.tr("Localizable", "screen_recovery_key_setup_generate_key_description") } + /// Recovery setup successful + public static var screenRecoveryKeySetupSuccess: String { return L10n.tr("Localizable", "screen_recovery_key_setup_success") } + /// Set up recovery + public static var screenRecoveryKeySetupTitle: String { return L10n.tr("Localizable", "screen_recovery_key_setup_title") } /// Block user public static var screenReportContentBlockUser: String { return L10n.tr("Localizable", "screen_report_content_block_user") } /// Check if you want to hide all current and future messages from this user @@ -1064,6 +1156,10 @@ public enum L10n { } /// You’re signed out public static var screenSignedOutTitle: String { return L10n.tr("Localizable", "screen_signed_out_title") } + /// Please wait for this to complete before signing out. + public static var screenSignoutBackingUpSubtitle: String { return L10n.tr("Localizable", "screen_signout_backing_up_subtitle") } + /// Your keys are still being backed up + public static var screenSignoutBackingUpTitle: String { return L10n.tr("Localizable", "screen_signout_backing_up_title") } /// Are you sure you want to sign out? public static var screenSignoutConfirmationDialogContent: String { return L10n.tr("Localizable", "screen_signout_confirmation_dialog_content") } /// Sign out @@ -1072,6 +1168,10 @@ public enum L10n { public static var screenSignoutConfirmationDialogTitle: String { return L10n.tr("Localizable", "screen_signout_confirmation_dialog_title") } /// Signing out… public static var screenSignoutInProgressDialogContent: String { return L10n.tr("Localizable", "screen_signout_in_progress_dialog_content") } + /// You are about to sign out of your last session. If you sign out now, you might lose access to your encrypted messages. + public static var screenSignoutLastSessionSubtitle: String { return L10n.tr("Localizable", "screen_signout_last_session_subtitle") } + /// Have you saved your recovery key? + public static var screenSignoutLastSessionTitle: String { return L10n.tr("Localizable", "screen_signout_last_session_title") } /// Sign out public static var screenSignoutPreferenceItem: String { return L10n.tr("Localizable", "screen_signout_preference_item") } /// An error occurred when trying to start a chat diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index e5e810c71..8b93ef1b9 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -2241,6 +2241,91 @@ class RoomTimelineProviderMock: RoomTimelineProviderProtocol { var underlyingBackPaginationState: BackPaginationStatus! } +class SecureBackupControllerMock: SecureBackupControllerProtocol { + var recoveryKeyState: CurrentValuePublisher { + get { return underlyingRecoveryKeyState } + set(value) { underlyingRecoveryKeyState = value } + } + var underlyingRecoveryKeyState: CurrentValuePublisher! + var keyBackupState: CurrentValuePublisher { + get { return underlyingKeyBackupState } + set(value) { underlyingKeyBackupState = value } + } + var underlyingKeyBackupState: CurrentValuePublisher! + + //MARK: - enableBackup + + var enableBackupCallsCount = 0 + var enableBackupCalled: Bool { + return enableBackupCallsCount > 0 + } + var enableBackupReturnValue: Result! + var enableBackupClosure: (() async -> Result)? + + func enableBackup() async -> Result { + enableBackupCallsCount += 1 + if let enableBackupClosure = enableBackupClosure { + return await enableBackupClosure() + } else { + return enableBackupReturnValue + } + } + //MARK: - disableBackup + + var disableBackupCallsCount = 0 + var disableBackupCalled: Bool { + return disableBackupCallsCount > 0 + } + var disableBackupReturnValue: Result! + var disableBackupClosure: (() async -> Result)? + + func disableBackup() async -> Result { + disableBackupCallsCount += 1 + if let disableBackupClosure = disableBackupClosure { + return await disableBackupClosure() + } else { + return disableBackupReturnValue + } + } + //MARK: - generateRecoveryKey + + var generateRecoveryKeyCallsCount = 0 + var generateRecoveryKeyCalled: Bool { + return generateRecoveryKeyCallsCount > 0 + } + var generateRecoveryKeyReturnValue: Result! + var generateRecoveryKeyClosure: (() async -> Result)? + + func generateRecoveryKey() async -> Result { + generateRecoveryKeyCallsCount += 1 + if let generateRecoveryKeyClosure = generateRecoveryKeyClosure { + return await generateRecoveryKeyClosure() + } else { + return generateRecoveryKeyReturnValue + } + } + //MARK: - confirmRecoveryKey + + var confirmRecoveryKeyCallsCount = 0 + var confirmRecoveryKeyCalled: Bool { + return confirmRecoveryKeyCallsCount > 0 + } + var confirmRecoveryKeyReceivedKey: String? + var confirmRecoveryKeyReceivedInvocations: [String] = [] + var confirmRecoveryKeyReturnValue: Result! + var confirmRecoveryKeyClosure: ((String) async -> Result)? + + func confirmRecoveryKey(_ key: String) async -> Result { + confirmRecoveryKeyCallsCount += 1 + confirmRecoveryKeyReceivedKey = key + confirmRecoveryKeyReceivedInvocations.append(key) + if let confirmRecoveryKeyClosure = confirmRecoveryKeyClosure { + return await confirmRecoveryKeyClosure(key) + } else { + return confirmRecoveryKeyReturnValue + } + } +} class SessionVerificationControllerProxyMock: SessionVerificationControllerProxyProtocol { var callbacks: PassthroughSubject { get { return underlyingCallbacks } diff --git a/ElementX/Sources/Other/AccessibilityIdentifiers.swift b/ElementX/Sources/Other/AccessibilityIdentifiers.swift index 17ab9e43e..f16d57e4a 100644 --- a/ElementX/Sources/Other/AccessibilityIdentifiers.swift +++ b/ElementX/Sources/Other/AccessibilityIdentifiers.swift @@ -170,6 +170,7 @@ struct A11yIdentifiers { struct SettingsScreen { let done = "settings-done" let account = "settings-account" + let secureBackup = "settings-secure_backup" let notifications = "settings-notifications" let analytics = "settings-analytics" let reportBug = "settings-report_bug" diff --git a/ElementX/Sources/Other/SwiftUI/Views/BadgeView.swift b/ElementX/Sources/Other/SwiftUI/Views/BadgeView.swift new file mode 100644 index 000000000..4dc76184d --- /dev/null +++ b/ElementX/Sources/Other/SwiftUI/Views/BadgeView.swift @@ -0,0 +1,87 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Compound +import SwiftUI + +struct BadgeView: View { + let size: Double + + var body: some View { + Circle() + .fill(.compound.iconCriticalPrimary) + .frame(width: size, height: size) + } +} + +struct BadgeViewModifier: ViewModifier { + let size: Double + + func body(content: Content) -> some View { + content.mask { + Rectangle() + .fill(.white) + .overlay(alignment: .topTrailing) { + Circle() + .fill(.black) + .frame(width: maskSize, height: maskSize) + .offset(maskOffset) + } + .compositingGroup() + .luminanceToAlpha() + } + .overlay(alignment: .topTrailing) { + BadgeView(size: size) + } + } + + private var maskSize: Double { + size * 1.25 + } + + private var maskOffset: CGSize { + .init(width: (maskSize - size) / 2, height: -(maskSize - size) / 2) + } +} + +extension View { + @ViewBuilder + func overlayBadge(_ size: Double, isBadged: Bool = true) -> some View { + if isBadged { + modifier(BadgeViewModifier(size: size)) + } else { + self + } + } +} + +struct BadgeView_Previews: PreviewProvider { + static let circleGradient = LinearGradient(colors: [.green, .orange], + startPoint: .topLeading, + endPoint: .bottomTrailing) + static let screenGradient = LinearGradient(colors: [.pink, .blue], + startPoint: .top, + endPoint: .bottom) + static var previews: some View { + Circle() + .fill(circleGradient) + .saturation(2.0) + .frame(width: 100, height: 100) + .overlayBadge(40) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background { screenGradient.opacity(0.3).ignoresSafeArea() } + } +} diff --git a/ElementX/Sources/Screens/Authentication/AuthenticationIconImage.swift b/ElementX/Sources/Other/SwiftUI/Views/HeroImage.swift similarity index 78% rename from ElementX/Sources/Screens/Authentication/AuthenticationIconImage.swift rename to ElementX/Sources/Other/SwiftUI/Views/HeroImage.swift index 727333e4f..5f6155317 100644 --- a/ElementX/Sources/Screens/Authentication/AuthenticationIconImage.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/HeroImage.swift @@ -16,8 +16,8 @@ import SwiftUI -/// An image that is styled for use as the screen icon in the onboarding flow. -struct AuthenticationIconImage: View { +/// An image that is styled for use as the main/top/hero screen icon. +struct HeroImage: View { /// The icon that is shown. let image: Image /// The amount of padding between the icon and the borders. Defaults to 16. @@ -41,11 +41,11 @@ struct AuthenticationIconImage: View { // MARK: - Previews -struct AuthenticationIconImage_Previews: PreviewProvider, TestablePreview { +struct HeroImage_Previews: PreviewProvider, TestablePreview { static var previews: some View { HStack(spacing: 20) { - AuthenticationIconImage(image: Image(asset: Asset.Images.serverSelectionIcon), insets: 19) - AuthenticationIconImage(image: Image(systemName: "hourglass")) + HeroImage(image: Image(asset: Asset.Images.serverSelectionIcon), insets: 19) + HeroImage(image: Image(systemName: "hourglass")) } } } diff --git a/ElementX/Sources/Screens/AnalyticsPromptScreen/View/AnalyticsPromptScreen.swift b/ElementX/Sources/Screens/AnalyticsPromptScreen/View/AnalyticsPromptScreen.swift index 260e6f149..2ec08c419 100644 --- a/ElementX/Sources/Screens/AnalyticsPromptScreen/View/AnalyticsPromptScreen.swift +++ b/ElementX/Sources/Screens/AnalyticsPromptScreen/View/AnalyticsPromptScreen.swift @@ -42,7 +42,7 @@ struct AnalyticsPromptScreen: View { private var header: some View { VStack(spacing: 8) { - AuthenticationIconImage(image: Image(systemName: "chart.bar")) + HeroImage(image: Image(systemName: "chart.bar")) .symbolVariant(.fill) .padding(.bottom, 8) diff --git a/ElementX/Sources/Screens/AppLock/AppLockScreen/View/AppLockScreen.swift b/ElementX/Sources/Screens/AppLock/AppLockScreen/View/AppLockScreen.swift index e2e7de7fd..d96f723f0 100644 --- a/ElementX/Sources/Screens/AppLock/AppLockScreen/View/AppLockScreen.swift +++ b/ElementX/Sources/Screens/AppLock/AppLockScreen/View/AppLockScreen.swift @@ -25,7 +25,7 @@ struct AppLockScreen: View { var body: some View { FullscreenDialog { VStack(spacing: 8) { - AuthenticationIconImage(image: Image(systemSymbol: .lock)) + HeroImage(image: Image(systemSymbol: .lock)) .symbolVariant(.fill) .padding(.bottom, 8) diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginScreen.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginScreen.swift index 7e6993275..2fe9def5d 100644 --- a/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginScreen.swift +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginScreen.swift @@ -52,7 +52,7 @@ struct LoginScreen: View { /// The header containing the title and icon. var header: some View { VStack(spacing: 8) { - AuthenticationIconImage(image: Image(systemName: "lock.fill")) + HeroImage(image: Image(systemName: "lock.fill")) .padding(.bottom, 8) Text(L10n.screenLoginTitleWithHomeserver(context.viewState.homeserver.address)) diff --git a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/View/ServerConfirmationScreen.swift b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/View/ServerConfirmationScreen.swift index 9d89a8cfd..80c2c1d26 100644 --- a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/View/ServerConfirmationScreen.swift +++ b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/View/ServerConfirmationScreen.swift @@ -35,7 +35,7 @@ struct ServerConfirmationScreen: View { /// The main content of the view to be shown in a scroll view. var header: some View { VStack(spacing: 8) { - AuthenticationIconImage(image: Image(systemName: "person.crop.circle.fill")) + HeroImage(image: Image(systemName: "person.crop.circle.fill")) .padding(.bottom, 8) Text(context.viewState.title) diff --git a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/View/ServerSelectionScreen.swift b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/View/ServerSelectionScreen.swift index f80742978..fd351b8be 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/View/ServerSelectionScreen.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/View/ServerSelectionScreen.swift @@ -40,7 +40,7 @@ struct ServerSelectionScreen: View { /// The title, message and icon at the top of the screen. var header: some View { VStack(spacing: 8) { - AuthenticationIconImage(image: Image(asset: Asset.Images.serverSelectionIcon), insets: 19) + HeroImage(image: Image(asset: Asset.Images.serverSelectionIcon), insets: 19) .padding(.bottom, 8) Text(L10n.screenChangeServerTitle) diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift index 1ff2ea29a..b0015407c 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift @@ -71,6 +71,8 @@ struct HomeScreenViewState: BindableState { var userDisplayName: String? var userAvatarURL: URL? var showSessionVerificationBanner = false + var showUserMenuBadge = false + var showSettingsMenuOptionBadge = false var rooms: [HomeScreenRoom] = [] var roomListMode: HomeScreenRoomListMode = .skeletons diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift index 7cd9361d2..da2fcc5c1 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift @@ -53,7 +53,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol roomSummaryProvider = userSession.clientProxy.roomSummaryProvider inviteSummaryProvider = userSession.clientProxy.inviteSummaryProvider - super.init(initialViewState: HomeScreenViewState(userID: userSession.userID), + super.init(initialViewState: .init(userID: userSession.userID), imageProvider: userSession.mediaProvider) userSession.callbacks @@ -80,6 +80,19 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol .weakAssign(to: \.state.userDisplayName, on: self) .store(in: &cancellables) + userSession.clientProxy.secureBackupController.recoveryKeyState + .receive(on: DispatchQueue.main) + .map { state in + state == .unknown || state == .disabled + } + .sink { [weak self] requiresSecureBackupSetup in + guard let self else { return } + + state.showUserMenuBadge = requiresSecureBackupSetup && appSettings.chatBackupEnabled + state.showSettingsMenuOptionBadge = requiresSecureBackupSetup && appSettings.chatBackupEnabled + } + .store(in: &cancellables) + selectedRoomPublisher .weakAssign(to: \.state.selectedRoomID, on: self) .store(in: &cancellables) diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenUserMenuButton.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenUserMenuButton.swift index e07a444dc..0f0c083a1 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenUserMenuButton.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenUserMenuButton.swift @@ -18,6 +18,7 @@ import SwiftUI struct HomeScreenUserMenuButton: View { @State private var showingLogoutConfirmation = false + @Environment(\.colorScheme) var colorScheme @ObservedObject var context: HomeScreenViewModel.Context @@ -27,7 +28,11 @@ struct HomeScreenUserMenuButton: View { Button { context.send(viewAction: .userMenu(action: .settings)) } label: { - Label(L10n.commonSettings, systemImage: "gearshape") + Label { + Text(L10n.commonSettings) + } icon: { + settingsIconImage + } } .accessibilityIdentifier(A11yIdentifiers.homeScreen.settings) } @@ -55,6 +60,8 @@ struct HomeScreenUserMenuButton: View { avatarSize: .user(on: .home), imageProvider: context.imageProvider) .accessibilityIdentifier(A11yIdentifiers.homeScreen.userAvatar) + .overlayBadge(10, isBadged: context.viewState.showUserMenuBadge) + .compositingGroup() } .alert(L10n.screenSignoutConfirmationDialogTitle, isPresented: $showingLogoutConfirmation) { @@ -67,4 +74,24 @@ struct HomeScreenUserMenuButton: View { } .accessibilityLabel(L10n.a11yUserMenu) } + + // MARK: - Private + + /// Menu doesn't render composed views. Trick it into showing a badge. + private var settingsIconImage: Image? { + let settingsIcon = Image(systemSymbol: .gearshape) + .resizable() + .frame(width: 100, height: 100) + .overlayBadge(40, isBadged: context.viewState.showSettingsMenuOptionBadge) + .colorScheme(colorScheme) + .padding() + + let renderer = ImageRenderer(content: settingsIcon) + + guard let image = renderer.uiImage else { + return nil + } + + return Image(uiImage: image) + } } diff --git a/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenViewModel.swift b/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenViewModel.swift index ef88b925c..2d1d8f85d 100644 --- a/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenViewModel.swift @@ -88,7 +88,7 @@ class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDe case .success(.image): state.localMedia = try? mediaResult.get() case .failure, .success: - userIndicatorController?.alertInfo = .init(id: .init(), title: L10n.commonError, message: L10n.errorUnknown) + userIndicatorController?.alertInfo = .init(id: .init()) } } } diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/SecureBackupKeyBackupScreenCoordinator.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/SecureBackupKeyBackupScreenCoordinator.swift new file mode 100644 index 000000000..671cecda0 --- /dev/null +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/SecureBackupKeyBackupScreenCoordinator.swift @@ -0,0 +1,62 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import SwiftUI + +struct SecureBackupKeyBackupScreenCoordinatorParameters { + let secureBackupController: SecureBackupControllerProtocol + weak var userIndicatorController: UserIndicatorControllerProtocol? +} + +enum SecureBackupKeyBackupScreenCoordinatorAction { + case done +} + +final class SecureBackupKeyBackupScreenCoordinator: CoordinatorProtocol { + private let parameters: SecureBackupKeyBackupScreenCoordinatorParameters + private var viewModel: SecureBackupKeyBackupScreenViewModelProtocol + private let actionsSubject: PassthroughSubject = .init() + private var cancellables = Set() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(parameters: SecureBackupKeyBackupScreenCoordinatorParameters) { + self.parameters = parameters + + viewModel = SecureBackupKeyBackupScreenViewModel(secureBackupController: parameters.secureBackupController, + userIndicatorController: parameters.userIndicatorController) + } + + func start() { + viewModel.actions.sink { [weak self] action in + MXLog.info("Coordinator: received view model action: \(action)") + + guard let self else { return } + switch action { + case .done: + self.actionsSubject.send(.done) + } + } + .store(in: &cancellables) + } + + func toPresentable() -> AnyView { + AnyView(SecureBackupKeyBackupScreen(context: viewModel.context)) + } +} diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/SecureBackupKeyBackupScreenModels.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/SecureBackupKeyBackupScreenModels.swift new file mode 100644 index 000000000..10090cb4f --- /dev/null +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/SecureBackupKeyBackupScreenModels.swift @@ -0,0 +1,41 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +enum SecureBackupKeyBackupScreenViewModelAction { + case done +} + +enum SecureBackupKeyBackupScreenViewMode { + case disableBackup +} + +struct SecureBackupKeyBackupScreenViewState: BindableState { + let mode: SecureBackupKeyBackupScreenViewMode + var bindings = SecureBackupKeyBackupScreenViewStateBindings() +} + +struct SecureBackupKeyBackupScreenViewStateBindings { + var alertInfo: AlertInfo? +} + +var alertInfo: AlertInfo? + +enum SecureBackupKeyBackupScreenViewAction { + case cancel + case toggleBackup +} diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/SecureBackupKeyBackupScreenViewModel.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/SecureBackupKeyBackupScreenViewModel.swift new file mode 100644 index 000000000..793cfd200 --- /dev/null +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/SecureBackupKeyBackupScreenViewModel.swift @@ -0,0 +1,107 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import SwiftUI + +typealias SecureBackupKeyBackupScreenViewModelType = StateStoreViewModel + +class SecureBackupKeyBackupScreenViewModel: SecureBackupKeyBackupScreenViewModelType, SecureBackupKeyBackupScreenViewModelProtocol { + private let secureBackupController: SecureBackupControllerProtocol + + private var actionsSubject: PassthroughSubject = .init() + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(secureBackupController: SecureBackupControllerProtocol, userIndicatorController: UserIndicatorControllerProtocol?) { + self.secureBackupController = secureBackupController + + super.init(initialViewState: .init(mode: secureBackupController.keyBackupState.value.viewMode)) + + secureBackupController.keyBackupState + .receive(on: DispatchQueue.main) + .sink { [weak userIndicatorController] state in + let loadingIndicatorIdentifier = "SecureBackupKeyBackupScreenLoading" + switch state { + case .disabling, .enabling, .unknown: + userIndicatorController?.submitIndicator(.init(id: loadingIndicatorIdentifier, type: .modal, title: L10n.commonLoading, persistent: true)) + default: + userIndicatorController?.retractIndicatorWithId(loadingIndicatorIdentifier) + } + } + .store(in: &cancellables) + } + + // MARK: - Public + + override func process(viewAction: SecureBackupKeyBackupScreenViewAction) { + MXLog.info("View model: received view action: \(viewAction)") + + switch viewAction { + case .cancel: + actionsSubject.send(.done) + case .toggleBackup: + guard secureBackupController.keyBackupState.value == .enabled else { + fatalError("Tried disabling backup when not enabled") + } + + state.bindings.alertInfo = .init(id: .init(), + title: L10n.screenKeyBackupDisableConfirmationTitle, + message: L10n.screenKeyBackupDisableConfirmationDescription, + primaryButton: .init(title: L10n.screenKeyBackupDisableConfirmationActionTurnOff, role: .destructive) { [weak self] in + self?.disableBackup() + }, + secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil)) + } + } + + // MARK: - Private + + private func enableBackup() { + Task { + switch await secureBackupController.enableBackup() { + case .success: + actionsSubject.send(.done) + case .failure(let error): + MXLog.error("Failed enabling key backup with error: \(error)") + state.bindings.alertInfo = .init(id: .init()) + } + } + } + + private func disableBackup() { + Task { + switch await secureBackupController.disableBackup() { + case .success: + actionsSubject.send(.done) + case .failure(let error): + MXLog.error("Failed disabling key backup with error: \(error)") + state.bindings.alertInfo = .init(id: .init()) + } + } + } +} + +extension SecureBackupKeyBackupState { + var viewMode: SecureBackupKeyBackupScreenViewMode { + guard self == .enabled else { + fatalError("Invalid key backup state") + } + + return .disableBackup + } +} diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/SecureBackupKeyBackupScreenViewModelProtocol.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/SecureBackupKeyBackupScreenViewModelProtocol.swift new file mode 100644 index 000000000..4dcbefa75 --- /dev/null +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/SecureBackupKeyBackupScreenViewModelProtocol.swift @@ -0,0 +1,23 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine + +@MainActor +protocol SecureBackupKeyBackupScreenViewModelProtocol { + var actions: AnyPublisher { get } + var context: SecureBackupKeyBackupScreenViewModelType.Context { get } +} diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/View/SecureBackupKeyBackupScreen.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/View/SecureBackupKeyBackupScreen.swift new file mode 100644 index 000000000..cf0544518 --- /dev/null +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/View/SecureBackupKeyBackupScreen.swift @@ -0,0 +1,117 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import Compound +import SwiftUI + +struct SecureBackupKeyBackupScreen: View { + @ObservedObject var context: SecureBackupKeyBackupScreenViewModel.Context + + @ScaledMetric private var iconSize = 70 + + var body: some View { + mainContent + .padding() + .interactiveDismissDisabled() + .toolbar { toolbar } + .background(Color.compound.bgCanvasDefault.ignoresSafeArea()) + .alert(item: $context.alertInfo) + } + + @ViewBuilder + private var mainContent: some View { + switch context.viewState.mode { + case .disableBackup: + disableBackupSection + } + } + + private var disableBackupSection: some View { + VStack(spacing: 16) { + HeroImage(image: Image(asset: Asset.Images.secureBackupOff)) + + Text(L10n.screenKeyBackupDisableTitle) + .foregroundColor(.compound.textPrimary) + .font(.compound.headingMDBold) + .multilineTextAlignment(.center) + + Text(L10n.screenKeyBackupDisableDescription) + .foregroundColor(.compound.textSecondary) + .font(.compound.bodyMD) + .multilineTextAlignment(.center) + + VStack(alignment: .leading) { + Label { + Text(L10n.screenKeyBackupDisableDescriptionPoint1) + .foregroundColor(.compound.textSecondary) + .font(.compound.bodyMD) + } icon: { + CompoundIcon(\.close) + .foregroundColor(.compound.iconCriticalPrimary) + } + + Label { + Text(L10n.screenKeyBackupDisableDescriptionPoint2(InfoPlistReader.main.bundleDisplayName)) + .foregroundColor(.compound.textSecondary) + .font(.compound.bodyMD) + } icon: { + CompoundIcon(\.close) + .foregroundColor(.compound.iconCriticalPrimary) + } + } + + Spacer() + + Button(role: .destructive) { + context.send(viewAction: .toggleBackup) + } label: { + Text(L10n.screenChatBackupKeyBackupActionDisable) + } + .buttonStyle(.compound(.primary)) + } + } + + @ToolbarContentBuilder + private var toolbar: some ToolbarContent { + ToolbarItem(placement: .cancellationAction) { + Button(L10n.actionCancel) { + context.send(viewAction: .cancel) + } + } + } +} + +// MARK: - Previews + +struct SecureBackupKeyBackupScreen_Previews: PreviewProvider, TestablePreview { + static let setupViewModel = viewModel(keyBackupState: .enabled) + + static var previews: some View { + NavigationStack { + SecureBackupKeyBackupScreen(context: setupViewModel.context) + } + .previewDisplayName("Set up") + } + + static func viewModel(keyBackupState: SecureBackupKeyBackupState) -> SecureBackupKeyBackupScreenViewModelType { + let backupController = SecureBackupControllerMock() + backupController.underlyingKeyBackupState = CurrentValueSubject(keyBackupState).asCurrentValuePublisher() + + return SecureBackupKeyBackupScreenViewModel(secureBackupController: backupController, + userIndicatorController: nil) + } +} diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenCoordinator.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenCoordinator.swift new file mode 100644 index 000000000..686fffc75 --- /dev/null +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenCoordinator.swift @@ -0,0 +1,74 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import SwiftUI + +struct SecureBackupRecoveryKeyScreenCoordinatorParameters { + let secureBackupController: SecureBackupControllerProtocol + weak var userIndicatorController: UserIndicatorControllerProtocol? +} + +enum SecureBackupRecoveryKeyScreenCoordinatorAction { + case cancel + case recoverySetUp + case recoveryChanged + case recoveryFixed +} + +final class SecureBackupRecoveryKeyScreenCoordinator: CoordinatorProtocol { + private let parameters: SecureBackupRecoveryKeyScreenCoordinatorParameters + private var viewModel: SecureBackupRecoveryKeyScreenViewModelProtocol + private let actionsSubject: PassthroughSubject = .init() + private var cancellables = Set() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(parameters: SecureBackupRecoveryKeyScreenCoordinatorParameters) { + self.parameters = parameters + + viewModel = SecureBackupRecoveryKeyScreenViewModel(secureBackupController: parameters.secureBackupController, + userIndicatorController: parameters.userIndicatorController) + } + + func start() { + viewModel.actions.sink { [weak self] action in + MXLog.info("Coordinator: received view model action: \(action)") + + guard let self else { return } + switch action { + case .cancel: + self.actionsSubject.send(.cancel) + case .done(let mode): + switch mode { + case .setupRecovery: + self.actionsSubject.send(.recoverySetUp) + case .changeRecovery: + self.actionsSubject.send(.recoveryChanged) + case .fixRecovery: + self.actionsSubject.send(.recoveryFixed) + } + } + } + .store(in: &cancellables) + } + + func toPresentable() -> AnyView { + AnyView(SecureBackupRecoveryKeyScreen(context: viewModel.context)) + } +} diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenModels.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenModels.swift new file mode 100644 index 000000000..44f60f78f --- /dev/null +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenModels.swift @@ -0,0 +1,84 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +enum SecureBackupRecoveryKeyScreenViewModelAction { + case done(mode: SecureBackupRecoveryKeyScreenViewMode) + case cancel +} + +enum SecureBackupRecoveryKeyScreenViewMode { + case setupRecovery + case changeRecovery + case fixRecovery +} + +struct SecureBackupRecoveryKeyScreenViewState: BindableState { + let mode: SecureBackupRecoveryKeyScreenViewMode + + var recoveryKey: String? + var doneButtonEnabled = false + + var bindings: SecureBackupRecoveryKeyScreenViewBindings + + var title: String { + switch mode { + case .setupRecovery: + return recoveryKey == nil ? L10n.screenRecoveryKeySetupTitle : L10n.screenRecoveryKeySaveTitle + case .changeRecovery: + return recoveryKey == nil ? L10n.screenRecoveryKeyChangeTitle : L10n.screenRecoveryKeySaveTitle + case .fixRecovery: + return L10n.screenRecoveryKeyConfirmTitle + } + } + + var subtitle: String { + switch mode { + case .setupRecovery: + return recoveryKey == nil ? L10n.screenRecoveryKeySetupDescription : L10n.screenRecoveryKeySaveDescription + case .changeRecovery: + return recoveryKey == nil ? L10n.screenRecoveryKeyChangeDescription : L10n.screenRecoveryKeySaveDescription + case .fixRecovery: + return L10n.screenRecoveryKeyConfirmDescription + } + } + + var recoveryKeySubtitle: String { + switch mode { + case .setupRecovery: + return recoveryKey == nil ? L10n.screenRecoveryKeySetupGenerateKeyDescription : L10n.screenRecoveryKeySaveKeyDescription + case .changeRecovery: + return recoveryKey == nil ? L10n.screenRecoveryKeyChangeGenerateKeyDescription : L10n.screenRecoveryKeySaveKeyDescription + case .fixRecovery: + return L10n.screenRecoveryKeyConfirmKeyDescription + } + } +} + +struct SecureBackupRecoveryKeyScreenViewBindings { + var confirmationRecoveryKey = "" + var alertInfo: AlertInfo? +} + +enum SecureBackupRecoveryKeyScreenViewAction { + case generateKey + case copyKey + case keySaved + case confirmKey + case done + case cancel +} diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenViewModel.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenViewModel.swift new file mode 100644 index 000000000..c2b666ed1 --- /dev/null +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenViewModel.swift @@ -0,0 +1,111 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import SwiftUI + +typealias SecureBackupRecoveryKeyScreenViewModelType = StateStoreViewModel + +class SecureBackupRecoveryKeyScreenViewModel: SecureBackupRecoveryKeyScreenViewModelType, SecureBackupRecoveryKeyScreenViewModelProtocol { + private let secureBackupController: SecureBackupControllerProtocol + private weak var userIndicatorController: UserIndicatorControllerProtocol? + + private var actionsSubject: PassthroughSubject = .init() + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(secureBackupController: SecureBackupControllerProtocol, userIndicatorController: UserIndicatorControllerProtocol?) { + self.secureBackupController = secureBackupController + self.userIndicatorController = userIndicatorController + + super.init(initialViewState: .init(mode: secureBackupController.recoveryKeyState.value.viewMode, bindings: .init())) + + secureBackupController.recoveryKeyState + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak userIndicatorController] state in + let loadingIndicatorIdentifier = "SecureBackupRecoveryKeyScreenLoading" + switch state { + case .settingUp: + userIndicatorController?.submitIndicator(.init(id: loadingIndicatorIdentifier, type: .modal, title: L10n.commonLoading, persistent: true)) + default: + userIndicatorController?.retractIndicatorWithId(loadingIndicatorIdentifier) + } + }) + .store(in: &cancellables) + } + + // MARK: - Public + + override func process(viewAction: SecureBackupRecoveryKeyScreenViewAction) { + MXLog.info("View model: received view action: \(viewAction)") + + switch viewAction { + case .generateKey: + Task { + switch await secureBackupController.generateRecoveryKey() { + case .success(let key): + state.recoveryKey = key + case .failure(let error): + MXLog.error("Failed generating recovery key with error: \(error)") + state.bindings.alertInfo = .init(id: .init()) + } + } + case .copyKey: + UIPasteboard.general.string = state.recoveryKey + userIndicatorController?.submitIndicator(.init(title: "Copied recovery key")) + state.doneButtonEnabled = true + case .keySaved: + state.doneButtonEnabled = true + case .confirmKey: + Task { + switch await secureBackupController.confirmRecoveryKey(state.bindings.confirmationRecoveryKey) { + case .success: + actionsSubject.send(.done(mode: context.viewState.mode)) + case .failure(let error): + MXLog.error("Failed confirming recovery key with error: \(error)") + state.bindings.alertInfo = .init(id: .init()) + } + } + case .cancel: + actionsSubject.send(.cancel) + case .done: + 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 + guard let self else { return } + actionsSubject.send(.done(mode: context.viewState.mode)) + })) + } + } +} + +extension SecureBackupRecoveryKeyState { + var viewMode: SecureBackupRecoveryKeyScreenViewMode { + switch self { + case .disabled: + return .setupRecovery + case .enabled: + return .changeRecovery + case .incomplete: + return .fixRecovery + default: + fatalError("Invalid recovery state") + } + } +} diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenViewModelProtocol.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenViewModelProtocol.swift new file mode 100644 index 000000000..30f64e623 --- /dev/null +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenViewModelProtocol.swift @@ -0,0 +1,23 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine + +@MainActor +protocol SecureBackupRecoveryKeyScreenViewModelProtocol { + var actions: AnyPublisher { get } + var context: SecureBackupRecoveryKeyScreenViewModelType.Context { get } +} diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/View/SecureBackupRecoveryKeyScreen.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/View/SecureBackupRecoveryKeyScreen.swift new file mode 100644 index 000000000..0d7ecd709 --- /dev/null +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/View/SecureBackupRecoveryKeyScreen.swift @@ -0,0 +1,213 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import Compound +import SwiftUI + +struct SecureBackupRecoveryKeyScreen: View { + @ObservedObject var context: SecureBackupRecoveryKeyScreenViewModel.Context + + @ScaledMetric private var iconSize = 70 + + var body: some View { + mainContent + .padding() + .interactiveDismissDisabled() + .toolbar { toolbar } + .toolbar(.visible, for: .navigationBar) + .background(Color.compound.bgCanvasDefault.ignoresSafeArea()) + .alert(item: $context.alertInfo) + } + + @ViewBuilder + private var mainContent: some View { + VStack(spacing: 48) { + switch context.viewState.mode { + case .setupRecovery, .changeRecovery: + header + newRecoveryKeySection + case .fixRecovery: + header + confirmRecoveryKeySection + } + } + } + + @ToolbarContentBuilder + private var toolbar: some ToolbarContent { + if context.viewState.recoveryKey == nil { + ToolbarItem(placement: .cancellationAction) { + Button(L10n.actionCancel) { + context.send(viewAction: .cancel) + } + } + } + } + + private var header: some View { + VStack(spacing: 16) { + HeroImage(image: Image(asset: Asset.Images.secureBackupOn)) + + Text(context.viewState.title) + .foregroundColor(.compound.textPrimary) + .font(.compound.headingMDBold) + .multilineTextAlignment(.center) + + Text(context.viewState.subtitle) + .foregroundColor(.compound.textSecondary) + .font(.compound.bodyMD) + .multilineTextAlignment(.center) + } + } + + @ViewBuilder + private var newRecoveryKeySection: some View { + generateRecoveryKeySection + + Spacer() + + if let recoveryKey = context.viewState.recoveryKey { + ShareLink(item: recoveryKey) { + Label(L10n.screenRecoveryKeySaveAction, icon: \.download) + } + .buttonStyle(.compound(.primary)) + .simultaneousGesture(TapGesture().onEnded { _ in + context.send(viewAction: .keySaved) + }) + } + + Button { + context.send(viewAction: .done) + } label: { + Text(L10n.actionDone) + } + .buttonStyle(.compound(.primary)) + .disabled(context.viewState.recoveryKey == nil || context.viewState.doneButtonEnabled == false) + } + + private var generateRecoveryKeySection: some View { + VStack(alignment: .leading) { + Text(L10n.commonRecoveryKey) + .foregroundColor(.compound.textPrimary) + .font(.compound.bodySM) + + HStack { + if context.viewState.recoveryKey == nil { + Button(generateButtonTitle) { + context.send(viewAction: .generateKey) + } + .font(.compound.bodyLGSemibold) + } else { + HStack(alignment: .top) { + Text(context.viewState.recoveryKey ?? "") + .foregroundColor(.compound.textPrimary) + .font(.compound.bodyLG) + + Spacer() + + Button { + context.send(viewAction: .copyKey) + } label: { + Image(asset: Asset.Images.copy) + } + .tint(.compound.iconSecondary) + .accessibilityLabel(L10n.actionCopy) + } + } + } + .frame(maxWidth: .infinity) + .padding() + .background(Color.compound.bgSubtleSecondaryLevel0) + .clipShape(RoundedRectangle(cornerRadius: 8)) + + HStack(alignment: .top) { + if context.viewState.recoveryKey == nil { + CompoundIcon(\.info, size: .small, relativeTo: .compound.bodySM) + } + + Text(context.viewState.recoveryKeySubtitle) + .foregroundColor(.compound.textSecondary) + .font(.compound.bodySM) + } + } + } + + private var generateButtonTitle: String { + context.viewState.mode == .setupRecovery ? L10n.screenRecoveryKeySetupGenerateKey : L10n.screenRecoveryKeyChangeGenerateKey + } + + @ViewBuilder + private var confirmRecoveryKeySection: some View { + VStack(alignment: .leading) { + Text(L10n.commonRecoveryKey) + .foregroundColor(.compound.textPrimary) + .font(.compound.bodySM) + + TextField(L10n.screenRecoveryKeyConfirmKeyPlaceholder, text: $context.confirmationRecoveryKey, axis: .vertical) + .frame(maxWidth: .infinity) + .padding() + .background(Color.compound.bgSubtleSecondaryLevel0) + .clipShape(RoundedRectangle(cornerRadius: 8)) + + Text(context.viewState.recoveryKeySubtitle) + .foregroundColor(.compound.textSecondary) + .font(.compound.bodySM) + + Spacer() + + Button { + context.send(viewAction: .confirmKey) + } label: { + Text(L10n.actionConfirm) + } + .buttonStyle(.compound(.primary)) + .disabled(context.confirmationRecoveryKey.isEmpty) + } + } +} + +// MARK: - Previews + +struct SecureBackupRecoveryKeyScreen_Previews: PreviewProvider, TestablePreview { + static let setupViewModel = viewModel(recoveryKeyState: .enabled) + static let notSetUpViewModel = viewModel(recoveryKeyState: .disabled) + static let incompleteViewModel = viewModel(recoveryKeyState: .incomplete) + + static var previews: some View { + NavigationStack { + SecureBackupRecoveryKeyScreen(context: notSetUpViewModel.context) + } + .previewDisplayName("Not set up") + + NavigationStack { + SecureBackupRecoveryKeyScreen(context: setupViewModel.context) + } + .previewDisplayName("Set up") + + NavigationStack { + SecureBackupRecoveryKeyScreen(context: incompleteViewModel.context) + } + .previewDisplayName("Incomplete") + } + + static func viewModel(recoveryKeyState: SecureBackupRecoveryKeyState) -> SecureBackupRecoveryKeyScreenViewModelType { + let backupController = SecureBackupControllerMock() + backupController.underlyingRecoveryKeyState = CurrentValueSubject(recoveryKeyState).asCurrentValuePublisher() + + return SecureBackupRecoveryKeyScreenViewModel(secureBackupController: backupController, userIndicatorController: nil) + } +} diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenCoordinator.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenCoordinator.swift new file mode 100644 index 000000000..fc01eef6d --- /dev/null +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenCoordinator.swift @@ -0,0 +1,116 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import SwiftUI + +struct SecureBackupScreenCoordinatorParameters { + let appSettings: AppSettings + let secureBackupController: SecureBackupControllerProtocol + weak var navigationStackCoordinator: NavigationStackCoordinator? + weak var userIndicatorController: UserIndicatorControllerProtocol? +} + +enum SecureBackupScreenCoordinatorAction { } + +final class SecureBackupScreenCoordinator: CoordinatorProtocol { + private let parameters: SecureBackupScreenCoordinatorParameters + private var viewModel: SecureBackupScreenViewModelProtocol + private let actionsSubject: PassthroughSubject = .init() + private var cancellables = Set() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(parameters: SecureBackupScreenCoordinatorParameters) { + self.parameters = parameters + + viewModel = SecureBackupScreenViewModel(secureBackupController: parameters.secureBackupController, + userIndicatorController: parameters.userIndicatorController, + chatBackupDetailsURL: parameters.appSettings.chatBackupDetailsURL) + } + + func start() { + viewModel.actions.sink { [weak self] action in + guard let self else { return } + + switch action { + case .recoveryKey: + let navigationStackCoordinator = NavigationStackCoordinator() + let userIndicatorController = UserIndicatorController(rootCoordinator: navigationStackCoordinator) + + let recoveryKeyCoordinator = SecureBackupRecoveryKeyScreenCoordinator(parameters: .init(secureBackupController: parameters.secureBackupController, + userIndicatorController: userIndicatorController)) + + recoveryKeyCoordinator.actions.sink { [weak self] action in + guard let self else { return } + + parameters.navigationStackCoordinator?.setSheetCoordinator(nil) + + switch action { + case .cancel: + break + case .recoverySetUp: + showSuccessIndicator(title: L10n.screenRecoveryKeySetupSuccess) + case .recoveryChanged: + showSuccessIndicator(title: L10n.screenRecoveryKeyChangeSuccess) + case .recoveryFixed: + showSuccessIndicator(title: L10n.screenRecoveryKeyConfirmSuccess) + } + } + .store(in: &cancellables) + + navigationStackCoordinator.setRootCoordinator(recoveryKeyCoordinator, animated: true) + + parameters.navigationStackCoordinator?.setSheetCoordinator(userIndicatorController) + case .keyBackup: + let navigationStackCoordinator = NavigationStackCoordinator() + let userIndicatorController = UserIndicatorController(rootCoordinator: navigationStackCoordinator) + + let keyBackupCoordinator = SecureBackupKeyBackupScreenCoordinator(parameters: .init(secureBackupController: parameters.secureBackupController, + userIndicatorController: 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(userIndicatorController) + } + } + .store(in: &cancellables) + } + + 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)) + } +} diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenModels.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenModels.swift new file mode 100644 index 000000000..4506e0aef --- /dev/null +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenModels.swift @@ -0,0 +1,38 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +enum SecureBackupScreenViewModelAction { + case recoveryKey + case keyBackup +} + +struct SecureBackupScreenViewState: BindableState { + let chatBackupDetailsURL: URL + var recoveryKeyState = SecureBackupRecoveryKeyState.unknown + var keyBackupState = SecureBackupKeyBackupState.unknown + var bindings = SecureBackupScreenViewStateBindings() +} + +struct SecureBackupScreenViewStateBindings { + var alertInfo: AlertInfo? +} + +enum SecureBackupScreenViewAction { + case recoveryKey + case keyBackup +} diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenViewModel.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenViewModel.swift new file mode 100644 index 000000000..aacba1732 --- /dev/null +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenViewModel.swift @@ -0,0 +1,85 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import SwiftUI + +typealias SecureBackupScreenViewModelType = StateStoreViewModel + +class SecureBackupScreenViewModel: SecureBackupScreenViewModelType, SecureBackupScreenViewModelProtocol { + private let secureBackupController: SecureBackupControllerProtocol + private weak var userIndicatorController: UserIndicatorControllerProtocol? + + private var actionsSubject: PassthroughSubject = .init() + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(secureBackupController: SecureBackupControllerProtocol, + userIndicatorController: UserIndicatorControllerProtocol?, + chatBackupDetailsURL: URL) { + self.secureBackupController = secureBackupController + self.userIndicatorController = userIndicatorController + + super.init(initialViewState: .init(chatBackupDetailsURL: chatBackupDetailsURL)) + + secureBackupController.recoveryKeyState + .receive(on: DispatchQueue.main) + .weakAssign(to: \.state.recoveryKeyState, on: self) + .store(in: &cancellables) + + secureBackupController.keyBackupState + .receive(on: DispatchQueue.main) + .weakAssign(to: \.state.keyBackupState, on: self) + .store(in: &cancellables) + } + + // MARK: - Public + + override func process(viewAction: SecureBackupScreenViewAction) { + switch viewAction { + case .recoveryKey: + actionsSubject.send(.recoveryKey) + case .keyBackup: + switch secureBackupController.keyBackupState.value { + case .disabled: + enableKeyBackup() + case .enabled: + actionsSubject.send(.keyBackup) + default: + break + } + } + } + + // MARK: - Private + + private func enableKeyBackup() { + Task { + let loadingIndicatorIdentifier = "SecureBackupScreenLoading" + userIndicatorController?.submitIndicator(.init(id: loadingIndicatorIdentifier, type: .modal, title: L10n.commonLoading, persistent: true)) + switch await secureBackupController.enableBackup() { + case .success: + break + case .failure(let error): + MXLog.error("Failed enabling key backup with error: \(error)") + state.bindings.alertInfo = .init(id: .init()) + } + + userIndicatorController?.retractIndicatorWithId(loadingIndicatorIdentifier) + } + } +} diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenViewModelProtocol.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenViewModelProtocol.swift new file mode 100644 index 000000000..df488d140 --- /dev/null +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenViewModelProtocol.swift @@ -0,0 +1,23 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine + +@MainActor +protocol SecureBackupScreenViewModelProtocol { + var actions: AnyPublisher { get } + var context: SecureBackupScreenViewModelType.Context { get } +} diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/View/SecureBackupScreen.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/View/SecureBackupScreen.swift new file mode 100644 index 000000000..2e6990b6a --- /dev/null +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/View/SecureBackupScreen.swift @@ -0,0 +1,168 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import Compound +import SwiftUI + +struct SecureBackupScreen: View { + @ObservedObject var context: SecureBackupScreenViewModel.Context + + var body: some View { + Form { + if context.viewState.keyBackupState != .enabled { + keyBackupSection + } else { + keyBackupSection + recoveryKeySection + } + } + .compoundList() + .navigationTitle(L10n.commonChatBackup) + .navigationBarTitleDisplayMode(.inline) + .alert(item: $context.alertInfo) + } + + // MARK: - Private + + @ViewBuilder + private var keyBackupSection: some View { + Section { + ListRow(kind: .custom { + VStack(alignment: .leading, spacing: 2) { + Text(L10n.screenChatBackupKeyBackupTitle) + .font(.compound.bodyLGSemibold) + .foregroundColor(.compound.textPrimary) + + Text(keyBackupDescriptionWithLearnMoreLink) + .font(.compound.bodySM) + .foregroundColor(.compound.textSecondary) + } + .padding(.horizontal, ListRowPadding.horizontal) + .padding(.vertical, ListRowPadding.vertical) + .accessibilityElement(children: .combine) + }) + + keyBackupButton + } + } + + private var keyBackupDescriptionWithLearnMoreLink: AttributedString { + let linkPlaceholder = "{link}" + var description = AttributedString(L10n.screenChatBackupKeyBackupDescription(linkPlaceholder)) + var linkString = AttributedString(L10n.actionLearnMore) + linkString.link = context.viewState.chatBackupDetailsURL + linkString.bold() + description.replace(linkPlaceholder, with: linkString) + return description + } + + @ViewBuilder + private var keyBackupButton: some View { + switch context.viewState.keyBackupState { + case .enabled, .disabling: + ListRow(label: .plain(title: L10n.screenChatBackupKeyBackupActionDisable, role: .destructive), kind: .navigationLink { + context.send(viewAction: .keyBackup) + }) + case .disabled, .enabling: + ListRow(label: .plain(title: L10n.screenChatBackupKeyBackupActionEnable), kind: .navigationLink { + context.send(viewAction: .keyBackup) + }) + default: + ListRow(label: .plain(title: L10n.commonLoading), details: .isWaiting(true), kind: .label) + } + } + + @ViewBuilder + private var recoveryKeySection: some View { + Section { + switch context.viewState.recoveryKeyState { + case .enabled: + ListRow(label: .plain(title: L10n.screenChatBackupRecoveryActionChange), + kind: .navigationLink { context.send(viewAction: .recoveryKey) }) + case .disabled: + ListRow(label: .plain(title: L10n.screenChatBackupRecoveryActionSetup), + details: .icon(BadgeView(size: 10)), + kind: .navigationLink { context.send(viewAction: .recoveryKey) }) + case .incomplete: + ListRow(label: .plain(title: L10n.screenChatBackupRecoveryActionConfirm), + kind: .navigationLink { context.send(viewAction: .recoveryKey) }) + default: + ListRow(label: .plain(title: L10n.commonLoading), details: .isWaiting(true), kind: .label) + } + } footer: { + recoveryKeySectionFooter + .compoundFormSectionFooter() + } + } + + @ViewBuilder + private var recoveryKeySectionFooter: some View { + switch context.viewState.recoveryKeyState { + case .disabled: + Text(L10n.screenChatBackupRecoveryActionSetupDescription(InfoPlistReader.main.bundleDisplayName)) + case .incomplete: + Text(L10n.screenChatBackupRecoveryActionConfirmDescription) + default: + EmptyView() + } + } +} + +// MARK: - Previews + +struct SecureBackupScreen_Previews: PreviewProvider, TestablePreview { + static let bothSetupViewModel = viewModel(keyBackupState: .enabled, recoveryKeyState: .enabled) + static let onlyKeyBackupSetUpViewModel = viewModel(keyBackupState: .enabled, recoveryKeyState: .disabled) + static let keyBackupDisabledViewModel = viewModel(keyBackupState: .disabled, recoveryKeyState: .disabled) + static let recoveryIncompleteViewModel = viewModel(keyBackupState: .enabled, recoveryKeyState: .incomplete) + + static var previews: some View { + Group { + NavigationStack { + SecureBackupScreen(context: bothSetupViewModel.context) + } + .previewDisplayName("Both setup") + + NavigationStack { + SecureBackupScreen(context: onlyKeyBackupSetUpViewModel.context) + } + .previewDisplayName("Only key backup setup") + + NavigationStack { + SecureBackupScreen(context: keyBackupDisabledViewModel.context) + } + .previewDisplayName("Key backup disabled") + + NavigationStack { + SecureBackupScreen(context: recoveryIncompleteViewModel.context) + } + .previewDisplayName("Recovery incomplete") + } + .snapshot(delay: 0.25) + } + + static func viewModel(keyBackupState: SecureBackupKeyBackupState, + recoveryKeyState: SecureBackupRecoveryKeyState) -> SecureBackupScreenViewModelType { + let backupController = SecureBackupControllerMock() + backupController.underlyingKeyBackupState = CurrentValueSubject(keyBackupState).asCurrentValuePublisher() + backupController.underlyingRecoveryKeyState = CurrentValueSubject(recoveryKeyState).asCurrentValuePublisher() + + return SecureBackupScreenViewModel(secureBackupController: backupController, + userIndicatorController: nil, + chatBackupDetailsURL: .sharedPublicDirectory) + } +} diff --git a/ElementX/Sources/Screens/SessionVerificationScreen/View/SessionVerificationScreen.swift b/ElementX/Sources/Screens/SessionVerificationScreen/View/SessionVerificationScreen.swift index 8f121bad9..03cc05bd9 100644 --- a/ElementX/Sources/Screens/SessionVerificationScreen/View/SessionVerificationScreen.swift +++ b/ElementX/Sources/Screens/SessionVerificationScreen/View/SessionVerificationScreen.swift @@ -71,7 +71,7 @@ struct SessionVerificationScreen: View { @ViewBuilder private var screenHeader: some View { VStack(spacing: 0) { - AuthenticationIconImage(image: Image(systemName: headerImageName)) + HeroImage(image: Image(systemName: headerImageName)) .padding(.bottom, 16) Text(context.viewState.title ?? "") diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift index 5b7f4fbe0..630a84119 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift @@ -53,6 +53,7 @@ protocol DeveloperOptionsProtocol: AnyObject { var mentionsEnabled: Bool { get set } var appLockFlowEnabled: Bool { get set } var elementCallEnabled: Bool { get set } + var chatBackupEnabled: Bool { get set } } extension AppSettings: DeveloperOptionsProtocol { } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift index be5408d3a..6b9eb0275 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift @@ -31,6 +31,18 @@ struct DeveloperOptionsScreen: View { } } + Section("Security") { + Toggle(isOn: $context.chatBackupEnabled) { + Text("Chat backup") + Text("Requires app reboot") + } + + Toggle(isOn: $context.appLockFlowEnabled) { + Text("PIN/Biometric lock") + Text("Resets on reboot") + } + } + Section("Timeline") { Toggle(isOn: $context.shouldCollapseRoomStateEvents) { Text("Collapse room state events") @@ -52,7 +64,7 @@ struct DeveloperOptionsScreen: View { } Toggle(isOn: $context.elementCallEnabled) { - Text("Elemement Call") + Text("Element Call") } } @@ -67,13 +79,6 @@ struct DeveloperOptionsScreen: View { Text("Enable voice messages") } } - - Section("Security") { - Toggle(isOn: $context.appLockFlowEnabled) { - Text("PIN/Biometric lock") - Text("Resets on reboot") - } - } Section { Button { diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift index e872f4b35..57cb71de4 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift @@ -24,6 +24,7 @@ struct SettingsScreenCoordinatorParameters { let appLockService: AppLockServiceProtocol let bugReportService: BugReportServiceProtocol let notificationSettings: NotificationSettingsProxyProtocol + let secureBackupController: SecureBackupControllerProtocol let appSettings: AppSettings } @@ -49,7 +50,8 @@ final class SettingsScreenCoordinator: CoordinatorProtocol { init(parameters: SettingsScreenCoordinatorParameters) { self.parameters = parameters - viewModel = SettingsScreenViewModel(userSession: parameters.userSession, appSettings: parameters.appSettings) + viewModel = SettingsScreenViewModel(userSession: parameters.userSession, + appSettings: parameters.appSettings) viewModel.actions .sink { [weak self] action in @@ -71,7 +73,9 @@ final class SettingsScreenCoordinator: CoordinatorProtocol { case .about: presentLegalInformationScreen() case .sessionVerification: - verifySession() + presentSessionVerificationScreen() + case .secureBackup: + presentSecureBackupScreen() case .accountSessionsList: presentAccountSessionsListURL() case .notifications: @@ -172,7 +176,7 @@ final class SettingsScreenCoordinator: CoordinatorProtocol { parameters.navigationStackCoordinator?.push(LegalInformationScreenCoordinator(appSettings: parameters.appSettings)) } - private func verifySession() { + private func presentSessionVerificationScreen() { guard let sessionVerificationController = parameters.userSession.sessionVerificationController else { fatalError("The sessionVerificationController should aways be valid at this point") } @@ -196,6 +200,15 @@ final class SettingsScreenCoordinator: CoordinatorProtocol { } } + private func presentSecureBackupScreen() { + let coordinator = SecureBackupScreenCoordinator(parameters: .init(appSettings: parameters.appSettings, + secureBackupController: parameters.userSession.clientProxy.secureBackupController, + navigationStackCoordinator: parameters.navigationStackCoordinator, + userIndicatorController: parameters.userIndicatorController)) + + parameters.navigationStackCoordinator?.push(coordinator) + } + private func presentNotificationSettings() { let notificationParameters = NotificationSettingsScreenCoordinatorParameters(navigationStackCoordinator: parameters.navigationStackCoordinator, userSession: parameters.userSession, diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift index e938090e1..1a65c55dd 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift @@ -26,6 +26,7 @@ enum SettingsScreenViewModelAction { case reportBug case about case sessionVerification + case secureBackup case accountSessionsList case notifications case advancedSettings @@ -40,7 +41,9 @@ struct SettingsScreenViewState: BindableState { var accountSessionsListURL: URL? var userAvatarURL: URL? var userDisplayName: String? - var showSessionVerificationSection: Bool + var isSessionVerified: Bool + var chatBackupEnabled = false + var showSecureBackupBadge = false var showAppLockSettings: Bool var showDeveloperOptions: Bool @@ -57,6 +60,7 @@ enum SettingsScreenViewAction { case reportBug case about case sessionVerification + case secureBackup case accountSessionsList case notifications case developerOptions diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift index 893c8fcad..b3716af3c 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift @@ -33,16 +33,16 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo self.userSession = userSession self.appSettings = appSettings - var showSessionVerificationSection = false + var isSessionVerified = true if let sessionVerificationController = userSession.sessionVerificationController { - showSessionVerificationSection = !sessionVerificationController.isVerified + isSessionVerified = sessionVerificationController.isVerified } super.init(initialViewState: .init(deviceID: userSession.deviceID, userID: userSession.userID, accountProfileURL: userSession.clientProxy.accountURL(action: .profile), accountSessionsListURL: userSession.clientProxy.accountURL(action: .sessionsList), - showSessionVerificationSection: showSessionVerificationSection, + isSessionVerified: isSessionVerified, showAppLockSettings: appSettings.appLockFlowEnabled, showDeveloperOptions: appSettings.canShowDeveloperOptions), imageProvider: userSession.mediaProvider) @@ -56,11 +56,24 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo .receive(on: DispatchQueue.main) .weakAssign(to: \.state.userDisplayName, on: self) .store(in: &cancellables) - + appSettings.$appLockFlowEnabled .weakAssign(to: \.state.showAppLockSettings, on: self) .store(in: &cancellables) + + appSettings.$chatBackupEnabled + .weakAssign(to: \.state.chatBackupEnabled, on: self) + .store(in: &cancellables) + + userSession.clientProxy.secureBackupController.recoveryKeyState + .receive(on: DispatchQueue.main) + .sink { [weak self] state in + guard let self else { return } + self.state.showSecureBackupBadge = (state == .unknown || state == .disabled) && appSettings.chatBackupEnabled + } + .store(in: &cancellables) + Task { await userSession.clientProxy.loadUserAvatarURL() await userSession.clientProxy.loadUserDisplayName() @@ -71,9 +84,9 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo .sink { [weak self] callback in switch callback { case .sessionVerificationNeeded: - self?.state.showSessionVerificationSection = true + self?.state.isSessionVerified = false case .didVerifySession: - self?.state.showSessionVerificationSection = false + self?.state.isSessionVerified = true default: break } @@ -101,6 +114,8 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo actionsSubject.send(.logout) case .sessionVerification: actionsSubject.send(.sessionVerification) + case .secureBackup: + actionsSubject.send(.secureBackup) case .notifications: actionsSubject.send(.notifications) case .accountSessionsList: diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift index ed82cfacc..a8bf67ade 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift @@ -27,9 +27,7 @@ struct SettingsScreen: View { Form { userSection - if context.viewState.showSessionVerificationSection { - sessionVerificationSection - } + accountSecuritySection mainSection @@ -86,11 +84,22 @@ struct SettingsScreen: View { } } - private var sessionVerificationSection: some View { - Section { - ListRow(label: .default(title: L10n.actionCompleteVerification, - systemIcon: .checkmarkShield), - kind: .button { context.send(viewAction: .sessionVerification) }) + @ViewBuilder + private var accountSecuritySection: some View { + if !context.viewState.isSessionVerified || context.viewState.chatBackupEnabled { + Section { + if !context.viewState.isSessionVerified { + ListRow(label: .default(title: L10n.actionCompleteVerification, + systemIcon: .checkmarkShield), + kind: .button { context.send(viewAction: .sessionVerification) }) + } else if context.viewState.chatBackupEnabled { + ListRow(label: .default(title: L10n.commonChatBackup, + icon: Image(asset: Asset.Images.secureBackupIcon)), + details: context.viewState.showSecureBackupBadge ? .icon(secureBackupBadge) : nil, + kind: .navigationLink { context.send(viewAction: .secureBackup) }) + .accessibilityIdentifier(A11yIdentifiers.settingsScreen.secureBackup) + } + } } } @@ -222,6 +231,13 @@ struct SettingsScreen: View { .accessibilityIdentifier(A11yIdentifiers.settingsScreen.done) } } + + @ViewBuilder + private var secureBackupBadge: some View { + if context.viewState.showSecureBackupBadge { + BadgeView(size: 10) + } + } } private extension TimelineStyle { diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index b46fa59db..63a36f148 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -45,6 +45,8 @@ class ClientProxy: ClientProxyProtocol { let notificationSettings: NotificationSettingsProxyProtocol + let secureBackupController: SecureBackupControllerProtocol + private let roomListRecencyOrderingAllowedEventTypes = ["m.room.message", "m.room.encrypted", "m.sticker"] private var loadCachedAvatarURLTask: Task? @@ -93,6 +95,8 @@ class ClientProxy: ClientProxyProtocol { notificationSettings = NotificationSettingsProxy(notificationSettings: client.getNotificationSettings(), backgroundTaskService: backgroundTaskService) + + secureBackupController = SecureBackupController() client.setDelegate(delegate: ClientDelegateWrapper { [weak self] isSoftLogout in self?.hasEncounteredAuthError = true diff --git a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift index a055effa8..e7a17ce6a 100644 --- a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift +++ b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift @@ -95,6 +95,8 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol { var notificationSettings: NotificationSettingsProxyProtocol { get } + var secureBackupController: SecureBackupControllerProtocol { get } + func startSync() func stopSync() diff --git a/ElementX/Sources/Services/Client/MockClientProxy.swift b/ElementX/Sources/Services/Client/MockClientProxy.swift index d8f1eeb42..52686cdf6 100644 --- a/ElementX/Sources/Services/Client/MockClientProxy.swift +++ b/ElementX/Sources/Services/Client/MockClientProxy.swift @@ -39,6 +39,13 @@ class MockClientProxy: ClientProxyProtocol { var userDisplayName: CurrentValuePublisher { CurrentValueSubject("User display name").asCurrentValuePublisher() } var notificationSettings: NotificationSettingsProxyProtocol = NotificationSettingsProxyMock(with: .init()) + + lazy var secureBackupController: SecureBackupControllerProtocol = { + let secureBackupController = SecureBackupControllerMock() + secureBackupController.underlyingRecoveryKeyState = .init(CurrentValueSubject(.disabled)) + secureBackupController.underlyingKeyBackupState = .init(CurrentValueSubject(.enabled)) + return secureBackupController + }() init(userID: String, deviceID: String? = nil, roomSummaryProvider: RoomSummaryProviderProtocol? = MockRoomSummaryProvider()) { self.userID = userID diff --git a/ElementX/Sources/Services/SecureBackup/SecureBackupController.swift b/ElementX/Sources/Services/SecureBackup/SecureBackupController.swift new file mode 100644 index 000000000..bf2abc544 --- /dev/null +++ b/ElementX/Sources/Services/SecureBackup/SecureBackupController.swift @@ -0,0 +1,79 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import Foundation + +class SecureBackupController: SecureBackupControllerProtocol { + private let recoveryKeyStateSubject = CurrentValueSubject(.disabled) + private let keyBackupStateSubject = CurrentValueSubject(.enabled) + + var recoveryKeyState: CurrentValuePublisher { + recoveryKeyStateSubject.asCurrentValuePublisher() + } + + var keyBackupState: CurrentValuePublisher { + keyBackupStateSubject.asCurrentValuePublisher() + } + + func enableBackup() async -> Result { + keyBackupStateSubject.send(.enabling) + + try? await Task.sleep(for: .seconds(1)) + + keyBackupStateSubject.send(.enabled) + + return .success(()) + } + + func disableBackup() async -> Result { + keyBackupStateSubject.send(.disabling) + + try? await Task.sleep(for: .seconds(1)) + + keyBackupStateSubject.send(.disabled) + + return .success(()) + } + + static var wohoo = 0 + + func generateRecoveryKey() async -> Result { + recoveryKeyStateSubject.send(.settingUp) + + try? await Task.sleep(for: .seconds(1)) + + if Self.wohoo > 0, Self.wohoo % 2 == 1 { + recoveryKeyStateSubject.send(.incomplete) + } else { + recoveryKeyStateSubject.send(.enabled) + } + + Self.wohoo += 1 + + return .success(UUID().uuidString) + } + + func confirmRecoveryKey(_ key: String) async -> Result { + recoveryKeyStateSubject.send(.settingUp) + + try? await Task.sleep(for: .seconds(1)) + + recoveryKeyStateSubject.send(.enabled) + + return .success(()) + } +} diff --git a/ElementX/Sources/Services/SecureBackup/SecureBackupControllerProtocol.swift b/ElementX/Sources/Services/SecureBackup/SecureBackupControllerProtocol.swift new file mode 100644 index 000000000..46197d17c --- /dev/null +++ b/ElementX/Sources/Services/SecureBackup/SecureBackupControllerProtocol.swift @@ -0,0 +1,65 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import Foundation + +enum SecureBackupRecoveryKeyState { + case unknown + case disabled + case enabled + /// Recovery is not set up properly, the user will need to re-enter it so we can cleanup + /// https://github.com/vector-im/element-meta/issues/2107 + case incomplete + case settingUp +} + +enum SecureBackupKeyBackupState { + case unknown + case enabling + case enabled + case disabling + case disabled +} + +enum SecureBackupControllerError: Error { + case failedEnablingKeyBackup + case failedDisablingKeyBackup + + case failedGeneratingRecoveryKey + case failedConfirmingRecoveryKey +} + +// sourcery: AutoMockable +protocol SecureBackupControllerProtocol { + var recoveryKeyState: CurrentValuePublisher { get } + + var keyBackupState: CurrentValuePublisher { get } + + func enableBackup() async -> Result + func disableBackup() async -> Result + + func generateRecoveryKey() async -> Result + func confirmRecoveryKey(_ key: String) async -> Result +} + +extension SecureBackupControllerMock { + convenience init(keyBackupState: SecureBackupKeyBackupState, recoveryKeyState: SecureBackupRecoveryKeyState) { + self.init() + underlyingKeyBackupState = CurrentValueSubject(keyBackupState).asCurrentValuePublisher() + underlyingRecoveryKeyState = CurrentValueSubject(recoveryKeyState).asCurrentValuePublisher() + } +} diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index 2ffa74341..33cee3d77 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -191,6 +191,7 @@ class MockScreen: Identifiable { appSettings: ServiceLocator.shared.settings), bugReportService: BugReportServiceMock(), notificationSettings: NotificationSettingsProxyMock(with: .init()), + secureBackupController: SecureBackupControllerMock(), appSettings: ServiceLocator.shared.settings)) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator diff --git a/UITests/Sources/SecureBackupKeyBackupScreenUITests.swift b/UITests/Sources/SecureBackupKeyBackupScreenUITests.swift new file mode 100644 index 000000000..0cc4f41ec --- /dev/null +++ b/UITests/Sources/SecureBackupKeyBackupScreenUITests.swift @@ -0,0 +1,21 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import ElementX +import XCTest + +@MainActor +class SecureBackupKeyBackupScreenUITests: XCTestCase { } diff --git a/UITests/Sources/SecureBackupRecoveryKeyScreenUITests.swift b/UITests/Sources/SecureBackupRecoveryKeyScreenUITests.swift new file mode 100644 index 000000000..b63cfe183 --- /dev/null +++ b/UITests/Sources/SecureBackupRecoveryKeyScreenUITests.swift @@ -0,0 +1,21 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import ElementX +import XCTest + +@MainActor +class SecureBackupRecoveryKeyScreenUITests: XCTestCase { } diff --git a/UITests/Sources/SecureBackupScreenUITests.swift b/UITests/Sources/SecureBackupScreenUITests.swift new file mode 100644 index 000000000..7dea46d8f --- /dev/null +++ b/UITests/Sources/SecureBackupScreenUITests.swift @@ -0,0 +1,21 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import ElementX +import XCTest + +@MainActor +class SecureBackupScreenUITests: XCTestCase { } diff --git a/UnitTests/Sources/SecureBackupKeyBackupScreenViewModelTests.swift b/UnitTests/Sources/SecureBackupKeyBackupScreenViewModelTests.swift new file mode 100644 index 000000000..2d108dc80 --- /dev/null +++ b/UnitTests/Sources/SecureBackupKeyBackupScreenViewModelTests.swift @@ -0,0 +1,22 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +@testable import ElementX + +@MainActor +class SecureBackupKeyBackupScreenViewModelTests: XCTestCase { } diff --git a/UnitTests/Sources/SecureBackupRecoveryKeyScreenViewModelTests.swift b/UnitTests/Sources/SecureBackupRecoveryKeyScreenViewModelTests.swift new file mode 100644 index 000000000..d6b6ac487 --- /dev/null +++ b/UnitTests/Sources/SecureBackupRecoveryKeyScreenViewModelTests.swift @@ -0,0 +1,22 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +@testable import ElementX + +@MainActor +class SecureBackupRecoveryKeyScreenViewModelTests: XCTestCase { } diff --git a/UnitTests/Sources/SecureBackupScreenViewModelTests.swift b/UnitTests/Sources/SecureBackupScreenViewModelTests.swift new file mode 100644 index 000000000..3ea215ef9 --- /dev/null +++ b/UnitTests/Sources/SecureBackupScreenViewModelTests.swift @@ -0,0 +1,22 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +@testable import ElementX + +@MainActor +class SecureBackupScreenViewModelTests: XCTestCase { } diff --git a/UnitTests/__Snapshots__/PreviewTests/test_authenticationIconImage.1.png b/UnitTests/__Snapshots__/PreviewTests/test_heroImage.1.png similarity index 100% rename from UnitTests/__Snapshots__/PreviewTests/test_authenticationIconImage.1.png rename to UnitTests/__Snapshots__/PreviewTests/test_heroImage.1.png diff --git a/UnitTests/__Snapshots__/PreviewTests/test_secureBackupKeyBackupScreen.Set-up.png b/UnitTests/__Snapshots__/PreviewTests/test_secureBackupKeyBackupScreen.Set-up.png new file mode 100644 index 000000000..e22734de4 --- /dev/null +++ b/UnitTests/__Snapshots__/PreviewTests/test_secureBackupKeyBackupScreen.Set-up.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cdf6b26d4174ebe03ea0bdd51c2ba8ecd6b720dbc3bac3f6960bd6b305259cda +size 150538 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen.Incomplete.png b/UnitTests/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen.Incomplete.png new file mode 100644 index 000000000..9c5deaf19 --- /dev/null +++ b/UnitTests/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen.Incomplete.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:04b0e56fb858bb91536de8a6235b00885e85a717f05ee5c386017a53105f7857 +size 106661 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen.Not-set-up.png b/UnitTests/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen.Not-set-up.png new file mode 100644 index 000000000..490219038 --- /dev/null +++ b/UnitTests/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen.Not-set-up.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:013ef683c6abcc0a90e935797d7acfbccef806d806dcc527126dcda08e3f6ea1 +size 133333 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen.Set-up.png b/UnitTests/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen.Set-up.png new file mode 100644 index 000000000..e3f930249 --- /dev/null +++ b/UnitTests/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen.Set-up.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0f9761d91b65d80356290996c1acb06848be21505c7fb1dc21faabc4e442c5d +size 132839 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_secureBackupScreen.Both-setup.png b/UnitTests/__Snapshots__/PreviewTests/test_secureBackupScreen.Both-setup.png new file mode 100644 index 000000000..cca8585d4 --- /dev/null +++ b/UnitTests/__Snapshots__/PreviewTests/test_secureBackupScreen.Both-setup.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:293ea2147f9c17ee230e707b544493e6730c1de3b71baf14c11ece4c3d70b361 +size 105268 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_secureBackupScreen.Key-backup-disabled.png b/UnitTests/__Snapshots__/PreviewTests/test_secureBackupScreen.Key-backup-disabled.png new file mode 100644 index 000000000..4079436e2 --- /dev/null +++ b/UnitTests/__Snapshots__/PreviewTests/test_secureBackupScreen.Key-backup-disabled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a0de28d166d78ce3f22263364983bdc20ebd0811c1ea8096ce1b8f43b117ed47 +size 95499 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_secureBackupScreen.Only-key-backup-setup.png b/UnitTests/__Snapshots__/PreviewTests/test_secureBackupScreen.Only-key-backup-setup.png new file mode 100644 index 000000000..958d6a719 --- /dev/null +++ b/UnitTests/__Snapshots__/PreviewTests/test_secureBackupScreen.Only-key-backup-setup.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b99e77edc65b81c297864a460389b6b660baeb5fea85332a5b72b8cbafc58ec4 +size 123104 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_secureBackupScreen.Recovery-incomplete.png b/UnitTests/__Snapshots__/PreviewTests/test_secureBackupScreen.Recovery-incomplete.png new file mode 100644 index 000000000..fde319ed2 --- /dev/null +++ b/UnitTests/__Snapshots__/PreviewTests/test_secureBackupScreen.Recovery-incomplete.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de37e556ac49c283bb92f363f7492b6f31862b579e4ee136677a2440bea34bd3 +size 111802