* Expose logout method from SDK

* Use logout from SDK, refactor logging out states

* Add some strings

* Introduce `initialDisplayName` on `UIDevice`

* Implement soft logout screen

* Add `softLogout` test screen identifier

* Add new fields into the client proxy and implement new delegate methods

* Add new fields into the user session and observe new client callbacks

* Implement updated login method

* Add remote logout state and event into the state machine

* Implement refreshing restore token on `UserSessionStore`

* Update app coordinator with new states and display soft logout screen when appropriate

* Add reference screenshots for soft logout screen

* Disable auto requesting photos access on screenshot detection

* Make initial device name optional, generate project file

* Add changelog

* Use logout from SDK, refactor logging out states

* Implement soft logout screen

* Implement updated login method

* Make initial device name optional, generate project file

* Fix renamed event

* Fix logout race

* Remove redundant strings

* Reuse existing strings

* Confirm clear all data

* Expose logout method from SDK

* Use logout from SDK, refactor logging out states

* Add some strings

* Introduce `initialDisplayName` on `UIDevice`

* Implement soft logout screen

* Add `softLogout` test screen identifier

* Add new fields into the client proxy and implement new delegate methods

* Add new fields into the user session and observe new client callbacks

* Implement updated login method

* Add remote logout state and event into the state machine

* Implement refreshing restore token on `UserSessionStore`

* Update app coordinator with new states and display soft logout screen when appropriate

* Add reference screenshots for soft logout screen

* Disable auto requesting photos access on screenshot detection

* Make initial device name optional, generate project file

* Add changelog

* Use logout from SDK, refactor logging out states

* Implement soft logout screen

* Implement updated login method

* Make initial device name optional, generate project file

* Fix renamed event

* Fix logout race

* Remove redundant strings

* Reuse existing strings

* Confirm clear all data

* Comment out new apis for now

* Make the PR compile
This commit is contained in:
ismailgulek
2022-09-15 12:41:37 +03:00
committed by GitHub
parent 6caaa3b06e
commit cfea204a3e
37 changed files with 1237 additions and 63 deletions

View File

@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 52;
objectVersion = 51;
objects = {
/* Begin PBXBuildFile section */
@@ -26,6 +26,7 @@
071A017E415AD378F2961B11 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227AC5D71A4CE43512062243 /* URL.swift */; };
07240B7159A3990C4C2E8FFC /* LoginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D256FEE2F1AF1E51D39B622 /* LoginTests.swift */; };
072BA9DBA932374CCA300125 /* MessageComposerTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE6C10032A77AE7DC5AA4C50 /* MessageComposerTextField.swift */; };
09AAF04B27732046C755D914 /* SoftLogoutViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32C5DAA1773F57653BF1C4F9 /* SoftLogoutViewModelTests.swift */; };
0B1F80C2BF7D223159FBA82C /* ImageAnonymizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6045E825AE900A92D61FEFF0 /* ImageAnonymizerTests.swift */; };
0C38C3E771B472E27295339D /* SessionVerificationModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4BB9A17AC512A7EF4B106E5 /* SessionVerificationModels.swift */; };
0E8C480700870BB34A2A360F /* AppAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 4346F63D53A346271577FD9C /* AppAuth */; };
@@ -44,6 +45,7 @@
1555A7643D85187D4851040C /* TemplateScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4549FCB53F43DB0B278374BC /* TemplateScreen.swift */; };
157E5FDDF419C0B2CA7E2C28 /* TimelineItemBubbledStylerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98A2932515EA11D3DD8A3506 /* TimelineItemBubbledStylerView.swift */; };
15D1F9C415D9C921643BA82E /* UserIndicatorRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B73D5E21F524A9BE44448D /* UserIndicatorRequest.swift */; };
165A883C29998EC779465068 /* SoftLogoutViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BC38904A9663F7FAFD47457 /* SoftLogoutViewModelProtocol.swift */; };
1702981A8085BE4FB0EC001B /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = D33116993D54FADC0C721C1F /* Application.swift */; };
172E6E9A612ADCF10A62CF13 /* BugReportServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A68BCE6438873D2661D93D0 /* BugReportServiceProtocol.swift */; };
17CC4FB64F3A670F43ECBE5F /* UITestsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCA431E6EDD71F7067B5F9E7 /* UITestsRootView.swift */; };
@@ -57,7 +59,9 @@
1E59B77A0B2CE83DCC1B203C /* LoginViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05707BF550D770168A406DB /* LoginViewModelTests.swift */; };
1F3232BD368DF430AB433907 /* DesignKit in Frameworks */ = {isa = PBXBuildFile; productRef = A5A56C4F47C368EBE5C5E870 /* DesignKit */; };
206F0DBAB6AF042CA1FF2C0D /* SettingsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */; };
214C6B416609E58CCBF6DCEE /* SoftLogoutModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC77FC5C4F2000133047AA27 /* SoftLogoutModels.swift */; };
224A55EEAEECF5336B14A4A5 /* EmoteRoomMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2DF459F1737A594667CC46 /* EmoteRoomMessage.swift */; };
2276870A19F34B3FFFDA690F /* SoftLogoutCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AEA20A6B4883E60469ACF8F /* SoftLogoutCoordinator.swift */; };
22DADD537401E79D66132134 /* NavigationRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4173A48FD8542CD4AD3645C /* NavigationRouter.swift */; };
237FC70AA257B935F53316BA /* SessionVerificationControllerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C55D7E514F9DE4E3D72FDCAD /* SessionVerificationControllerProxy.swift */; };
23B2CD5A06B16055BDDD0994 /* ApplicationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44D8C8431416EB8DFEC7E235 /* ApplicationTests.swift */; };
@@ -180,6 +184,7 @@
77E192BA943B90F9F310CA23 /* WeakDictionaryKeyReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FFCC48E7F701B6C24484593 /* WeakDictionaryKeyReference.swift */; };
77FACC29F98FE2E65BBB6A5F /* ServerSelectionUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 054F469E433864CC6FE6EE8E /* ServerSelectionUITests.swift */; };
78B71D53C1FC55FB7A9B75F0 /* RoomTimelineController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24B0C97D2F560BCB72BE73B1 /* RoomTimelineController.swift */; };
78BF60C696FFED63AAF58D10 /* SoftLogoutViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22D46DB0CC6C55EBA7AE67A3 /* SoftLogoutViewModel.swift */; };
7963F98CDFDEAC75E072BD81 /* TextRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6A8C632CEF4600107792899 /* TextRoomTimelineItem.swift */; };
79A6E08ADE6E7C460A8A17A5 /* UserSessionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C37FB986891D90BEAA93EAE /* UserSessionStore.swift */; };
7A54700193DC1F264368746A /* UserIndicatorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E077F76026C85ED96FEBB810 /* UserIndicatorPresenter.swift */; };
@@ -242,6 +247,7 @@
A0A0D2A9564BDA3FDE2E360F /* FormattedBodyText.swift in Sources */ = {isa = PBXBuildFile; fileRef = F73FF1A33198F5FAE9D34B1F /* FormattedBodyText.swift */; };
A32517FB1CA0BBCE2BC75249 /* BugReportCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD6C07DA7D3FF193F7419F55 /* BugReportCoordinator.swift */; };
A371629728E597C5FCA3C2B2 /* Analytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73FC861755C6388F62B9280A /* Analytics.swift */; };
A37EED79941AD3B7140B3822 /* UIDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 287FC98AF2664EAD79C0D902 /* UIDevice.swift */; };
A4E885358D7DD5A072A06824 /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = CCE5BF78B125320CBF3BB834 /* PostHog */; };
A50849766F056FD1DB942DEA /* AlertInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EEB64CC6F3DF5B68736A6B4 /* AlertInfo.swift */; };
A5C8F013ED9FB8AA6FEE18A7 /* InfoPlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A901D95158B02CA96C79C7F /* InfoPlist.swift */; };
@@ -258,6 +264,8 @@
ABF3FAB234AD3565B214309B /* TimelineSenderAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BC588051E6572A1AF51D738 /* TimelineSenderAvatarView.swift */; };
ACF094CF3BF02DBFA6DFDE60 /* AuthenticationCoordinatorUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D2D0A6F1ABC99D29462FB84 /* AuthenticationCoordinatorUITests.swift */; };
B037C365CF8A58A0D149A2DB /* AuthenticationIconImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97755C01C3971474EFAD5367 /* AuthenticationIconImage.swift */; };
B064D42BA087649ACAE462E8 /* SoftLogoutUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55F30E764BED111C81739844 /* SoftLogoutUITests.swift */; };
B09514A0A3EB3C19A4FD0B71 /* SoftLogoutScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CCBDE671A613B3EB70794C4 /* SoftLogoutScreen.swift */; };
B245583C63F8F90357B87FAE /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 04C28663564E008DB32B5972 /* Introspect */; };
B3357B00F1AA930E54F76609 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47EBB5D698CE9A25BB553A2D /* Strings.swift */; };
B3FDB1D9CF40777695DBBD1D /* AppCoordinatorStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A9AB74614131D6706894E0C /* AppCoordinatorStateMachine.swift */; };
@@ -281,6 +289,7 @@
C4180F418235DAD9DD173951 /* TemplateScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9873076F224E4CE09D8BD47D /* TemplateScreenUITests.swift */; };
C4F69156C31A447FEFF2A47C /* DTHTMLElement+AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E508AB0EDEE017FF4F6F8D1 /* DTHTMLElement+AttributedStringBuilder.swift */; };
C55A44C99F64A479ABA85B46 /* RoomScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5221DFDF809142A2D6AC82B9 /* RoomScreen.swift */; };
C74EE50257ED925C2B8EFCE6 /* MockSoftLogoutScreenState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B869438A1B52836F912A702 /* MockSoftLogoutScreenState.swift */; };
C76892321558E75101E68ED6 /* ReadableFrameModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398817652FA8ABAE0A31AC6D /* ReadableFrameModifier.swift */; };
C7B251DC896C0867C51B616D /* AnalyticsPrompt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541542F5AC323709D8563458 /* AnalyticsPrompt.swift */; };
C7CFDB4929DDD9A3B5BA085D /* BugReportViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB7ED3A898B07976F3AA90F /* BugReportViewModelTests.swift */; };
@@ -388,6 +397,7 @@
09747989908EC5E4AA29F844 /* MemberDetailsProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberDetailsProviderProtocol.swift; sourceTree = "<group>"; };
0A191D3FDB995309C7E2DE7D /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = "<group>"; };
0AB7A0C06CB527A1095DEB33 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = da; path = da.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
0B869438A1B52836F912A702 /* MockSoftLogoutScreenState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSoftLogoutScreenState.swift; sourceTree = "<group>"; };
0BC588051E6572A1AF51D738 /* TimelineSenderAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineSenderAvatarView.swift; sourceTree = "<group>"; };
0C13A92C1E9C79F055B8133D /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ar; path = ar.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
0CB569EAA5017B5B23970655 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/Localizable.strings; sourceTree = "<group>"; };
@@ -427,26 +437,31 @@
21BA866267F84BF4350B0CB7 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "pt-BR"; path = "pt-BR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
227AC5D71A4CE43512062243 /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = "<group>"; };
22B384D54464FA39C6C7F6E7 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ca; path = ca.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
22D46DB0CC6C55EBA7AE67A3 /* SoftLogoutViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutViewModel.swift; sourceTree = "<group>"; };
233D5F7E5E9F49ABF3413291 /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = hr; path = hr.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
24A534A4619D8FEFB6439FCC /* SplashScreenPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenPageView.swift; sourceTree = "<group>"; };
24B0C97D2F560BCB72BE73B1 /* RoomTimelineController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineController.swift; sourceTree = "<group>"; };
24F5530B2212862FA4BEFF2D /* HomeScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModelProtocol.swift; sourceTree = "<group>"; };
2583416C8974272ADBADDBE1 /* zh-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-TW"; path = "zh-TW.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
26C4D226FCD20BAC53F1E092 /* ml */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ml; path = ml.lproj/Localizable.strings; sourceTree = "<group>"; };
287FC98AF2664EAD79C0D902 /* UIDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIDevice.swift; sourceTree = "<group>"; };
28959C7DB36C7688A01D4045 /* BugReportViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportViewModelProtocol.swift; sourceTree = "<group>"; };
289FA233E896FBC5956C67E0 /* RoomTimelineItemProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProperties.swift; sourceTree = "<group>"; };
28EA8BE9EEDBD17555141C7E /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = el; path = el.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
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>"; };
2AE83A3DD63BCFBB956FE5CB /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nl; path = nl.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
2AEA20A6B4883E60469ACF8F /* SoftLogoutCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutCoordinator.swift; sourceTree = "<group>"; };
2B80895CE021B49847BD7D74 /* TemplateViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateViewModelProtocol.swift; sourceTree = "<group>"; };
2CA028DCD4157F9A1F999827 /* BackgroundTaskProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundTaskProtocol.swift; sourceTree = "<group>"; };
2CCBDE671A613B3EB70794C4 /* SoftLogoutScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutScreen.swift; sourceTree = "<group>"; };
2CF9FE7E0CF9F40D1509E63A /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = bg; path = bg.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
2D256FEE2F1AF1E51D39B622 /* LoginTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginTests.swift; sourceTree = "<group>"; };
2EEB64CC6F3DF5B68736A6B4 /* AlertInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertInfo.swift; sourceTree = "<group>"; };
31B01468022EC826CB2FD2C0 /* LoginModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginModels.swift; sourceTree = "<group>"; };
31D6764D6976D235926FE5FC /* HomeScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModel.swift; sourceTree = "<group>"; };
325A2B3278875554DDEB8A9B /* SplashScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenUITests.swift; sourceTree = "<group>"; };
32C5DAA1773F57653BF1C4F9 /* SoftLogoutViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutViewModelTests.swift; sourceTree = "<group>"; };
32CE6D4FF64C9A3C18619224 /* SplashScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreen.swift; sourceTree = "<group>"; };
3340ABAE3A4647E80163AE18 /* TemplateViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateViewModelTests.swift; sourceTree = "<group>"; };
33E49C5C6F802B4D94CA78D1 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Localizable.strings; sourceTree = "<group>"; };
@@ -520,6 +535,7 @@
54E438DBCBDC7A41B95DDDD9 /* MXLogObjcWrapper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MXLogObjcWrapper.m; sourceTree = "<group>"; };
55BC11560C8A2598964FFA4C /* bs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bs; path = bs.lproj/Localizable.strings; sourceTree = "<group>"; };
55D7187F6B0C0A651AC3DFFA /* in */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = in; path = in.lproj/Localizable.strings; sourceTree = "<group>"; };
55F30E764BED111C81739844 /* SoftLogoutUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutUITests.swift; sourceTree = "<group>"; };
56F01DD1BBD4450E18115916 /* LabelledActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelledActivityIndicatorView.swift; sourceTree = "<group>"; };
5773C86AF04AEF26515AD00C /* sl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sl; path = sl.lproj/Localizable.strings; sourceTree = "<group>"; };
5872785B9C7934940146BFBA /* MXLogger.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MXLogger.h; sourceTree = "<group>"; };
@@ -533,6 +549,7 @@
5F12E996BFBEB43815189ABF /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = uk; path = uk.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
5F4134FEFE4EB55759017408 /* UserSessionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionProtocol.swift; sourceTree = "<group>"; };
5F77E8010D41AA3F5F9A1FCA /* NavigationModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModule.swift; sourceTree = "<group>"; };
5F9C6A30C7FB368AD539E7F7 /* matrix-rust-components-swift */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "matrix-rust-components-swift"; path = "../matrix-rust-components-swift"; sourceTree = SOURCE_ROOT; };
5FD9D66B75292F2CC11AA4D2 /* UITestScreenIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestScreenIdentifier.swift; sourceTree = "<group>"; };
5FF214969B25BFCBF87B908B /* bn-BD */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "bn-BD"; path = "bn-BD.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
6033779EB37259F27F938937 /* ClientProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxyProtocol.swift; sourceTree = "<group>"; };
@@ -558,6 +575,7 @@
6A901D95158B02CA96C79C7F /* InfoPlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoPlist.swift; sourceTree = "<group>"; };
6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixEntityRegex.swift; sourceTree = "<group>"; };
6B73A8C3118EAC7BF3F3EE7A /* SplashScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenViewModelProtocol.swift; sourceTree = "<group>"; };
6BC38904A9663F7FAFD47457 /* SoftLogoutViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutViewModelProtocol.swift; sourceTree = "<group>"; };
6D4777F0142E330A75C46FE4 /* SessionVerificationUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationUITests.swift; sourceTree = "<group>"; };
6DB53055CB130F0651C70763 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Localizable.strings; sourceTree = "<group>"; };
6DFCAA239095A116976E32C4 /* BackgroundTaskTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundTaskTests.swift; sourceTree = "<group>"; };
@@ -611,7 +629,7 @@
8C37FB986891D90BEAA93EAE /* UserSessionStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionStore.swift; sourceTree = "<group>"; };
8D8169443E5AC5FF71BFB3DB /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; };
8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyle.swift; sourceTree = "<group>"; };
8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UITests.xctestplan; sourceTree = "<group>"; };
8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = "<group>"; };
9010EE0CC913D095887EF36E /* OIDCService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OIDCService.swift; sourceTree = "<group>"; };
90733775209F4D4D366A268F /* RootRouterType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootRouterType.swift; sourceTree = "<group>"; };
92B61C243325DC76D3086494 /* EventBriefFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBriefFactoryProtocol.swift; sourceTree = "<group>"; };
@@ -749,6 +767,7 @@
D77B3D4950F1707E66E4A45A /* AnalyticsConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsConfiguration.swift; sourceTree = "<group>"; };
DBD460ED7ED1E03B85DEA25C /* TemplateCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateCoordinator.swift; sourceTree = "<group>"; };
DBFEAC3AC691CBB84983E275 /* ElementXTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementXTests.swift; sourceTree = "<group>"; };
DC77FC5C4F2000133047AA27 /* SoftLogoutModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutModels.swift; sourceTree = "<group>"; };
DCE978A6118C131D7F2A04B3 /* SplashScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenModels.swift; sourceTree = "<group>"; };
DD667C4BB98CF4F3FE2CE3B0 /* LoginCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginCoordinator.swift; sourceTree = "<group>"; };
DD73FAAA4A76CE4A1F3014D9 /* UserIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicator.swift; sourceTree = "<group>"; };
@@ -1078,6 +1097,7 @@
B6E89E530A8E92EC44301CA1 /* Bundle.swift */,
E26747B3154A5DBC3A7E24A5 /* Image.swift */,
40B21E611DADDEF00307E7AC /* String.swift */,
287FC98AF2664EAD79C0D902 /* UIDevice.swift */,
227AC5D71A4CE43512062243 /* URL.swift */,
);
path = Extensions;
@@ -1302,6 +1322,7 @@
A1C22B1B5FA3A765EADB2CC9 /* SessionVerificationStateMachineTests.swift */,
DF05DA24F71B455E8EFEBC3B /* SessionVerificationViewModelTests.swift */,
3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */,
32C5DAA1773F57653BF1C4F9 /* SoftLogoutViewModelTests.swift */,
7B5CF94E124616FD89424B73 /* SplashScreenViewModelTests.swift */,
AF552BB969DC98A4BB8CF8D5 /* UserIndicators */,
);
@@ -1454,6 +1475,7 @@
isa = PBXGroup;
children = (
D31DC8105C6233E5FFD9B84C /* element-x-ios */,
5F9C6A30C7FB368AD539E7F7 /* matrix-rust-components-swift */,
);
name = Packages;
sourceTree = SOURCE_ROOT;
@@ -1471,6 +1493,7 @@
054F469E433864CC6FE6EE8E /* ServerSelectionUITests.swift */,
6D4777F0142E330A75C46FE4 /* SessionVerificationUITests.swift */,
E3E29F98CF0E960689A410E3 /* SettingsUITests.swift */,
55F30E764BED111C81739844 /* SoftLogoutUITests.swift */,
325A2B3278875554DDEB8A9B /* SplashScreenUITests.swift */,
);
path = Sources;
@@ -1639,6 +1662,19 @@
path = Timeline;
sourceTree = "<group>";
};
B9F8C25B353B751013FAACC7 /* SoftLogout */ = {
isa = PBXGroup;
children = (
0B869438A1B52836F912A702 /* MockSoftLogoutScreenState.swift */,
2AEA20A6B4883E60469ACF8F /* SoftLogoutCoordinator.swift */,
DC77FC5C4F2000133047AA27 /* SoftLogoutModels.swift */,
22D46DB0CC6C55EBA7AE67A3 /* SoftLogoutViewModel.swift */,
6BC38904A9663F7FAFD47457 /* SoftLogoutViewModelProtocol.swift */,
CE6E6768A3FF47C9EABD3007 /* View */,
);
path = SoftLogout;
sourceTree = "<group>";
};
C0937E3B06A8F0E2DB7C8241 /* Other */ = {
isa = PBXGroup;
children = (
@@ -1690,6 +1726,14 @@
path = Layout;
sourceTree = "<group>";
};
CE6E6768A3FF47C9EABD3007 /* View */ = {
isa = PBXGroup;
children = (
2CCBDE671A613B3EB70794C4 /* SoftLogoutScreen.swift */,
);
path = View;
sourceTree = "<group>";
};
D958761758AA1110476DE6A3 /* SessionVerification */ = {
isa = PBXGroup;
children = (
@@ -1773,6 +1817,7 @@
9E6D88E8AFFBF2C1D589C0FA /* UIConstants.swift */,
90F48FEF84016ED42A94BA24 /* LoginScreen */,
3510020809E49EFA146296AD /* ServerSelection */,
B9F8C25B353B751013FAACC7 /* SoftLogout */,
);
path = Authentication;
sourceTree = "<group>";
@@ -2069,7 +2114,6 @@
9A472EE0218FE7DCF5283429 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */,
61916C63E3F5BD900F08DA0C /* XCRemoteSwiftPackageReference "KeychainAccess" */,
D283517192CAC3E2E6920765 /* XCRemoteSwiftPackageReference "Kingfisher" */,
80B898A3AD2AC63F3ABFC218 /* XCRemoteSwiftPackageReference "matrix-rust-components-swift" */,
96495DD8554E2F39D3954354 /* XCRemoteSwiftPackageReference "posthog-ios" */,
A08925A9D5E3770DEB9D8509 /* XCRemoteSwiftPackageReference "sentry-cocoa" */,
E9C4F3A12AA1F65C13A8C8EB /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */,
@@ -2239,6 +2283,7 @@
86675910612A12409262DFBD /* SessionVerificationStateMachineTests.swift in Sources */,
755727E0B756430DFFEC4732 /* SessionVerificationViewModelTests.swift in Sources */,
206F0DBAB6AF042CA1FF2C0D /* SettingsViewModelTests.swift in Sources */,
09AAF04B27732046C755D914 /* SoftLogoutViewModelTests.swift in Sources */,
94E062D08E27B0387658E364 /* SplashScreenViewModelTests.swift in Sources */,
7AE1FFB132F2B84EB8A2AEBC /* TemplateViewModelTests.swift in Sources */,
1151DCC5EC2C6585826545EC /* UserIndicatorPresenterSpy.swift in Sources */,
@@ -2360,6 +2405,7 @@
E81EEC1675F2371D12A880A3 /* MockRoomTimelineController.swift in Sources */,
9BE7A9CF6C593251D734B461 /* MockServerSelectionScreenState.swift in Sources */,
D034A195A3494E38BF060485 /* MockSessionVerificationControllerProxy.swift in Sources */,
C74EE50257ED925C2B8EFCE6 /* MockSoftLogoutScreenState.swift in Sources */,
D8359F67AF3A83516E9083C1 /* MockUserSession.swift in Sources */,
4ED453A61AF45EBE18D8BC69 /* NavigationModule.swift in Sources */,
22DADD537401E79D66132134 /* NavigationRouter.swift in Sources */,
@@ -2425,6 +2471,11 @@
7FED310F6AB7A70CBFB7C8A3 /* SettingsScreen.swift in Sources */,
4A2E0DBB63919AC8309B6D40 /* SettingsViewModel.swift in Sources */,
438FB9BC535BC95948AA5F34 /* SettingsViewModelProtocol.swift in Sources */,
2276870A19F34B3FFFDA690F /* SoftLogoutCoordinator.swift in Sources */,
214C6B416609E58CCBF6DCEE /* SoftLogoutModels.swift in Sources */,
B09514A0A3EB3C19A4FD0B71 /* SoftLogoutScreen.swift in Sources */,
78BF60C696FFED63AAF58D10 /* SoftLogoutViewModel.swift in Sources */,
165A883C29998EC779465068 /* SoftLogoutViewModelProtocol.swift in Sources */,
684BDE198AE5AA1392288A73 /* SplashScreen.swift in Sources */,
CE7A715947ABAB1DEB5C21D7 /* SplashScreenCoordinator.swift in Sources */,
EF99A92701E401C4CD5ADC50 /* SplashScreenModels.swift in Sources */,
@@ -2458,6 +2509,7 @@
4669804D0369FBED4E8625D1 /* ToastViewPresenter.swift in Sources */,
9CB5129C83F75921E5E28028 /* ToastViewState.swift in Sources */,
36AC963F2F04069B7FF1AA0C /* UIConstants.swift in Sources */,
A37EED79941AD3B7140B3822 /* UIDevice.swift in Sources */,
0EE5EBA18BA1FE10254BB489 /* UIFont+AttributedStringBuilder.m in Sources */,
706F79A39BDB32F592B8C2C7 /* UIKitBackgroundTask.swift in Sources */,
3097A0A867D2B19CE32DAE58 /* UIKitBackgroundTaskService.swift in Sources */,
@@ -2504,6 +2556,7 @@
77FACC29F98FE2E65BBB6A5F /* ServerSelectionUITests.swift in Sources */,
05EC896A4B9AF4A56670C0BB /* SessionVerificationUITests.swift in Sources */,
490E606044B18985055FF690 /* SettingsUITests.swift in Sources */,
B064D42BA087649ACAE462E8 /* SoftLogoutUITests.swift in Sources */,
A00DFC1DD3567B1EDC9F8D16 /* SplashScreenUITests.swift in Sources */,
DD9B70DE54B24E0694A35D8A /* Strings+Untranslated.swift in Sources */,
B3357B00F1AA930E54F76609 /* Strings.swift in Sources */,
@@ -3090,14 +3143,6 @@
minimumVersion = 1.3.0;
};
};
80B898A3AD2AC63F3ABFC218 /* XCRemoteSwiftPackageReference "matrix-rust-components-swift" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/matrix-org/matrix-rust-components-swift";
requirement = {
kind = exactVersion;
version = "1.0.13-alpha";
};
};
96495DD8554E2F39D3954354 /* XCRemoteSwiftPackageReference "posthog-ios" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/PostHog/posthog-ios";
@@ -3278,7 +3323,6 @@
};
A678E40E917620059695F067 /* MatrixRustSDK */ = {
isa = XCSwiftPackageProductDependency;
package = 80B898A3AD2AC63F3ABFC218 /* XCRemoteSwiftPackageReference "matrix-rust-components-swift" */;
productName = MatrixRustSDK;
};
A981A4CA233FB5C13B9CA690 /* SwiftyBeaver */ = {
@@ -3298,12 +3342,10 @@
};
B1E8B697DF78FE7F61FC6CA4 /* MatrixRustSDK */ = {
isa = XCSwiftPackageProductDependency;
package = 80B898A3AD2AC63F3ABFC218 /* XCRemoteSwiftPackageReference "matrix-rust-components-swift" */;
productName = MatrixRustSDK;
};
C2C69B8BA5A9702E7A8BC08F /* MatrixRustSDK */ = {
isa = XCSwiftPackageProductDependency;
package = 80B898A3AD2AC63F3ABFC218 /* XCRemoteSwiftPackageReference "matrix-rust-components-swift" */;
productName = MatrixRustSDK;
};
CCE5BF78B125320CBF3BB834 /* PostHog */ = {

View File

@@ -28,3 +28,6 @@
"server_selection_server_url" = "Server URL";
"server_selection_server_footer" = "You can only connect to a server that has already been set up";
"server_selection_generic_error" = "Cannot find a server at this URL, please check it is correct.";
"login_mobile_device" = "Mobile";
"login_tablet_device" = "Tablet";

View File

@@ -31,7 +31,7 @@ struct ServiceLocator {
let userIndicatorPresenter: UserIndicatorTypePresenter
}
class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
class AppCoordinator: Coordinator {
private let window: UIWindow
private let stateMachine: AppCoordinatorStateMachine
@@ -43,7 +43,14 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
private let userSessionStore: UserSessionStoreProtocol
private var userSession: UserSessionProtocol!
private var userSession: UserSessionProtocol! {
didSet {
deobserveUserSessionChanges()
if let userSession = userSession, !userSession.isSoftLogout {
observeUserSessionChanges()
}
}
}
private let memberDetailProviderManager: MemberDetailProviderManager
@@ -53,6 +60,8 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
private var loadingIndicator: UserIndicator?
private var statusIndicator: UserIndicator?
private var cancellables = Set<AnyCancellable>()
var childCoordinators: [Coordinator] = []
@@ -96,15 +105,7 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
window.makeKeyAndVisible()
stateMachine.processEvent(userSessionStore.hasSessions ? .startWithExistingSession : .startWithAuthentication)
}
// MARK: - AuthenticationCoordinatorDelegate
func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator, didLoginWithSession userSession: UserSessionProtocol) {
self.userSession = userSession
remove(childCoordinator: authenticationCoordinator)
stateMachine.processEvent(.succeededSigningIn)
}
// MARK: - Private
private func setupLogging() {
@@ -147,6 +148,7 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
case (.restoringSession, .failedRestoringSession, .signedOut):
self.hideLoadingIndicator()
self.showLoginErrorToast()
self.startAuthentication()
case (.restoringSession, .succeededRestoringSession, .homeScreen):
self.hideLoadingIndicator()
self.presentHomeScreen()
@@ -155,13 +157,20 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
self.presentRoomWithIdentifier(roomId)
case(.roomScreen, .dismissedRoomScreen, .homeScreen):
self.tearDownDismissedRoomScreen()
case (_, .attemptSignOut, .signingOut):
self.userSessionStore.logout(userSession: self.userSession)
self.stateMachine.processEvent(.succeededSigningOut)
case (.signingOut, .succeededSigningOut, .signedOut):
case (_, .signOut, .signingOut):
self.showLoadingIndicator()
self.tearDownUserSession()
case (.signingOut, .failedSigningOut, _):
self.showLogoutErrorToast()
case (.signingOut, .completedSigningOut, .signedOut):
self.presentSplashScreen()
self.hideLoadingIndicator()
case (_, .remoteSignOut(let isSoft), .remoteSigningOut):
self.showLoadingIndicator()
self.tearDownUserSession(isSoftLogout: isSoft)
case (.remoteSigningOut(let isSoft), .completedSigningOut, .signedOut):
self.presentSplashScreen(isSoftLogout: isSoft)
self.hideLoadingIndicator()
case (.homeScreen, .showSettingsScreen, .settingsScreen):
self.presentSettingsScreen()
@@ -190,7 +199,11 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
switch await userSessionStore.restoreUserSession() {
case .success(let userSession):
self.userSession = userSession
stateMachine.processEvent(.succeededRestoringSession)
if userSession.isSoftLogout {
stateMachine.processEvent(.remoteSignOut(isSoft: true))
} else {
stateMachine.processEvent(.succeededRestoringSession)
}
case .failure:
MXLog.error("Failed to restore an existing session.")
stateMachine.processEvent(.failedRestoringSession)
@@ -207,17 +220,80 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
add(childCoordinator: coordinator)
coordinator.start()
}
private func startAuthenticationSoftLogout() {
Task {
var displayName = ""
if case .success(let name) = await userSession.clientProxy.loadUserDisplayName() {
displayName = name
}
let credentials = SoftLogoutCredentials(userId: userSession.userID,
homeserverName: userSession.homeserver,
userDisplayName: displayName,
deviceId: userSession.deviceId)
let authenticationService = AuthenticationServiceProxy(userSessionStore: userSessionStore)
_ = await authenticationService.configure(for: userSession.homeserver)
let parameters = SoftLogoutCoordinatorParameters(authenticationService: authenticationService,
credentials: credentials,
keyBackupNeeded: false)
let coordinator = SoftLogoutCoordinator(parameters: parameters)
coordinator.callback = { result in
switch result {
case .signedIn(let session):
self.userSession = session
self.remove(childCoordinator: coordinator)
self.stateMachine.processEvent(.succeededSigningIn)
case .clearAllData:
self.confirmClearAllData {
// clear user data
self.userSessionStore.logout(userSession: self.userSession)
self.userSession = nil
self.remove(childCoordinator: coordinator)
self.startAuthentication()
}
}
}
add(childCoordinator: coordinator)
coordinator.start()
navigationRouter.setRootModule(coordinator)
}
}
private func tearDownUserSession() {
private func tearDownUserSession(isSoftLogout: Bool = false) {
Task {
deobserveUserSessionChanges()
if !isSoftLogout {
// first log out from the server
_ = await userSession.clientProxy.logout()
// regardless of the result, clear user data
userSessionStore.logout(userSession: userSession)
userSession = nil
}
// complete logging out
stateMachine.processEvent(.completedSigningOut)
}
}
private func presentSplashScreen(isSoftLogout: Bool = false) {
if let presentedCoordinator = childCoordinators.first {
remove(childCoordinator: presentedCoordinator)
}
userSession = nil
mainNavigationController.setViewControllers([splashViewController], animated: false)
startAuthentication()
if isSoftLogout {
startAuthenticationSoftLogout()
} else {
startAuthentication()
}
}
private func presentHomeScreen() {
@@ -250,6 +326,29 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
showCrashPopup()
}
}
private func observeUserSessionChanges() {
userSession.callbacks
.receive(on: DispatchQueue.main)
.sink { [weak self] callback in
guard let self = self else { return }
switch callback {
case .didReceiveAuthError(let isSoftLogout):
self.stateMachine.processEvent(.remoteSignOut(isSoft: isSoftLogout))
case .updateRestoreTokenNeeded:
if let userSession = self.userSession {
_ = self.userSessionStore.refreshRestoreToken(for: userSession)
}
default:
break
}
}.store(in: &cancellables)
}
private func deobserveUserSessionChanges() {
cancellables.forEach { $0.cancel() }
cancellables.removeAll()
}
// MARK: Rooms
@@ -305,7 +404,7 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
guard let self = self else { return }
switch action {
case .logout:
self.stateMachine.processEvent(.attemptSignOut)
self.stateMachine.processEvent(.signOut)
}
}
@@ -346,7 +445,20 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
alert.addAction(UIAlertAction(title: ElementL10n.actionCancel, style: .cancel))
alert.addAction(UIAlertAction(title: ElementL10n.actionSignOut, style: .destructive) { [weak self] _ in
self?.stateMachine.processEvent(.attemptSignOut)
self?.stateMachine.processEvent(.signOut)
})
navigationRouter.present(alert, animated: true)
}
/// Shows a confirmation to clear all data, and proceeds to do so if the user confirms.
private func confirmClearAllData(_ confirmed: @escaping () -> Void) {
let alert = UIAlertController(title: ElementL10n.softLogoutClearDataDialogTitle,
message: ElementL10n.softLogoutClearDataDialogContent,
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: ElementL10n.actionCancel, style: .cancel, handler: nil))
alert.addAction(UIAlertAction(title: ElementL10n.actionSignOut, style: .destructive) { _ in
confirmed()
})
navigationRouter.present(alert, animated: true)
@@ -444,8 +556,14 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
private func showLoginErrorToast() {
statusIndicator = ServiceLocator.shared.userIndicatorPresenter.present(.error(label: "Failed logging in"))
}
private func showLogoutErrorToast() {
statusIndicator = ServiceLocator.shared.userIndicatorPresenter.present(.error(label: "Failed logging out"))
}
// MARK: - AuthenticationCoordinatorDelegate
extension AppCoordinator: AuthenticationCoordinatorDelegate {
func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator, didLoginWithSession userSession: UserSessionProtocol) {
self.userSession = userSession
remove(childCoordinator: authenticationCoordinator)
stateMachine.processEvent(.succeededSigningIn)
}
}

View File

@@ -41,6 +41,9 @@ class AppCoordinatorStateMachine {
/// Processing a sign out request
case signingOut
/// Processing a remote sign out
case remoteSigningOut(isSoft: Bool)
}
/// Events that can be triggered on the AppCoordinator state machine
@@ -58,11 +61,11 @@ class AppCoordinatorStateMachine {
case failedRestoringSession
/// Request sign out
case attemptSignOut
/// Signing out succeeded
case succeededSigningOut
/// Signing out failed
case failedSigningOut
case signOut
/// Remote sign out.
case remoteSignOut(isSoft: Bool)
/// Signing out completed
case completedSigningOut
/// Request presentation for a particular room
/// - Parameter roomId:the room identifier
@@ -92,10 +95,8 @@ class AppCoordinatorStateMachine {
machine.addRoutes(event: .succeededRestoringSession, transitions: [.restoringSession => .homeScreen])
machine.addRoutes(event: .failedRestoringSession, transitions: [.restoringSession => .signedOut])
machine.addRoutes(event: .attemptSignOut, transitions: [.any => .signingOut])
machine.addRoutes(event: .succeededSigningOut, transitions: [.signingOut => .signedOut])
machine.addRoutes(event: .failedSigningOut, transitions: [.signingOut => .settingsScreen])
machine.addRoutes(event: .signOut, transitions: [.any => .signingOut])
machine.addRoutes(event: .completedSigningOut, transitions: [.signingOut => .signedOut])
machine.addRoutes(event: .showSettingsScreen, transitions: [.homeScreen => .settingsScreen])
machine.addRoutes(event: .dismissedSettingsScreen, transitions: [.settingsScreen => .homeScreen])
@@ -110,6 +111,10 @@ class AppCoordinatorStateMachine {
return .roomScreen(roomId: roomId)
case (.dismissedRoomScreen, .roomScreen):
return .homeScreen
case (.remoteSignOut(let isSoft), _):
return .remoteSigningOut(isSoft: isSoft)
case (.completedSigningOut, .remoteSigningOut):
return .signedOut
default:
return nil
}

View File

@@ -22,6 +22,10 @@ extension ElementL10n {
public static let authenticationServerInfoMatrixDescription = ElementL10n.tr("Untranslated", "authentication_server_info_matrix_description")
/// Choose your server to store your data
public static let authenticationServerInfoTitle = ElementL10n.tr("Untranslated", "authentication_server_info_title")
/// Mobile
public static let loginMobileDevice = ElementL10n.tr("Untranslated", "login_mobile_device")
/// Tablet
public static let loginTabletDevice = ElementL10n.tr("Untranslated", "login_tablet_device")
/// Failed creating the permalink
public static let roomTimelinePermalinkCreationFailure = ElementL10n.tr("Untranslated", "room_timeline_permalink_creation_failure")
/// Replying to %@

View File

@@ -0,0 +1,29 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import UIKit
extension UIDevice {
/// Returns if the device is a Phone
var isPhone: Bool {
userInterfaceIdiom == .phone
}
var initialDisplayName: String {
let string = isPhone ? ElementL10n.loginMobileDevice : ElementL10n.loginTabletDevice
return "X \(string)"
}
}

View File

@@ -165,7 +165,10 @@ final class LoginCoordinator: Coordinator, Presentable {
startLoading(isInteractionBlocking: true)
Task {
switch await authenticationService.login(username: username, password: password) {
switch await authenticationService.login(username: username,
password: password,
initialDeviceName: UIDevice.current.initialDisplayName,
deviceId: nil) {
case .success(let userSession):
callback?(.signedIn(userSession))
stopLoading()

View File

@@ -0,0 +1,68 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
import SwiftUI
/// Using an enum for the screen allows you define the different state cases with
/// the relevant associated data for each case.
enum MockSoftLogoutScreenState: String, CaseIterable {
// A case for each state you want to represent
// with specific, minimal associated data that will allow you
// mock that screen.
case emptyPassword
case enteredPassword
case oidc
case unsupported
case keyBackupNeeded
/// Generate the view struct for the screen state.
@MainActor var viewModel: SoftLogoutViewModel {
let credentials = SoftLogoutCredentials(userId: "@mock:matrix.org",
homeserverName: "matrix.org",
userDisplayName: "mock",
deviceId: nil)
switch self {
case .emptyPassword:
return SoftLogoutViewModel(credentials: credentials,
homeserver: .mockMatrixDotOrg,
keyBackupNeeded: false)
case .enteredPassword:
return SoftLogoutViewModel(credentials: credentials,
homeserver: .mockMatrixDotOrg,
keyBackupNeeded: false,
password: "12345678")
case .oidc:
return SoftLogoutViewModel(credentials: credentials,
homeserver: .mockOIDC,
keyBackupNeeded: false)
case .unsupported:
return SoftLogoutViewModel(credentials: credentials,
homeserver: .mockUnsupported,
keyBackupNeeded: false)
case .keyBackupNeeded:
return SoftLogoutViewModel(credentials: credentials,
homeserver: .mockMatrixDotOrg,
keyBackupNeeded: true)
}
}
}
extension MockSoftLogoutScreenState: Identifiable {
var id: String {
rawValue
}
}

View File

@@ -0,0 +1,184 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import AppAuth
import MatrixRustSDK
import SwiftUI
struct SoftLogoutCoordinatorParameters {
let authenticationService: AuthenticationServiceProxyProtocol
let credentials: SoftLogoutCredentials
let keyBackupNeeded: Bool
}
enum SoftLogoutCoordinatorResult: CustomStringConvertible {
/// Login was successful.
case signedIn(UserSessionProtocol)
/// Clear all user data
case clearAllData
/// A string representation of the result, ignoring any associated values that could leak PII.
var description: String {
switch self {
case .signedIn:
return "signedIn"
case .clearAllData:
return "clearAllData"
}
}
}
final class SoftLogoutCoordinator: Coordinator, Presentable {
// MARK: - Properties
// MARK: Private
private let parameters: SoftLogoutCoordinatorParameters
private let softLogoutHostingController: UIViewController
private var softLogoutViewModel: SoftLogoutViewModelProtocol
/// Passed to the OIDC service to provide a view controller from which to present the authentication session.
private let oidcUserAgent: OIDExternalUserAgentIOS?
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
private var loadingIndicator: UserIndicator?
private var successIndicator: UserIndicator?
/// The wizard used to handle the registration flow.
private var authenticationService: AuthenticationServiceProxyProtocol { parameters.authenticationService }
// MARK: Public
// Must be used only internally
var childCoordinators: [Coordinator] = []
var callback: (@MainActor (SoftLogoutCoordinatorResult) -> Void)?
// MARK: - Setup
@MainActor init(parameters: SoftLogoutCoordinatorParameters) {
self.parameters = parameters
let homeserver = parameters.authenticationService.homeserver
let viewModel = SoftLogoutViewModel(credentials: parameters.credentials,
homeserver: homeserver,
keyBackupNeeded: parameters.keyBackupNeeded)
softLogoutViewModel = viewModel
let view = SoftLogoutScreen(context: viewModel.context)
softLogoutHostingController = UIHostingController(rootView: view)
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: softLogoutHostingController)
oidcUserAgent = OIDExternalUserAgentIOS(presenting: softLogoutHostingController)
}
// MARK: - Public
func start() {
MXLog.debug("[SoftLogoutCoordinator] did start.")
softLogoutViewModel.callback = { [weak self] result in
guard let self = self else { return }
MXLog.debug("[SoftLogoutCoordinator] SoftLogoutViewModel did complete with result: \(result).")
switch result {
case .login(let password):
self.login(withPassword: password)
case .forgotPassword:
self.showForgotPasswordScreen()
case .clearAllData:
self.callback?(.clearAllData)
case .continueWithOIDC:
self.loginWithOIDC()
}
}
}
func toPresentable() -> UIViewController {
softLogoutHostingController
}
// MARK: - Private
/// Show an activity indicator whilst loading.
@MainActor private func startLoading() {
loadingIndicator = indicatorPresenter.present(.loading(label: ElementL10n.loading, isInteractionBlocking: true))
}
/// Hide the currently displayed activity indicator.
@MainActor private func stopLoading() {
loadingIndicator = nil
}
/// Shows the forgot password screen.
@MainActor private func showForgotPasswordScreen() {
MXLog.debug("[SoftLogoutCoordinator] showForgotPasswordScreen")
softLogoutViewModel.displayError(.alert("Not implemented."))
}
/// Login with the supplied username and password.
@MainActor private func login(withPassword password: String) {
let username = parameters.credentials.userId
startLoading()
Task {
switch await authenticationService.login(username: username,
password: password,
initialDeviceName: UIDevice.current.initialDisplayName,
deviceId: parameters.credentials.deviceId) {
case .success(let userSession):
callback?(.signedIn(userSession))
stopLoading()
case .failure(let error):
stopLoading()
handleError(error)
}
}
}
private func loginWithOIDC() {
guard let oidcUserAgent = oidcUserAgent else {
handleError(AuthenticationServiceError.oidcError(.notSupported))
return
}
startLoading()
Task {
switch await authenticationService.loginWithOIDC(userAgent: oidcUserAgent) {
case .success(let userSession):
callback?(.signedIn(userSession))
stopLoading()
case .failure(let error):
stopLoading()
handleError(error)
}
}
}
/// Processes an error to either update the flow or display it to the user.
private func handleError(_ error: AuthenticationServiceError) {
switch error {
case .invalidCredentials:
softLogoutViewModel.displayError(.alert(ElementL10n.authInvalidLoginParam))
case .accountDeactivated:
softLogoutViewModel.displayError(.alert(ElementL10n.authInvalidLoginDeactivatedAccount))
default:
softLogoutViewModel.displayError(.alert(ElementL10n.unknownError))
}
}
}

View File

@@ -0,0 +1,107 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
// MARK: Data
struct SoftLogoutCredentials {
let userId: String
let homeserverName: String
let userDisplayName: String
let deviceId: String?
}
// MARK: View model
enum SoftLogoutViewModelAction: CustomStringConvertible {
/// Login with password
case login(String)
/// Forgot password
case forgotPassword
/// Clear all user data
case clearAllData
/// Continue using OIDC.
case continueWithOIDC
/// A string representation of the result, ignoring any associated values that could leak PII.
var description: String {
switch self {
case .login:
return "login"
case .forgotPassword:
return "forgotPassword"
case .clearAllData:
return "clearAllData"
case .continueWithOIDC:
return "continueWithOIDC"
}
}
}
// MARK: View
struct SoftLogoutViewState: BindableState {
/// Soft logout credentials
var credentials: SoftLogoutCredentials
/// Data about the selected homeserver.
var homeserver: LoginHomeserver
/// Flag indicating soft logged out user needs backup for some keys
var keyBackupNeeded: Bool
/// View state that can be bound to from SwiftUI.
var bindings: SoftLogoutBindings
/// The types of login supported by the homeserver.
var loginMode: LoginMode { homeserver.loginMode }
/// Whether to show recover encryption keys message
var showRecoverEncryptionKeysMessage: Bool {
keyBackupNeeded
}
/// `true` when valid credentials have been entered and a homeserver has been loaded.
var canSubmit: Bool {
!bindings.password.isEmpty
}
}
struct SoftLogoutBindings {
/// The password input by the user.
var password: String
/// Information describing the currently displayed alert.
var alertInfo: AlertInfo<SoftLogoutErrorType>?
}
enum SoftLogoutViewAction {
/// Login.
case login
/// Forgot password
case forgotPassword
/// Clear all user data.
case clearAllData
/// Continue using OIDC.
case continueWithOIDC
}
enum SoftLogoutErrorType: Hashable {
/// A specific error message shown in an alert.
case alert(String)
/// An unknown error occurred.
case unknown
}

View File

@@ -0,0 +1,69 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
typealias SoftLogoutViewModelType = StateStoreViewModel<SoftLogoutViewState, SoftLogoutViewAction>
class SoftLogoutViewModel: SoftLogoutViewModelType, SoftLogoutViewModelProtocol {
// MARK: - Properties
// MARK: Private
// MARK: Public
var callback: (@MainActor (SoftLogoutViewModelAction) -> Void)?
// MARK: - Setup
init(credentials: SoftLogoutCredentials,
homeserver: LoginHomeserver,
keyBackupNeeded: Bool,
password: String = "") {
let bindings = SoftLogoutBindings(password: password)
let viewState = SoftLogoutViewState(credentials: credentials,
homeserver: homeserver,
keyBackupNeeded: keyBackupNeeded,
bindings: bindings)
super.init(initialViewState: viewState)
}
// MARK: - Public
override func process(viewAction: SoftLogoutViewAction) async {
switch viewAction {
case .login:
callback?(.login(state.bindings.password))
case .forgotPassword:
callback?(.forgotPassword)
case .clearAllData:
callback?(.clearAllData)
case .continueWithOIDC:
callback?(.continueWithOIDC)
}
}
@MainActor func displayError(_ type: SoftLogoutErrorType) {
switch type {
case .alert(let message):
state.bindings.alertInfo = AlertInfo(id: type,
title: ElementL10n.dialogTitleError,
message: message)
case .unknown:
state.bindings.alertInfo = AlertInfo(id: type)
}
}
}

View File

@@ -0,0 +1,25 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
protocol SoftLogoutViewModelProtocol {
var callback: (@MainActor (SoftLogoutViewModelAction) -> Void)? { get set }
var context: SoftLogoutViewModelType.Context { get }
/// Display an error to the user.
@MainActor func displayError(_ type: SoftLogoutErrorType)
}

View File

@@ -0,0 +1,190 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
struct SoftLogoutScreen: View {
// MARK: - Properties
// MARK: Private
/// The focus state of the password text field.
@FocusState private var isPasswordFocused: Bool
// MARK: Public
@ObservedObject var context: SoftLogoutViewModel.Context
// MARK: Views
var body: some View {
ScrollView {
VStack(spacing: 0) {
header
.padding(.top, UIConstants.topPaddingToNavigationBar)
.padding(.bottom, 36)
switch context.viewState.loginMode {
case .password:
loginForm
case .oidc:
oidcButton
default:
loginUnavailableText
}
clearDataForm
.padding(.top, 16)
}
.readableFrame()
.padding(.horizontal, 16)
.padding(.bottom, 16)
}
.background(Color.element.background.ignoresSafeArea())
.alert(item: $context.alertInfo) { $0.alert }
}
/// The title, message and icon at the top of the screen.
var header: some View {
VStack(alignment: .leading, spacing: 16) {
Text(ElementL10n.softLogoutSigninTitle)
.font(.element.title2Bold)
.multilineTextAlignment(.leading)
.foregroundColor(.element.primaryContent)
.accessibilityIdentifier("titleLabel")
Text(ElementL10n.softLogoutSigninNotice(context.viewState.credentials.homeserverName, context.viewState.credentials.userDisplayName, context.viewState.credentials.userId))
.font(.element.body)
.multilineTextAlignment(.leading)
.foregroundColor(.element.primaryContent)
.accessibilityIdentifier("messageLabel1")
if context.viewState.showRecoverEncryptionKeysMessage {
Text(ElementL10n.softLogoutSigninE2eWarningNotice)
.font(.element.body)
.multilineTextAlignment(.leading)
.foregroundColor(.element.primaryContent)
.accessibilityIdentifier("messageLabel2")
}
}
}
/// The form with text fields for username and password, along with a submit button.
var loginForm: some View {
VStack(spacing: 14) {
SecureField(ElementL10n.loginSignupPasswordHint, text: $context.password)
.focused($isPasswordFocused)
.textFieldStyle(.elementInput())
.textContentType(.password)
.submitLabel(.done)
.onSubmit(submit)
.accessibilityIdentifier("passwordTextField")
Button { context.send(viewAction: .forgotPassword) } label: {
Text(ElementL10n.authenticationLoginForgotPassword)
.font(.element.body)
}
.frame(maxWidth: .infinity, alignment: .trailing)
.padding(.bottom, 8)
.accessibilityIdentifier("forgotPasswordButton")
Button(action: submit) {
Text(ElementL10n.loginSignupSubmit)
}
.buttonStyle(.elementAction(.xLarge))
.disabled(!context.viewState.canSubmit)
.accessibilityIdentifier("nextButton")
}
}
/// The OIDC button that can be used for login.
var oidcButton: some View {
Button { context.send(viewAction: .continueWithOIDC) } label: {
Text(ElementL10n.loginContinue)
}
.buttonStyle(.elementAction(.xLarge))
.accessibilityIdentifier("oidcButton")
}
/// Text shown if neither password or OIDC login is supported.
var loginUnavailableText: some View {
Text(ElementL10n.autodiscoverWellKnownError)
.font(.body)
.multilineTextAlignment(.center)
.foregroundColor(.element.primaryContent)
.frame(maxWidth: .infinity)
.accessibilityIdentifier("unsupportedServerText")
}
/// The text field and submit button where the user enters an email address.
var clearDataForm: some View {
VStack(alignment: .leading, spacing: 12) {
Text(ElementL10n.softLogoutClearDataTitle)
.font(.element.title2Bold)
.multilineTextAlignment(.leading)
.foregroundColor(.element.primaryContent)
.accessibilityIdentifier("clearDataTitleLabel")
Text(ElementL10n.softLogoutClearDataNotice)
.font(.element.body)
.multilineTextAlignment(.leading)
.foregroundColor(.element.primaryContent)
.accessibilityIdentifier("clearDataMessageLabel")
.padding(.bottom, 12)
Button(action: clearData) {
Text(ElementL10n.softLogoutClearDataSubmit)
}
.buttonStyle(.elementAction(.xLarge, color: .element.alert))
.accessibilityIdentifier("clearDataButton")
}
}
/// Sends the `login` view action so long as a valid email address has been input.
func submit() {
guard context.viewState.canSubmit else { return }
context.send(viewAction: .login)
}
/// Sends the `forgotPassword` view action.
func forgotPassword() {
context.send(viewAction: .forgotPassword)
}
/// Sends the `clearAllData` view action.
func clearData() {
context.send(viewAction: .clearAllData)
}
}
// MARK: - Previews
struct SoftLogout_Previews: PreviewProvider {
static var previews: some View {
ForEach(MockSoftLogoutScreenState.allCases) { state in
screen(for: state.viewModel)
}
}
static func screen(for viewModel: SoftLogoutViewModel) -> some View {
NavigationView {
SoftLogoutScreen(context: viewModel.context)
.navigationBarTitleDisplayMode(.inline)
.tint(.element.accent)
}
.navigationViewStyle(.stack)
}
}

View File

@@ -90,6 +90,8 @@ final class HomeScreenCoordinator: Coordinator, Presentable {
self?.viewModel.showSessionVerificationBanner()
case .didVerifySession:
self?.viewModel.hideSessionVerificationBanner()
default:
break
}
}.store(in: &cancellables)

View File

@@ -104,11 +104,17 @@ class AuthenticationServiceProxy: AuthenticationServiceProxyProtocol {
}
}
func login(username: String, password: String) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
func login(username: String, password: String, initialDeviceName: String?, deviceId: String?) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
Benchmark.startTrackingForIdentifier("Login", message: "Started new login")
let loginTask: Task<Client, Error> = Task.detached {
try self.authenticationService.login(username: username, password: password)
#warning("Use new api on next SDK release.")
return try self.authenticationService.login(username: username,
password: password)
// try self.authenticationService.login(username: username,
// password: password,
// initialDeviceName: initialDeviceName,
// deviceId: deviceId)
}
switch await loginTask.result {

View File

@@ -36,5 +36,5 @@ protocol AuthenticationServiceProxyProtocol {
/// Performs login using OIDC for the current homeserver.
func loginWithOIDC(userAgent: OIDExternalUserAgentIOS) async -> Result<UserSessionProtocol, AuthenticationServiceError>
/// Performs a password login using the current homeserver.
func login(username: String, password: String) async -> Result<UserSessionProtocol, AuthenticationServiceError>
func login(username: String, password: String, initialDeviceName: String?, deviceId: String?) async -> Result<UserSessionProtocol, AuthenticationServiceError>
}

View File

@@ -45,8 +45,8 @@ class MockAuthenticationServiceProxy: AuthenticationServiceProxyProtocol {
func loginWithOIDC(userAgent: OIDExternalUserAgentIOS) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
.failure(.oidcError(.notSupported))
}
func login(username: String, password: String) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
func login(username: String, password: String, initialDeviceName: String?, deviceId: String?) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
// Login only succeeds if the username and password match the valid credentials property
guard username == validCredentials.username, password == validCredentials.password else {
return .failure(.invalidCredentials)

View File

@@ -28,7 +28,7 @@ class ScreenshotDetector {
var callback: (@MainActor (UIImage?, Error?) -> Void)?
/// Flag to whether ask for photos authorization by default if needed.
var autoRequestPHAuthorization = true
var autoRequestPHAuthorization = false
init() {
startObservingScreenshots()

View File

@@ -29,6 +29,14 @@ private class WeakClientProxyWrapper: ClientDelegate {
func didReceiveSyncUpdate() {
clientProxy?.didReceiveSyncUpdate()
}
func didReceiveAuthError(isSoftLogout: Bool) {
clientProxy?.didReceiveAuthError(isSoftLogout: isSoftLogout)
}
func didUpdateRestoreToken() {
clientProxy?.didUpdateRestoreToken()
}
}
class ClientProxy: ClientProxyProtocol {
@@ -57,11 +65,18 @@ class ClientProxy: ClientProxyProtocol {
self.backgroundTaskService = backgroundTaskService
client.setDelegate(delegate: WeakClientProxyWrapper(clientProxy: self))
#warning("Use isSoftLogout() api on next SDK release.")
Benchmark.startTrackingForIdentifier("ClientSync", message: "Started sync.")
client.startSync(timelineLimit: ClientProxy.syncLimit)
Task { await updateRooms() }
// if !client.isSoftLogout() {
// Benchmark.startTrackingForIdentifier("ClientSync", message: "Started sync.")
// client.startSync(timelineLimit: ClientProxy.syncLimit)
//
// Task { await updateRooms() }
// }
}
var userIdentifier: String {
@@ -72,6 +87,34 @@ class ClientProxy: ClientProxyProtocol {
return "Unknown user identifier"
}
}
var isSoftLogout: Bool {
#warning("Use isSoftLogout() api on next SDK release.")
return false
// client.isSoftLogout()
}
var deviceId: String? {
do {
return try client.deviceId()
} catch {
MXLog.error("Failed retrieving device id with error: \(error)")
return nil
}
}
var homeserver: String {
client.homeserver()
}
var restoreToken: String? {
do {
return try client.restoreToken()
} catch {
MXLog.error("Failed retrieving restore token with error: \(error)")
return nil
}
}
func loadUserDisplayName() async -> Result<String, ClientProxyError> {
await Task.detached {
@@ -91,7 +134,7 @@ class ClientProxy: ClientProxyProtocol {
let avatarURL = try self.client.avatarUrl()
return .success(avatarURL)
} catch {
return .failure(.failedRetrievingDisplayName)
return .failure(.failedRetrievingAvatarURL)
}
}
.value
@@ -125,6 +168,15 @@ class ClientProxy: ClientProxyProtocol {
}
.value
}
func logout() async {
do {
#warning("Use logout() api on next SDK release.")
// try client.logout()
} catch {
MXLog.error("Failed logging out with error: \(error)")
}
}
// MARK: Private
@@ -137,6 +189,14 @@ class ClientProxy: ClientProxyProtocol {
await self.updateRooms()
}
}
fileprivate func didReceiveAuthError(isSoftLogout: Bool) {
callbacks.send(.receivedAuthError(isSoftLogout: isSoftLogout))
}
fileprivate func didUpdateRestoreToken() {
callbacks.send(.updatedRestoreToken)
}
private func updateRooms() async {
var currentRooms = rooms

View File

@@ -21,6 +21,8 @@ import MatrixRustSDK
enum ClientProxyCallback {
case updatedRoomsList
case receivedSyncUpdate
case receivedAuthError(isSoftLogout: Bool)
case updatedRestoreToken
}
enum ClientProxyError: Error {
@@ -36,6 +38,14 @@ protocol ClientProxyProtocol {
var callbacks: PassthroughSubject<ClientProxyCallback, Never> { get }
var userIdentifier: String { get }
var isSoftLogout: Bool { get }
var deviceId: String? { get }
var homeserver: String { get }
var restoreToken: String? { get }
var rooms: [RoomProxy] { get }
@@ -52,4 +62,6 @@ protocol ClientProxyProtocol {
func loadMediaContentForSource(_ source: MatrixRustSDK.MediaSource) throws -> Data
func sessionVerificationControllerProxy() async -> Result<SessionVerificationControllerProxyProtocol, ClientProxyError>
func logout() async
}

View File

@@ -21,6 +21,10 @@ struct MockClientProxy: ClientProxyProtocol {
let callbacks = PassthroughSubject<ClientProxyCallback, Never>()
let userIdentifier: String
let isSoftLogout = false
let deviceId: String? = nil
let homeserver = ""
let restoreToken: String? = nil
let rooms = [RoomProxy]()
@@ -51,4 +55,8 @@ struct MockClientProxy: ClientProxyProtocol {
func sessionVerificationControllerProxy() async -> Result<SessionVerificationControllerProxyProtocol, ClientProxyError> {
.failure(.failedRetrievingSessionVerificationController)
}
func logout() async {
// no-op
}
}

View File

@@ -21,6 +21,9 @@ struct MockUserSession: UserSessionProtocol {
let sessionVerificationController: SessionVerificationControllerProxyProtocol? = nil
var userID: String { clientProxy.userIdentifier }
var isSoftLogout: Bool { clientProxy.isSoftLogout }
var deviceId: String? { clientProxy.deviceId }
var homeserver: String { clientProxy.homeserver }
let clientProxy: ClientProxyProtocol
let mediaProvider: MediaProviderProtocol
}

View File

@@ -20,9 +20,14 @@ import Foundation
class UserSession: UserSessionProtocol {
private var cancellables = Set<AnyCancellable>()
private var checkForSessionVerificationControllerCancellable: AnyCancellable?
private var authErrorCancellable: AnyCancellable?
private var restoreTokenUpdateCancellable: AnyCancellable?
var userID: String { clientProxy.userIdentifier }
var isSoftLogout: Bool { clientProxy.isSoftLogout }
var deviceId: String? { clientProxy.deviceId }
var homeserver: String { clientProxy.homeserver }
let clientProxy: ClientProxyProtocol
let mediaProvider: MediaProviderProtocol
let callbacks = PassthroughSubject<UserSessionCallback, Never>()
@@ -33,6 +38,8 @@ class UserSession: UserSessionProtocol {
self.mediaProvider = mediaProvider
setupSessionVerificationWatchdog()
setupAuthErrorWatchdog()
setupRestoreTokenUpdateWatchdog()
}
// MARK: - Private
@@ -77,4 +84,38 @@ class UserSession: UserSessionProtocol {
private func tearDownSessionVerificationControllerWatchdog() {
checkForSessionVerificationControllerCancellable = nil
}
// MARK: Auth Error Watchdog
private func setupAuthErrorWatchdog() {
authErrorCancellable = clientProxy.callbacks
.receive(on: DispatchQueue.main)
.sink { [weak self] callback in
if case .receivedAuthError(let isSoftLogout) = callback {
self?.callbacks.send(.didReceiveAuthError(isSoftLogout: isSoftLogout))
self?.tearDownAuthErrorWatchdog()
}
}
}
private func tearDownAuthErrorWatchdog() {
authErrorCancellable = nil
}
// MARK: Restore Token Update Watchdog
private func setupRestoreTokenUpdateWatchdog() {
restoreTokenUpdateCancellable = clientProxy.callbacks
.receive(on: DispatchQueue.main)
.sink { [weak self] callback in
if case .updatedRestoreToken = callback {
self?.callbacks.send(.updateRestoreTokenNeeded)
self?.tearDownRestoreTokenUpdateWatchdog()
}
}
}
private func tearDownRestoreTokenUpdateWatchdog() {
restoreTokenUpdateCancellable = nil
}
}

View File

@@ -20,10 +20,15 @@ import Foundation
enum UserSessionCallback {
case sessionVerificationNeeded
case didVerifySession
case didReceiveAuthError(isSoftLogout: Bool)
case updateRestoreTokenNeeded
}
protocol UserSessionProtocol {
var userID: String { get }
var isSoftLogout: Bool { get }
var deviceId: String? { get }
var homeserver: String { get }
var clientProxy: ClientProxyProtocol { get }
var mediaProvider: MediaProviderProtocol { get }

View File

@@ -69,6 +69,16 @@ class UserSessionStore: UserSessionStoreProtocol {
return .failure(error)
}
}
func refreshRestoreToken(for userSession: UserSessionProtocol) -> Result<Void, UserSessionStoreError> {
guard let accessToken = userSession.clientProxy.restoreToken else {
return .failure(.failedRefreshingRestoreToken)
}
keychainController.setRestoreToken(accessToken, forUsername: userSession.clientProxy.userIdentifier)
return .success(())
}
func logout(userSession: UserSessionProtocol) {
let userID = userSession.clientProxy.userIdentifier

View File

@@ -21,6 +21,7 @@ enum UserSessionStoreError: Error {
case missingCredentials
case failedRestoringLogin
case failedSettingUpSession
case failedRefreshingRestoreToken
}
@MainActor
@@ -36,6 +37,9 @@ protocol UserSessionStoreProtocol {
/// Creates a user session for a new client from the SDK.
func userSession(for client: Client) async -> Result<UserSession, UserSessionStoreError>
/// Refresh the restore token of the client for a given session.
func refreshRestoreToken(for userSession: UserSessionProtocol) -> Result<Void, UserSessionStoreError>
/// Logs out of the specified session.
func logout(userSession: UserSessionProtocol)

View File

@@ -21,6 +21,7 @@ enum UITestScreenIdentifier: String {
case serverSelection
case serverSelectionNonModal
case authenticationFlow
case softLogout
case analyticsPrompt
case simpleRegular
case simpleUpgrade

View File

@@ -82,6 +82,14 @@ class MockScreen: Identifiable {
case .authenticationFlow:
return AuthenticationCoordinator(authenticationService: MockAuthenticationServiceProxy(),
navigationRouter: navigationRouter)
case .softLogout:
let credentials = SoftLogoutCredentials(userId: "@mock:matrix.org",
homeserverName: "matrix.org",
userDisplayName: "mock",
deviceId: "ABCDEFGH")
return SoftLogoutCoordinator(parameters: .init(authenticationService: MockAuthenticationServiceProxy(),
credentials: credentials,
keyBackupNeeded: false))
case .simpleRegular:
return TemplateCoordinator(parameters: .init(promptType: .regular))
case .simpleUpgrade:

View File

@@ -0,0 +1,57 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import ElementX
import XCTest
@MainActor
class SoftLogoutUITests: XCTestCase {
var app: XCUIApplication!
@MainActor
override func setUp() async throws {
app = nil
}
func testInitialState() {
app = Application.launch()
app.goToScreenWithIdentifier(.softLogout)
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
XCTAssertTrue(app.staticTexts["messageLabel1"].exists, "The message 1 should be shown.")
XCTAssertTrue(app.staticTexts["clearDataTitleLabel"].exists, "The clear data title should be shown.")
XCTAssertTrue(app.staticTexts["clearDataMessageLabel"].exists, "The clear data message should be shown.")
let passwordTextField = app.secureTextFields["passwordTextField"]
XCTAssertTrue(passwordTextField.exists, "The password text field should be shown.")
XCTAssertTrue(passwordTextField.label.isEmpty, "The password text field text should be empty before text is input.")
XCTAssertEqual(passwordTextField.placeholderValue, ElementL10n.loginSignupPasswordHint, "The password text field should be showing the placeholder before text is input.")
let nextButton = app.buttons["nextButton"]
XCTAssertTrue(nextButton.exists, "The next button should be shown.")
XCTAssertFalse(nextButton.isEnabled, "The next button should be disabled before text is input.")
let forgotPasswordButton = app.buttons["forgotPasswordButton"]
XCTAssertTrue(forgotPasswordButton.exists, "The forgot password button should be shown.")
XCTAssertTrue(forgotPasswordButton.isEnabled, "The forgot password button should be enabled.")
let clearDataButton = app.buttons["clearDataButton"]
XCTAssertTrue(clearDataButton.exists, "The clear data button should be shown.")
XCTAssertTrue(clearDataButton.isEnabled, "The clear data button should be enabled.")
app.assertScreenshot(.softLogout)
}
}

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:251a91df282cc57f5014657fd72babd14dfd6b052943d33651d2ec2bffd363f6
size 125943

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6f3f78efc97f56080f8c10e411104ae24ead1872316be3bf88cd4d5cecdd6e9c
size 162578

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bb7fa08f6d0bef9fd94ea9b40b39724ffbdf27b0ffb0e84eabe8fe00903e2d3f
size 125863

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d4c99d6c7f66150e73b8d12381c2161f8bc681b2488cbb042b26623805994738
size 162369

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ca80888ec562d5f0dc82133ba690e4b8a4dd9f546013662b43b301ed8168ab03
size 127108

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d76e249f29ab646ac6d911aea1277495b0670e7dffd8e0271170fa60fe08ab0a
size 163890

View File

@@ -0,0 +1,91 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import XCTest
@testable import ElementX
class SoftLogoutViewModelTests: XCTestCase {
let credentials = SoftLogoutCredentials(userId: "mock_user_id",
homeserverName: "https://matrix.org",
userDisplayName: "mock_username",
deviceId: "ABCDEFGH")
@MainActor func testInitialStateForMatrixOrg() {
let viewModel = SoftLogoutViewModel(credentials: credentials,
homeserver: .mockMatrixDotOrg,
keyBackupNeeded: true)
let context = viewModel.context
// Given a view model where the user hasn't yet sent the verification email.
XCTAssert(context.password.isEmpty, "The view model should start with an empty password.")
XCTAssertFalse(context.viewState.canSubmit, "The view model should start with an invalid password.")
XCTAssertEqual(context.viewState.loginMode, .password, "The view model should show login form for the given homeserver.")
XCTAssert(context.viewState.showRecoverEncryptionKeysMessage, "The view model should show recover encryption keys message.")
}
@MainActor func testInitialStateForMatrixOrgPasswordEntered() {
let viewModel = SoftLogoutViewModel(credentials: credentials,
homeserver: .mockMatrixDotOrg,
keyBackupNeeded: true,
password: "12345678")
let context = viewModel.context
// Given a view model where the user hasn't yet sent the verification email.
XCTAssertTrue(context.viewState.canSubmit, "The view model should start with a valid password.")
XCTAssertEqual(context.viewState.loginMode, .password, "The view model should show login form for the given homeserver.")
XCTAssert(context.viewState.showRecoverEncryptionKeysMessage, "The view model should show recover encryption keys message.")
}
@MainActor func testInitialStateForBasicServer() {
let viewModel = SoftLogoutViewModel(credentials: credentials,
homeserver: .mockBasicServer,
keyBackupNeeded: false)
let context = viewModel.context
// Given a view model where the user hasn't yet sent the verification email.
XCTAssert(context.password.isEmpty, "The view model should start with an empty password.")
XCTAssertFalse(context.viewState.canSubmit, "The view model should start with an invalid password.")
XCTAssertEqual(context.viewState.loginMode, .password, "The view model should show login form for the given homeserver.")
XCTAssertFalse(context.viewState.showRecoverEncryptionKeysMessage, "The view model should not show recover encryption keys message.")
}
@MainActor func testInitialStateForOIDC() {
let viewModel = SoftLogoutViewModel(credentials: credentials,
homeserver: .mockOIDC,
keyBackupNeeded: false)
let context = viewModel.context
// Given a view model where the user hasn't yet sent the verification email.
XCTAssert(context.password.isEmpty, "The view model should start with an empty password.")
XCTAssertFalse(context.viewState.canSubmit, "The view model should start with an invalid password.")
XCTAssertTrue(context.viewState.loginMode.supportsOIDCFlow, "The view model should show OIDC button for the given homeserver.")
XCTAssertFalse(context.viewState.showRecoverEncryptionKeysMessage, "The view model should not show recover encryption keys message.")
}
@MainActor func testInitialStateForUnsupported() {
let viewModel = SoftLogoutViewModel(credentials: credentials,
homeserver: .mockUnsupported,
keyBackupNeeded: false)
let context = viewModel.context
// Given a view model where the user hasn't yet sent the verification email.
XCTAssert(context.password.isEmpty, "The view model should start with an empty password.")
XCTAssertFalse(context.viewState.canSubmit, "The view model should start with an invalid password.")
XCTAssertEqual(context.viewState.loginMode, .unsupported, "The view model should show unsupported text for the given homeserver.")
XCTAssertFalse(context.viewState.showRecoverEncryptionKeysMessage, "The view model should not show recover encryption keys message.")
}
}

1
changelog.d/104.feature Normal file
View File

@@ -0,0 +1 @@
Logout from the server & implement soft logout flow.