diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 332ade65ea..71b9c24fc7 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -119,9 +119,11 @@ 2C5E832434EE94E21AB3B238 /* EmojiPickerScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3EAE3E9D5EF4A6D5D9C6CFD /* EmojiPickerScreenViewModel.swift */; }; 2CA6ABBC9A88EB89EA52FCCB /* ConfettiScene.scn in Resources */ = {isa = PBXBuildFile; fileRef = B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */; }; 2CB6787E25B11711518E9588 /* OnboardingCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6281B199D8A8F0892490C2E /* OnboardingCoordinator.swift */; }; + 2DA90E38FF4E696825810C1A /* WaitlistScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECB08484CD5D77C9BF97AA78 /* WaitlistScreenUITests.swift */; }; 2E43A3D221BE9587BC19C3F1 /* MatrixEntityRegexTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */; }; 2E8C6672D0EE7D5B1BEDB8E2 /* ServerConfirmationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7478623CECC9438014244BA /* ServerConfirmationScreen.swift */; }; 2F1CF90A3460C153154427F0 /* RoomScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086B997409328F091EBA43CE /* RoomScreenUITests.swift */; }; + 2F66701B15657A87B4AC3A0A /* WaitlistScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09CE2B7AD979BDEE09FEDB08 /* WaitlistScreenModels.swift */; }; 2F94054F50E312AF30BE07F3 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B21E611DADDEF00307E7AC /* String.swift */; }; 308BD9343B95657FAA583FB7 /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 19CD5B074D7DD44AF4C58BB6 /* SwiftState */; }; 3097A0A867D2B19CE32DAE58 /* UIKitBackgroundTaskService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF1FFC3336EB23374BBBFCC /* UIKitBackgroundTaskService.swift */; }; @@ -317,6 +319,7 @@ 7B5DAB915357BE596529BF25 /* MapTilerStaticMapProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20872C3887F835958CE2F1D0 /* MapTilerStaticMapProtocol.swift */; }; 7BB31E67648CF32D2AB5E502 /* RoomScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CE3C90E487B255B735D73C8 /* RoomScreenViewModel.swift */; }; 7C1A7B594B2F8143F0DD0005 /* ElementXAttributeScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = C024C151639C4E1B91FCC68B /* ElementXAttributeScope.swift */; }; + 7C384A8E54A4B60A14CDE8E5 /* WaitlistScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12F1E7F9C2BE8BB751037826 /* WaitlistScreenCoordinator.swift */; }; 7C6376192F578E0BA801BFEC /* AnalyticsSettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C64A14EE89928207E3B42B /* AnalyticsSettingsScreenModels.swift */; }; 7CD16990BA843BE9ED639129 /* ImageRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DFE4453AB0B34C203447162 /* ImageRoomTimelineItem.swift */; }; 7E3C34BC10936AD4F77975F4 /* EmojiMartJSONLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39001365B76B89983FDB7AD8 /* EmojiMartJSONLoader.swift */; }; @@ -407,6 +410,7 @@ 9A3B0CDF097E3838FB1B9595 /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E89E530A8E92EC44301CA1 /* Bundle.swift */; }; 9A4E3D5AA44B041DAC3A0D81 /* OIDCAuthenticationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92390F9FA98255440A6BF5F8 /* OIDCAuthenticationPresenter.swift */; }; 9AC5F8142413862A9E3A2D98 /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = A7CA6F33C553805035C3B114 /* DeviceKit */; }; + 9AFEE46B03B7E995B3E1A53D /* WaitlistScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80C4927D09099497233E9980 /* WaitlistScreen.swift */; }; 9B582B3EEFEA615D4A6FBF1A /* TimelineReactionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351E89CE2ED9B73C5CC47955 /* TimelineReactionsView.swift */; }; 9B872FF37DBE6BE054903831 /* MediaUploadPreviewScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54E12B98252F6C527E31FEE /* MediaUploadPreviewScreenViewModelProtocol.swift */; }; 9BD3A773186291560DF92B62 /* RoomTimelineProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66F2402D738694F98729A441 /* RoomTimelineProvider.swift */; }; @@ -501,6 +505,7 @@ B6DA66EFC13A90846B625836 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 91DE43B8815918E590912DDA /* InfoPlist.strings */; }; B6DF6B6FA8734B70F9BF261E /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */; }; B6EC2148FA5443C9289BEEBA /* MediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17EFA1D3D09FC2F9C5E1CB2 /* MediaProvider.swift */; }; + B717A820BE02C6FE2CB53F6E /* WaitlistScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B697816AF93DA06EC58C5D70 /* WaitlistScreenViewModelProtocol.swift */; }; B721125D17A0BA86794F29FB /* MockServerSelectionScreenState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E057FB1F07A5C201C89061 /* MockServerSelectionScreenState.swift */; }; B796A25F282C0A340D1B9C12 /* ImageRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B5EDCD05D50BA9B815C66C /* ImageRoomTimelineItemContent.swift */; }; B80C4FABB5529DF12436FFDA /* AppIcon.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 16DC8C5B2991724903F1FA6A /* AppIcon.pdf */; }; @@ -570,6 +575,7 @@ CE7148E80F09B7305E026AC6 /* OnboardingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1198B925F4A88DA74083662 /* OnboardingViewModel.swift */; }; CE9530A4CA661E090635C2F2 /* NotificationItemProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F7FE40EF7490A7E09D7BE6 /* NotificationItemProxy.swift */; }; CEB8FB1269DE20536608B957 /* LoginMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B41FABA2B0AEF4389986495 /* LoginMode.swift */; }; + CF3827071B0BC9638BD44F5D /* WaitlistScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AB58EF0176D4CFB1040DA22 /* WaitlistScreenViewModel.swift */; }; CF4044A8EED5C41BC0ED6ABE /* SoftLogoutScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D316BB02636AF2174F2580E6 /* SoftLogoutScreenViewModelProtocol.swift */; }; CF82143AA4A4F7BD11D22946 /* RoomTimelineViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACB6C5E4950B6C9842F35A38 /* RoomTimelineViewProvider.swift */; }; D02AA6208C7ACB9BE6332394 /* UNNotificationContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE148A4FFEE853C5A281500C /* UNNotificationContent.swift */; }; @@ -689,6 +695,7 @@ FA5A7E32B1920FCB4EEDC1BA /* RoomDetailsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6493AC9979CEB1410302BFE3 /* RoomDetailsScreenCoordinator.swift */; }; FA9C427FFB11B1AA2DCC5602 /* RoomProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */; }; FB53CD9B74A15B3B94F9F788 /* CreateRoomModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B849D2FF2CC12BA411A1651 /* CreateRoomModels.swift */; }; + FB9A1DD83EF641A75ABBCE69 /* WaitlistScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C796FC1DFDBCDD5573D0360F /* WaitlistScreenViewModelTests.swift */; }; FBCCF1EA25A071324FCD8544 /* TimelineItemDebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7023EB4F3B7C7D1FBA68638B /* TimelineItemDebugView.swift */; }; FBF09B6C900415800DDF2A21 /* EmojiProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C113E0CB7E15E9765B1817A /* EmojiProvider.swift */; }; FC10228E73323BDC09526F97 /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 9573B94B1C86C6DF751AF3FD /* SwiftState */; }; @@ -774,6 +781,7 @@ 095AED4CF56DFF3EB7BB84C8 /* RoomTimelineProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineProviderProtocol.swift; sourceTree = ""; }; 0960A7F5C1B0B6679BDF26F9 /* ElementToggleStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementToggleStyle.swift; sourceTree = ""; }; 099F2D36C141D845A445B1E6 /* EmojiProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiProviderTests.swift; sourceTree = ""; }; + 09CE2B7AD979BDEE09FEDB08 /* WaitlistScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenModels.swift; sourceTree = ""; }; 0A3E77399BD262D301451BF2 /* RoomDetailsEditScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenCoordinator.swift; sourceTree = ""; }; 0B987FC3FDBAA0E1C5AA235C /* PaginationIndicatorRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationIndicatorRoomTimelineItem.swift; sourceTree = ""; }; 0BC588051E6572A1AF51D738 /* TimelineSenderAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineSenderAvatarView.swift; sourceTree = ""; }; @@ -799,6 +807,7 @@ 1222DB76B917EB8A55365BA5 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; 127A57D053CE8C87B5EFB089 /* Consumable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Consumable.swift; sourceTree = ""; }; 127C8472672A5BA09EF1ACF8 /* CurrentValuePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentValuePublisher.swift; sourceTree = ""; }; + 12F1E7F9C2BE8BB751037826 /* WaitlistScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenCoordinator.swift; sourceTree = ""; }; 1304D9191300873EADA52D6E /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; path = IntegrationTests.xctestplan; sourceTree = ""; }; 130ED565A078F7E0B59D9D25 /* UNTextInputNotificationResponse+Creator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNTextInputNotificationResponse+Creator.swift"; sourceTree = ""; }; 13802897C7AFA360EA74C0B0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -822,6 +831,7 @@ 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Strings+Untranslated.swift"; sourceTree = ""; }; 1A4D29F2683F5772AC72406F /* MapTilerStaticMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerStaticMap.swift; sourceTree = ""; }; 1A7ED2EF5BDBAD2A7DBC4636 /* GeoURITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoURITests.swift; sourceTree = ""; }; + 1AB58EF0176D4CFB1040DA22 /* WaitlistScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenViewModel.swift; sourceTree = ""; }; 1ABDE6F66532CBEB0E016F94 /* RoomProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxyMock.swift; sourceTree = ""; }; 1B1EE0908B2BF9212436AD3E /* SessionVerificationScreenStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenStateMachine.swift; sourceTree = ""; }; 1B2AC540DE619B36832A5DB5 /* LocationRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineItem.swift; sourceTree = ""; }; @@ -1062,6 +1072,7 @@ 7DDBF99755A9008CF8C8499E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 7DDF49CEBC0DFC59C308335F /* RoomMemberDetailsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreenViewModelProtocol.swift; sourceTree = ""; }; 7F615A00DB223FF3280204D2 /* UserDiscoveryServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoveryServiceProtocol.swift; sourceTree = ""; }; + 80C4927D09099497233E9980 /* WaitlistScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreen.swift; sourceTree = ""; }; 818695BED971753243FEF897 /* StickerRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerRoomTimelineItem.swift; sourceTree = ""; }; 818CBE6249ED6E8FC30E8366 /* ViewModelContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelContext.swift; sourceTree = ""; }; 8196D64EB9CF2AF1F43E4ED1 /* AnalyticsPromptScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -1198,6 +1209,7 @@ B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineView.swift; sourceTree = ""; }; B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; path = ConfettiScene.scn; sourceTree = ""; }; B6311F21F911E23BE4DF51B4 /* ReadMarkerRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadMarkerRoomTimelineView.swift; sourceTree = ""; }; + B697816AF93DA06EC58C5D70 /* WaitlistScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenViewModelProtocol.swift; sourceTree = ""; }; B6E89E530A8E92EC44301CA1 /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = ""; }; B7AE92E7BFF71797BDE1D261 /* MapTilerStyleBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerStyleBuilder.swift; sourceTree = ""; }; B7F0192CE2F891141A25B49F /* UITestsSignalling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsSignalling.swift; sourceTree = ""; }; @@ -1249,6 +1261,7 @@ C75EF87651B00A176AB08E97 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; C7661EFFCAA307A97D71132A /* HomeScreenRoomList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRoomList.swift; sourceTree = ""; }; C789E7BFC066CF39B8AE0974 /* NetworkMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitor.swift; sourceTree = ""; }; + C796FC1DFDBCDD5573D0360F /* WaitlistScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenViewModelTests.swift; sourceTree = ""; }; C830A64609CBD152F06E0457 /* NotificationConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationConstants.swift; sourceTree = ""; }; C843CF833BF6485B64AC87E1 /* AppRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouter.swift; sourceTree = ""; }; C8F2A7A4E3F5060F52ACFFB0 /* RedactedRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedactedRoomTimelineView.swift; sourceTree = ""; }; @@ -1335,6 +1348,7 @@ E9DFC0FBA0FC6FC4DC0FC9FC /* NSESettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSESettings.swift; sourceTree = ""; }; EB3B237387B8288A5A938F1B /* UserAgentBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAgentBuilderTests.swift; sourceTree = ""; }; EC589E641AE46EFB2962534D /* RoomMemberDetailsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsViewModelTests.swift; sourceTree = ""; }; + ECB08484CD5D77C9BF97AA78 /* WaitlistScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenUITests.swift; sourceTree = ""; }; ECD5FCBA169B6A82F501CA1B /* AnalyticsSettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenViewModelProtocol.swift; sourceTree = ""; }; ECF79FB25E2D4BD6F50CE7C9 /* RoomMembersListScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenViewModel.swift; sourceTree = ""; }; ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRoomCell.swift; sourceTree = ""; }; @@ -2382,6 +2396,7 @@ EB3B237387B8288A5A938F1B /* UserAgentBuilderTests.swift */, 2429224EB0EEA34D35CE9249 /* UserIndicatorControllerTests.swift */, BA241DEEF7C8A7181C0AEDC9 /* UserPreferenceTests.swift */, + C796FC1DFDBCDD5573D0360F /* WaitlistScreenViewModelTests.swift */, 53280D2292E6C9C7821773FD /* UserSession */, 70C5B842301AC281DF374E41 /* Extensions */, 7583EAC171059A86B767209F /* MediaProvider */, @@ -2706,6 +2721,7 @@ 55F30E764BED111C81739844 /* SoftLogoutUITests.swift */, DA2AEC1AB349A341FE13DEC1 /* StartChatScreenUITests.swift */, F899D02CF26EA7675EEBE74C /* UserSessionScreenTests.swift */, + ECB08484CD5D77C9BF97AA78 /* WaitlistScreenUITests.swift */, ); path = Sources; sourceTree = ""; @@ -2744,6 +2760,14 @@ path = Templates; sourceTree = ""; }; + 9F4A1E90C924DE7954BA5005 /* View */ = { + isa = PBXGroup; + children = ( + 80C4927D09099497233E9980 /* WaitlistScreen.swift */, + ); + path = View; + sourceTree = ""; + }; 9FD8D798D879069243A7E7F7 /* View */ = { isa = PBXGroup; children = ( @@ -2804,6 +2828,18 @@ path = UnitTests; sourceTree = ""; }; + A67C1933D33AAC64642E9A82 /* WaitlistScreen */ = { + isa = PBXGroup; + children = ( + 12F1E7F9C2BE8BB751037826 /* WaitlistScreenCoordinator.swift */, + 09CE2B7AD979BDEE09FEDB08 /* WaitlistScreenModels.swift */, + 1AB58EF0176D4CFB1040DA22 /* WaitlistScreenViewModel.swift */, + B697816AF93DA06EC58C5D70 /* WaitlistScreenViewModelProtocol.swift */, + 9F4A1E90C924DE7954BA5005 /* View */, + ); + path = WaitlistScreen; + sourceTree = ""; + }; A78C2592419CA4C76FBA8FD2 /* Application */ = { isa = PBXGroup; children = ( @@ -3296,6 +3332,7 @@ BA1938A75D8C780F694CEB62 /* ServerConfirmationScreen */, 2D0D49B0533C4C2EB889BF3A /* ServerSelectionScreen */, 5B2C520AB9863B8CBC8EB3CA /* SoftLogoutScreen */, + A67C1933D33AAC64642E9A82 /* WaitlistScreen */, ); path = Authentication; sourceTree = ""; @@ -3905,6 +3942,7 @@ 04F17DE71A50206336749BAC /* UserPreferenceTests.swift in Sources */, 81A7C020CB5F6232242A8414 /* UserSessionTests.swift in Sources */, 99F8DA4CCC6772EE5FE68E24 /* ViewModelContext.swift in Sources */, + FB9A1DD83EF641A75ABBCE69 /* WaitlistScreenViewModelTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4385,6 +4423,11 @@ 1A83DD22F3E6F76B13B6E2F9 /* VideoRoomTimelineItemContent.swift in Sources */, 64F43D7390DA2A0AFD6BA911 /* VideoRoomTimelineView.swift in Sources */, 6FC10A00D268FCD48B631E37 /* ViewFrameReader.swift in Sources */, + 9AFEE46B03B7E995B3E1A53D /* WaitlistScreen.swift in Sources */, + 7C384A8E54A4B60A14CDE8E5 /* WaitlistScreenCoordinator.swift in Sources */, + 2F66701B15657A87B4AC3A0A /* WaitlistScreenModels.swift in Sources */, + CF3827071B0BC9638BD44F5D /* WaitlistScreenViewModel.swift in Sources */, + B717A820BE02C6FE2CB53F6E /* WaitlistScreenViewModelProtocol.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4425,6 +4468,7 @@ 54AE8860D668AFD96E7E177B /* UITestsScreenIdentifier.swift in Sources */, 84EFCB95F9DA2979C8042B26 /* UITestsSignalling.swift in Sources */, B22D857D1E8FCA6DD74A58E3 /* UserSessionScreenTests.swift in Sources */, + 2DA90E38FF4E696825810C1A /* WaitlistScreenUITests.swift in Sources */, 588411C8FD72B2A2DFE5F7DE /* XCUIElement.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings index 2b31da559c..12736b9328 100644 --- a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings +++ b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings @@ -14,3 +14,9 @@ "soft_logout_clear_data_submit" = "Clear all data"; "soft_logout_clear_data_dialog_title" = "Clear data"; "soft_logout_clear_data_dialog_content" = "Clear all data currently stored on this device?\nSign in again to access your account data and messages."; + +"common_refreshing" = "Refreshing…"; +"screen_waitlist_title" = "You're on the waitlist!"; +"screen_waitlist_title_success" = "You're in!"; +"screen_waitlist_message" = "There's a high demand for %1$@ on %2$@ at the moment. Come back to the app in a few days and try again.\n\nThanks for your patience!"; +"screen_waitlist_message_success" = "Welcome to %1$@"; diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index e516b3fd94..e072d24704 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -291,10 +291,10 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate, displayName = name } - let credentials = SoftLogoutScreenCredentials(userId: userSession.userID, + let credentials = SoftLogoutScreenCredentials(userID: userSession.userID, homeserverName: userSession.homeserver, userDisplayName: displayName, - deviceId: userSession.deviceID) + deviceID: userSession.deviceID) let authenticationService = AuthenticationServiceProxy(userSessionStore: userSessionStore) _ = await authenticationService.configure(for: userSession.homeserver) diff --git a/ElementX/Sources/Generated/Strings+Untranslated.swift b/ElementX/Sources/Generated/Strings+Untranslated.swift index 2fb62c2fab..a2c4ded533 100644 --- a/ElementX/Sources/Generated/Strings+Untranslated.swift +++ b/ElementX/Sources/Generated/Strings+Untranslated.swift @@ -10,6 +10,22 @@ import Foundation // swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces public enum UntranslatedL10n { + /// Refreshing… + public static var commonRefreshing: String { return UntranslatedL10n.tr("Untranslated", "common_refreshing") } + /// There's a high demand for %1$@ on %2$@ at the moment. Come back to the app in a few days and try again. + /// + /// Thanks for your patience! + public static func screenWaitlistMessage(_ p1: Any, _ p2: Any) -> String { + return UntranslatedL10n.tr("Untranslated", "screen_waitlist_message", String(describing: p1), String(describing: p2)) + } + /// Welcome to %1$@ + public static func screenWaitlistMessageSuccess(_ p1: Any) -> String { + return UntranslatedL10n.tr("Untranslated", "screen_waitlist_message_success", String(describing: p1)) + } + /// You're on the waitlist! + public static var screenWaitlistTitle: String { return UntranslatedL10n.tr("Untranslated", "screen_waitlist_title") } + /// You're in! + public static var screenWaitlistTitleSuccess: String { return UntranslatedL10n.tr("Untranslated", "screen_waitlist_title_success") } /// Clear all data currently stored on this device? /// Sign in again to access your account data and messages. public static var softLogoutClearDataDialogContent: String { return UntranslatedL10n.tr("Untranslated", "soft_logout_clear_data_dialog_content") } diff --git a/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift b/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift index f2be6af9c7..8ddf72d130 100644 --- a/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift @@ -168,12 +168,33 @@ class AuthenticationCoordinator: CoordinatorProtocol { case .configuredForOIDC: // Pop back to the confirmation screen for OIDC login to continue. navigationStackCoordinator.pop(animated: false) + case .isOnWaitlist(let credentials): + showWaitlistScreen(for: credentials) } } navigationStackCoordinator.push(coordinator) } + private func showWaitlistScreen(for credentials: WaitlistScreenCredentials) { + let parameters = WaitlistScreenCoordinatorParameters(credentials: credentials, + authenticationService: authenticationService) + let coordinator = WaitlistScreenCoordinator(parameters: parameters) + + coordinator.actions.sink { [weak self] action in + guard let self else { return } + switch action { + case .signedIn(let userSession): + userHasSignedIn(userSession: userSession) + case .cancel: + navigationStackCoordinator.pop() + } + } + .store(in: &cancellables) + + navigationStackCoordinator.push(coordinator) + } + private func userHasSignedIn(userSession: UserSessionProtocol) { showAnalyticsPromptIfNeeded { [weak self] in guard let self else { return } diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenCoordinator.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenCoordinator.swift index ea5b3bd7dc..cf2e93fb4f 100644 --- a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenCoordinator.swift @@ -27,6 +27,8 @@ enum LoginScreenCoordinatorAction { case configuredForOIDC /// Login was successful. case signedIn(UserSessionProtocol) + /// The user's request to login failed due to being on the proxy waitlist. + case isOnWaitlist(WaitlistScreenCredentials) } final class LoginScreenCoordinator: CoordinatorProtocol { @@ -117,13 +119,22 @@ final class LoginScreenCoordinator: CoordinatorProtocol { switch await authenticationService.login(username: username, password: password, initialDeviceName: UIDevice.current.initialDeviceName, - deviceId: nil) { + deviceID: nil) { case .success(let userSession): callback?(.signedIn(userSession)) stopLoading() case .failure(let error): stopLoading() - handleError(error) + switch error { + case .isOnWaitlist: + callback?(.isOnWaitlist(.init(username: username, + password: password, + initialDeviceName: UIDevice.current.initialDeviceName, + deviceID: nil, + homeserver: authenticationService.homeserver.value))) + default: + handleError(error) + } } } } diff --git a/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/MockSoftLogoutScreenState.swift b/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/MockSoftLogoutScreenState.swift index 334aedb48f..235d60b330 100644 --- a/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/MockSoftLogoutScreenState.swift +++ b/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/MockSoftLogoutScreenState.swift @@ -31,10 +31,10 @@ enum MockSoftLogoutScreenState: String, CaseIterable { /// Generate the view struct for the screen state. @MainActor var viewModel: SoftLogoutScreenViewModel { - let credentials = SoftLogoutScreenCredentials(userId: "@mock:matrix.org", + let credentials = SoftLogoutScreenCredentials(userID: "@mock:matrix.org", homeserverName: "matrix.org", userDisplayName: "mock", - deviceId: nil) + deviceID: nil) switch self { case .emptyPassword: return SoftLogoutScreenViewModel(credentials: credentials, diff --git a/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenCoordinator.swift b/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenCoordinator.swift index 408d55c824..238bfc0d2e 100644 --- a/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenCoordinator.swift @@ -108,7 +108,7 @@ final class SoftLogoutScreenCoordinator: CoordinatorProtocol { /// Login with the supplied username and password. @MainActor private func login(withPassword password: String) { - let username = parameters.credentials.userId + let username = parameters.credentials.userID startLoading() @@ -116,7 +116,7 @@ final class SoftLogoutScreenCoordinator: CoordinatorProtocol { switch await authenticationService.login(username: username, password: password, initialDeviceName: UIDevice.current.initialDeviceName, - deviceId: parameters.credentials.deviceId) { + deviceID: parameters.credentials.deviceID) { case .success(let userSession): callback?(.signedIn(userSession)) stopLoading() diff --git a/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenModels.swift b/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenModels.swift index 054b1073c1..e75dc3ede8 100644 --- a/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenModels.swift +++ b/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenModels.swift @@ -17,10 +17,10 @@ import SwiftUI struct SoftLogoutScreenCredentials { - let userId: String + let userID: String let homeserverName: String let userDisplayName: String - let deviceId: String? + let deviceID: String? } enum SoftLogoutScreenViewModelAction: CustomStringConvertible { diff --git a/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/View/SoftLogoutScreen.swift b/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/View/SoftLogoutScreen.swift index 07f5a33ec7..4f2a88eba1 100644 --- a/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/View/SoftLogoutScreen.swift +++ b/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/View/SoftLogoutScreen.swift @@ -64,7 +64,7 @@ struct SoftLogoutScreen: View { .foregroundColor(.compound.textPrimary) .accessibilityIdentifier(A11yIdentifiers.softLogoutScreen.title) - Text(UntranslatedL10n.softLogoutSigninNotice(context.viewState.credentials.homeserverName, context.viewState.credentials.userDisplayName, context.viewState.credentials.userId)) + Text(UntranslatedL10n.softLogoutSigninNotice(context.viewState.credentials.homeserverName, context.viewState.credentials.userDisplayName, context.viewState.credentials.userID)) .font(.compound.bodyLG) .multilineTextAlignment(.leading) .foregroundColor(.compound.textPrimary) diff --git a/ElementX/Sources/Screens/Authentication/WaitlistScreen/View/WaitlistScreen.swift b/ElementX/Sources/Screens/Authentication/WaitlistScreen/View/WaitlistScreen.swift new file mode 100644 index 0000000000..e5813d6686 --- /dev/null +++ b/ElementX/Sources/Screens/Authentication/WaitlistScreen/View/WaitlistScreen.swift @@ -0,0 +1,106 @@ +// +// 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 WaitlistScreen: View { + @ObservedObject var context: WaitlistScreenViewModel.Context + + var body: some View { + FullscreenDialog(topPadding: UIConstants.iconTopPaddingToNavigationBar) { + header + } bottomContent: { + buttons + } + .background() + .environment(\.backgroundStyle, AnyShapeStyle(Color.compound.bgCanvasDefault)) + .navigationBarBackButtonHidden() + .toolbar { toolbar } + .toolbar(.visible, for: .navigationBar) // Layout consistency in all states. + .overlay { + EffectsView(effect: context.viewState.isWaiting ? .none : .confetti) + .ignoresSafeArea() + .allowsHitTesting(false) + } + } + + /// The main content of the view to be shown in a scroll view. + var header: some View { + VStack(spacing: 8) { + AuthenticationIconImage(image: Image(systemName: context.viewState.iconSymbolName)) + .fontWeight(.semibold) + .padding(.bottom, 8) + + Text(context.viewState.title) + .font(.compound.headingMDBold) + .multilineTextAlignment(.center) + .foregroundColor(.compound.textPrimary) + .fixedSize(horizontal: false, vertical: true) + + Text(context.viewState.message) + .font(.compound.bodyMD) + .multilineTextAlignment(.center) + .foregroundColor(.compound.textSecondary) + } + .padding(.horizontal, 16) + } + + /// The action buttons shown at the bottom of the view. + @ViewBuilder + var buttons: some View { + if let userSession = context.viewState.userSession { + Button { context.send(viewAction: .continue(userSession)) } label: { + Text(L10n.actionContinue) + } + .buttonStyle(.elementAction(.xLarge)) + } + } + + @ToolbarContentBuilder + var toolbar: some ToolbarContent { + if context.viewState.isWaiting { + ToolbarItem(placement: .cancellationAction) { + Button(L10n.actionCancel) { + context.send(viewAction: .cancel) + } + } + } + } +} + +// MARK: - Previews + +struct WaitlistScreen_Previews: PreviewProvider { + static let viewModel = WaitlistScreenViewModel(homeserver: .mockMatrixDotOrg) + static let successViewModel = { + let viewModel = WaitlistScreenViewModel(homeserver: .mockMatrixDotOrg) + viewModel.update(userSession: MockUserSession(clientProxy: MockClientProxy(userID: "@alice:matrix.org"), + mediaProvider: MockMediaProvider())) + return viewModel + }() + + static var previews: some View { + NavigationStack { + WaitlistScreen(context: viewModel.context) + } + .previewDisplayName("Waiting") + + NavigationStack { + WaitlistScreen(context: successViewModel.context) + } + .previewDisplayName("Success") + } +} diff --git a/ElementX/Sources/Screens/Authentication/WaitlistScreen/WaitlistScreenCoordinator.swift b/ElementX/Sources/Screens/Authentication/WaitlistScreen/WaitlistScreenCoordinator.swift new file mode 100644 index 0000000000..401b54cef2 --- /dev/null +++ b/ElementX/Sources/Screens/Authentication/WaitlistScreen/WaitlistScreenCoordinator.swift @@ -0,0 +1,128 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import SwiftUI + +struct WaitlistScreenCoordinatorParameters { + /// The credentials for the login. + let credentials: WaitlistScreenCredentials + /// The service used to authenticate the user. + let authenticationService: AuthenticationServiceProxyProtocol + /// The service locator for the screen. + var userIndicatorController: UserIndicatorControllerProtocol = ServiceLocator.shared.userIndicatorController +} + +enum WaitlistScreenCoordinatorAction { + /// Login was successful after a retry attempt. + case signedIn(UserSessionProtocol) + /// The user would like to try sign in another way. + case cancel +} + +final class WaitlistScreenCoordinator: CoordinatorProtocol { + private let parameters: WaitlistScreenCoordinatorParameters + private var viewModel: WaitlistScreenViewModelProtocol + private let actionsSubject: PassthroughSubject = .init() + private var cancellables: Set = .init() + private var refreshCancellable: AnyCancellable? + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(parameters: WaitlistScreenCoordinatorParameters) { + self.parameters = parameters + + viewModel = WaitlistScreenViewModel(homeserver: parameters.credentials.homeserver) + + refreshCancellable = NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification) + .sink { [weak self] _ in + self?.refresh() + } + } + + func start() { + viewModel.actions.sink { [weak self] action in + guard let self else { return } + switch action { + case .cancel: + actionsSubject.send(.cancel) + case .continue(let userSession): + actionsSubject.send(.signedIn(userSession)) + } + } + .store(in: &cancellables) + } + + func toPresentable() -> AnyView { + AnyView(WaitlistScreen(context: viewModel.context)) + } + + // MARK: - Private + + /// Refresh the screen by retrying login to see if the waitlist has opened up. + private func refresh() { + guard parameters.credentials.homeserver == parameters.authenticationService.homeserver.value else { + MXLog.warning("Homeserver configuration changed.") + actionsSubject.send(.cancel) + return + } + + showRefreshIndicator() + + Task { + switch await parameters.authenticationService.login(username: parameters.credentials.username, + password: parameters.credentials.password, + initialDeviceName: parameters.credentials.initialDeviceName, + deviceID: parameters.credentials.deviceID) { + case .success(let userSession): + hideRefreshIndicator() + refreshCancellable = nil + viewModel.update(userSession: userSession) + case .failure(.isOnWaitlist): + hideRefreshIndicator() // Nothing to do, still waiting for availability. + case .failure(.invalidCredentials): + hideRefreshIndicator() + actionsSubject.send(.cancel) + case .failure: + hideRefreshIndicator() + showFailureIndicator() + } + } + } + + private static let refreshIndicatorID = "WaitlistCoordinatorRefresh" + private static let failureIndicatorID = "WaitlistCoordinatorFailure" + + private func showRefreshIndicator() { + parameters.userIndicatorController.submitIndicator(UserIndicator(id: Self.refreshIndicatorID, + type: .modal, + title: UntranslatedL10n.commonRefreshing, + persistent: true)) + } + + private func hideRefreshIndicator() { + parameters.userIndicatorController.retractIndicatorWithId(Self.refreshIndicatorID) + } + + private func showFailureIndicator() { + parameters.userIndicatorController.submitIndicator(UserIndicator(id: Self.failureIndicatorID, + type: .toast, + title: L10n.errorUnknown, + iconName: "xmark")) + } +} diff --git a/ElementX/Sources/Screens/Authentication/WaitlistScreen/WaitlistScreenModels.swift b/ElementX/Sources/Screens/Authentication/WaitlistScreen/WaitlistScreenModels.swift new file mode 100644 index 0000000000..74cd4bd290 --- /dev/null +++ b/ElementX/Sources/Screens/Authentication/WaitlistScreen/WaitlistScreenModels.swift @@ -0,0 +1,74 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +enum WaitlistScreenViewModelAction { + case cancel + case `continue`(UserSessionProtocol) +} + +/// The user's credentials used to retry login and refresh the waiting list. +struct WaitlistScreenCredentials: CustomStringConvertible, CustomDebugStringConvertible { + let username: String + let password: String + let initialDeviceName: String? + let deviceID: String? + + let homeserver: LoginHomeserver + + var description: String { "Redacted" } + var debugDescription: String { "Redacted" } +} + +struct WaitlistScreenViewState: BindableState { + /// The homeserver the user is waiting for. + let homeserver: LoginHomeserver + /// When refresh was successful, the user session that was returned by the login. + var userSession: UserSessionProtocol? + + /// Whether or not the user is still waiting in the queue. + var isWaiting: Bool { userSession == nil } + + var iconSymbolName: String { + if isWaiting { + return "stopwatch" + } else { + return "sparkles" + } + } + + var title: String { + if isWaiting { + return UntranslatedL10n.screenWaitlistTitle + } else { + return UntranslatedL10n.screenWaitlistTitleSuccess + } + } + + var message: String { + if isWaiting { + return UntranslatedL10n.screenWaitlistMessage(InfoPlistReader.main.bundleDisplayName, homeserver.address) + } else { + return UntranslatedL10n.screenWaitlistMessageSuccess(InfoPlistReader.main.bundleDisplayName) + } + } +} + +enum WaitlistScreenViewAction { + case cancel + case `continue`(UserSessionProtocol) +} diff --git a/ElementX/Sources/Screens/Authentication/WaitlistScreen/WaitlistScreenViewModel.swift b/ElementX/Sources/Screens/Authentication/WaitlistScreen/WaitlistScreenViewModel.swift new file mode 100644 index 0000000000..02ba91c464 --- /dev/null +++ b/ElementX/Sources/Screens/Authentication/WaitlistScreen/WaitlistScreenViewModel.swift @@ -0,0 +1,47 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import SwiftUI + +typealias WaitlistScreenViewModelType = StateStoreViewModel + +class WaitlistScreenViewModel: WaitlistScreenViewModelType, WaitlistScreenViewModelProtocol { + private var actionsSubject: PassthroughSubject = .init() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(homeserver: LoginHomeserver) { + super.init(initialViewState: WaitlistScreenViewState(homeserver: homeserver)) + } + + // MARK: - Public + + override func process(viewAction: WaitlistScreenViewAction) { + switch viewAction { + case .cancel: + actionsSubject.send(.cancel) + case .continue(let userSession): + actionsSubject.send(.continue(userSession)) + } + } + + func update(userSession: UserSessionProtocol) { + state.userSession = userSession + } +} diff --git a/ElementX/Sources/Screens/Authentication/WaitlistScreen/WaitlistScreenViewModelProtocol.swift b/ElementX/Sources/Screens/Authentication/WaitlistScreen/WaitlistScreenViewModelProtocol.swift new file mode 100644 index 0000000000..9d62d1c487 --- /dev/null +++ b/ElementX/Sources/Screens/Authentication/WaitlistScreen/WaitlistScreenViewModelProtocol.swift @@ -0,0 +1,26 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine + +@MainActor +protocol WaitlistScreenViewModelProtocol { + var actions: AnyPublisher { get } + var context: WaitlistScreenViewModelType.Context { get } + + /// Set a user session on the screen to transition to the success state. + func update(userSession: UserSessionProtocol) +} diff --git a/ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift b/ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift index 1e41ce7fd0..94e0717849 100644 --- a/ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift +++ b/ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift @@ -102,13 +102,13 @@ class AuthenticationServiceProxy: AuthenticationServiceProxyProtocol { // } } - func login(username: String, password: String, initialDeviceName: String?, deviceId: String?) async -> Result { + func login(username: String, password: String, initialDeviceName: String?, deviceID: String?) async -> Result { do { let client = try await Task.dispatch(on: .global()) { try self.authenticationService.login(username: username, password: password, initialDeviceName: initialDeviceName, - deviceId: deviceId) + deviceId: deviceID) } return await userSession(for: client) @@ -116,6 +116,10 @@ class AuthenticationServiceProxy: AuthenticationServiceProxyProtocol { MXLog.error("Failed logging in with error: \(error)") guard let error = error as? AuthenticationError else { return .failure(.failedLoggingIn) } + if error.isElementWaitlist { + return .failure(.isOnWaitlist) + } + switch error.code { case .forbidden: return .failure(.invalidCredentials) diff --git a/ElementX/Sources/Services/Authentication/AuthenticationServiceProxyProtocol.swift b/ElementX/Sources/Services/Authentication/AuthenticationServiceProxyProtocol.swift index 85a196c851..676d1fc9df 100644 --- a/ElementX/Sources/Services/Authentication/AuthenticationServiceProxyProtocol.swift +++ b/ElementX/Sources/Services/Authentication/AuthenticationServiceProxyProtocol.swift @@ -34,6 +34,8 @@ enum AuthenticationServiceError: Error { case slidingSyncNotAvailable case accountDeactivated case failedLoggingIn + + case isOnWaitlist } protocol AuthenticationServiceProxyProtocol { @@ -47,7 +49,7 @@ protocol AuthenticationServiceProxyProtocol { /// Add docs. func loginWithOIDCCallback(_ callbackURL: URL, data: OIDCAuthenticationDataProxy) async -> Result /// Performs a password login using the current homeserver. - func login(username: String, password: String, initialDeviceName: String?, deviceId: String?) async -> Result + func login(username: String, password: String, initialDeviceName: String?, deviceID: String?) async -> Result } // MARK: - OIDC diff --git a/ElementX/Sources/Services/Authentication/MockAuthenticationServiceProxy.swift b/ElementX/Sources/Services/Authentication/MockAuthenticationServiceProxy.swift index ba36725ace..f3acd718bd 100644 --- a/ElementX/Sources/Services/Authentication/MockAuthenticationServiceProxy.swift +++ b/ElementX/Sources/Services/Authentication/MockAuthenticationServiceProxy.swift @@ -56,7 +56,7 @@ class MockAuthenticationServiceProxy: AuthenticationServiceProxyProtocol { .failure(.oidcError(.notSupported)) } - func login(username: String, password: String, initialDeviceName: String?, deviceId: String?) async -> Result { + func login(username: String, password: String, initialDeviceName: String?, deviceID: String?) async -> Result { // Login only succeeds if the username and password match the valid credentials property guard username == validCredentials.username, password == validCredentials.password else { return .failure(.invalidCredentials) diff --git a/ElementX/Sources/Services/Client/ClientError.swift b/ElementX/Sources/Services/Client/ClientError.swift index 894ff106d3..41bcdb6081 100644 --- a/ElementX/Sources/Services/Client/ClientError.swift +++ b/ElementX/Sources/Services/Client/ClientError.swift @@ -46,4 +46,12 @@ extension AuthenticationError { return first } + + /// Whether or not the error is related to the sliding sync proxy being full. + /// + /// This is a temporary error whilst we scale the backend infrastructure. + var isElementWaitlist: Bool { + guard case let .Generic(message) = self else { return false } + return message.contains("IO_ELEMENT_X_WAIT_LIST") + } } diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index e7f032b7cf..6229c3d33e 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -116,13 +116,22 @@ class MockScreen: Identifiable { navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .softLogout: - let credentials = SoftLogoutScreenCredentials(userId: "@mock:matrix.org", + let credentials = SoftLogoutScreenCredentials(userID: "@mock:matrix.org", homeserverName: "matrix.org", userDisplayName: "mock", - deviceId: "ABCDEFGH") + deviceID: "ABCDEFGH") return SoftLogoutScreenCoordinator(parameters: .init(authenticationService: MockAuthenticationServiceProxy(), credentials: credentials, keyBackupNeeded: false)) + case .waitlist: + let credentials = WaitlistScreenCredentials(username: "alice", + password: "password", + initialDeviceName: nil, + deviceID: nil, + homeserver: .mockMatrixDotOrg) + return WaitlistScreenCoordinator(parameters: .init(credentials: credentials, + authenticationService: MockAuthenticationServiceProxy(), + userIndicatorController: UserIndicatorControllerMock.default)) case .simpleRegular: return TemplateScreenCoordinator(parameters: .init(promptType: .regular)) case .simpleUpgrade: diff --git a/ElementX/Sources/UITests/UITestsScreenIdentifier.swift b/ElementX/Sources/UITests/UITestsScreenIdentifier.swift index 0107c71bd8..930484f997 100644 --- a/ElementX/Sources/UITests/UITestsScreenIdentifier.swift +++ b/ElementX/Sources/UITests/UITestsScreenIdentifier.swift @@ -24,6 +24,7 @@ enum UITestsScreenIdentifier: String { case serverSelectionNonModal case authenticationFlow case softLogout + case waitlist case analyticsPrompt case analyticsSettingsScreen case simpleRegular diff --git a/UITests/Sources/WaitlistScreenUITests.swift b/UITests/Sources/WaitlistScreenUITests.swift new file mode 100644 index 0000000000..7b875a28d2 --- /dev/null +++ b/UITests/Sources/WaitlistScreenUITests.swift @@ -0,0 +1,26 @@ +// +// 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 WaitlistScreenUITests: XCTestCase { + func testWaitingState() async throws { + let app = Application.launch(.waitlist) + try await app.assertScreenshot(.waitlist) + } +} diff --git a/UnitTests/Sources/SoftLogoutViewModelTests.swift b/UnitTests/Sources/SoftLogoutViewModelTests.swift index 1a802752e3..4200c43511 100644 --- a/UnitTests/Sources/SoftLogoutViewModelTests.swift +++ b/UnitTests/Sources/SoftLogoutViewModelTests.swift @@ -19,10 +19,10 @@ import XCTest @testable import ElementX class SoftLogoutViewModelTests: XCTestCase { - let credentials = SoftLogoutScreenCredentials(userId: "mock_user_id", + let credentials = SoftLogoutScreenCredentials(userID: "mock_user_id", homeserverName: "https://matrix.org", userDisplayName: "mock_username", - deviceId: "ABCDEFGH") + deviceID: "ABCDEFGH") @MainActor func testInitialStateForMatrixOrg() { let viewModel = SoftLogoutScreenViewModel(credentials: credentials, diff --git a/UnitTests/Sources/WaitlistScreenViewModelTests.swift b/UnitTests/Sources/WaitlistScreenViewModelTests.swift new file mode 100644 index 0000000000..22d5a03dab --- /dev/null +++ b/UnitTests/Sources/WaitlistScreenViewModelTests.swift @@ -0,0 +1,40 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +@testable import ElementX + +@MainActor +class WaitlistScreenViewModelTests: XCTestCase { + var viewModel: WaitlistScreenViewModelProtocol! + var context: WaitlistScreenViewModelType.Context { viewModel.context } + + override func setUpWithError() throws { + viewModel = WaitlistScreenViewModel(homeserver: .mockMatrixDotOrg) + } + + func testSuccess() async throws { + XCTAssertNil(context.viewState.userSession, "No user session should be set on a new view model.") + XCTAssertTrue(context.viewState.isWaiting, "The view should start off in the waiting state.") + + viewModel.update(userSession: MockUserSession(clientProxy: MockClientProxy(userID: "@alice:matrix.org"), + mediaProvider: MockMediaProvider())) + + XCTAssertNotNil(context.viewState.userSession, "The user session should have been updated.") + XCTAssertFalse(context.viewState.isWaiting, "The view should not be in the waiting state after setting a user session.") + } +} diff --git a/changelog.d/1154.change b/changelog.d/1154.change new file mode 100644 index 0000000000..441dfaab29 --- /dev/null +++ b/changelog.d/1154.change @@ -0,0 +1 @@ +Add a screen to be shown when new users are on the waiting list. \ No newline at end of file