diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 8d1cb87dc..69660e323 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -25,7 +25,7 @@ 072BA9DBA932374CCA300125 /* MessageComposerTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE6C10032A77AE7DC5AA4C50 /* MessageComposerTextField.swift */; }; 0B1F80C2BF7D223159FBA82C /* ImageAnonymizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6045E825AE900A92D61FEFF0 /* ImageAnonymizerTests.swift */; }; 0C38C3E771B472E27295339D /* SessionVerificationModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4BB9A17AC512A7EF4B106E5 /* SessionVerificationModels.swift */; }; - 0E8C480700870BB34A2A360F /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 78A5A8DE1E2B09C978C7F3B0 /* KeychainAccess */; }; + 0E8C480700870BB34A2A360F /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 36B7FC232711031AA2B0D188 /* DTCoreText */; }; 0EA6537A07E2DC882AEA5962 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 187853A7E643995EE49FAD43 /* Localizable.stringsdict */; }; 0ED951768EC443A8728DE1D7 /* TimelineStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */; }; 0EE5EBA18BA1FE10254BB489 /* UIFont+AttributedStringBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = E8CA187FE656EE5A3F6C7DE5 /* UIFont+AttributedStringBuilder.m */; }; @@ -63,8 +63,8 @@ 28410F3DE89C2C44E4F75C92 /* MockBugReportService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E7BF8F7BB1021F889C6483 /* MockBugReportService.swift */; }; 297CD0A27C87B0C50FF192EE /* RoomTimelineViewFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEE384418EB1FEDFA62C9CD0 /* RoomTimelineViewFactoryProtocol.swift */; }; 29AEE68A604940180AB9EBFF /* MockRoomSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6BDAC8895AB2B77B47703AE /* MockRoomSummary.swift */; }; - 29EE1791E0AFA1ABB7F23D2F /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 67E7A6F388D3BF85767609D9 /* Sentry */; }; - 2BA59D0AEFB4B82A2EC2A326 /* SwiftyBeaver in Frameworks */ = {isa = PBXBuildFile; productRef = A981A4CA233FB5C13B9CA690 /* SwiftyBeaver */; }; + 29EE1791E0AFA1ABB7F23D2F /* GZIP in Frameworks */ = {isa = PBXBuildFile; productRef = 1BCD21310B997A6837B854D6 /* GZIP */; }; + 2BA59D0AEFB4B82A2EC2A326 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 04C28663564E008DB32B5972 /* Introspect */; }; 2BAA5B222856068158D0B3C6 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = B1E8B697DF78FE7F61FC6CA4 /* MatrixRustSDK */; }; 2C0CE61E5DC177938618E0B1 /* RootRouterType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90733775209F4D4D366A268F /* RootRouterType.swift */; }; 2E59008365E01F0AFB3A6B24 /* ImageRoomMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BF686BA36D0C2FA3C63DFDF /* ImageRoomMessage.swift */; }; @@ -76,6 +76,7 @@ 3097A0A867D2B19CE32DAE58 /* UIKitBackgroundTaskService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF1FFC3336EB23374BBBFCC /* UIKitBackgroundTaskService.swift */; }; 313382FC5D38064EAAA35CB2 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8D1CC633517D695FEC54208 /* FileManager.swift */; }; 33B4E59D408AE6E02323EE41 /* NoticeRoomMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBDA364DFFC3AC71C4771251 /* NoticeRoomMessage.swift */; }; + 33CAC1226DFB8B5D8447D286 /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 67E7A6F388D3BF85767609D9 /* Sentry */; }; 344AF4CBB6D8786214878642 /* NavigationRouterStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B9D5F812E5AD6DC786DBC9B /* NavigationRouterStoreProtocol.swift */; }; 34966D4C1C2C6D37FE3F7F50 /* SettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD2D50A7EAA4FC78417730E /* SettingsCoordinator.swift */; }; 352C439BE0F75E101EF11FB1 /* RoomScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2886615BEBAE33A0AA4D5F8 /* RoomScreenModels.swift */; }; @@ -87,12 +88,12 @@ 388FD50AC66E9E684DDFA9D8 /* ServerSelectionScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5D2C0950F8196232D88045C /* ServerSelectionScreen.swift */; }; 38C76D586404C1FDED095F3A /* LoginModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B01468022EC826CB2FD2C0 /* LoginModels.swift */; }; 3B770CB4DED51CC362C66D47 /* SettingsModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4990FDBDA96B88E214F92F48 /* SettingsModels.swift */; }; - 3C549A0BF39F8A854D45D9FD /* GZIP in Frameworks */ = {isa = PBXBuildFile; productRef = 997C7385E1A07E061D7E2100 /* GZIP */; }; + 3C549A0BF39F8A854D45D9FD /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 9573B94B1C86C6DF751AF3FD /* SwiftState */; }; 3D325A1147F6281C57BFCDF6 /* EventBrief.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4411C0DA0087A1CB143E96FA /* EventBrief.swift */; }; 3DA57CA0D609A6B37CA1DC2F /* BugReportService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DC38E64A5ED3FDB201029A /* BugReportService.swift */; }; 3ED2725734568F6B8CC87544 /* AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5C6FBF97B6EED3D4FA5EFF /* AttributedStringBuilder.swift */; }; 418B4AEFD03DC7A6D2C9D5C8 /* EventBriefFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36322DD0D4E29D31B0945ADC /* EventBriefFactory.swift */; }; - 41DFDD212D1BE57CA50D783B /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 7731767AE437BA3BD2CC14A8 /* Sentry */; }; + 41DFDD212D1BE57CA50D783B /* GZIP in Frameworks */ = {isa = PBXBuildFile; productRef = 997C7385E1A07E061D7E2100 /* GZIP */; }; 438FB9BC535BC95948AA5F34 /* SettingsViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B2F9D5C39A4494D19F33E38 /* SettingsViewModelProtocol.swift */; }; 43FD77998F33C32718C51450 /* TemplateCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD460ED7ED1E03B85DEA25C /* TemplateCoordinator.swift */; }; 462813B93C39DF93B1249403 /* RoundedToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFABDF2E19D349DAAAC18C65 /* RoundedToastView.swift */; }; @@ -117,6 +118,7 @@ 5375902175B2FEA2949D7D74 /* LoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDDDDD9FE1A699D23A5E096 /* LoginScreen.swift */; }; 53B9C2240C2F5533246EE230 /* RectangleToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6235E1CE00A6D989D7DB6D47 /* RectangleToastView.swift */; }; 541374590CA7E8318BD480FD /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 187853A7E643995EE49FAD43 /* Localizable.stringsdict */; }; + 563A05B43207D00A6B698211 /* OIDCService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9010EE0CC913D095887EF36E /* OIDCService.swift */; }; 56F0A22972A3BB519DA2261C /* HomeScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24F5530B2212862FA4BEFF2D /* HomeScreenViewModelProtocol.swift */; }; 59C41313AED7566C3AC51163 /* RoomSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A953B6C0C431DBF4DD00B4 /* RoomSummary.swift */; }; 5B2C4C17888FC095ED6880B2 /* SplashViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 48971F1FFD7FC5C466889FC7 /* SplashViewController.xib */; }; @@ -127,13 +129,14 @@ 5F1FDE49DFD0C680386E48F9 /* TemplateViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B80895CE021B49847BD7D74 /* TemplateViewModelProtocol.swift */; }; 5F5488FBC9CFEB6F433D74A4 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7109E709A7738E6BCC4553E6 /* Localizable.strings */; }; 617624A97BDBB75ED3DD8156 /* RoomScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A00C7A331B72C0F05C00392F /* RoomScreenViewModelProtocol.swift */; }; + 6298AB0906DDD3525CD78C6B /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 7731767AE437BA3BD2CC14A8 /* Sentry */; }; 62BBF5BE7B905222F0477FF2 /* MediaSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8210612D17A39369480FC183 /* MediaSource.swift */; }; 63C9AF0FB8278AF1C0388A0C /* TemplateModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB10E673916D2B8D21FD197 /* TemplateModels.swift */; }; 64FF5CB4E35971255872E1BB /* AuthenticationServiceProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F0CB536D1C3CC15AA740CC6 /* AuthenticationServiceProxyProtocol.swift */; }; 6647430A45B4A8E692909A8F /* EmoteRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77C060C2ACC4CB7336A29E7 /* EmoteRoomTimelineItem.swift */; }; 67C05C50AD734283374605E3 /* MatrixEntityRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */; }; 67E391A2E00709FB41903B36 /* MockMediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6920A4869821BF72FFC58842 /* MockMediaProvider.swift */; }; - 6832733838C57A7D3FE8FEB5 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 04C28663564E008DB32B5972 /* Introspect */; }; + 6832733838C57A7D3FE8FEB5 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 50009897F60FAE7D63EF5E5B /* Kingfisher */; }; 684BDE198AE5AA1392288A73 /* SplashScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32CE6D4FF64C9A3C18619224 /* SplashScreen.swift */; }; 68AC3C84E2B438036B174E30 /* EmoteRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */; }; 69BCBB4FB2DC3D61A28D3FD8 /* TimelineStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */; }; @@ -142,7 +145,7 @@ 6AC1DC1EAD9F7568360DA1BA /* ServerSelectionModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A30A1758E2B73EF38E7C42F8 /* ServerSelectionModels.swift */; }; 6D046D653DA28ADF1E6E59A4 /* BackgroundTaskServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE73D571D4F9C36DD45255A /* BackgroundTaskServiceProtocol.swift */; }; 6EA61FCA55D950BDE326A1A7 /* ImageAnonymizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12A626D74BBE9F4A60763B45 /* ImageAnonymizer.swift */; }; - 6F2AB43A1EFAD8A97AF41A15 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 0DD568A494247444A4B56031 /* Kingfisher */; }; + 6F2AB43A1EFAD8A97AF41A15 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 020597E28A4BC8E1BE8EDF6E /* KeychainAccess */; }; 6FC10A00D268FCD48B631E37 /* ViewFrameReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFF7BF82A950B91BC5469E91 /* ViewFrameReader.swift */; }; 7002C55A4C917F3715765127 /* MediaProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C888BCD78E2A55DCE364F160 /* MediaProviderProtocol.swift */; }; 706F79A39BDB32F592B8C2C7 /* UIKitBackgroundTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92FCD9116ADDE820E4E30F92 /* UIKitBackgroundTask.swift */; }; @@ -187,7 +190,7 @@ 90DF83A6A347F7EE7EDE89EE /* AttributedStringBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */; }; 90EB25D13AE6EEF034BDE9D2 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D52BAA5BADB06E5E8C295D /* Assets.swift */; }; 93875ADD456142D20823ED24 /* ServerSelectionViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */; }; - 93BA4A81B6D893271101F9F0 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 5986E300FC849DEAB2EE7AEB /* Introspect */; }; + 93BA4A81B6D893271101F9F0 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 0DD568A494247444A4B56031 /* Kingfisher */; }; 94A65DD8A353DF112EBEF67A /* SessionVerificationControllerProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D56469A9EE0CFA2B7BA9760 /* SessionVerificationControllerProxyProtocol.swift */; }; 94D0F36A87E596A93C0C178A /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E89E530A8E92EC44301CA1 /* Bundle.swift */; }; 94E062D08E27B0387658E364 /* SplashScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B5CF94E124616FD89424B73 /* SplashScreenViewModelTests.swift */; }; @@ -196,21 +199,21 @@ 978BB24F2A5D31EE59EEC249 /* UserSessionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4134FEFE4EB55759017408 /* UserSessionProtocol.swift */; }; 989029A28C9E2F828AD6658A /* AppIcon.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 16DC8C5B2991724903F1FA6A /* AppIcon.pdf */; }; 992F5E750F5030C4BA2D0D03 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 01C4C7DB37597D7D8379511A /* Assets.xcassets */; }; - 99ED42B8F8D6BFB1DBCF4C45 /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 36B7FC232711031AA2B0D188 /* DTCoreText */; }; - 9AC5F8142413862A9E3A2D98 /* SwiftyBeaver in Frameworks */ = {isa = PBXBuildFile; productRef = FD43A50D9B75C9D6D30F006B /* SwiftyBeaver */; }; + 99ED42B8F8D6BFB1DBCF4C45 /* AppAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 4346F63D53A346271577FD9C /* AppAuth */; }; + 9AC5F8142413862A9E3A2D98 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 5986E300FC849DEAB2EE7AEB /* Introspect */; }; 9B8DE1D424E37581C7D99CCC /* RoomTimelineControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC7CCC6DE5FA623E31BA8546 /* RoomTimelineControllerProtocol.swift */; }; 9BD3A773186291560DF92B62 /* RoomTimelineProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66F2402D738694F98729A441 /* RoomTimelineProvider.swift */; }; 9BE7A9CF6C593251D734B461 /* MockServerSelectionScreenState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0A20AE75FF4FF35B1FF6CA7 /* MockServerSelectionScreenState.swift */; }; 9C45CE85325CD591DADBC4CA /* ElementXTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEAC3AC691CBB84983E275 /* ElementXTests.swift */; }; 9C9E48A627C7C166084E3F5B /* LabelledActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56F01DD1BBD4450E18115916 /* LabelledActivityIndicatorView.swift */; }; 9CB5129C83F75921E5E28028 /* ToastViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C82DAE0B8EB28234E84E6CF /* ToastViewState.swift */; }; - 9D2E03DB175A6AB14589076D /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 020597E28A4BC8E1BE8EDF6E /* KeychainAccess */; }; + 9D2E03DB175A6AB14589076D /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 531CE4334AC5CA8DFF6AEB84 /* DTCoreText */; }; 9DC5FB22B8F86C3B51E907C1 /* HomeScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D6E4C37E9F0E53D3DF951AC /* HomeScreenUITests.swift */; }; 9E8AE387FD03E4F1C1B8815A /* SessionVerificationStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06FCD42EEFEFC220F14EAC5 /* SessionVerificationStateMachine.swift */; }; A00DFC1DD3567B1EDC9F8D16 /* SplashScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 325A2B3278875554DDEB8A9B /* SplashScreenUITests.swift */; }; A0A0D2A9564BDA3FDE2E360F /* FormattedBodyText.swift in Sources */ = {isa = PBXBuildFile; fileRef = F73FF1A33198F5FAE9D34B1F /* FormattedBodyText.swift */; }; A32517FB1CA0BBCE2BC75249 /* BugReportCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD6C07DA7D3FF193F7419F55 /* BugReportCoordinator.swift */; }; - A4E885358D7DD5A072A06824 /* GZIP in Frameworks */ = {isa = PBXBuildFile; productRef = 1BCD21310B997A6837B854D6 /* GZIP */; }; + A4E885358D7DD5A072A06824 /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 3853B78FB8531B83936C5DA6 /* SwiftState */; }; A50849766F056FD1DB942DEA /* AlertInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EEB64CC6F3DF5B68736A6B4 /* AlertInfo.swift */; }; A5C8F013ED9FB8AA6FEE18A7 /* InfoPlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A901D95158B02CA96C79C7F /* InfoPlist.swift */; }; A5EC21A071F58FC1229C20D0 /* MemberDetailsProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09747989908EC5E4AA29F844 /* MemberDetailsProviderProtocol.swift */; }; @@ -223,7 +226,7 @@ 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 */; }; - B245583C63F8F90357B87FAE /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 3853B78FB8531B83936C5DA6 /* SwiftState */; }; + B245583C63F8F90357B87FAE /* SwiftyBeaver in Frameworks */ = {isa = PBXBuildFile; productRef = A981A4CA233FB5C13B9CA690 /* SwiftyBeaver */; }; B3357B00F1AA930E54F76609 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47EBB5D698CE9A25BB553A2D /* Strings.swift */; }; B3FDB1D9CF40777695DBBD1D /* AppCoordinatorStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A9AB74614131D6706894E0C /* AppCoordinatorStateMachine.swift */; }; B4AAB3257A83B73F53FB2689 /* StateStoreViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F3DFE5B444F131648066F05 /* StateStoreViewModel.swift */; }; @@ -247,9 +250,9 @@ C76892321558E75101E68ED6 /* ReadableFrameModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398817652FA8ABAE0A31AC6D /* ReadableFrameModifier.swift */; }; C7CFDB4929DDD9A3B5BA085D /* BugReportViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB7ED3A898B07976F3AA90F /* BugReportViewModelTests.swift */; }; CA1E41AE5CDCB8D801DE0830 /* BuildSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F87116470221880017CF522 /* BuildSettings.swift */; }; - CB137BFB3E083C33E398A6CB /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 9573B94B1C86C6DF751AF3FD /* SwiftState */; }; + CB137BFB3E083C33E398A6CB /* SwiftyBeaver in Frameworks */ = {isa = PBXBuildFile; productRef = FD43A50D9B75C9D6D30F006B /* SwiftyBeaver */; }; CB326BAB54E9B68658909E36 /* Benchmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49EAD710A2C16EFF7C3EA16F /* Benchmark.swift */; }; - CB498F4E27AA0545DCEF0F6F /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 50009897F60FAE7D63EF5E5B /* Kingfisher */; }; + CB498F4E27AA0545DCEF0F6F /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 78A5A8DE1E2B09C978C7F3B0 /* KeychainAccess */; }; CC736DA1AA8F8B9FD8785009 /* ScreenshotDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5C4AF6E3885730CD560311C /* ScreenshotDetector.swift */; }; CE1694C7BB93C3311524EF28 /* Untranslated.strings in Resources */ = {isa = PBXBuildFile; fileRef = D2F7194F440375338F8E2487 /* Untranslated.strings */; }; CE7A715947ABAB1DEB5C21D7 /* SplashScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F7A812F160E75B69A9181A2 /* SplashScreenCoordinator.swift */; }; @@ -286,7 +289,7 @@ F4C3FEDB1B3A05376A1723A3 /* KeychainController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4427F9E0571B4E6E048A2B /* KeychainController.swift */; }; F508683B76EF7B23BB2CBD6D /* TimelineItemPlainStylerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94BCC8A9C73C1F838122C645 /* TimelineItemPlainStylerView.swift */; }; F56261126E368C831B3DE976 /* NavigationRouterType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 752DEC02D93AFF46BC13313A /* NavigationRouterType.swift */; }; - F656F92A63D3DC1978D79427 /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 531CE4334AC5CA8DFF6AEB84 /* DTCoreText */; }; + F656F92A63D3DC1978D79427 /* AppAuth in Frameworks */ = {isa = PBXBuildFile; productRef = AA4E1BEB4E9BC2467006E12B /* AppAuth */; }; F6F49E37272AD7397CD29A01 /* HomeScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505208F28007C0FEC14E1FF0 /* HomeScreenViewModelTests.swift */; }; F78C57B197DA74735FEBB42C /* EventBriefFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92B61C243325DC76D3086494 /* EventBriefFactoryProtocol.swift */; }; FA9C427FFB11B1AA2DCC5602 /* RoomProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */; }; @@ -533,6 +536,7 @@ 8C37FB986891D90BEAA93EAE /* UserSessionStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionStore.swift; sourceTree = ""; }; 8D8169443E5AC5FF71BFB3DB /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyle.swift; sourceTree = ""; }; + 9010EE0CC913D095887EF36E /* OIDCService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OIDCService.swift; sourceTree = ""; }; 90733775209F4D4D366A268F /* RootRouterType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootRouterType.swift; sourceTree = ""; }; 92B61C243325DC76D3086494 /* EventBriefFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBriefFactoryProtocol.swift; sourceTree = ""; }; 92FCD9116ADDE820E4E30F92 /* UIKitBackgroundTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitBackgroundTask.swift; sourceTree = ""; }; @@ -707,14 +711,15 @@ buildActionMask = 2147483647; files = ( 2BAA5B222856068158D0B3C6 /* MatrixRustSDK in Frameworks */, - 99ED42B8F8D6BFB1DBCF4C45 /* DTCoreText in Frameworks */, - 0E8C480700870BB34A2A360F /* KeychainAccess in Frameworks */, - CB498F4E27AA0545DCEF0F6F /* Kingfisher in Frameworks */, - 6832733838C57A7D3FE8FEB5 /* Introspect in Frameworks */, - 2BA59D0AEFB4B82A2EC2A326 /* SwiftyBeaver in Frameworks */, - B245583C63F8F90357B87FAE /* SwiftState in Frameworks */, - A4E885358D7DD5A072A06824 /* GZIP in Frameworks */, - 29EE1791E0AFA1ABB7F23D2F /* Sentry in Frameworks */, + 99ED42B8F8D6BFB1DBCF4C45 /* AppAuth in Frameworks */, + 0E8C480700870BB34A2A360F /* DTCoreText in Frameworks */, + CB498F4E27AA0545DCEF0F6F /* KeychainAccess in Frameworks */, + 6832733838C57A7D3FE8FEB5 /* Kingfisher in Frameworks */, + 2BA59D0AEFB4B82A2EC2A326 /* Introspect in Frameworks */, + B245583C63F8F90357B87FAE /* SwiftyBeaver in Frameworks */, + A4E885358D7DD5A072A06824 /* SwiftState in Frameworks */, + 29EE1791E0AFA1ABB7F23D2F /* GZIP in Frameworks */, + 33CAC1226DFB8B5D8447D286 /* Sentry in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -724,14 +729,15 @@ files = ( 1A70A2199394B5EC660934A5 /* MatrixRustSDK in Frameworks */, 1F3232BD368DF430AB433907 /* DesignKit in Frameworks */, - F656F92A63D3DC1978D79427 /* DTCoreText in Frameworks */, - 9D2E03DB175A6AB14589076D /* KeychainAccess in Frameworks */, - 6F2AB43A1EFAD8A97AF41A15 /* Kingfisher in Frameworks */, - 93BA4A81B6D893271101F9F0 /* Introspect in Frameworks */, - 9AC5F8142413862A9E3A2D98 /* SwiftyBeaver in Frameworks */, - CB137BFB3E083C33E398A6CB /* SwiftState in Frameworks */, - 3C549A0BF39F8A854D45D9FD /* GZIP in Frameworks */, - 41DFDD212D1BE57CA50D783B /* Sentry in Frameworks */, + F656F92A63D3DC1978D79427 /* AppAuth in Frameworks */, + 9D2E03DB175A6AB14589076D /* DTCoreText in Frameworks */, + 6F2AB43A1EFAD8A97AF41A15 /* KeychainAccess in Frameworks */, + 93BA4A81B6D893271101F9F0 /* Kingfisher in Frameworks */, + 9AC5F8142413862A9E3A2D98 /* Introspect in Frameworks */, + CB137BFB3E083C33E398A6CB /* SwiftyBeaver in Frameworks */, + 3C549A0BF39F8A854D45D9FD /* SwiftState in Frameworks */, + 41DFDD212D1BE57CA50D783B /* GZIP in Frameworks */, + 6298AB0906DDD3525CD78C6B /* Sentry in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1377,6 +1383,7 @@ CF48AF076424DBC1615C74AD /* AuthenticationServiceProxy.swift */, 4F0CB536D1C3CC15AA740CC6 /* AuthenticationServiceProxyProtocol.swift */, 65C2B80DD0BF6F10BB5FA922 /* MockAuthenticationServiceProxy.swift */, + 9010EE0CC913D095887EF36E /* OIDCService.swift */, ); path = Authentication; sourceTree = ""; @@ -1628,6 +1635,7 @@ name = UITests; packageProductDependencies = ( B1E8B697DF78FE7F61FC6CA4 /* MatrixRustSDK */, + 4346F63D53A346271577FD9C /* AppAuth */, 36B7FC232711031AA2B0D188 /* DTCoreText */, 78A5A8DE1E2B09C978C7F3B0 /* KeychainAccess */, 50009897F60FAE7D63EF5E5B /* Kingfisher */, @@ -1677,6 +1685,7 @@ packageProductDependencies = ( A678E40E917620059695F067 /* MatrixRustSDK */, A5A56C4F47C368EBE5C5E870 /* DesignKit */, + AA4E1BEB4E9BC2467006E12B /* AppAuth */, 531CE4334AC5CA8DFF6AEB84 /* DTCoreText */, 020597E28A4BC8E1BE8EDF6E /* KeychainAccess */, 0DD568A494247444A4B56031 /* Kingfisher */, @@ -1788,6 +1797,7 @@ ); mainGroup = 405B00F139AEE3994601B36A; packageReferences = ( + 4CE94127E27181B8B72188F0 /* XCRemoteSwiftPackageReference "AppAuth-iOS" */, C13F55E4518415CB4C278E73 /* XCRemoteSwiftPackageReference "DTCoreText" */, 701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */, 9A472EE0218FE7DCF5283429 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */, @@ -2047,6 +2057,7 @@ 33B4E59D408AE6E02323EE41 /* NoticeRoomMessage.swift in Sources */, 8BBD3AA589DEE02A1B0923B2 /* NoticeRoomTimelineItem.swift in Sources */, 368C8758FCD079E6AAA18C2C /* NoticeRoomTimelineView.swift in Sources */, + 563A05B43207D00A6B698211 /* OIDCService.swift in Sources */, 7D1DAAA364A9A29D554BD24E /* PlaceholderAvatarImage.swift in Sources */, BF35062D06888FA80BD139FF /* Presentable.swift in Sources */, C76892321558E75101E68ED6 /* ReadableFrameModifier.swift in Sources */, @@ -2659,6 +2670,14 @@ minimumVersion = 1.9.5; }; }; + 4CE94127E27181B8B72188F0 /* XCRemoteSwiftPackageReference "AppAuth-iOS" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/openid/AppAuth-iOS"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.5.0; + }; + }; 61916C63E3F5BD900F08DA0C /* XCRemoteSwiftPackageReference "KeychainAccess" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/kishikawakatsumi/KeychainAccess"; @@ -2756,6 +2775,11 @@ package = 6582B5AF3F104B0F7E031E7D /* XCRemoteSwiftPackageReference "SwiftState" */; productName = SwiftState; }; + 4346F63D53A346271577FD9C /* AppAuth */ = { + isa = XCSwiftPackageProductDependency; + package = 4CE94127E27181B8B72188F0 /* XCRemoteSwiftPackageReference "AppAuth-iOS" */; + productName = AppAuth; + }; 50009897F60FAE7D63EF5E5B /* Kingfisher */ = { isa = XCSwiftPackageProductDependency; package = D283517192CAC3E2E6920765 /* XCRemoteSwiftPackageReference "Kingfisher" */; @@ -2810,6 +2834,11 @@ package = 25B4484A6A20B9F1705DEEDA /* XCRemoteSwiftPackageReference "SwiftyBeaver" */; productName = SwiftyBeaver; }; + AA4E1BEB4E9BC2467006E12B /* AppAuth */ = { + isa = XCSwiftPackageProductDependency; + package = 4CE94127E27181B8B72188F0 /* XCRemoteSwiftPackageReference "AppAuth-iOS" */; + productName = AppAuth; + }; B1E8B697DF78FE7F61FC6CA4 /* MatrixRustSDK */ = { isa = XCSwiftPackageProductDependency; package = 80B898A3AD2AC63F3ABFC218 /* XCRemoteSwiftPackageReference "matrix-rust-components-swift" */; diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 721b6f2d9..f77904184 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,14 @@ { "pins" : [ + { + "identity" : "appauth-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/openid/AppAuth-iOS", + "state" : { + "revision" : "33660c271c961f8ce1084cc13f2ea8195e864f7d", + "version" : "1.5.0" + } + }, { "identity" : "dtcoretext", "kind" : "remoteSourceControl", diff --git a/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift b/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift index d27308835..18cb59102 100644 --- a/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift @@ -110,8 +110,6 @@ class AuthenticationCoordinator: Coordinator, Presentable { switch action { case .signedIn(let userSession): self.delegate?.authenticationCoordinator(self, didLoginWithSession: userSession) - case .continueWithOIDC: - break } } diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginCoordinator.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginCoordinator.swift index f3145a78d..d279f35a3 100644 --- a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginCoordinator.swift @@ -14,6 +14,7 @@ // limitations under the License. // +import AppAuth import MatrixRustSDK import SwiftUI @@ -27,8 +28,6 @@ struct LoginCoordinatorParameters { enum LoginCoordinatorAction { /// Login was successful. case signedIn(UserSessionProtocol) - /// Continue using OIDC. - case continueWithOIDC } final class LoginCoordinator: Coordinator, Presentable { @@ -39,6 +38,8 @@ final class LoginCoordinator: Coordinator, Presentable { private let parameters: LoginCoordinatorParameters private let loginHostingController: UIViewController private var loginViewModel: LoginViewModelProtocol + /// Passed to the OIDC service to provide a view controller from which to present the authentication session. + private let oidcUserAgent: OIDExternalUserAgentIOS? private var currentTask: Task? { willSet { @@ -69,6 +70,7 @@ final class LoginCoordinator: Coordinator, Presentable { loginHostingController = UIHostingController(rootView: view) indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: loginHostingController) + oidcUserAgent = OIDExternalUserAgentIOS(presenting: loginHostingController) } // MARK: - Public @@ -90,7 +92,7 @@ final class LoginCoordinator: Coordinator, Presentable { case .login(let username, let password): self.login(username: username, password: password) case .continueWithOIDC: - self.callback?(.continueWithOIDC) + self.loginWithOIDC() } } } @@ -138,6 +140,26 @@ final class LoginCoordinator: Coordinator, Presentable { } } + private func loginWithOIDC() { + guard let oidcUserAgent = oidcUserAgent else { + handleError(AuthenticationServiceError.oidcError(.notSupported)) + return + } + + startLoading(isInteractionBlocking: true) + + Task { + switch await authenticationService.loginWithOIDC(userAgent: oidcUserAgent) { + case .success(let userSession): + callback?(.signedIn(userSession)) + stopLoading() + case .failure(let error): + stopLoading() + handleError(error) + } + } + } + /// Requests the authentication coordinator to log in using the specified credentials. private func login(username: String, password: String) { startLoading(isInteractionBlocking: true) diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift index d1d80f3f9..cc701209d 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift @@ -53,7 +53,8 @@ final class HomeScreenCoordinator: Coordinator, Presentable { init(parameters: HomeScreenCoordinatorParameters) { self.parameters = parameters - viewModel = HomeScreenViewModel(attributedStringBuilder: parameters.attributedStringBuilder) + viewModel = HomeScreenViewModel(initialDisplayName: parameters.userSession.userID, + attributedStringBuilder: parameters.attributedStringBuilder) let view = HomeScreen(context: viewModel.context) hostingController = UIHostingController(rootView: view) diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift index a6da705da..3c023c62a 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift @@ -31,7 +31,7 @@ enum HomeScreenViewAction { } struct HomeScreenViewState: BindableState { - var userDisplayName: String? + var userDisplayName: String var userAvatar: UIImage? var showSessionVerificationBanner = false diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift index 26a8e7010..1d73049e1 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift @@ -39,10 +39,11 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol // MARK: - Setup - init(attributedStringBuilder: AttributedStringBuilderProtocol) { + init(initialDisplayName: String, attributedStringBuilder: AttributedStringBuilderProtocol) { self.attributedStringBuilder = attributedStringBuilder - super.init(initialViewState: HomeScreenViewState(isLoadingRooms: true)) + super.init(initialViewState: HomeScreenViewState(userDisplayName: initialDisplayName, + isLoadingRooms: true)) } // MARK: - Public diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift index dec506b29..956e56bd8 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift @@ -84,7 +84,7 @@ struct HomeScreen: View { } } } - + @ViewBuilder private var userAvatarImage: some View { if let avatar = context.viewState.userAvatar { @@ -94,21 +94,14 @@ struct HomeScreen: View { .frame(width: 32, height: 32, alignment: .center) .clipShape(Circle()) .accessibilityIdentifier("userAvatarImage") - } else { - EmptyView() } } - - @ViewBuilder + private var userDisplayNameView: some View { - if let displayName = context.viewState.userDisplayName { - Text(displayName) - .font(.headline) - .fontWeight(.bold) - .foregroundColor(.primary) - } else { - EmptyView() - } + Text(context.viewState.userDisplayName) + .font(.headline) + .fontWeight(.bold) + .foregroundColor(.primary) } } @@ -173,11 +166,14 @@ struct RoomCell: View { struct HomeScreen_Previews: PreviewProvider { static var previews: some View { body.preferredColorScheme(.light) + .tint(.element.accent) body.preferredColorScheme(.dark) + .tint(.element.accent) } static var body: some View { - let viewModel = HomeScreenViewModel(attributedStringBuilder: AttributedStringBuilder()) + let viewModel = HomeScreenViewModel(initialDisplayName: "@username:server.com", + attributedStringBuilder: AttributedStringBuilder()) let eventBrief = EventBrief(eventId: "id", senderId: "senderId", diff --git a/ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift b/ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift index c2e79459a..a81588dc0 100644 --- a/ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift +++ b/ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift @@ -6,6 +6,7 @@ // Copyright © 2022 Element. All rights reserved. // +import AppAuth import Foundation import MatrixRustSDK @@ -59,6 +60,42 @@ class AuthenticationServiceProxy: AuthenticationServiceProxyProtocol { } } + func loginWithOIDC(userAgent: OIDExternalUserAgentIOS) async -> Result { + guard case let .oidc(issuerURL) = homeserver.loginMode else { + return .failure(.oidcError(.notSupported)) + } + + let token: String + let deviceID = generateDeviceID() + do { + let oidcService = OIDCService(issuerURL: issuerURL) + let configuration = try await oidcService.metadata() + let registationResponse = try await oidcService.registerClient(metadata: configuration) + let authResponse = try await oidcService.presentWebAuthentication(metadata: configuration, + clientID: registationResponse.clientID, + scope: "urn:matrix:device:\(deviceID)", + userAgent: userAgent) + let tokenResponse = try await oidcService.redeemCodeForTokens(authResponse: authResponse) + + guard let accessToken = tokenResponse.accessToken else { return .failure(.oidcError(.unknown)) } + token = accessToken + } catch let error as OIDCError { + MXLog.error("Login with OIDC failed: \(error)") + return .failure(.oidcError(error)) + } catch { + MXLog.error("Login with OIDC failed: \(error)") + return .failure(.failedLoggingIn) + } + + do { + let client = try authenticationService.restoreWithAccessToken(token: token, deviceId: deviceID) + return await userSession(for: client) + } catch { + MXLog.debug(error) + return .failure(.failedLoggingIn) + } + } + func login(username: String, password: String) async -> Result { Benchmark.startTrackingForIdentifier("Login", message: "Started new login") @@ -97,4 +134,13 @@ class AuthenticationServiceProxy: AuthenticationServiceProxyProtocol { return .failure(.failedLoggingIn) } } + + private func generateDeviceID() -> String { + var deviceID = "" + for _ in 0..<10 { + guard let scalar = UnicodeScalar(Int.random(in: 65...90)) else { fatalError() } + deviceID.append(Character(scalar)) + } + return deviceID + } } diff --git a/ElementX/Sources/Services/Authentication/AuthenticationServiceProxyProtocol.swift b/ElementX/Sources/Services/Authentication/AuthenticationServiceProxyProtocol.swift index 8bf281c77..3cc817920 100644 --- a/ElementX/Sources/Services/Authentication/AuthenticationServiceProxyProtocol.swift +++ b/ElementX/Sources/Services/Authentication/AuthenticationServiceProxyProtocol.swift @@ -6,9 +6,12 @@ // Copyright © 2022 Element. All rights reserved. // +import AppAuth import Foundation enum AuthenticationServiceError: Error { + /// An error occurred during OIDC authentication. + case oidcError(OIDCError) case invalidServer case invalidCredentials case invalidHomeserverAddress @@ -22,6 +25,8 @@ protocol AuthenticationServiceProxyProtocol { /// Sets up the service for login on the specified homeserver address. func configure(for homeserverAddress: String) async -> Result + /// Performs login using OIDC for the current homeserver. + func loginWithOIDC(userAgent: OIDExternalUserAgentIOS) async -> Result /// Performs a password login using the current homeserver. func login(username: String, password: String) async -> Result } diff --git a/ElementX/Sources/Services/Authentication/MockAuthenticationServiceProxy.swift b/ElementX/Sources/Services/Authentication/MockAuthenticationServiceProxy.swift index b76c4313b..a9f6e814e 100644 --- a/ElementX/Sources/Services/Authentication/MockAuthenticationServiceProxy.swift +++ b/ElementX/Sources/Services/Authentication/MockAuthenticationServiceProxy.swift @@ -6,11 +6,13 @@ // Copyright © 2022 Element. All rights reserved. // -import Foundation +import AppAuth class MockAuthenticationServiceProxy: AuthenticationServiceProxyProtocol { let validCredentials = (username: "alice", password: "12345678") + private(set) var homeserver: LoginHomeserver = .mockMatrixDotOrg + var oidcUserAgent: OIDExternalUserAgentIOS? func configure(for homeserverAddress: String) async -> Result { // Map the address to the mock homeservers @@ -32,6 +34,10 @@ class MockAuthenticationServiceProxy: AuthenticationServiceProxyProtocol { } } + func loginWithOIDC(userAgent: OIDExternalUserAgentIOS) async -> Result { + .failure(.oidcError(.notSupported)) + } + func login(username: String, password: String) async -> Result { // Login only succeeds if the username and password match the valid credentials property guard username == validCredentials.username, password == validCredentials.password else { diff --git a/ElementX/Sources/Services/Authentication/OIDCService.swift b/ElementX/Sources/Services/Authentication/OIDCService.swift new file mode 100644 index 000000000..dc744bd68 --- /dev/null +++ b/ElementX/Sources/Services/Authentication/OIDCService.swift @@ -0,0 +1,140 @@ +// +// OIDCAuthenticationService.swift +// ElementX +// +// Created by Doug on 07/07/2022. +// Copyright © 2022 Element. All rights reserved. +// + +import AppAuth + +/// Errors thrown by the OIDC service. +enum OIDCError: Error { + case notSupported + case metadataMissingRegistrationEndpoint + case userCancellation + case missingTokenExchangeRequest + case unknown +} + +/// A proof of concept implementation of a service that assists with authentication via OIDC. +/// It will be replaced by an implementation in the Rust SDK tracked in the following issue: +/// https://github.com/matrix-org/matrix-rust-sdk/issues/859 +class OIDCService { + private let issuerURL: URL + private var authState: OIDAuthState + + private var metadata: OIDServiceConfiguration? + /// Redirect URI for the request. Must match the `client_uri` in reverse DNS format. + private let redirectURI = URL(string: "io.element:/callback")! + // swiftlint:disable:previous force_unwrapping + + /// Maintains a strong ref to the authorization session that's in progress. + private var session: OIDExternalUserAgentSession? + + init(issuerURL: URL) { + self.issuerURL = issuerURL + authState = OIDAuthState(authorizationResponse: nil, tokenResponse: nil, registrationResponse: nil) + } + + /// Get OpenID Connect endpoints and ensure that dynamic client registration is configured. + func metadata() async throws -> OIDServiceConfiguration { + let metadata = try await OIDAuthorizationService.discoverConfiguration(forIssuer: issuerURL) + + guard metadata.registrationEndpoint != nil else { + throw OIDCError.metadataMissingRegistrationEndpoint + } + + return metadata + } + + /// Perform dynamic client registration and then store the response + func registerClient(metadata: OIDServiceConfiguration) async throws -> OIDRegistrationResponse { + let extraParams = [ + "client_name": "ElementX iOS", + "client_uri": "https://element.io", + "tos_uri": "https://example.com/tos", + "policy_uri": "https://example.com/policy" + ] + + let nonTemplatizedRequest = OIDRegistrationRequest( + configuration: metadata, + redirectURIs: [redirectURI], + responseTypes: nil, + grantTypes: [OIDGrantTypeAuthorizationCode], + subjectType: nil, + tokenEndpointAuthMethod: "none", + additionalParameters: extraParams + ) + + let registrationResponse = try await OIDAuthorizationService.perform(nonTemplatizedRequest) + + MXLog.info("Registration data retrieved successfully") + MXLog.debug("Created dynamic client: ID: \(registrationResponse.clientID)") + + // This is a PoC, a complete implementation would persist the client ID and secret for reuse. + + return registrationResponse + } + + /// Trigger a redirect with standard parameters. + /// `acr_values` can be sent as an extra parameter, to control authentication methods. + func presentWebAuthentication(metadata: OIDServiceConfiguration, + clientID: String, + scope: String, + userAgent: OIDExternalUserAgent) async throws -> OIDAuthorizationResponse { + let scopesArray = scope.components(separatedBy: " ") + let request = OIDAuthorizationRequest(configuration: metadata, + clientId: clientID, + clientSecret: nil, + scopes: scopesArray, + redirectURL: redirectURI, + responseType: OIDResponseTypeCode, + additionalParameters: nil) + let result: OIDAuthorizationResponse = try await withCheckedThrowingContinuation { continuation in + self.session = OIDAuthorizationService.present(request, externalUserAgent: userAgent) { response, error in + guard let response = response else { + if let error = error { + MXLog.info("User cancelled the ASWebAuthenticationSession window") + continuation.resume(with: .failure(self.isUserCancellationError(error) ? OIDCError.userCancellation : error)) + } else { + continuation.resume(with: .failure(OIDCError.unknown)) + } + return + } + + MXLog.info("Authorization response received successfully") + continuation.resume(with: .success(response)) + } + } + return result + } + + /// Handle the authorization response, including the user closing the Chrome Custom Tab + func redeemCodeForTokens(authResponse: OIDAuthorizationResponse) async throws -> OIDTokenResponse { + guard let request = authResponse.tokenExchangeRequest() else { throw OIDCError.missingTokenExchangeRequest } + return try await OIDAuthorizationService.perform(request, originalAuthorizationResponse: authResponse) + } + + /// We can check for specific error codes to handle the user cancelling the ASWebAuthenticationSession window. + private func isUserCancellationError(_ error: Error) -> Bool { + let error = error as NSError + return error.domain == OIDGeneralErrorDomain && error.code == OIDErrorCode.userCanceledAuthorizationFlow.rawValue + } +} + +extension OIDAuthorizationService { + /// An async version of `perform(_:originalAuthorizationResponse:callback:)`. + class func perform(_ request: OIDTokenRequest, + originalAuthorizationResponse authorizationResponse: OIDAuthorizationResponse?) async throws -> OIDTokenResponse { + try await withCheckedThrowingContinuation { continuation in + perform(request, originalAuthorizationResponse: authorizationResponse) { response, error in + guard let response = response else { + continuation.resume(with: .failure(error ?? OIDCError.unknown)) + return + } + continuation.resume(with: .success(response)) + } + } + } +} diff --git a/ElementX/Sources/Services/Session/MockUserSession.swift b/ElementX/Sources/Services/Session/MockUserSession.swift index 87aba767e..23e7543b2 100644 --- a/ElementX/Sources/Services/Session/MockUserSession.swift +++ b/ElementX/Sources/Services/Session/MockUserSession.swift @@ -12,6 +12,7 @@ struct MockUserSession: UserSessionProtocol { let callbacks = PassthroughSubject() let sessionVerificationController: SessionVerificationControllerProxyProtocol? = nil + let userID = "@mock:usersession.com" let clientProxy: ClientProxyProtocol let mediaProvider: MediaProviderProtocol } diff --git a/ElementX/Sources/Services/Session/UserSession.swift b/ElementX/Sources/Services/Session/UserSession.swift index 8cf709d43..168157d17 100644 --- a/ElementX/Sources/Services/Session/UserSession.swift +++ b/ElementX/Sources/Services/Session/UserSession.swift @@ -13,6 +13,8 @@ class UserSession: UserSessionProtocol { private var cancellables = Set() private var checkForSessionVerificationControllerCancellable: AnyCancellable? + var userID: String { clientProxy.userIdentifier } + let clientProxy: ClientProxyProtocol let mediaProvider: MediaProviderProtocol let callbacks = PassthroughSubject() diff --git a/ElementX/Sources/Services/Session/UserSessionProtocol.swift b/ElementX/Sources/Services/Session/UserSessionProtocol.swift index 3b39dd573..5f05cb1ef 100644 --- a/ElementX/Sources/Services/Session/UserSessionProtocol.swift +++ b/ElementX/Sources/Services/Session/UserSessionProtocol.swift @@ -15,6 +15,8 @@ enum UserSessionCallback { } protocol UserSessionProtocol { + var userID: String { get } + var clientProxy: ClientProxyProtocol { get } var mediaProvider: MediaProviderProtocol { get } diff --git a/ElementX/SupportingFiles/target.yml b/ElementX/SupportingFiles/target.yml index 1c0ac5e35..d0483d973 100644 --- a/ElementX/SupportingFiles/target.yml +++ b/ElementX/SupportingFiles/target.yml @@ -101,6 +101,7 @@ targets: dependencies: - package: MatrixRustSDK - package: DesignKit + - package: AppAuth - package: DTCoreText - package: KeychainAccess - package: Kingfisher diff --git a/UITests/SupportingFiles/target.yml b/UITests/SupportingFiles/target.yml index af7b8a080..29d361eef 100644 --- a/UITests/SupportingFiles/target.yml +++ b/UITests/SupportingFiles/target.yml @@ -9,6 +9,8 @@ targets: - target: ElementX - package: MatrixRustSDK linkType: static + - package: AppAuth + linkType: static - package: DTCoreText linkType: static - package: KeychainAccess diff --git a/UnitTests/Sources/HomeScreenViewModelTests.swift b/UnitTests/Sources/HomeScreenViewModelTests.swift index 2644202f2..847463b4d 100644 --- a/UnitTests/Sources/HomeScreenViewModelTests.swift +++ b/UnitTests/Sources/HomeScreenViewModelTests.swift @@ -23,7 +23,8 @@ class HomeScreenViewModelTests: XCTestCase { var context: HomeScreenViewModelType.Context! @MainActor override func setUpWithError() throws { - viewModel = HomeScreenViewModel(attributedStringBuilder: AttributedStringBuilder()) + viewModel = HomeScreenViewModel(initialDisplayName: "@test:example.com", + attributedStringBuilder: AttributedStringBuilder()) context = viewModel.context } diff --git a/changelog.d/42.wip b/changelog.d/42.wip new file mode 100644 index 000000000..3b609ac24 --- /dev/null +++ b/changelog.d/42.wip @@ -0,0 +1 @@ +Add a proof of concept implementation for login with OIDC. diff --git a/project.yml b/project.yml index 81abf03a7..5950b11a8 100644 --- a/project.yml +++ b/project.yml @@ -35,6 +35,9 @@ packages: # path: ../matrix-rust-components-swift DesignKit: path: ./ + AppAuth: + url: https://github.com/openid/AppAuth-iOS + majorVersion: 1.5.0 DTCoreText: url: https://github.com/Cocoanetics/DTCoreText majorVersion: 1.6.26