Log out (#174)
* 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:
@@ -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 */ = {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 %@
|
||||
|
||||
29
ElementX/Sources/Other/Extensions/UIDevice.swift
Normal file
29
ElementX/Sources/Other/Extensions/UIDevice.swift
Normal 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)"
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -90,6 +90,8 @@ final class HomeScreenCoordinator: Coordinator, Presentable {
|
||||
self?.viewModel.showSessionVerificationBanner()
|
||||
case .didVerifySession:
|
||||
self?.viewModel.hideSessionVerificationBanner()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}.store(in: &cancellables)
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -21,6 +21,7 @@ enum UITestScreenIdentifier: String {
|
||||
case serverSelection
|
||||
case serverSelectionNonModal
|
||||
case authenticationFlow
|
||||
case softLogout
|
||||
case analyticsPrompt
|
||||
case simpleRegular
|
||||
case simpleUpgrade
|
||||
|
||||
@@ -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:
|
||||
|
||||
57
UITests/Sources/SoftLogoutUITests.swift
Normal file
57
UITests/Sources/SoftLogoutUITests.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:251a91df282cc57f5014657fd72babd14dfd6b052943d33651d2ec2bffd363f6
|
||||
size 125943
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6f3f78efc97f56080f8c10e411104ae24ead1872316be3bf88cd4d5cecdd6e9c
|
||||
size 162578
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bb7fa08f6d0bef9fd94ea9b40b39724ffbdf27b0ffb0e84eabe8fe00903e2d3f
|
||||
size 125863
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d4c99d6c7f66150e73b8d12381c2161f8bc681b2488cbb042b26623805994738
|
||||
size 162369
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ca80888ec562d5f0dc82133ba690e4b8a4dd9f546013662b43b301ed8168ab03
|
||||
size 127108
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d76e249f29ab646ac6d911aea1277495b0670e7dffd8e0271170fa60fe08ab0a
|
||||
size 163890
|
||||
91
UnitTests/Sources/SoftLogoutViewModelTests.swift
Normal file
91
UnitTests/Sources/SoftLogoutViewModelTests.swift
Normal 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
1
changelog.d/104.feature
Normal file
@@ -0,0 +1 @@
|
||||
Logout from the server & implement soft logout flow.
|
||||
Reference in New Issue
Block a user