From 5625e78fc9129ae3dab034dd9fa2c3d905f48032 Mon Sep 17 00:00:00 2001 From: Doug <6060466+pixlwave@users.noreply.github.com> Date: Tue, 17 Sep 2024 20:08:35 +0100 Subject: [PATCH] Add support for account deactivation when not using OIDC. (#3295) --- ElementX.xcodeproj/project.pbxproj | 42 ++++++- .../xcshareddata/swiftpm/Package.resolved | 2 +- .../en.lproj/Localizable.strings | 23 +++- .../SettingsFlowCoordinator.swift | 26 ++++ ElementX/Sources/Generated/Strings.swift | 44 ++++++- ElementX/Sources/Mocks/ClientProxyMock.swift | 1 + .../Mocks/Generated/GeneratedMocks.swift | 75 ++++++++++++ .../Mocks/Generated/SDKGeneratedMocks.swift | 111 ++++++++++++++++++ .../DeactivateAccountScreenCoordinator.swift | 57 +++++++++ .../DeactivateAccountScreenModels.swift | 54 +++++++++ .../DeactivateAccountScreenViewModel.swift | 84 +++++++++++++ ...tivateAccountScreenViewModelProtocol.swift | 14 +++ .../View/DeactivateAccountScreen.swift | 111 ++++++++++++++++++ .../View/EncryptionResetPasswordScreen.swift | 2 + .../View/SecureBackupRecoveryKeyScreen.swift | 1 + .../SettingsScreenCoordinator.swift | 3 + .../SettingsScreen/SettingsScreenModels.swift | 9 ++ .../SettingsScreenViewModel.swift | 3 + .../SettingsScreen/View/SettingsScreen.swift | 16 ++- .../Sources/Services/Client/ClientProxy.swift | 14 +++ .../Services/Client/ClientProxyProtocol.swift | 4 + PreviewTests/Sources/PreviewTests.swift | 6 + ...t_deactivateAccountScreen-iPad-en-GB.1.png | 3 + ..._deactivateAccountScreen-iPad-pseudo.1.png | 3 + ...ctivateAccountScreen-iPhone-15-en-GB.1.png | 3 + ...tivateAccountScreen-iPhone-15-pseudo.1.png | 3 + ...ilureScreen-iPad-en-GB.Unsigned-Device.png | 4 +- ...lureScreen-iPad-pseudo.Unsigned-Device.png | 4 +- ...Screen-iPhone-15-en-GB.Unsigned-Device.png | 4 +- ...creen-iPhone-15-pseudo.Unsigned-Device.png | 4 +- .../test_settingsScreen-iPad-en-GB.1.png | 4 +- .../test_settingsScreen-iPad-pseudo.1.png | 4 +- .../test_settingsScreen-iPhone-15-en-GB.1.png | 4 +- ...test_settingsScreen-iPhone-15-pseudo.1.png | 4 +- ...neItemMenu-iPad-en-GB.Unsigned-Devices.png | 4 +- ...eItemMenu-iPad-pseudo.Unsigned-Devices.png | 4 +- ...mMenu-iPhone-15-en-GB.Unsigned-Devices.png | 4 +- ...Menu-iPhone-15-pseudo.Unsigned-Devices.png | 4 +- .../ElementX/TemplateScreenCoordinator.swift | 2 +- ...eactivateAccountScreenViewModelTests.swift | 91 ++++++++++++++ project.yml | 2 +- 41 files changed, 822 insertions(+), 35 deletions(-) create mode 100644 ElementX/Sources/Screens/DeactivateAccountScreen/DeactivateAccountScreenCoordinator.swift create mode 100644 ElementX/Sources/Screens/DeactivateAccountScreen/DeactivateAccountScreenModels.swift create mode 100644 ElementX/Sources/Screens/DeactivateAccountScreen/DeactivateAccountScreenViewModel.swift create mode 100644 ElementX/Sources/Screens/DeactivateAccountScreen/DeactivateAccountScreenViewModelProtocol.swift create mode 100644 ElementX/Sources/Screens/DeactivateAccountScreen/View/DeactivateAccountScreen.swift create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_deactivateAccountScreen-iPad-en-GB.1.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_deactivateAccountScreen-iPad-pseudo.1.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_deactivateAccountScreen-iPhone-15-en-GB.1.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_deactivateAccountScreen-iPhone-15-pseudo.1.png create mode 100644 UnitTests/Sources/DeactivateAccountScreenViewModelTests.swift diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 833090cd0..972d894c1 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -48,6 +48,7 @@ 07756D532EFE33DD1FA258E5 /* GeoURITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A7ED2EF5BDBAD2A7DBC4636 /* GeoURITests.swift */; }; 077CB230153E072C94B1E6C3 /* AppAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D65BCC659FD9087E49B3C25 /* AppAppearance.swift */; }; 07CC13C5729C24255348CBBD /* ElementCallWidgetDriver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 309AD8BAE6437C31BA7157BF /* ElementCallWidgetDriver.swift */; }; + 07F6382E29845D235BFA3308 /* DeactivateAccountScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE78CAD0B964C66FD06EF83E /* DeactivateAccountScreenModels.swift */; }; 086D01E79C8E8D3F004FAF21 /* AudioPlayerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC9104846487244648D32C6D /* AudioPlayerProtocol.swift */; }; 08CB4BD12CEEDE6AAE4A18DD /* WindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035177BCD8E8308B098AC3C2 /* WindowManager.swift */; }; 095C0ACFC234E0550A6404C5 /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FC803282F9268D49F4ABF14 /* AppCoordinator.swift */; }; @@ -162,6 +163,7 @@ 238D561CA231339C6D4D06F3 /* ClientBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1C33355FFB0F0953C35036 /* ClientBuilder.swift */; }; 241CDEFE23819867D9B39066 /* RoomChangePermissionsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE75941583A033A9EDC9FE0 /* RoomChangePermissionsScreenViewModel.swift */; }; 244407B18B2F2D6466BA5961 /* RoomChangeRolesScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82DFA1B7B088D033E0794B82 /* RoomChangeRolesScreenCoordinator.swift */; }; + 244CB93DD7390379D905AFA8 /* DeactivateAccountScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E49D10BFA7E4D70A947888C /* DeactivateAccountScreen.swift */; }; 24A1BBADAC43DC3F3A7347DA /* AnalyticsPromptScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = E53BFB7E4F329621C844E8C3 /* AnalyticsPromptScreen.swift */; }; 24A75F72EEB7561B82D726FD /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2141693488CE5446BB391964 /* Date.swift */; }; 24B7CD41342C143117ADA768 /* Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B1CC9AA154F4D5435BF60A /* Comparable.swift */; }; @@ -333,6 +335,7 @@ 4A85928E27D4C1A548A06EE9 /* StartChatScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 052B2F924572AFD70B5F500E /* StartChatScreenViewModel.swift */; }; 4A9CEEE612D6D8B3DDBD28BA /* RoomListFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24EC819497BB5F8C4998D760 /* RoomListFilterView.swift */; }; 4AAA8606FBA290E23D15422E /* AvatarHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC743C7A85E3171BCBF0A653 /* AvatarHeaderView.swift */; }; + 4AD2B5426DBED97196AA4783 /* DeactivateAccountScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82EE3B877D91030248B1242D /* DeactivateAccountScreenViewModelProtocol.swift */; }; 4B978C09567387EF4366BD7A /* MediaLoaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EF1AC723C2609C7705569CA /* MediaLoaderTests.swift */; }; 4BAB8222DBA0B4207D1223E0 /* NotificationSettingsProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 382B50F7E379B3DBBD174364 /* NotificationSettingsProxyMock.swift */; }; 4BB282209EA82015D0DF8F89 /* NavigationStackCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C698E30698EC59302A8EEBD /* NavigationStackCoordinatorTests.swift */; }; @@ -383,6 +386,7 @@ 55CDD3968D95D1A820B5491E /* PlaceholderAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */; }; 55D18AA4F4A2257642EBDB94 /* GlobalSearchScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38354164AF59C5006CD05878 /* GlobalSearchScreenViewModel.swift */; }; 562EFB9AB62B38830D9AA778 /* TimelineMediaFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = 933B074F006F8E930DB98B4E /* TimelineMediaFrame.swift */; }; + 564910A38858306301C1C21E /* DeactivateAccountScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A1009E4A78F86DA42E1EAF0 /* DeactivateAccountScreenCoordinator.swift */; }; 564BF06B3E93D6DD55F903B2 /* CreateRoomCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C618CA2B6C8758B06C88013C /* CreateRoomCoordinator.swift */; }; 565868808A1DA565707394ED /* CurrentValuePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 127C8472672A5BA09EF1ACF8 /* CurrentValuePublisher.swift */; }; 56DACDD379A86A1F5DEFE7BE /* AuthenticationServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E75948AA1FE1D1A7809931F /* AuthenticationServiceProtocol.swift */; }; @@ -571,6 +575,7 @@ 8015842CB4DE1BE414D2CDED /* AppLockSetupBiometricsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C62E07C1164F5120727A2A8 /* AppLockSetupBiometricsScreenCoordinator.swift */; }; 804C15D8ADE0EA7A5268F58A /* OverridableAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648DD1C10E4957CB791FE0B8 /* OverridableAvatarImage.swift */; }; 80DEA2A4B20F9E279EAE6B2B /* UserProfile+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAD01F7FC2BBAC7351948595 /* UserProfile+Mock.swift */; }; + 80F6C8EFCA4564B67F0D34B0 /* DeactivateAccountScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77F75B3E9F99864048A422A /* DeactivateAccountScreenViewModelTests.swift */; }; 81A7C020CB5F6232242A8414 /* UserSessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F36C0A6D59717193F49EA986 /* UserSessionTests.swift */; }; 8285FF4B2C2331758C437FF7 /* ReportContentScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 713B48DBF65DE4B0DD445D66 /* ReportContentScreenViewModelProtocol.swift */; }; 828EA5009557C2B9DCD4CA0F /* UserDiscoverySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D071F86CD47582B9196C9D16 /* UserDiscoverySection.swift */; }; @@ -893,6 +898,7 @@ C915347779B3C7FDD073A87A /* AVMetadataMachineReadableCodeObjectExtensionsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93E1FF0DFBB3768F79FDBF6D /* AVMetadataMachineReadableCodeObjectExtensionsTest.swift */; }; C97325EFDCCEE457432A9E82 /* MessageText.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E0B4A34E69BD2132BEC521 /* MessageText.swift */; }; C9A631FD968249B4BA0B7B3C /* ReactionsSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02EE0FABA8ED6D6C1D6CE71D /* ReactionsSummaryView.swift */; }; + C9ABF75A43F2D26F1D9A1F27 /* DeactivateAccountScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AC3FDB58F57386741A4FC7F /* DeactivateAccountScreenViewModel.swift */; }; C9BE065FA7D4E77E4C61CB69 /* MapLibreModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = B81B6170DB690013CEB646F4 /* MapLibreModels.swift */; }; C9F5B48D15B9BCAE1F8D564E /* RoomNotificationModeProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1511766C534367700C8DD75 /* RoomNotificationModeProxy.swift */; }; CA12AE0DCD57D49CD96C699A /* WaveformCursorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB9EABCA9348DFA27439A809 /* WaveformCursorView.swift */; }; @@ -1321,6 +1327,7 @@ 1DE7969EBCAF078813E18EA1 /* RoomRolesAndPermissionsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomRolesAndPermissionsScreenModels.swift; sourceTree = ""; }; 1DF8F7A3AD83D04C08D75E01 /* RoomDetailsEditScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenViewModelProtocol.swift; sourceTree = ""; }; 1DFE0E493FB55E5A62E7852A /* ProposedViewSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProposedViewSize.swift; sourceTree = ""; }; + 1E49D10BFA7E4D70A947888C /* DeactivateAccountScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeactivateAccountScreen.swift; sourceTree = ""; }; 1E508AB0EDEE017FF4F6F8D1 /* DTHTMLElement+AttributedStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DTHTMLElement+AttributedStringBuilder.swift"; sourceTree = ""; }; 1F2529D434C750ED78ADF1ED /* UserAgentBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAgentBuilder.swift; sourceTree = ""; }; 1F7C6DDBB5D12F6EF6A3D6E1 /* CollapsibleReactionLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleReactionLayout.swift; sourceTree = ""; }; @@ -1380,6 +1387,7 @@ 29A953B6C0C431DBF4DD00B4 /* RoomSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummary.swift; sourceTree = ""; }; 2A5C6FBF97B6EED3D4FA5EFF /* AttributedStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilder.swift; sourceTree = ""; }; 2AB2C848BB9A7A9B618B7B89 /* TextBasedRoomTimelineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBasedRoomTimelineTests.swift; sourceTree = ""; }; + 2AC3FDB58F57386741A4FC7F /* DeactivateAccountScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeactivateAccountScreenViewModel.swift; sourceTree = ""; }; 2AE807361805463F5AEDD1CA /* VoiceMessagePreviewComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessagePreviewComposer.swift; sourceTree = ""; }; 2AE83A3DD63BCFBB956FE5CB /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nl; path = nl.lproj/Localizable.stringsdict; sourceTree = ""; }; 2AF715D4FD4710EBB637D661 /* SettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -1745,6 +1753,7 @@ 8296D6FB451E25CEC0767BBA /* RoomNotificationSettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsScreenCoordinator.swift; sourceTree = ""; }; 82B612853BFB68373249777B /* SecureBackupKeyBackupScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupKeyBackupScreenViewModel.swift; sourceTree = ""; }; 82DFA1B7B088D033E0794B82 /* RoomChangeRolesScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangeRolesScreenCoordinator.swift; sourceTree = ""; }; + 82EE3B877D91030248B1242D /* DeactivateAccountScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeactivateAccountScreenViewModelProtocol.swift; sourceTree = ""; }; 8319173DD66C07F45DC48848 /* IdentityConfirmedScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmedScreenViewModelProtocol.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 = ""; }; @@ -1781,6 +1790,7 @@ 897DF5E9A70CE05A632FC8AF /* UTType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTType.swift; sourceTree = ""; }; 89AAEA70CFF3284920811941 /* RoomChangePermissionsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangePermissionsScreen.swift; sourceTree = ""; }; 89FBFC09F9DAFF1E4BA97849 /* FormButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormButtonStyles.swift; sourceTree = ""; }; + 8A1009E4A78F86DA42E1EAF0 /* DeactivateAccountScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeactivateAccountScreenCoordinator.swift; sourceTree = ""; }; 8A9AE4967817E9608E22EB44 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = ""; }; 8AE0C9653870803E4F91F474 /* RoomListFiltersStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomListFiltersStateTests.swift; sourceTree = ""; }; 8AE78FA0011E07920AE83135 /* PlainMentionBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlainMentionBuilder.swift; sourceTree = ""; }; @@ -1998,6 +2008,7 @@ BC8AA23D4F37CC26564F63C5 /* LayoutMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutMocks.swift; sourceTree = ""; }; BCDA016D05107DED3B9495CB /* TimelineItemDebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemDebugView.swift; sourceTree = ""; }; BE148A4FFEE853C5A281500C /* UNNotificationContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UNNotificationContent.swift; sourceTree = ""; }; + BE78CAD0B964C66FD06EF83E /* DeactivateAccountScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeactivateAccountScreenModels.swift; sourceTree = ""; }; BE89A8BD65CCE3FCC925CA14 /* TimelineItemReplyDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemReplyDetails.swift; sourceTree = ""; }; BE9BBB18FB27F09032AD8769 /* NotificationPermissionsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPermissionsScreenViewModel.swift; sourceTree = ""; }; BEA38B9851CFCC4D67F5587D /* EmojiPickerScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenCoordinator.swift; sourceTree = ""; }; @@ -2115,6 +2126,7 @@ D66B5D86A9AB95E0E01BED82 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/InfoPlist.strings; 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 = ""; }; + D77F75B3E9F99864048A422A /* DeactivateAccountScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeactivateAccountScreenViewModelTests.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 = ""; }; @@ -2446,6 +2458,14 @@ path = Logging; sourceTree = ""; }; + 07831A7BA411CC407B4727E2 /* View */ = { + isa = PBXGroup; + children = ( + 1E49D10BFA7E4D70A947888C /* DeactivateAccountScreen.swift */, + ); + path = View; + sourceTree = ""; + }; 0787F81684E503024BD0C051 /* Services */ = { isa = PBXGroup; children = ( @@ -3640,6 +3660,18 @@ path = TimelineItemContent; sourceTree = ""; }; + 6C708A9F46EDE1105C640335 /* DeactivateAccountScreen */ = { + isa = PBXGroup; + children = ( + 8A1009E4A78F86DA42E1EAF0 /* DeactivateAccountScreenCoordinator.swift */, + BE78CAD0B964C66FD06EF83E /* DeactivateAccountScreenModels.swift */, + 2AC3FDB58F57386741A4FC7F /* DeactivateAccountScreenViewModel.swift */, + 82EE3B877D91030248B1242D /* DeactivateAccountScreenViewModelProtocol.swift */, + 07831A7BA411CC407B4727E2 /* View */, + ); + path = DeactivateAccountScreen; + sourceTree = ""; + }; 6D7503E64A458DD09E65A3F7 /* View */ = { isa = PBXGroup; children = ( @@ -3769,6 +3801,7 @@ CA29952595B804DA221A0C1D /* ComposerToolbarViewModelTests.swift */, 69D42EE0102D2857933625DD /* CreateRoomViewModelTests.swift */, 3B5E97E9615A158C76B2AB77 /* DateTests.swift */, + D77F75B3E9F99864048A422A /* DeactivateAccountScreenViewModelTests.swift */, 6D0A27607AB09784C8501B5C /* DeveloperOptionsScreenViewModelTests.swift */, A58E93D91DE3288010390DEE /* EmojiDetectionTests.swift */, 099F2D36C141D845A445B1E6 /* EmojiProviderTests.swift */, @@ -5171,6 +5204,7 @@ 1185EECDD07495D65AC84AFC /* CallScreen */, 90DC2E28718955ED87AD1456 /* CreatePollScreen */, C18958141C8ED6D778F779A4 /* CreateRoom */, + 6C708A9F46EDE1105C640335 /* DeactivateAccountScreen */, F5A65D1D3B83593598DC278D /* EmojiPickerScreen */, 8656AFF06650360A5D0695FF /* EncryptionReset */, 448435400B561C40E514BE1C /* FilePreviewScreen */, @@ -6050,6 +6084,7 @@ 0C932A5158C1D0604DFC5750 /* ComposerToolbarViewModelTests.swift in Sources */, D3FD96913D2B1AAA3149DAC7 /* CreateRoomViewModelTests.swift in Sources */, CD0088B763CD970CF1CBF8CB /* DateTests.swift in Sources */, + 80F6C8EFCA4564B67F0D34B0 /* DeactivateAccountScreenViewModelTests.swift in Sources */, 864C69CF951BF36D25BE0C03 /* DeveloperOptionsScreenViewModelTests.swift in Sources */, F697284B9B5F2C00CFEA3B12 /* EmojiDetectionTests.swift in Sources */, 25618589E0DE0F1E95FC7B5C /* EmojiProviderTests.swift in Sources */, @@ -6329,6 +6364,11 @@ C4F69156C31A447FEFF2A47C /* DTHTMLElement+AttributedStringBuilder.swift in Sources */, 9905C1B1C6EFE38F3A6533F3 /* Data.swift in Sources */, C6C06DDA8881260303FBA3A0 /* Date.swift in Sources */, + 244CB93DD7390379D905AFA8 /* DeactivateAccountScreen.swift in Sources */, + 564910A38858306301C1C21E /* DeactivateAccountScreenCoordinator.swift in Sources */, + 07F6382E29845D235BFA3308 /* DeactivateAccountScreenModels.swift in Sources */, + C9ABF75A43F2D26F1D9A1F27 /* DeactivateAccountScreenViewModel.swift in Sources */, + 4AD2B5426DBED97196AA4783 /* DeactivateAccountScreenViewModelProtocol.swift in Sources */, EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */, 5780E444F405AA1304E1C23E /* DeveloperOptionsScreen.swift in Sources */, 5DD85A0FE3D85AEC3C7EFE36 /* DeveloperOptionsScreenCoordinator.swift in Sources */, @@ -7847,7 +7887,7 @@ repositoryURL = "https://github.com/element-hq/compound-ios"; requirement = { kind = revision; - revision = 22f9d801dd001e8aaed0f62546cdb42c7594cf92; + revision = a9270392b3269ef072c47dea623815a9fb87311d; }; }; F76A08D0EA29A07A54F4EB4D /* XCRemoteSwiftPackageReference "swift-collections" */ = { diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 1d61d79de..86931e529 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,7 +15,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/element-hq/compound-ios", "state" : { - "revision" : "22f9d801dd001e8aaed0f62546cdb42c7594cf92" + "revision" : "a9270392b3269ef072c47dea623815a9fb87311d" } }, { diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index 199503724..7bfce082e 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -33,12 +33,14 @@ "action_close" = "Close"; "action_complete_verification" = "Complete verification"; "action_confirm" = "Confirm"; +"action_confirm_password" = "Confirm password"; "action_continue" = "Continue"; "action_copy" = "Copy"; "action_copy_link" = "Copy link"; "action_copy_link_to_message" = "Copy link to message"; "action_create" = "Create"; "action_create_a_room" = "Create a room"; +"action_deactivate" = "Deactivate"; "action_decline" = "Decline"; "action_delete_poll" = "Delete Poll"; "action_disable" = "Disable"; @@ -107,6 +109,7 @@ "action_view_source" = "View source"; "action_yes" = "Yes"; "action.load_more" = "Load more"; +"action_deactivate_account" = "Deactivate account"; "banner_migrate_to_native_sliding_sync_action" = "Log Out & Upgrade"; "banner_migrate_to_native_sliding_sync_description" = "Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later."; "banner_migrate_to_native_sliding_sync_force_logout_title" = "Your homeserver no longer supports the old protocol. Please log out and log back in to continue using the app."; @@ -255,7 +258,7 @@ "emoji_picker_category_people" = "Smileys & People"; "emoji_picker_category_places" = "Travel & Places"; "emoji_picker_category_symbols" = "Symbols"; -"error_account_creation_not_possible" = "Your homeserver needs to be upgraded to support Matrix Authentication Server and account creation."; +"error_account_creation_not_possible" = "Your homeserver needs to be upgraded to support Matrix Authentication Service and account creation."; "error_failed_creating_the_permalink" = "Failed creating the permalink"; "error_failed_loading_map" = "%1$@ could not load the map. Please try again later."; "error_failed_loading_messages" = "Failed loading messages"; @@ -336,7 +339,9 @@ "screen_resolve_send_failure_changed_identity_title" = "Your message was not sent because %1$@’s verified identity has changed"; "screen_resolve_send_failure_unsigned_device_primary_button_title" = "Send message anyway"; "screen_resolve_send_failure_unsigned_device_subtitle" = "%1$@ is using one or more unverified devices. You can send the message anyway, or you can cancel for now and try again later after %2$@ has verified all their devices."; -"screen_resolve_send_failure_unsigned_device_title" = "Your message was not sent because %1$@ has not verified one or more devices"; +"screen_resolve_send_failure_unsigned_device_title" = "Your message was not sent because %1$@ has not verified all devices"; +"screen_resolve_send_failure_you_unsigned_device_subtitle" = "One or more of your devices are unverified. You can send the message anyway, or you can cancel for now and try again later after you have verified all of your devices."; +"screen_resolve_send_failure_you_unsigned_device_title" = "Your message was not sent because you have not verified one or more of your devices"; "screen_room_mentions_at_room_subtitle" = "Notify the whole room"; "screen_room_pinned_banner_indicator" = "%1$@ of %2$@"; "screen_room_pinned_banner_indicator_description" = "%1$@ Pinned messages"; @@ -344,7 +349,8 @@ "screen_room_pinned_banner_view_all_button_title" = "View All"; "screen_room_details_pinned_events_row_title" = "Pinned messages"; "screen_timeline_item_menu_send_failure_changed_identity" = "Message not sent because %1$@’s verified identity has changed."; -"screen_timeline_item_menu_send_failure_unsigned_device" = "Message not sent because %1$@ has not verified one or more devices."; +"screen_timeline_item_menu_send_failure_unsigned_device" = "Message not sent because %1$@ has not verified all devices."; +"screen_timeline_item_menu_send_failure_you_unsigned_device" = "Message not sent because you have not verified one or more of your devices."; "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."; @@ -451,6 +457,17 @@ "screen_create_room_public_option_description" = "Messages are not encrypted and anyone can read them. You can enable encryption at a later date."; "screen_create_room_public_option_title" = "Public room (anyone)"; "screen_create_room_topic_label" = "Topic (optional)"; +"screen_deactivate_account_confirmation_dialog_content" = "Please confirm that you want to deactivate your account. This action cannot be undone."; +"screen_deactivate_account_delete_all_messages" = "Delete all my messages"; +"screen_deactivate_account_delete_all_messages_notice" = "Warning: Future users may see incomplete conversations."; +"screen_deactivate_account_description" = "Deactivating your account is %1$@, it will:"; +"screen_deactivate_account_description_bold_part" = "irreversible"; +"screen_deactivate_account_list_item_1" = "%1$@ your account (you can't log back in, and your ID can't be reused)."; +"screen_deactivate_account_list_item_1_bold_part" = "Permanently disable"; +"screen_deactivate_account_list_item_2" = "Remove you from all chat rooms."; +"screen_deactivate_account_list_item_3" = "Delete your account information from our identity server."; +"screen_deactivate_account_list_item_4" = "Your messages will still be visible to registered users but won’t be available to new or unregistered users if you choose to delete them."; +"screen_deactivate_account_title" = "Deactivate account"; "screen_edit_poll_delete_confirmation" = "Are you sure you want to delete this poll?"; "screen_edit_profile_display_name" = "Display name"; "screen_edit_profile_display_name_placeholder" = "Your display name"; diff --git a/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift index 38bb1813b..109cc75f6 100644 --- a/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift @@ -126,6 +126,8 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol { presentAdvancedSettings() case .developerOptions: presentDeveloperOptions() + case .deactivateAccount: + presentDeactivateAccount() } } .store(in: &cancellables) @@ -238,6 +240,30 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol { navigationStackCoordinator.push(coordinator) } + + private func presentDeactivateAccount() { + let navigationCoordinator = NavigationStackCoordinator() + + let parameters = DeactivateAccountScreenCoordinatorParameters(clientProxy: parameters.userSession.clientProxy, + userIndicatorController: parameters.userIndicatorController) + let coordinator = DeactivateAccountScreenCoordinator(parameters: parameters) + + coordinator.actionsPublisher + .sink { [weak self] action in + guard let self else { return } + + switch action { + case .cancel: + navigationStackCoordinator.setSheetCoordinator(nil) + case .accountDeactivated: + actionsSubject.send(.forceLogout) + } + } + .store(in: &cancellables) + + navigationCoordinator.setRootCoordinator(coordinator) + navigationStackCoordinator.setSheetCoordinator(navigationCoordinator) + } // MARK: OIDC Account Management diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index 106eacc75..4f53c0119 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -96,6 +96,8 @@ internal enum L10n { internal static var actionCompleteVerification: String { return L10n.tr("Localizable", "action_complete_verification") } /// Confirm internal static var actionConfirm: String { return L10n.tr("Localizable", "action_confirm") } + /// Confirm password + internal static var actionConfirmPassword: String { return L10n.tr("Localizable", "action_confirm_password") } /// Continue internal static var actionContinue: String { return L10n.tr("Localizable", "action_continue") } /// Copy @@ -108,6 +110,10 @@ internal enum L10n { internal static var actionCreate: String { return L10n.tr("Localizable", "action_create") } /// Create a room internal static var actionCreateARoom: String { return L10n.tr("Localizable", "action_create_a_room") } + /// Deactivate + internal static var actionDeactivate: String { return L10n.tr("Localizable", "action_deactivate") } + /// Deactivate account + internal static var actionDeactivateAccount: String { return L10n.tr("Localizable", "action_deactivate_account") } /// Decline internal static var actionDecline: String { return L10n.tr("Localizable", "action_decline") } /// Delete Poll @@ -558,7 +564,7 @@ internal enum L10n { internal static var emojiPickerCategoryPlaces: String { return L10n.tr("Localizable", "emoji_picker_category_places") } /// Symbols internal static var emojiPickerCategorySymbols: String { return L10n.tr("Localizable", "emoji_picker_category_symbols") } - /// Your homeserver needs to be upgraded to support Matrix Authentication Server and account creation. + /// Your homeserver needs to be upgraded to support Matrix Authentication Service and account creation. internal static var errorAccountCreationNotPossible: String { return L10n.tr("Localizable", "error_account_creation_not_possible") } /// Failed creating the permalink internal static var errorFailedCreatingThePermalink: String { return L10n.tr("Localizable", "error_failed_creating_the_permalink") } @@ -1041,6 +1047,32 @@ internal enum L10n { internal static var screenCreateRoomTitle: String { return L10n.tr("Localizable", "screen_create_room_title") } /// Topic (optional) internal static var screenCreateRoomTopicLabel: String { return L10n.tr("Localizable", "screen_create_room_topic_label") } + /// Please confirm that you want to deactivate your account. This action cannot be undone. + internal static var screenDeactivateAccountConfirmationDialogContent: String { return L10n.tr("Localizable", "screen_deactivate_account_confirmation_dialog_content") } + /// Delete all my messages + internal static var screenDeactivateAccountDeleteAllMessages: String { return L10n.tr("Localizable", "screen_deactivate_account_delete_all_messages") } + /// Warning: Future users may see incomplete conversations. + internal static var screenDeactivateAccountDeleteAllMessagesNotice: String { return L10n.tr("Localizable", "screen_deactivate_account_delete_all_messages_notice") } + /// Deactivating your account is %1$@, it will: + internal static func screenDeactivateAccountDescription(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_deactivate_account_description", String(describing: p1)) + } + /// irreversible + internal static var screenDeactivateAccountDescriptionBoldPart: String { return L10n.tr("Localizable", "screen_deactivate_account_description_bold_part") } + /// %1$@ your account (you can't log back in, and your ID can't be reused). + internal static func screenDeactivateAccountListItem1(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_deactivate_account_list_item_1", String(describing: p1)) + } + /// Permanently disable + internal static var screenDeactivateAccountListItem1BoldPart: String { return L10n.tr("Localizable", "screen_deactivate_account_list_item_1_bold_part") } + /// Remove you from all chat rooms. + internal static var screenDeactivateAccountListItem2: String { return L10n.tr("Localizable", "screen_deactivate_account_list_item_2") } + /// Delete your account information from our identity server. + internal static var screenDeactivateAccountListItem3: String { return L10n.tr("Localizable", "screen_deactivate_account_list_item_3") } + /// Your messages will still be visible to registered users but won’t be available to new or unregistered users if you choose to delete them. + internal static var screenDeactivateAccountListItem4: String { return L10n.tr("Localizable", "screen_deactivate_account_list_item_4") } + /// Deactivate account + internal static var screenDeactivateAccountTitle: String { return L10n.tr("Localizable", "screen_deactivate_account_title") } /// Block internal static var screenDmDetailsBlockAlertAction: String { return L10n.tr("Localizable", "screen_dm_details_block_alert_action") } /// Blocked users won't be able to send you messages and all their messages will be hidden. You can unblock them anytime. @@ -1475,10 +1507,14 @@ internal enum L10n { internal static func screenResolveSendFailureUnsignedDeviceSubtitle(_ p1: Any, _ p2: Any) -> String { return L10n.tr("Localizable", "screen_resolve_send_failure_unsigned_device_subtitle", String(describing: p1), String(describing: p2)) } - /// Your message was not sent because %1$@ has not verified one or more devices + /// Your message was not sent because %1$@ has not verified all devices internal static func screenResolveSendFailureUnsignedDeviceTitle(_ p1: Any) -> String { return L10n.tr("Localizable", "screen_resolve_send_failure_unsigned_device_title", String(describing: p1)) } + /// One or more of your devices are unverified. You can send the message anyway, or you can cancel for now and try again later after you have verified all of your devices. + internal static var screenResolveSendFailureYouUnsignedDeviceSubtitle: String { return L10n.tr("Localizable", "screen_resolve_send_failure_you_unsigned_device_subtitle") } + /// Your message was not sent because you have not verified one or more of your devices + internal static var screenResolveSendFailureYouUnsignedDeviceTitle: String { return L10n.tr("Localizable", "screen_resolve_send_failure_you_unsigned_device_title") } /// Failed to resolve room alias. internal static var screenRoomAliasResolverResolveAliasFailure: String { return L10n.tr("Localizable", "screen_room_alias_resolver_resolve_alias_failure") } /// Camera @@ -1981,10 +2017,12 @@ internal enum L10n { internal static func screenTimelineItemMenuSendFailureChangedIdentity(_ p1: Any) -> String { return L10n.tr("Localizable", "screen_timeline_item_menu_send_failure_changed_identity", String(describing: p1)) } - /// Message not sent because %1$@ has not verified one or more devices. + /// Message not sent because %1$@ has not verified all devices. internal static func screenTimelineItemMenuSendFailureUnsignedDevice(_ p1: Any) -> String { return L10n.tr("Localizable", "screen_timeline_item_menu_send_failure_unsigned_device", String(describing: p1)) } + /// Message not sent because you have not verified one or more of your devices. + internal static var screenTimelineItemMenuSendFailureYouUnsignedDevice: String { return L10n.tr("Localizable", "screen_timeline_item_menu_send_failure_you_unsigned_device") } /// Location internal static var screenViewLocationTitle: String { return L10n.tr("Localizable", "screen_view_location_title") } /// Calls, polls, search and more will be added later this year. diff --git a/ElementX/Sources/Mocks/ClientProxyMock.swift b/ElementX/Sources/Mocks/ClientProxyMock.swift index 3d4421791..e727ba6af 100644 --- a/ElementX/Sources/Mocks/ClientProxyMock.swift +++ b/ElementX/Sources/Mocks/ClientProxyMock.swift @@ -47,6 +47,7 @@ extension ClientProxyMock { isOnlyDeviceLeftReturnValue = .success(false) accountURLActionReturnValue = "https://matrix.org/account" + canDeactivateAccount = false directRoomForUserIDReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) createDirectRoomWithExpectedRoomNameReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) createRoomNameTopicIsRoomPrivateUserIDsAvatarURLReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 1b788c023..78eeae919 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -1947,6 +1947,11 @@ class ClientProxyMock: ClientProxyProtocol { } var underlyingAvailableSlidingSyncVersions: [SlidingSyncVersion]! var availableSlidingSyncVersionsClosure: (() async -> [SlidingSyncVersion])? + var canDeactivateAccount: Bool { + get { return underlyingCanDeactivateAccount } + set(value) { underlyingCanDeactivateAccount = value } + } + var underlyingCanDeactivateAccount: Bool! var userIDServerName: String? var userDisplayNamePublisher: CurrentValuePublisher { get { return underlyingUserDisplayNamePublisher } @@ -3209,6 +3214,76 @@ class ClientProxyMock: ClientProxyProtocol { return sessionVerificationControllerProxyReturnValue } } + //MARK: - deactivateAccount + + var deactivateAccountPasswordEraseDataUnderlyingCallsCount = 0 + var deactivateAccountPasswordEraseDataCallsCount: Int { + get { + if Thread.isMainThread { + return deactivateAccountPasswordEraseDataUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = deactivateAccountPasswordEraseDataUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + deactivateAccountPasswordEraseDataUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + deactivateAccountPasswordEraseDataUnderlyingCallsCount = newValue + } + } + } + } + var deactivateAccountPasswordEraseDataCalled: Bool { + return deactivateAccountPasswordEraseDataCallsCount > 0 + } + var deactivateAccountPasswordEraseDataReceivedArguments: (password: String?, eraseData: Bool)? + var deactivateAccountPasswordEraseDataReceivedInvocations: [(password: String?, eraseData: Bool)] = [] + + var deactivateAccountPasswordEraseDataUnderlyingReturnValue: Result! + var deactivateAccountPasswordEraseDataReturnValue: Result! { + get { + if Thread.isMainThread { + return deactivateAccountPasswordEraseDataUnderlyingReturnValue + } else { + var returnValue: Result? = nil + DispatchQueue.main.sync { + returnValue = deactivateAccountPasswordEraseDataUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + deactivateAccountPasswordEraseDataUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + deactivateAccountPasswordEraseDataUnderlyingReturnValue = newValue + } + } + } + } + var deactivateAccountPasswordEraseDataClosure: ((String?, Bool) async -> Result)? + + func deactivateAccount(password: String?, eraseData: Bool) async -> Result { + deactivateAccountPasswordEraseDataCallsCount += 1 + deactivateAccountPasswordEraseDataReceivedArguments = (password: password, eraseData: eraseData) + DispatchQueue.main.async { + self.deactivateAccountPasswordEraseDataReceivedInvocations.append((password: password, eraseData: eraseData)) + } + if let deactivateAccountPasswordEraseDataClosure = deactivateAccountPasswordEraseDataClosure { + return await deactivateAccountPasswordEraseDataClosure(password, eraseData) + } else { + return deactivateAccountPasswordEraseDataReturnValue + } + } //MARK: - logout var logoutUnderlyingCallsCount = 0 diff --git a/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift index f7b9cff3c..152c0940c 100644 --- a/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift @@ -485,6 +485,71 @@ open class ClientSDKMock: MatrixRustSDK.Client { } } + //MARK: - canDeactivateAccount + + var canDeactivateAccountUnderlyingCallsCount = 0 + open var canDeactivateAccountCallsCount: Int { + get { + if Thread.isMainThread { + return canDeactivateAccountUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = canDeactivateAccountUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + canDeactivateAccountUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + canDeactivateAccountUnderlyingCallsCount = newValue + } + } + } + } + open var canDeactivateAccountCalled: Bool { + return canDeactivateAccountCallsCount > 0 + } + + var canDeactivateAccountUnderlyingReturnValue: Bool! + open var canDeactivateAccountReturnValue: Bool! { + get { + if Thread.isMainThread { + return canDeactivateAccountUnderlyingReturnValue + } else { + var returnValue: Bool? = nil + DispatchQueue.main.sync { + returnValue = canDeactivateAccountUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + canDeactivateAccountUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + canDeactivateAccountUnderlyingReturnValue = newValue + } + } + } + } + open var canDeactivateAccountClosure: (() -> Bool)? + + open override func canDeactivateAccount() -> Bool { + canDeactivateAccountCallsCount += 1 + if let canDeactivateAccountClosure = canDeactivateAccountClosure { + return canDeactivateAccountClosure() + } else { + return canDeactivateAccountReturnValue + } + } + //MARK: - createRoom open var createRoomRequestThrowableError: Error? @@ -560,6 +625,52 @@ open class ClientSDKMock: MatrixRustSDK.Client { } } + //MARK: - deactivateAccount + + open var deactivateAccountAuthDataEraseDataThrowableError: Error? + var deactivateAccountAuthDataEraseDataUnderlyingCallsCount = 0 + open var deactivateAccountAuthDataEraseDataCallsCount: Int { + get { + if Thread.isMainThread { + return deactivateAccountAuthDataEraseDataUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = deactivateAccountAuthDataEraseDataUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + deactivateAccountAuthDataEraseDataUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + deactivateAccountAuthDataEraseDataUnderlyingCallsCount = newValue + } + } + } + } + open var deactivateAccountAuthDataEraseDataCalled: Bool { + return deactivateAccountAuthDataEraseDataCallsCount > 0 + } + open var deactivateAccountAuthDataEraseDataReceivedArguments: (authData: AuthData?, eraseData: Bool)? + open var deactivateAccountAuthDataEraseDataReceivedInvocations: [(authData: AuthData?, eraseData: Bool)] = [] + open var deactivateAccountAuthDataEraseDataClosure: ((AuthData?, Bool) async throws -> Void)? + + open override func deactivateAccount(authData: AuthData?, eraseData: Bool) async throws { + if let error = deactivateAccountAuthDataEraseDataThrowableError { + throw error + } + deactivateAccountAuthDataEraseDataCallsCount += 1 + deactivateAccountAuthDataEraseDataReceivedArguments = (authData: authData, eraseData: eraseData) + DispatchQueue.main.async { + self.deactivateAccountAuthDataEraseDataReceivedInvocations.append((authData: authData, eraseData: eraseData)) + } + try await deactivateAccountAuthDataEraseDataClosure?(authData, eraseData) + } + //MARK: - deletePusher open var deletePusherIdentifiersThrowableError: Error? diff --git a/ElementX/Sources/Screens/DeactivateAccountScreen/DeactivateAccountScreenCoordinator.swift b/ElementX/Sources/Screens/DeactivateAccountScreen/DeactivateAccountScreenCoordinator.swift new file mode 100644 index 000000000..a6ff98fe1 --- /dev/null +++ b/ElementX/Sources/Screens/DeactivateAccountScreen/DeactivateAccountScreenCoordinator.swift @@ -0,0 +1,57 @@ +// +// Copyright 2022-2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Combine +import SwiftUI + +struct DeactivateAccountScreenCoordinatorParameters { + let clientProxy: ClientProxyProtocol + let userIndicatorController: UserIndicatorControllerProtocol +} + +enum DeactivateAccountScreenCoordinatorAction { + case cancel + case accountDeactivated +} + +final class DeactivateAccountScreenCoordinator: CoordinatorProtocol { + private let parameters: DeactivateAccountScreenCoordinatorParameters + private let viewModel: DeactivateAccountScreenViewModelProtocol + + private var cancellables = Set() + + private let actionsSubject: PassthroughSubject = .init() + var actionsPublisher: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(parameters: DeactivateAccountScreenCoordinatorParameters) { + self.parameters = parameters + + viewModel = DeactivateAccountScreenViewModel(clientProxy: parameters.clientProxy, + userIndicatorController: parameters.userIndicatorController) + } + + func start() { + viewModel.actionsPublisher.sink { [weak self] action in + MXLog.info("Coordinator: received view model action: \(action)") + + guard let self else { return } + switch action { + case .cancel: + actionsSubject.send(.cancel) + case .accountDeactivated: + actionsSubject.send(.accountDeactivated) + } + } + .store(in: &cancellables) + } + + func toPresentable() -> AnyView { + AnyView(DeactivateAccountScreen(context: viewModel.context)) + } +} diff --git a/ElementX/Sources/Screens/DeactivateAccountScreen/DeactivateAccountScreenModels.swift b/ElementX/Sources/Screens/DeactivateAccountScreen/DeactivateAccountScreenModels.swift new file mode 100644 index 000000000..f3fb7c088 --- /dev/null +++ b/ElementX/Sources/Screens/DeactivateAccountScreen/DeactivateAccountScreenModels.swift @@ -0,0 +1,54 @@ +// +// Copyright 2022-2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Foundation + +enum DeactivateAccountScreenViewModelAction { + case cancel + case accountDeactivated +} + +struct DeactivateAccountScreenViewState: BindableState { + let info: AttributedString + let infoPoint1: AttributedString + let infoPoint2 = AttributedString(L10n.screenDeactivateAccountListItem2) + let infoPoint3 = AttributedString(L10n.screenDeactivateAccountListItem3) + let infoPoint4 = AttributedString(L10n.screenDeactivateAccountListItem4) + + var bindings = DeactivateAccountScreenViewStateBindings() + + init() { + let boldPlaceholder = "{bold}" + var attributedString = AttributedString(L10n.screenDeactivateAccountDescription(boldPlaceholder)) + var boldString = AttributedString(L10n.screenDeactivateAccountDescriptionBoldPart) + boldString.bold() + attributedString.replace(boldPlaceholder, with: boldString) + info = attributedString + + attributedString = AttributedString(L10n.screenDeactivateAccountListItem1(boldPlaceholder)) + boldString = AttributedString(L10n.screenDeactivateAccountListItem1BoldPart) + boldString.bold() + attributedString.replace(boldPlaceholder, with: boldString) + infoPoint1 = attributedString + } +} + +struct DeactivateAccountScreenViewStateBindings { + var password = "" + var eraseData = false + var alertInfo: AlertInfo? +} + +enum DeactivateAccountScreenAlert { + case confirmation + case deactivationFailed +} + +enum DeactivateAccountScreenViewAction { + case deactivate + case cancel +} diff --git a/ElementX/Sources/Screens/DeactivateAccountScreen/DeactivateAccountScreenViewModel.swift b/ElementX/Sources/Screens/DeactivateAccountScreen/DeactivateAccountScreenViewModel.swift new file mode 100644 index 000000000..36b29a027 --- /dev/null +++ b/ElementX/Sources/Screens/DeactivateAccountScreen/DeactivateAccountScreenViewModel.swift @@ -0,0 +1,84 @@ +// +// Copyright 2022-2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Combine +import SwiftUI + +typealias DeactivateAccountScreenViewModelType = StateStoreViewModel + +class DeactivateAccountScreenViewModel: DeactivateAccountScreenViewModelType, DeactivateAccountScreenViewModelProtocol { + private let clientProxy: ClientProxyProtocol + private let userIndicatorController: UserIndicatorControllerProtocol + + private let actionsSubject: PassthroughSubject = .init() + var actionsPublisher: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(clientProxy: ClientProxyProtocol, userIndicatorController: UserIndicatorControllerProtocol) { + self.clientProxy = clientProxy + self.userIndicatorController = userIndicatorController + + super.init(initialViewState: DeactivateAccountScreenViewState()) + } + + override func process(viewAction: DeactivateAccountScreenViewAction) { + MXLog.info("View model: received view action: \(viewAction)") + + switch viewAction { + case .cancel: + actionsSubject.send(.cancel) + case .deactivate: + showDeactivationConfirmation() + } + } + + // MARK: - Private + + private let deactivatingIndicatorID = "\(DeactivateAccountScreenViewModel.self)-Deactivating" + + func showDeactivationConfirmation() { + state.bindings.alertInfo = .init(id: .confirmation, + title: L10n.screenDeactivateAccountTitle, + message: L10n.screenDeactivateAccountConfirmationDialogContent, + primaryButton: .init(title: L10n.actionDeactivate, action: { + Task { await self.deactivateAccount() } + }), + secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil)) + } + + func deactivateAccount() async { + userIndicatorController.submitIndicator(UserIndicator(id: deactivatingIndicatorID, + type: .modal(progress: .indeterminate, interactiveDismissDisabled: true, allowsInteraction: false), + title: L10n.commonPleaseWait, + persistent: true)) + + MXLog.warning("Deactivating account.") + + switch await clientProxy.deactivateAccount(password: nil, eraseData: state.bindings.eraseData) { + case .success: + MXLog.info("Account deactivated (no password needed).") + actionsSubject.send(.accountDeactivated) + return + case .failure: + MXLog.info("Request failed, including password.") + } + + switch await clientProxy.deactivateAccount(password: state.bindings.password, eraseData: state.bindings.eraseData) { + case .success: + MXLog.info("Account deactivated.") + actionsSubject.send(.accountDeactivated) + return + case .failure(let failure): + MXLog.info("Deactivation failed \(failure).") + state.bindings.alertInfo = .init(id: .deactivationFailed, + title: L10n.errorUnknown, + message: String(describing: failure)) + userIndicatorController.retractIndicatorWithId(deactivatingIndicatorID) + } + } +} diff --git a/ElementX/Sources/Screens/DeactivateAccountScreen/DeactivateAccountScreenViewModelProtocol.swift b/ElementX/Sources/Screens/DeactivateAccountScreen/DeactivateAccountScreenViewModelProtocol.swift new file mode 100644 index 000000000..4321e45bf --- /dev/null +++ b/ElementX/Sources/Screens/DeactivateAccountScreen/DeactivateAccountScreenViewModelProtocol.swift @@ -0,0 +1,14 @@ +// +// Copyright 2022-2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Combine + +@MainActor +protocol DeactivateAccountScreenViewModelProtocol { + var actionsPublisher: AnyPublisher { get } + var context: DeactivateAccountScreenViewModelType.Context { get } +} diff --git a/ElementX/Sources/Screens/DeactivateAccountScreen/View/DeactivateAccountScreen.swift b/ElementX/Sources/Screens/DeactivateAccountScreen/View/DeactivateAccountScreen.swift new file mode 100644 index 000000000..c26c1b52f --- /dev/null +++ b/ElementX/Sources/Screens/DeactivateAccountScreen/View/DeactivateAccountScreen.swift @@ -0,0 +1,111 @@ +// +// Copyright 2022-2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Compound +import SwiftUI + +struct DeactivateAccountScreen: View { + @ObservedObject var context: DeactivateAccountScreenViewModel.Context + + var body: some View { + Form { + infoSection + eraseDataSection + passwordSection + } + .compoundList() + .safeAreaInset(edge: .bottom) { + Button(L10n.actionDeactivateAccount, role: .destructive) { + context.send(viewAction: .deactivate) + } + .buttonStyle(.compound(.primary)) + .disabled(context.password.isEmpty) + .padding(16) + .background(Color.compound.bgSubtleSecondaryLevel0.ignoresSafeArea()) + } + .navigationTitle(L10n.screenDeactivateAccountTitle) + .navigationBarTitleDisplayMode(.inline) + .toolbar { toolbar } + .alert(item: $context.alertInfo) + } + + private var infoSection: some View { + ListRow(kind: .custom { + VStack(alignment: .leading, spacing: 16) { + Text(context.viewState.info) + + VStack(alignment: .leading, spacing: 8) { + InfoItem(title: context.viewState.infoPoint1) + InfoItem(title: context.viewState.infoPoint2) + InfoItem(title: context.viewState.infoPoint3) + InfoItem(title: context.viewState.infoPoint4, isSuccess: true) + } + } + .foregroundColor(.compound.textSecondary) + .font(.compound.bodyMD) + .listRowBackground(Color.clear) + }) + } + + private var eraseDataSection: some View { + Section { + ListRow(label: .plain(title: L10n.screenDeactivateAccountDeleteAllMessages), + kind: .toggle($context.eraseData)) + } footer: { + Text(L10n.screenDeactivateAccountDeleteAllMessagesNotice) + .compoundListSectionFooter() + } + } + + private var passwordSection: some View { + Section { + ListRow(label: .plain(title: L10n.commonPassword), + kind: .secureField(text: $context.password)) + .submitLabel(.done) + } header: { + Text(L10n.actionConfirmPassword) + .compoundListSectionHeader() + } + } + + private var toolbar: some ToolbarContent { + ToolbarItem(placement: .cancellationAction) { + Button(L10n.actionCancel) { + context.send(viewAction: .cancel) + } + } + } +} + +private struct InfoItem: View { + let title: AttributedString + var isSuccess = false + + var body: some View { + Label { + Text(title).padding(.vertical, 1) + } icon: { + CompoundIcon(isSuccess ? \.check : \.close, + size: .small, + relativeTo: .compound.bodyMD) + .foregroundStyle(isSuccess ? .compound.iconSuccessPrimary : .compound.iconCriticalPrimary) + } + .labelStyle(.custom(spacing: 8, alignment: .top)) + } +} + +// MARK: - Previews + +struct DeactivateAccountScreen_Previews: PreviewProvider, TestablePreview { + static let viewModel = DeactivateAccountScreenViewModel(clientProxy: ClientProxyMock(.init()), + userIndicatorController: UserIndicatorControllerMock()) + static var previews: some View { + NavigationStack { + DeactivateAccountScreen(context: viewModel.context) + } + } +} diff --git a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/View/EncryptionResetPasswordScreen.swift b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/View/EncryptionResetPasswordScreen.swift index 33eb491fb..20b934e1c 100644 --- a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/View/EncryptionResetPasswordScreen.swift +++ b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/View/EncryptionResetPasswordScreen.swift @@ -36,6 +36,7 @@ struct EncryptionResetPasswordScreen: View { } .buttonStyle(.compound(.primary)) } + .background() .backgroundStyle(.compound.bgCanvasDefault) .interactiveDismissDisabled() .onAppear { textFieldFocus = true } @@ -49,6 +50,7 @@ struct EncryptionResetPasswordScreen: View { .font(.compound.bodySMSemibold) SecureField(L10n.screenResetEncryptionPasswordPlaceholder, text: $context.password) + .tint(.compound.iconAccentTertiary) .frame(maxWidth: .infinity) .padding() .background(Color.compound.bgSubtleSecondaryLevel0) diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/View/SecureBackupRecoveryKeyScreen.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/View/SecureBackupRecoveryKeyScreen.swift index a539a3dbe..f9d17174a 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/View/SecureBackupRecoveryKeyScreen.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/View/SecureBackupRecoveryKeyScreen.swift @@ -196,6 +196,7 @@ struct SecureBackupRecoveryKeyScreen: View { .font(.compound.bodySMSemibold) SecureField(L10n.screenRecoveryKeyConfirmKeyPlaceholder, text: $context.confirmationRecoveryKey) + .tint(.compound.iconAccentTertiary) .frame(maxWidth: .infinity) .padding() .background(Color.compound.bgSubtleSecondaryLevel0) diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift index d889b00ab..d1ac3ee8d 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift @@ -27,6 +27,7 @@ enum SettingsScreenCoordinatorAction { case notifications case advancedSettings case developerOptions + case deactivateAccount } final class SettingsScreenCoordinator: CoordinatorProtocol { @@ -75,6 +76,8 @@ final class SettingsScreenCoordinator: CoordinatorProtocol { actionsSubject.send(.developerOptions) case .logout: actionsSubject.send(.logout) + case .deactivateAccount: + actionsSubject.send(.deactivateAccount) } } .store(in: &cancellables) diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift index 26afd01a0..f16b531c9 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift @@ -22,6 +22,7 @@ enum SettingsScreenViewModelAction: Equatable { case advancedSettings case developerOptions case logout + case deactivateAccount } enum SettingsScreenSecuritySectionMode { @@ -34,6 +35,7 @@ struct SettingsScreenViewState: BindableState { var userID: String var accountProfileURL: URL? var accountSessionsListURL: URL? + var showAccountDeactivation: Bool var userAvatarURL: URL? var userDisplayName: String? var showDeveloperOptions: Bool @@ -42,6 +44,12 @@ struct SettingsScreenViewState: BindableState { var showSecuritySectionBadge = false var showBlockedUsers = false + + var bindings = SettingsScreenViewStateBindings() +} + +struct SettingsScreenViewStateBindings { + var isPresentingAccountDeactivationConfirmation = false } enum SettingsScreenViewAction { @@ -59,4 +67,5 @@ enum SettingsScreenViewAction { case developerOptions case advancedSettings case logout + case deactivateAccount } diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift index f19db509d..d9001b3e5 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift @@ -20,6 +20,7 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo init(userSession: UserSessionProtocol) { super.init(initialViewState: .init(deviceID: userSession.clientProxy.deviceID, userID: userSession.clientProxy.userID, + showAccountDeactivation: userSession.clientProxy.canDeactivateAccount, showDeveloperOptions: AppSettings.isDevelopmentBuild), mediaProvider: userSession.mediaProvider) @@ -105,6 +106,8 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo state.showDeveloperOptions = true case .developerOptions: actionsSubject.send(.developerOptions) + case .deactivateAccount: + actionsSubject.send(.deactivateAccount) } } } diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift index f12b68637..ea20de92b 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift @@ -29,6 +29,8 @@ struct SettingsScreen: View { } generalSection + + signOutSection } .compoundList() .navigationTitle(L10n.commonSettings) @@ -167,7 +169,11 @@ struct SettingsScreen: View { }) .accessibilityIdentifier(A11yIdentifiers.settingsScreen.developerOptions) } - + } + } + + private var signOutSection: some View { + Section { ListRow(label: .action(title: L10n.screenSignoutPreferenceItem, icon: \.signOut, role: .destructive), @@ -175,6 +181,14 @@ struct SettingsScreen: View { context.send(viewAction: .logout) }) .accessibilityIdentifier(A11yIdentifiers.settingsScreen.logout) + if context.viewState.showAccountDeactivation { + ListRow(label: .action(title: L10n.actionDeactivateAccount, + icon: \.warning, + role: .destructive), + kind: .button { + context.send(viewAction: .deactivateAccount) + }) + } } footer: { VStack(spacing: 0) { versionText diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index c1fedbb38..b205d5aed 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -213,6 +213,10 @@ class ClientProxy: ClientProxyProtocol { } } + var canDeactivateAccount: Bool { + client.canDeactivateAccount() + } + var userIDServerName: String? { do { return try client.userIdServerName() @@ -546,6 +550,16 @@ class ClientProxy: ClientProxyProtocol { } } + func deactivateAccount(password: String?, eraseData: Bool) async -> Result { + do { + try await client.deactivateAccount(authData: password.map { .password(passwordDetails: .init(identifier: userID, password: $0)) }, + eraseData: eraseData) + return .success(()) + } catch { + return .failure(.sdkError(error)) + } + } + func setPusher(with configuration: PusherConfiguration) async throws { try await client.setPusher(identifiers: configuration.identifiers, kind: configuration.kind, diff --git a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift index eba44ddaf..5eca4a140 100644 --- a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift +++ b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift @@ -99,6 +99,8 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol { var slidingSyncVersion: SlidingSyncVersion { get } var availableSlidingSyncVersions: [SlidingSyncVersion] { get async } + var canDeactivateAccount: Bool { get } + var userIDServerName: String? { get } var userDisplayNamePublisher: CurrentValuePublisher { get } @@ -157,6 +159,8 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol { func sessionVerificationControllerProxy() async -> Result + func deactivateAccount(password: String?, eraseData: Bool) async -> Result + func logout() async -> URL? func setPusher(with configuration: PusherConfiguration) async throws diff --git a/PreviewTests/Sources/PreviewTests.swift b/PreviewTests/Sources/PreviewTests.swift index e5be4bc5a..795c9e0b0 100644 --- a/PreviewTests/Sources/PreviewTests.swift +++ b/PreviewTests/Sources/PreviewTests.swift @@ -147,6 +147,12 @@ class PreviewTests: XCTestCase { } } + func test_deactivateAccountScreen() { + for preview in DeactivateAccountScreen_Previews._allPreviews { + assertSnapshots(matching: preview) + } + } + func test_emojiPickerScreenHeaderView() { for preview in EmojiPickerScreenHeaderView_Previews._allPreviews { assertSnapshots(matching: preview) diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_deactivateAccountScreen-iPad-en-GB.1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_deactivateAccountScreen-iPad-en-GB.1.png new file mode 100644 index 000000000..3f0eaf439 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_deactivateAccountScreen-iPad-en-GB.1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49a19ce44c0cc157d621ad1d5067e9f0437646804778dae6b7090dac2a8d0cbe +size 168006 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_deactivateAccountScreen-iPad-pseudo.1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_deactivateAccountScreen-iPad-pseudo.1.png new file mode 100644 index 000000000..217e2cf6c --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_deactivateAccountScreen-iPad-pseudo.1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f92b9a6497e4b1262ff279021ced2787d662e243e85e1a11d43e36d495cab9c8 +size 214697 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_deactivateAccountScreen-iPhone-15-en-GB.1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_deactivateAccountScreen-iPhone-15-en-GB.1.png new file mode 100644 index 000000000..b865a950d --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_deactivateAccountScreen-iPhone-15-en-GB.1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:edd11721586663568ff973660f16fcff783dabba4bf5a2805caa0aa956ec2fd7 +size 124025 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_deactivateAccountScreen-iPhone-15-pseudo.1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_deactivateAccountScreen-iPhone-15-pseudo.1.png new file mode 100644 index 000000000..9c203d9cb --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_deactivateAccountScreen-iPhone-15-pseudo.1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f4b0057cb0a667fee987afdad9fe1bf2bc4ee3455e2e87f1a5eac98d51be364 +size 192025 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPad-en-GB.Unsigned-Device.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPad-en-GB.Unsigned-Device.png index ff34cc539..da16c7232 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPad-en-GB.Unsigned-Device.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPad-en-GB.Unsigned-Device.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4555eb5aab386a6dc73622b063a871f3d2c103e9078d14e0f65ec8d072d6cd0a -size 124524 +oid sha256:89391abf747aef94c9b315e19607fd4256956d269d2f53141477952e604e8d24 +size 123919 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPad-pseudo.Unsigned-Device.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPad-pseudo.Unsigned-Device.png index 938f9e81e..a618927ca 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPad-pseudo.Unsigned-Device.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPad-pseudo.Unsigned-Device.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:197d010abc66e31f74620898772fccdbe9b15e77ca72c764be65fca3e96ef5bd -size 160426 +oid sha256:f58a9af3a9780ad49b7b5a90fa393b8f69bc1c752713477cbfb574389d101840 +size 159142 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPhone-15-en-GB.Unsigned-Device.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPhone-15-en-GB.Unsigned-Device.png index df5833dbc..55ad8a9b9 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPhone-15-en-GB.Unsigned-Device.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPhone-15-en-GB.Unsigned-Device.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e6651a6be9527f47ede782f8a7f3a0c630884b4d2f14c34d67e3a7262bb39908 -size 89346 +oid sha256:c37c7425834dd88af07803bf1a414cfc6155957874cd2fe5a09fd6e17ff5458b +size 88086 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPhone-15-pseudo.Unsigned-Device.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPhone-15-pseudo.Unsigned-Device.png index c6d6dad5a..b7a343a63 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPhone-15-pseudo.Unsigned-Device.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPhone-15-pseudo.Unsigned-Device.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:90f15401000e28a427e45f5a3d2e4abaa6dd6074bdd541a04ad1ec5fc062b891 -size 134865 +oid sha256:63fbe1cf5d4c4b23b1f6fbe2b33d41c0d74654ccd33a473fd71527b7aa75a921 +size 134230 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_settingsScreen-iPad-en-GB.1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_settingsScreen-iPad-en-GB.1.png index aa489c1c8..0fc2de7af 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_settingsScreen-iPad-en-GB.1.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_settingsScreen-iPad-en-GB.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e5c825d5ed31fa05047c973d512e15d19e30840959115f821511587181a7c34 -size 165789 +oid sha256:86dae0a192710571d948d4bb45a7eecace0b7fcc242bd03692f34f1e2cdf493a +size 165901 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_settingsScreen-iPad-pseudo.1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_settingsScreen-iPad-pseudo.1.png index 6b9a9d66f..a0e97043e 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_settingsScreen-iPad-pseudo.1.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_settingsScreen-iPad-pseudo.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d367574abb9dd564373e2a701bff79b38a966ae58fe0d9ca2148bacd093a7369 -size 183521 +oid sha256:dbedd8dd0b052ed8fedf803adbc1eb0e91b4a0ea3b725cb771b63c6b906d0e12 +size 183660 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_settingsScreen-iPhone-15-en-GB.1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_settingsScreen-iPhone-15-en-GB.1.png index 89294a46f..b342ee452 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_settingsScreen-iPhone-15-en-GB.1.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_settingsScreen-iPhone-15-en-GB.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7f982a5b5ef3fa972445a1c8b8f71052a02bb8fcd00284b7767eb3f2e055547 -size 106445 +oid sha256:92933fcdfa65c428e73e8533bb2d53db209d52ee7cbb89668885fcde9326b60c +size 106451 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_settingsScreen-iPhone-15-pseudo.1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_settingsScreen-iPhone-15-pseudo.1.png index c191c5f8a..a54efe6f7 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_settingsScreen-iPhone-15-pseudo.1.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_settingsScreen-iPhone-15-pseudo.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:227b46eab3e664ecb78f40031814589e6cfd0416c9babe8b0a4aaedfc45eabca -size 123175 +oid sha256:5031a19fe97819af6b9de3c8a0b746e7b4b58293edb5c9a83549324057c3e6bf +size 123184 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Unsigned-Devices.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Unsigned-Devices.png index bccb72682..4d94dbcc2 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Unsigned-Devices.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Unsigned-Devices.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:268ca2367cd6fae133b5aa193bac1516a4a5ed56268bb5b061453bd607501398 -size 138558 +oid sha256:6e0f5abc613626d47406cb2cdad815a735d3e3a1bc49a6e8202d7e11d4ec03a0 +size 137485 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Unsigned-Devices.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Unsigned-Devices.png index 925b2e73c..f3a1644db 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Unsigned-Devices.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Unsigned-Devices.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:25c106563019a4e83c4537f3063b8a18c23bde138ba724dc03d54f289ae8efbb -size 145432 +oid sha256:459f29fc5cf3bdc3ab64ca8224235ed15ec2e81279a953f3dbd8b3564364ba16 +size 144009 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Unsigned-Devices.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Unsigned-Devices.png index 3da17ab0c..d730a6e7b 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Unsigned-Devices.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Unsigned-Devices.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74260e444cf6dffe7302066e81901b4e235d967a71586528e1a5a74691487f24 -size 91442 +oid sha256:766145a34e12c8d2bb0c9eb9ed70f0b72b5dca3268ce4fc9619ea3133b29a995 +size 90291 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Unsigned-Devices.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Unsigned-Devices.png index 852eaf1d3..3bea77814 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Unsigned-Devices.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Unsigned-Devices.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a212b2a93a757524ffc9085ac15e0c115add5b20f353507632d6eb7c794701b0 -size 105467 +oid sha256:69cc665b75d48187715e23e980891ad25b4a3ce201a852c55ab54827ad7dfdc9 +size 103997 diff --git a/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateScreenCoordinator.swift b/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateScreenCoordinator.swift index efd0dae4e..6df6e89f3 100644 --- a/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateScreenCoordinator.swift +++ b/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateScreenCoordinator.swift @@ -42,7 +42,7 @@ final class TemplateScreenCoordinator: CoordinatorProtocol { guard let self else { return } switch action { case .done: - self.actionsSubject.send(.done) + actionsSubject.send(.done) } } .store(in: &cancellables) diff --git a/UnitTests/Sources/DeactivateAccountScreenViewModelTests.swift b/UnitTests/Sources/DeactivateAccountScreenViewModelTests.swift new file mode 100644 index 000000000..e38c491b6 --- /dev/null +++ b/UnitTests/Sources/DeactivateAccountScreenViewModelTests.swift @@ -0,0 +1,91 @@ +// +// Copyright 2022-2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import XCTest + +@testable import ElementX + +@MainActor +class DeactivateAccountScreenViewModelTests: XCTestCase { + var clientProxy: ClientProxyMock! + var viewModel: DeactivateAccountScreenViewModelProtocol! + + var context: DeactivateAccountScreenViewModelType.Context { + viewModel.context + } + + override func setUpWithError() throws { + clientProxy = ClientProxyMock(.init()) + viewModel = DeactivateAccountScreenViewModel(clientProxy: clientProxy, userIndicatorController: UserIndicatorControllerMock()) + } + + func testDeactivate() async throws { + try await validateDeactivate(erasingData: false) + } + + func testDeactivateAndErase() async throws { + try await validateDeactivate(erasingData: true) + } + + func validateDeactivate(erasingData shouldErase: Bool) async throws { + let enteredPassword = UUID().uuidString + + clientProxy.deactivateAccountPasswordEraseDataClosure = { [weak self] password, eraseData in + guard let self else { return .failure(.sdkError(ClientProxyMockError.generic)) } + + if clientProxy.deactivateAccountPasswordEraseDataCallsCount == 1 { + if password != nil { + XCTFail("The password shouldn't be sent first time round.") + } + if eraseData != shouldErase { + XCTFail("The erase parameter is unexpected.") + } + return .failure(.sdkError(ClientProxyMockError.generic)) + } else { + if password != enteredPassword { + XCTFail("The password should match the user's input on the second call.") + } + if eraseData != shouldErase { + XCTFail("The erase parameter is unexpected.") + } + return .success(()) + } + } + + context.eraseData = shouldErase + context.password = enteredPassword + + XCTAssertNil(context.alertInfo) + + let deferredState = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil } + context.send(viewAction: .deactivate) + try await deferredState.fulfill() + + guard let confirmationAction = context.alertInfo?.primaryButton.action else { + XCTFail("Couldn't find the confirmation action.") + return + } + + let deferredAction = deferFulfillment(viewModel.actionsPublisher) { $0 == .accountDeactivated } + confirmationAction() + try await deferredAction.fulfill() + + XCTAssertEqual(clientProxy.deactivateAccountPasswordEraseDataCallsCount, 2) + XCTAssertEqual(clientProxy.deactivateAccountPasswordEraseDataReceivedArguments?.password, enteredPassword) + XCTAssertEqual(clientProxy.deactivateAccountPasswordEraseDataReceivedArguments?.eraseData, shouldErase) + } + + func testCancel() async throws { + // When cancelling the view. + let deferred = deferFulfillment(viewModel.actionsPublisher) { $0 == .cancel } + context.send(viewAction: .cancel) + try await deferred.fulfill() + + // Then no API call should be made to deactivate the account. + XCTAssertFalse(clientProxy.deactivateAccountPasswordEraseDataCalled) + } +} diff --git a/project.yml b/project.yml index 4616a4e18..28507fd0d 100644 --- a/project.yml +++ b/project.yml @@ -64,7 +64,7 @@ packages: # path: ../matrix-rust-sdk Compound: url: https://github.com/element-hq/compound-ios - revision: 22f9d801dd001e8aaed0f62546cdb42c7594cf92 + revision: a9270392b3269ef072c47dea623815a9fb87311d # path: ../compound-ios AnalyticsEvents: url: https://github.com/matrix-org/matrix-analytics-events