Add support for account deactivation when not using OIDC. (#3295)

This commit is contained in:
Doug 2024-09-17 20:08:35 +01:00 committed by GitHub
parent 8660a22b3e
commit 5625e78fc9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 822 additions and 35 deletions

View File

@ -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 = "<group>"; };
1DF8F7A3AD83D04C08D75E01 /* RoomDetailsEditScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenViewModelProtocol.swift; sourceTree = "<group>"; };
1DFE0E493FB55E5A62E7852A /* ProposedViewSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProposedViewSize.swift; sourceTree = "<group>"; };
1E49D10BFA7E4D70A947888C /* DeactivateAccountScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeactivateAccountScreen.swift; sourceTree = "<group>"; };
1E508AB0EDEE017FF4F6F8D1 /* DTHTMLElement+AttributedStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DTHTMLElement+AttributedStringBuilder.swift"; sourceTree = "<group>"; };
1F2529D434C750ED78ADF1ED /* UserAgentBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAgentBuilder.swift; sourceTree = "<group>"; };
1F7C6DDBB5D12F6EF6A3D6E1 /* CollapsibleReactionLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleReactionLayout.swift; sourceTree = "<group>"; };
@ -1380,6 +1387,7 @@
29A953B6C0C431DBF4DD00B4 /* RoomSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummary.swift; sourceTree = "<group>"; };
2A5C6FBF97B6EED3D4FA5EFF /* AttributedStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilder.swift; sourceTree = "<group>"; };
2AB2C848BB9A7A9B618B7B89 /* TextBasedRoomTimelineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBasedRoomTimelineTests.swift; sourceTree = "<group>"; };
2AC3FDB58F57386741A4FC7F /* DeactivateAccountScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeactivateAccountScreenViewModel.swift; sourceTree = "<group>"; };
2AE807361805463F5AEDD1CA /* VoiceMessagePreviewComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessagePreviewComposer.swift; sourceTree = "<group>"; };
2AE83A3DD63BCFBB956FE5CB /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nl; path = nl.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
2AF715D4FD4710EBB637D661 /* SettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenViewModelProtocol.swift; sourceTree = "<group>"; };
@ -1745,6 +1753,7 @@
8296D6FB451E25CEC0767BBA /* RoomNotificationSettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsScreenCoordinator.swift; sourceTree = "<group>"; };
82B612853BFB68373249777B /* SecureBackupKeyBackupScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupKeyBackupScreenViewModel.swift; sourceTree = "<group>"; };
82DFA1B7B088D033E0794B82 /* RoomChangeRolesScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangeRolesScreenCoordinator.swift; sourceTree = "<group>"; };
82EE3B877D91030248B1242D /* DeactivateAccountScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeactivateAccountScreenViewModelProtocol.swift; sourceTree = "<group>"; };
8319173DD66C07F45DC48848 /* IdentityConfirmedScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmedScreenViewModelProtocol.swift; sourceTree = "<group>"; };
837B440C4705E4B899BCB899 /* RoomDetailsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreenViewModel.swift; sourceTree = "<group>"; };
839E2C35DF3F9C7B54C3CE49 /* RoundedCornerShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedCornerShape.swift; sourceTree = "<group>"; };
@ -1781,6 +1790,7 @@
897DF5E9A70CE05A632FC8AF /* UTType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTType.swift; sourceTree = "<group>"; };
89AAEA70CFF3284920811941 /* RoomChangePermissionsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangePermissionsScreen.swift; sourceTree = "<group>"; };
89FBFC09F9DAFF1E4BA97849 /* FormButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormButtonStyles.swift; sourceTree = "<group>"; };
8A1009E4A78F86DA42E1EAF0 /* DeactivateAccountScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeactivateAccountScreenCoordinator.swift; sourceTree = "<group>"; };
8A9AE4967817E9608E22EB44 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = "<group>"; };
8AE0C9653870803E4F91F474 /* RoomListFiltersStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomListFiltersStateTests.swift; sourceTree = "<group>"; };
8AE78FA0011E07920AE83135 /* PlainMentionBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlainMentionBuilder.swift; sourceTree = "<group>"; };
@ -1998,6 +2008,7 @@
BC8AA23D4F37CC26564F63C5 /* LayoutMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutMocks.swift; sourceTree = "<group>"; };
BCDA016D05107DED3B9495CB /* TimelineItemDebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemDebugView.swift; sourceTree = "<group>"; };
BE148A4FFEE853C5A281500C /* UNNotificationContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UNNotificationContent.swift; sourceTree = "<group>"; };
BE78CAD0B964C66FD06EF83E /* DeactivateAccountScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeactivateAccountScreenModels.swift; sourceTree = "<group>"; };
BE89A8BD65CCE3FCC925CA14 /* TimelineItemReplyDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemReplyDetails.swift; sourceTree = "<group>"; };
BE9BBB18FB27F09032AD8769 /* NotificationPermissionsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPermissionsScreenViewModel.swift; sourceTree = "<group>"; };
BEA38B9851CFCC4D67F5587D /* EmojiPickerScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenCoordinator.swift; sourceTree = "<group>"; };
@ -2115,6 +2126,7 @@
D66B5D86A9AB95E0E01BED82 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/InfoPlist.strings; sourceTree = "<group>"; };
D6DC38E64A5ED3FDB201029A /* BugReportService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportService.swift; sourceTree = "<group>"; };
D77B3D4950F1707E66E4A45A /* AnalyticsConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsConfiguration.swift; sourceTree = "<group>"; };
D77F75B3E9F99864048A422A /* DeactivateAccountScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeactivateAccountScreenViewModelTests.swift; sourceTree = "<group>"; };
D79BB714D28C9F588DD69353 /* SecureBackupScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupScreenViewModelProtocol.swift; sourceTree = "<group>"; };
D7BB243B26D54EF1A0C422C0 /* NotificationContentBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContentBuilder.swift; sourceTree = "<group>"; };
D7BEB970F500BFB248443FA1 /* BloomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloomView.swift; sourceTree = "<group>"; };
@ -2446,6 +2458,14 @@
path = Logging;
sourceTree = "<group>";
};
07831A7BA411CC407B4727E2 /* View */ = {
isa = PBXGroup;
children = (
1E49D10BFA7E4D70A947888C /* DeactivateAccountScreen.swift */,
);
path = View;
sourceTree = "<group>";
};
0787F81684E503024BD0C051 /* Services */ = {
isa = PBXGroup;
children = (
@ -3640,6 +3660,18 @@
path = TimelineItemContent;
sourceTree = "<group>";
};
6C708A9F46EDE1105C640335 /* DeactivateAccountScreen */ = {
isa = PBXGroup;
children = (
8A1009E4A78F86DA42E1EAF0 /* DeactivateAccountScreenCoordinator.swift */,
BE78CAD0B964C66FD06EF83E /* DeactivateAccountScreenModels.swift */,
2AC3FDB58F57386741A4FC7F /* DeactivateAccountScreenViewModel.swift */,
82EE3B877D91030248B1242D /* DeactivateAccountScreenViewModelProtocol.swift */,
07831A7BA411CC407B4727E2 /* View */,
);
path = DeactivateAccountScreen;
sourceTree = "<group>";
};
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" */ = {

View File

@ -15,7 +15,7 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/element-hq/compound-ios",
"state" : {
"revision" : "22f9d801dd001e8aaed0f62546cdb42c7594cf92"
"revision" : "a9270392b3269ef072c47dea623815a9fb87311d"
}
},
{

View File

@ -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 wont 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";

View File

@ -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

View File

@ -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 wont 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.

View File

@ -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))

View File

@ -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<String?, Never> {
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<Void, ClientProxyError>!
var deactivateAccountPasswordEraseDataReturnValue: Result<Void, ClientProxyError>! {
get {
if Thread.isMainThread {
return deactivateAccountPasswordEraseDataUnderlyingReturnValue
} else {
var returnValue: Result<Void, ClientProxyError>? = 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<Void, ClientProxyError>)?
func deactivateAccount(password: String?, eraseData: Bool) async -> Result<Void, ClientProxyError> {
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

View File

@ -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?

View File

@ -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<AnyCancellable>()
private let actionsSubject: PassthroughSubject<DeactivateAccountScreenCoordinatorAction, Never> = .init()
var actionsPublisher: AnyPublisher<DeactivateAccountScreenCoordinatorAction, Never> {
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))
}
}

View File

@ -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<DeactivateAccountScreenAlert>?
}
enum DeactivateAccountScreenAlert {
case confirmation
case deactivationFailed
}
enum DeactivateAccountScreenViewAction {
case deactivate
case cancel
}

View File

@ -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<DeactivateAccountScreenViewState, DeactivateAccountScreenViewAction>
class DeactivateAccountScreenViewModel: DeactivateAccountScreenViewModelType, DeactivateAccountScreenViewModelProtocol {
private let clientProxy: ClientProxyProtocol
private let userIndicatorController: UserIndicatorControllerProtocol
private let actionsSubject: PassthroughSubject<DeactivateAccountScreenViewModelAction, Never> = .init()
var actionsPublisher: AnyPublisher<DeactivateAccountScreenViewModelAction, Never> {
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)
}
}
}

View File

@ -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<DeactivateAccountScreenViewModelAction, Never> { get }
var context: DeactivateAccountScreenViewModelType.Context { get }
}

View File

@ -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)
}
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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
}

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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<Void, ClientProxyError> {
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,

View File

@ -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<String?, Never> { get }
@ -157,6 +159,8 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
func sessionVerificationControllerProxy() async -> Result<SessionVerificationControllerProxyProtocol, ClientProxyError>
func deactivateAccount(password: String?, eraseData: Bool) async -> Result<Void, ClientProxyError>
func logout() async -> URL?
func setPusher(with configuration: PusherConfiguration) async throws

View File

@ -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)

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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