diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index a542ffc38..5ca994cba 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -25,6 +25,7 @@ /* Begin PBXBuildFile section */ 0033481EE363E4914295F188 /* LocalizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C070FD43DC6BF4E50217965A /* LocalizationTests.swift */; }; 00C3023B6DF55024D8876B76 /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 3D8BEEFCA07BEA43F4F4BF77 /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 012D9DDCDE6278E4E0CDCC0F /* LinkNewDeviceFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94EE2C5F0A06F146BBE3A1B1 /* LinkNewDeviceFlowCoordinator.swift */; }; 01373C1AC4839604C4FDA404 /* test_apple_image.heic in Resources */ = {isa = PBXBuildFile; fileRef = BB576F4118C35E6B5124FA22 /* test_apple_image.heic */; }; 01681E8B20AD6F0D237F2DC1 /* IdentityConfirmedScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C6624240FFD32B7F0834229 /* IdentityConfirmedScreenViewModel.swift */; }; 0180C44B997EDA8D21F883AC /* RoomNotificationSettingsCustomSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B746EFA112532A7B701FB914 /* RoomNotificationSettingsCustomSectionView.swift */; }; @@ -783,6 +784,7 @@ 88F348E2CB14FF71CBBB665D /* AudioRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7475C5AE20BA896930907EA8 /* AudioRoomTimelineItemContent.swift */; }; 890F0D453FE388756479AC97 /* AnalyticsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C687844F60BFF532D49A994C /* AnalyticsTests.swift */; }; 89198AE2649DD77673D5793B /* ExtensionLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A8571A8A071FB41778C016 /* ExtensionLogger.swift */; }; + 89261D215E4A432E887CD156 /* GrantLoginWithQrCodeHandlerSDKMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBD288168C48D3F76177FCBF /* GrantLoginWithQrCodeHandlerSDKMock.swift */; }; 8944548A684F1C837CEC47F4 /* RoomMembersListScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D0946F77B696176E062D037 /* RoomMembersListScreenModels.swift */; }; 89658A44C9FC19B58FD1C226 /* ServerConfirmationScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F08776C48FFB47CACF64ED10 /* ServerConfirmationScreenViewModelTests.swift */; }; 899793EFC63DF93C3E0141E7 /* RoomMemberDetailsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FA60F848D1C14F873F9621A /* RoomMemberDetailsScreenCoordinator.swift */; }; @@ -936,6 +938,7 @@ A32384E3D85CA65342D3A908 /* TimelineMediaPreviewRedactConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C75FE3F524B575D53787868C /* TimelineMediaPreviewRedactConfirmationView.swift */; }; A33784831AD880A670CAA9F9 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */; }; A36AD251013402EDBD666C75 /* AppMediatorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BAC027034248429A438886B /* AppMediatorMock.swift */; }; + A37BFB32EAB8AEF6DD5BA0DC /* LinkNewDeviceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 007C16779FDCF10DA4F1A510 /* LinkNewDeviceService.swift */; }; A37EED79941AD3B7140B3822 /* UIDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 287FC98AF2664EAD79C0D902 /* UIDevice.swift */; }; A3A7A05E8F9B7EB0E1A09A2A /* SoftLogoutScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05596E4A11A8C9346E9E54AE /* SoftLogoutScreenCoordinator.swift */; }; A3D7110C1E75E7B4A73BE71C /* VoiceMessageRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93C94C30E3135BC9290DE13 /* VoiceMessageRecorderTests.swift */; }; @@ -1253,6 +1256,7 @@ DFD5AA8688A34C72D48AF3B1 /* StaticLocationScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5311C989EC15B4C2D699025 /* StaticLocationScreenViewModel.swift */; }; DFF7D6A6C26DDD40D00AE579 /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = F012CB5EE3F2B67359F6CC52 /* target.yml */; }; E010DDE938032D3B8E84CC35 /* TracingHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A95C9B8299A36A6495DECA6 /* TracingHook.swift */; }; + E02DAD9FD8D62587049FFFEC /* LinkNewDeviceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80F04B12FA231E797B7151A8 /* LinkNewDeviceTests.swift */; }; E0B6A569AC3E81D233B43D60 /* SettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E625B0EB2F86B37C14EF7E6 /* SettingsScreenViewModel.swift */; }; E0C167D41A48EDB30B447DE3 /* VoiceMessageRecordingComposer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73A5C3F7C9C1DA10CAEC6A98 /* VoiceMessageRecordingComposer.swift */; }; E14E469CD97550D0FC58F3CA /* CancellableTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE52983FAFB4E0998C00EE8A /* CancellableTask.swift */; }; @@ -1526,6 +1530,7 @@ /* Begin PBXFileReference section */ 002399C6CB875C4EBB01CBC0 /* MediaEventsTimelineScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaEventsTimelineScreen.swift; sourceTree = ""; }; 00245D40CD90FD71D6A05239 /* EmojiPickerScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreen.swift; sourceTree = ""; }; + 007C16779FDCF10DA4F1A510 /* LinkNewDeviceService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkNewDeviceService.swift; sourceTree = ""; }; 00AFC5F08734C2EA4EE79C59 /* IdentityConfirmationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmationScreen.swift; sourceTree = ""; }; 00E5B2CBEF8F96424F095508 /* RoomDetailsEditScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenViewModelTests.swift; sourceTree = ""; }; 011AFA4990C585D157829679 /* DeclineAndBlockScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeclineAndBlockScreenViewModel.swift; sourceTree = ""; }; @@ -1719,6 +1724,7 @@ 22730A30C50AC2E3D5BA8642 /* InviteUsersScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenViewModelProtocol.swift; sourceTree = ""; }; 227AC5D71A4CE43512062243 /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = ""; }; 22DB19219E6CC4D002E15D48 /* GlobalSearchScreenCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSearchScreenCell.swift; sourceTree = ""; }; + 233D5F7E5E9F49ABF3413291 /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = hr; path = hr.lproj/Localizable.stringsdict; sourceTree = ""; }; 2355398E4A55DA5A89128AD1 /* EncryptionKeyProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionKeyProvider.swift; sourceTree = ""; }; 2363DB6162BBCC511B67B527 /* AccessibilityTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = AccessibilityTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 23674BF78CE814366EBD8762 /* cy */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cy; path = cy.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -1985,6 +1991,7 @@ 5445FCE0CE15E634FDC1A2E2 /* AnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsService.swift; sourceTree = ""; }; 5484457C81B325660901B161 /* AppLockSetupSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupSettingsScreen.swift; sourceTree = ""; }; 54A5E6F398C269AD52C9AE21 /* EncryptionResetPasswordScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetPasswordScreenModels.swift; sourceTree = ""; }; + 54A7923F0115CF17ABC8047F /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/SAS.strings; sourceTree = ""; }; 54AD70D6E03D2031AE1B5A52 /* TimelineReactionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReactionsView.swift; sourceTree = ""; }; 54C4E7B46099462F12000C91 /* DeveloperOptionsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreenViewModelProtocol.swift; sourceTree = ""; }; 5557DDA438841AF5DC003D0B /* SpaceListScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceListScreenViewModelTests.swift; sourceTree = ""; }; @@ -2198,6 +2205,7 @@ 7FDF541AE914059942B575B4 /* IdentityConfirmationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmationScreenModels.swift; sourceTree = ""; }; 8063E65441E771200108C558 /* ReadReceiptsSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadReceiptsSummaryView.swift; sourceTree = ""; }; 80E815FF3CC5E5A355E3A25E /* RoomMessageEventStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMessageEventStringBuilder.swift; sourceTree = ""; }; + 80F04B12FA231E797B7151A8 /* LinkNewDeviceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkNewDeviceTests.swift; sourceTree = ""; }; 810133CF215075C285FC6F3A /* test_image.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = test_image.png; sourceTree = ""; }; 8112846C9D9D3817689CBAF8 /* TimelineTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewController.swift; sourceTree = ""; }; 811E8BF34E931D51552C9C13 /* EncryptionResetScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetScreen.swift; sourceTree = ""; }; @@ -2317,6 +2325,7 @@ 94028A227645FA880B966211 /* WaveformSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaveformSource.swift; sourceTree = ""; }; 9475FD81B13D50103E5290EB /* SpaceSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceSettingsScreen.swift; sourceTree = ""; }; 94D670124FC3E84F23A62CCF /* APNSPayload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APNSPayload.swift; sourceTree = ""; }; + 94EE2C5F0A06F146BBE3A1B1 /* LinkNewDeviceFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkNewDeviceFlowCoordinator.swift; sourceTree = ""; }; 9501D11B4258DFA33BA3B40F /* ServerSelectionScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenModels.swift; sourceTree = ""; }; 951F277E0585E50AC91987C8 /* DeclineAndBlockScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeclineAndBlockScreenViewModelProtocol.swift; sourceTree = ""; }; 955336CBD5ED73C792D1F580 /* EncryptionAuthenticity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionAuthenticity.swift; sourceTree = ""; }; @@ -2389,6 +2398,7 @@ A1C22B1B5FA3A765EADB2CC9 /* SessionVerificationStateMachineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationStateMachineTests.swift; sourceTree = ""; }; A232D9156D225BD9FD1D0C43 /* PhotoLibraryPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoLibraryPicker.swift; sourceTree = ""; }; A243A6E6207297123E60DE48 /* TimelineItemMacContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMacContextMenu.swift; sourceTree = ""; }; + A2723A4AF3AB9F5E18D26E49 /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/InfoPlist.strings; sourceTree = ""; }; A3B4B58B79A6FA250B24A1EC /* HomeScreenContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenContent.swift; sourceTree = ""; }; A3FBD9C2B9A5479526920399 /* BugReportScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreenCoordinator.swift; sourceTree = ""; }; A40C19719687984FD9478FBE /* Task.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Task.swift; sourceTree = ""; }; @@ -2668,6 +2678,7 @@ D653265D006E708E4E51AD64 /* HomeScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenCoordinator.swift; sourceTree = ""; }; D66B5D86A9AB95E0E01BED82 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/InfoPlist.strings; sourceTree = ""; }; D67CBAFA48ED0B6FCE74F88F /* lt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lt; path = lt.lproj/Localizable.strings; sourceTree = ""; }; + D6D094C15E8DB424F1C6FC94 /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/Localizable.strings; sourceTree = ""; }; D6DC38E64A5ED3FDB201029A /* BugReportService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportService.swift; sourceTree = ""; }; D7149BDDE47F8AD104E644E2 /* TraceLogPack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceLogPack.swift; sourceTree = ""; }; D751AA05AD2182BFC4608DE6 /* ur */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ur; path = ur.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -2864,6 +2875,7 @@ FB9EABCA9348DFA27439A809 /* WaveformCursorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaveformCursorView.swift; sourceTree = ""; }; FBB0328F2887BF0A65BC5D49 /* NotificationSettingsEditScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreen.swift; sourceTree = ""; }; FBC776F301D374A3298C69DA /* AppCoordinatorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinatorProtocol.swift; sourceTree = ""; }; + FBD288168C48D3F76177FCBF /* GrantLoginWithQrCodeHandlerSDKMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GrantLoginWithQrCodeHandlerSDKMock.swift; sourceTree = ""; }; FC3797A2325BE44FFB478BE9 /* LeaveSpaceRoomDetailsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeaveSpaceRoomDetailsCell.swift; sourceTree = ""; }; FC83F47D2173B7538AA72E0E /* RoomSummaryProviderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProviderMock.swift; sourceTree = ""; }; FC9044BE0E4A66F5B963E834 /* AudioFileEventsTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioFileEventsTimelineView.swift; sourceTree = ""; }; @@ -4360,6 +4372,7 @@ 566FB9DA81607C2739D8C6A0 /* ChatsFlowCoordinatorStateMachine.swift */, A07B011547B201A836C03052 /* EncryptionResetFlowCoordinator.swift */, ECB836DD8BE31931F51B8AC9 /* EncryptionSettingsFlowCoordinator.swift */, + 94EE2C5F0A06F146BBE3A1B1 /* LinkNewDeviceFlowCoordinator.swift */, 2178B951602AA921A5FD9DC8 /* MediaEventsTimelineFlowCoordinator.swift */, C3285BD95B564CA2A948E511 /* OnboardingFlowCoordinator.swift */, A54AAF72E821B4084B7E4298 /* PinnedEventsTimelineFlowCoordinator.swift */, @@ -5340,6 +5353,7 @@ 27DF257F5D968E5DD719583C /* BugReportTests.swift */, 89BB11A792EF6F70B95B467E /* EncryptionResetTests.swift */, 57AD14D3ADADE8F6A10F9E88 /* EncryptionSettingsTests.swift */, + 80F04B12FA231E797B7151A8 /* LinkNewDeviceTests.swift */, DCDAB580109C09A6AA97AF7E /* PollFormScreenTests.swift */, E2520C4F33AA0C061D209C28 /* RoomMembersListScreenTests.swift */, 44DDC82DB6A84E700CD5DEC0 /* RoomRolesAndPermissionsTests.swift */, @@ -5683,6 +5697,7 @@ EBD19057FDB154A44335CE62 /* AuthenticationClientFactory.swift */, F3A1AB5A84D843B6AC8D5F1E /* AuthenticationService.swift */, 5E75948AA1FE1D1A7809931F /* AuthenticationServiceProtocol.swift */, + 007C16779FDCF10DA4F1A510 /* LinkNewDeviceService.swift */, ); path = Authentication; sourceTree = ""; @@ -6380,6 +6395,7 @@ children = ( 8EAF4A49F3ACD8BB8B0D2371 /* ClientSDKMock.swift */, 0A81FD0C60175FA081EB19AD /* EventTimelineItem.swift */, + FBD288168C48D3F76177FCBF /* GrantLoginWithQrCodeHandlerSDKMock.swift */, 5EFB1D29B0870AFB6A56E9B8 /* IdentityResetHandleSDKMock.swift */, 580BDCD23DD02481AB5FFB47 /* LeaveSpaceHandleSDKMock.swift */, ); @@ -7025,6 +7041,7 @@ fa, fi, fr, + hr, hu, id, it, @@ -7932,6 +7949,7 @@ D34E328E9E65904358248FDD /* GlobalSearchScreenModels.swift in Sources */, 55D18AA4F4A2257642EBDB94 /* GlobalSearchScreenViewModel.swift in Sources */, E32A18802EB37EEE3EF7B965 /* GlobalSearchScreenViewModelProtocol.swift in Sources */, + 89261D215E4A432E887CD156 /* GrantLoginWithQrCodeHandlerSDKMock.swift in Sources */, F3C9CAD26FD4D7D6EBACF501 /* HTMLFixtures.swift in Sources */, E8C4D9F93F0DCED211D5F187 /* HTMLParserStyle.swift in Sources */, 0C1E537A49ABB386F7554D4A /* HighlightedTimelineItemModifier.swift in Sources */, @@ -8024,11 +8042,13 @@ A9D349478F7D4A2B1E40CEF9 /* LegalInformationScreenViewModelProtocol.swift in Sources */, 5E597B9959BDAE7A67DBD5B2 /* LinkMetadataProvider.swift in Sources */, 112C6F1C493B3F26AB22716A /* LinkMetadataProviderProtocol.swift in Sources */, + 012D9DDCDE6278E4E0CDCC0F /* LinkNewDeviceFlowCoordinator.swift in Sources */, DE3BF0ED68E56BF625591D49 /* LinkNewDeviceScreen.swift in Sources */, B466827F3766FF8E0CD0D34F /* LinkNewDeviceScreenCoordinator.swift in Sources */, 69B9CC733A880E1BB097C113 /* LinkNewDeviceScreenModels.swift in Sources */, C16E25C41B858BF27E0C4FC6 /* LinkNewDeviceScreenViewModel.swift in Sources */, 92FE657CDFAFE3031576EB43 /* LinkNewDeviceScreenViewModelProtocol.swift in Sources */, + A37BFB32EAB8AEF6DD5BA0DC /* LinkNewDeviceService.swift in Sources */, 866FA35E7A2339EF8B6D91CA /* LinkPreviewView.swift in Sources */, 6E47D126DD7585E8F8237CE7 /* LoadableAvatarImage.swift in Sources */, D9F80CE61BF8FF627FDB0543 /* LoadableImage.swift in Sources */, @@ -8670,6 +8690,7 @@ 94D0F36A87E596A93C0C178A /* Bundle.swift in Sources */, 8D24671992A1C1753B211221 /* EncryptionResetTests.swift in Sources */, E5E43A0CA99AF5BA11B194A2 /* EncryptionSettingsTests.swift in Sources */, + E02DAD9FD8D62587049FFFEC /* LinkNewDeviceTests.swift in Sources */, A950C95855C474F75B30CA7B /* PollFormScreenTests.swift in Sources */, B5BCE012F9E7C45D1C76108E /* RoomMembersListScreenTests.swift in Sources */, DBC3FDE1540B39702A117D8E /* RoomRolesAndPermissionsTests.swift in Sources */, @@ -8751,6 +8772,7 @@ C715CFE00686DACA59D836EA /* fa */, A9E88667D393612FD5D84718 /* fi */, CEE20623EB4A9B88FB29F2BA /* fr */, + 54A7923F0115CF17ABC8047F /* hr */, D196116D2DD3F2757D45FCB7 /* hu */, 330AF4D121C3396F7A14B21D /* id */, 61B33F23681660E940BA57F4 /* it */, @@ -8783,6 +8805,7 @@ 48CE6BF18E542B32FA52CE06 /* fa */, 057B747CF045D3C6C30EAB2C /* fi */, 653610CB5F9776EAAAB98155 /* fr */, + 233D5F7E5E9F49ABF3413291 /* hr */, C95ADE8D9527523572532219 /* hu */, 475D47D0BFE961B02BAC5D49 /* id */, 6FC5015B9634698BDB8701AF /* it */, @@ -8826,6 +8849,7 @@ A9873374E72AA53260AE90A2 /* fa */, 434522ED2BDED08759048077 /* fi */, CC680E0E79D818706CB28CF8 /* fr */, + D6D094C15E8DB424F1C6FC94 /* hr */, 624244C398804ADC885239AA /* hu */, EF98A02DED04075F7CF0C721 /* id */, 7B04BD3874D736127A8156B8 /* it */, @@ -8867,6 +8891,7 @@ 70F8DAEF1A8131BDFD4CDE83 /* eu */, 24E637CF570711FB5FD63DEA /* fi */, ACD7BD6BEE21264F6677904A /* fr */, + A2723A4AF3AB9F5E18D26E49 /* hr */, 1D652E78832289CD9EB64488 /* hu */, 7199693797B66245EF97BCF5 /* id */, 44C314C00533E2C297796B60 /* it */, diff --git a/ElementX/Sources/FlowCoordinators/LinkNewDeviceFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/LinkNewDeviceFlowCoordinator.swift new file mode 100644 index 000000000..7254eee43 --- /dev/null +++ b/ElementX/Sources/FlowCoordinators/LinkNewDeviceFlowCoordinator.swift @@ -0,0 +1,64 @@ +// +// Copyright 2025 Element Creations Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. +// + +import Combine +import Foundation + +enum LinkNewDeviceFlowCoordinatorAction { + case requestOIDCAuthorisation(URL) + case dismiss +} + +class LinkNewDeviceFlowCoordinator: FlowCoordinatorProtocol { + private let navigationStackCoordinator: NavigationStackCoordinator + private let flowParameters: CommonFlowParameters + + private var cancellables = Set() + + private let actionsSubject: PassthroughSubject = .init() + var actionsPublisher: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(navigationStackCoordinator: NavigationStackCoordinator, + flowParameters: CommonFlowParameters) { + self.navigationStackCoordinator = navigationStackCoordinator + self.flowParameters = flowParameters + } + + func start(animated: Bool) { + presentLinkNewDeviceScreen() + } + + func handleAppRoute(_ appRoute: AppRoute, animated: Bool) { + fatalError() + } + + func clearRoute(animated: Bool) { + fatalError() + } + + private func presentLinkNewDeviceScreen() { + let coordinator = LinkNewDeviceScreenCoordinator(parameters: .init(clientProxy: flowParameters.userSession.clientProxy)) + coordinator.actionsPublisher + .sink { [weak self] action in + guard let self else { return } + + switch action { + case .linkMobileDevice(let progressPublisher): + break + case .linkDesktopComputer: + break + case .dismiss: + actionsSubject.send(.dismiss) + } + } + .store(in: &cancellables) + + navigationStackCoordinator.setRootCoordinator(coordinator) + } +} diff --git a/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift index 3fa2f4c65..47bfb30c3 100644 --- a/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift @@ -28,6 +28,8 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol { private var bugReportFlowCoordinator: BugReportFlowCoordinator? // periphery:ignore - retaining purpose private var encryptionSettingsFlowCoordinator: EncryptionSettingsFlowCoordinator? + // periphery:ignore - retaining purpose + private var linkNewDeviceFlowCoordinator: LinkNewDeviceFlowCoordinator? private var cancellables = Set() @@ -84,7 +86,7 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol { case .userDetails: presentUserDetailsEditScreen() case .linkNewDevice: - presentLinkNewDeviceScreen() + startLinkNewDeviceFlow() case let .manageAccount(url): presentAccountManagementURL(url) case .analytics: @@ -169,6 +171,31 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol { navigationStackCoordinator.push(coordinator) } + private func startLinkNewDeviceFlow() { + let stackCoordinator = NavigationStackCoordinator() + let flowCoordinator = LinkNewDeviceFlowCoordinator(navigationStackCoordinator: stackCoordinator, + flowParameters: flowParameters) + flowCoordinator.actionsPublisher + .sink { [weak self] action in + guard let self else { return } + + switch action { + case .dismiss: + navigationStackCoordinator.setSheetCoordinator(nil) + case .requestOIDCAuthorisation(let url): + presentAccountManagementURL(url) + } + } + .store(in: &cancellables) + + linkNewDeviceFlowCoordinator = flowCoordinator + flowCoordinator.start() + + navigationStackCoordinator.setSheetCoordinator(stackCoordinator) { [weak self] in + self?.linkNewDeviceFlowCoordinator = nil + } + } + private func presentAnalyticsScreen() { let coordinator = AnalyticsSettingsScreenCoordinator(parameters: .init(appSettings: flowParameters.appSettings, analytics: flowParameters.analytics)) @@ -259,9 +286,9 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol { navigationStackCoordinator.push(coordinator) } - + // MARK: OIDC Account Management - + private var accountSettingsPresenter: OIDCAccountSettingsPresenter? private func presentAccountManagementURL(_ url: URL) { // Note to anyone in the future if you come back here to make this open in Safari instead of a WAS. @@ -271,27 +298,4 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol { appSettings: flowParameters.appSettings) accountSettingsPresenter?.start() } - - // MARK: - Link New Device - - private func presentLinkNewDeviceScreen() { - let parameters = LinkNewDeviceScreenCoordinatorParameters(clientProxy: flowParameters.userSession.clientProxy) - let coordinator = LinkNewDeviceScreenCoordinator(parameters: parameters) - coordinator.actionsPublisher - .sink { [weak self] action in - guard let self else { return } - - switch action { - case .linkMobileDevice: - break - case .linkDesktopComputer: - break - case .dismiss: - navigationStackCoordinator.pop() - } - } - .store(in: &cancellables) - - navigationStackCoordinator.push(coordinator) - } } diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 1110d6c16..72e46cf08 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -4019,6 +4019,70 @@ class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable { return removeUserAvatarReturnValue } } + //MARK: - linkNewDeviceService + + var linkNewDeviceServiceUnderlyingCallsCount = 0 + var linkNewDeviceServiceCallsCount: Int { + get { + if Thread.isMainThread { + return linkNewDeviceServiceUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = linkNewDeviceServiceUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + linkNewDeviceServiceUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + linkNewDeviceServiceUnderlyingCallsCount = newValue + } + } + } + } + var linkNewDeviceServiceCalled: Bool { + return linkNewDeviceServiceCallsCount > 0 + } + + var linkNewDeviceServiceUnderlyingReturnValue: LinkNewDeviceService! + var linkNewDeviceServiceReturnValue: LinkNewDeviceService! { + get { + if Thread.isMainThread { + return linkNewDeviceServiceUnderlyingReturnValue + } else { + var returnValue: LinkNewDeviceService? = nil + DispatchQueue.main.sync { + returnValue = linkNewDeviceServiceUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + linkNewDeviceServiceUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + linkNewDeviceServiceUnderlyingReturnValue = newValue + } + } + } + } + var linkNewDeviceServiceClosure: (() -> LinkNewDeviceService)? + + func linkNewDeviceService() -> LinkNewDeviceService { + linkNewDeviceServiceCallsCount += 1 + if let linkNewDeviceServiceClosure = linkNewDeviceServiceClosure { + return linkNewDeviceServiceClosure() + } else { + return linkNewDeviceServiceReturnValue + } + } //MARK: - deactivateAccount var deactivateAccountPasswordEraseDataUnderlyingCallsCount = 0 diff --git a/ElementX/Sources/Mocks/SDK/GrantLoginWithQrCodeHandlerSDKMock.swift b/ElementX/Sources/Mocks/SDK/GrantLoginWithQrCodeHandlerSDKMock.swift new file mode 100644 index 000000000..33bf553ff --- /dev/null +++ b/ElementX/Sources/Mocks/SDK/GrantLoginWithQrCodeHandlerSDKMock.swift @@ -0,0 +1,33 @@ +// +// Copyright 2025 Element Creations Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. +// + +import Foundation +import MatrixRustSDK + +extension GrantLoginWithQrCodeHandlerSDKMock { + struct Configuration { + var generateDelay: Duration = .seconds(0) + var generatedBase64QRCode = """ + TUFUUklYAgS0yzZ1QVpQ1jlnoxWX3d5jrWRFfELxjS2gN7pz9y+3PABaaHR0 + cHM6Ly9zeW5hcHNlLW9pZGMubGFiLmVsZW1lbnQuZGV2L19zeW5hcHNlL2Ns + aWVudC9yZW5kZXp2b3VzLzAxSFg5SzAwUTFINktQRDQ3RUc0RzFUM1hHACVo + dHRwczovL3N5bmFwc2Utb2lkYy5sYWIuZWxlbWVudC5kZXYv + """ + } + + convenience init(_ configuration: Configuration) { + self.init() + + generateProgressListenerClosure = { listener in + Task { + try await Task.sleep(for: configuration.generateDelay) + let bytes = Data(base64Encoded: configuration.generatedBase64QRCode) ?? Data() + try listener.onUpdate(state: .qrReady(qrCode: .fromBytes(bytes: bytes))) + } + } + } +} diff --git a/ElementX/Sources/Other/AccessibilityIdentifiers.swift b/ElementX/Sources/Other/AccessibilityIdentifiers.swift index 40f014ebe..d90079e29 100644 --- a/ElementX/Sources/Other/AccessibilityIdentifiers.swift +++ b/ElementX/Sources/Other/AccessibilityIdentifiers.swift @@ -45,6 +45,7 @@ enum A11yIdentifiers { static let roomPollsHistoryScreen = RoomPollsHistoryScreen() static let manageRoomMemberSheet = ManageRoomMemberSheet() static let spaceListScreen = SpaceListScreen() + static let linkNewDeviceScreen = LinkNewDeviceScreen() struct AlertInfo { let primaryButton = "alert_info-primary_button" @@ -314,4 +315,10 @@ enum A11yIdentifiers { "\(roomNamePrefix):\(name)" } } + + struct LinkNewDeviceScreen { + let cancel = "link_new_device_screen-cancel" + let mobileDevice = "link_new_device_screen-mobile_device" + let desktopComputer = "link_new_device_screen-desktop_computer" + } } diff --git a/ElementX/Sources/Other/SDKListener.swift b/ElementX/Sources/Other/SDKListener.swift index 43c9f13e8..c6a9159f7 100644 --- a/ElementX/Sources/Other/SDKListener.swift +++ b/ElementX/Sources/Other/SDKListener.swift @@ -29,6 +29,14 @@ extension SDKListener: QrLoginProgressListener where T == QrLoginProgress { func onUpdate(state: QrLoginProgress) { onUpdateClosure(state) } } +extension SDKListener: GrantQrLoginProgressListener where T == GrantQrLoginProgress { + func onUpdate(state: GrantQrLoginProgress) { onUpdateClosure(state) } +} + +extension SDKListener: GrantGeneratedQrLoginProgressListener where T == GrantGeneratedQrLoginProgress { + func onUpdate(state: GrantGeneratedQrLoginProgress) { onUpdateClosure(state) } +} + // MARK: ClientProxy extension SDKListener: MediaPreviewConfigListener where T == MediaPreviewConfig? { diff --git a/ElementX/Sources/Screens/LinkNewDeviceScreen/LinkNewDeviceScreenCoordinator.swift b/ElementX/Sources/Screens/LinkNewDeviceScreen/LinkNewDeviceScreenCoordinator.swift index 0b188daef..4feee1b43 100644 --- a/ElementX/Sources/Screens/LinkNewDeviceScreen/LinkNewDeviceScreenCoordinator.swift +++ b/ElementX/Sources/Screens/LinkNewDeviceScreen/LinkNewDeviceScreenCoordinator.swift @@ -9,7 +9,7 @@ import Combine import SwiftUI enum LinkNewDeviceScreenCoordinatorAction { - case linkMobileDevice + case linkMobileDevice(LinkNewDeviceService.GenerateProgressPublisher) case linkDesktopComputer case dismiss } @@ -38,8 +38,8 @@ final class LinkNewDeviceScreenCoordinator: CoordinatorProtocol { guard let self else { return } switch action { - case .linkMobileDevice: - actionsSubject.send(.linkMobileDevice) + case .linkMobileDevice(let progressPublisher): + actionsSubject.send(.linkMobileDevice(progressPublisher)) case .linkDesktopComputer: actionsSubject.send(.linkDesktopComputer) case .dismiss: diff --git a/ElementX/Sources/Screens/LinkNewDeviceScreen/LinkNewDeviceScreenModels.swift b/ElementX/Sources/Screens/LinkNewDeviceScreen/LinkNewDeviceScreenModels.swift index e2804f2ef..a66b0b544 100644 --- a/ElementX/Sources/Screens/LinkNewDeviceScreen/LinkNewDeviceScreenModels.swift +++ b/ElementX/Sources/Screens/LinkNewDeviceScreen/LinkNewDeviceScreenModels.swift @@ -8,7 +8,7 @@ import Foundation enum LinkNewDeviceScreenViewModelAction { - case linkMobileDevice + case linkMobileDevice(LinkNewDeviceService.GenerateProgressPublisher) case linkDesktopComputer case dismiss } diff --git a/ElementX/Sources/Screens/LinkNewDeviceScreen/LinkNewDeviceScreenViewModel.swift b/ElementX/Sources/Screens/LinkNewDeviceScreen/LinkNewDeviceScreenViewModel.swift index 7b65cd189..82185e310 100644 --- a/ElementX/Sources/Screens/LinkNewDeviceScreen/LinkNewDeviceScreenViewModel.swift +++ b/ElementX/Sources/Screens/LinkNewDeviceScreen/LinkNewDeviceScreenViewModel.swift @@ -6,6 +6,7 @@ // import Combine +import MatrixRustSDK import SwiftUI typealias LinkNewDeviceScreenViewModelType = StateStoreViewModelV2 @@ -33,7 +34,7 @@ class LinkNewDeviceScreenViewModel: LinkNewDeviceScreenViewModelType, LinkNewDev switch viewAction { case .linkMobileDevice: - linkMobileDevice() + Task { await linkMobileDevice() } case .linkDesktopComputer: actionsSubject.send(.linkDesktopComputer) case .dismiss: @@ -51,11 +52,27 @@ class LinkNewDeviceScreenViewModel: LinkNewDeviceScreenViewModelType, LinkNewDev } } - private func linkMobileDevice() { + private func linkMobileDevice() async { state.mode = .readyToLink(isGeneratingCode: true) - // TODO: Generate a QR code. + let linkNewDeviceService = clientProxy.linkNewDeviceService() - actionsSubject.send(.linkMobileDevice) + let progressPublisher = linkNewDeviceService.generateQRCode() + + do { + _ = try await progressPublisher.values + .first { progress in + switch progress { + case .qrReady: true + default: false + } + } + + actionsSubject.send(.linkMobileDevice(progressPublisher)) + state.mode = .readyToLink(isGeneratingCode: false) + } catch { + #warning("Needs some form of re-usable error handling, will handle with the next screen.") + state.mode = .notSupported + } } } diff --git a/ElementX/Sources/Screens/LinkNewDeviceScreen/View/LinkNewDeviceScreen.swift b/ElementX/Sources/Screens/LinkNewDeviceScreen/View/LinkNewDeviceScreen.swift index 152dd7ac0..259421c65 100644 --- a/ElementX/Sources/Screens/LinkNewDeviceScreen/View/LinkNewDeviceScreen.swift +++ b/ElementX/Sources/Screens/LinkNewDeviceScreen/View/LinkNewDeviceScreen.swift @@ -21,6 +21,7 @@ struct LinkNewDeviceScreen: View { .backgroundStyle(.compound.bgSubtleSecondary) .navigationTitle(L10n.commonLinkNewDevice) .navigationBarTitleDisplayMode(.inline) + .toolbar { toolbar } } @ViewBuilder @@ -86,11 +87,13 @@ struct LinkNewDeviceScreen: View { } } .buttonStyle(.compound(.primary)) + .accessibilityIdentifier(A11yIdentifiers.linkNewDeviceScreen.mobileDevice) Button { context.send(viewAction: .linkDesktopComputer) } label: { Label(L10n.screenLinkNewDeviceRootDesktopComputer, icon: \.computer) } .buttonStyle(.compound(.primary)) + .accessibilityIdentifier(A11yIdentifiers.linkNewDeviceScreen.desktopComputer) } .disabled(isGeneratingCode) case .notSupported: @@ -100,6 +103,15 @@ struct LinkNewDeviceScreen: View { .buttonStyle(.compound(.primary)) } } + + var toolbar: some ToolbarContent { + ToolbarItem(placement: .cancellationAction) { + Button(L10n.actionCancel) { + context.send(viewAction: .dismiss) + } + .accessibilityIdentifier(A11yIdentifiers.linkNewDeviceScreen.cancel) + } + } } // MARK: - Previews @@ -148,6 +160,7 @@ struct LinkNewDeviceScreen_Previews: PreviewProvider, TestablePreview { return false } } + clientProxy.linkNewDeviceServiceReturnValue = .init(handler: GrantLoginWithQrCodeHandlerSDKMock(.init(generateDelay: .seconds(20)))) let viewModel = LinkNewDeviceScreenViewModel(clientProxy: clientProxy) diff --git a/ElementX/Sources/Screens/QRCodeLoginScreen/QRCodeLoginScreenViewModel.swift b/ElementX/Sources/Screens/QRCodeLoginScreen/QRCodeLoginScreenViewModel.swift index 418b990ad..0a66b74b6 100644 --- a/ElementX/Sources/Screens/QRCodeLoginScreen/QRCodeLoginScreenViewModel.swift +++ b/ElementX/Sources/Screens/QRCodeLoginScreen/QRCodeLoginScreenViewModel.swift @@ -140,7 +140,7 @@ class QRCodeLoginScreenViewModel: QRCodeLoginScreenViewModelType, QRCodeLoginScr state.state = .error(.expired) case .deviceNotSupported: state.state = .error(.deviceNotSupported) - case .unknown: + case .deviceAlreadySignedIn, .unknown: state.state = .error(.unknown) } } diff --git a/ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift b/ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift index b4300d2e3..62c33cac1 100644 --- a/ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift +++ b/ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift @@ -105,6 +105,7 @@ enum QRCodeLoginError: Error, Equatable { case expired case deviceNotSupported case deviceNotSignedIn + case deviceAlreadySignedIn case unknown } diff --git a/ElementX/Sources/Services/Authentication/LinkNewDeviceService.swift b/ElementX/Sources/Services/Authentication/LinkNewDeviceService.swift new file mode 100644 index 000000000..aaf9027ea --- /dev/null +++ b/ElementX/Sources/Services/Authentication/LinkNewDeviceService.swift @@ -0,0 +1,148 @@ +// +// Copyright 2025 Element Creations Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. +// + +import Combine +import Foundation +import MatrixRustSDK + +class LinkNewDeviceService { + typealias GenerateProgressPublisher = CurrentValuePublisher + typealias ScanProgressPublisher = CurrentValuePublisher + + enum GenerateProgress { + case starting + case qrReady(QrCodeData) + case qrScanned(CheckCodeSenderProtocol) + case waitingForAuthorisation(verificationURI: String) + case syncingSecrets + case done + } + + enum ScanProgress { + case starting + case establishingSecureChannel(checkCode: UInt8, checkCodeString: String) + case waitingForAuth(verificationURI: String) + case syncingSecrets + case done + } + + private let grantLoginHandler: GrantLoginWithQrCodeHandlerProtocol + + init(handler: GrantLoginWithQrCodeHandlerProtocol) { + grantLoginHandler = handler + } + + func generateQRCode() -> GenerateProgressPublisher { + let progressSubject = CurrentValueSubject(.starting) + let listener = SDKListener { progressSubject.send(.init(rustProgress: $0)) } + + Task { + do { + // TODO: we need a way to cancel the in progress grant if the user hit the cancel button + try await grantLoginHandler.generate(progressListener: listener) // The success state is handled by the listener. + } catch let error as HumanQrGrantLoginError { + MXLog.error("QR code reciprocate error: \(error)") + progressSubject.send(completion: .failure(.init(rustError: error))) + } catch { + MXLog.error("QR code reciprocate unknown error: \(error)") + progressSubject.send(completion: .failure(.unknown)) + } + } + + return progressSubject.asCurrentValuePublisher() + } + + func scanQRCode(_ scannedQRData: Data) -> ScanProgressPublisher { + let progressSubject = CurrentValueSubject(.starting) + let listener = SDKListener { progressSubject.send(.init(rustProgress: $0)) } + + let qrCodeData: QrCodeData + do { + qrCodeData = try QrCodeData.fromBytes(bytes: scannedQRData) + } catch { + MXLog.error("QR code decode error: \(error)") + progressSubject.send(completion: .failure(.invalidQRCode)) + return progressSubject.asCurrentValuePublisher() + } + + #warning("Check intent/server name here…") + + Task { + do { + // TODO: it would be nice to be able to cancel the grant at the SDK level if the user hits the cancel button + try await grantLoginHandler.scan(qrCodeData: qrCodeData, progressListener: listener) // The success state is handled by the listener. + } catch let error as HumanQrGrantLoginError { + MXLog.error("QR code reciprocate error: \(error)") + progressSubject.send(completion: .failure(.init(rustError: error))) + } catch { + MXLog.error("QR code reciprocate unknown error: \(error)") + progressSubject.send(completion: .failure(.unknown)) + } + } + + return progressSubject.asCurrentValuePublisher() + } +} + +extension LinkNewDeviceService.GenerateProgress: CustomStringConvertible { + init(rustProgress: GrantGeneratedQrLoginProgress) { + self = switch rustProgress { + case .starting: .starting + case .qrReady(let qrCode): .qrReady(qrCode) + case .qrScanned(let checkCodeSender): .qrScanned(checkCodeSender) + case .waitingForAuth(let verificationUri): .waitingForAuthorisation(verificationURI: verificationUri) + case .syncingSecrets: .syncingSecrets + case .done: .done + } + } + + var description: String { + switch self { + case .starting: "starting" + case .qrReady: "qrReady" + case .qrScanned: "qrScanned" + case .waitingForAuthorisation: "waitingForAuthorisation" + case .syncingSecrets: "syncingSecrets" + case .done: "done" + } + } +} + +extension LinkNewDeviceService.ScanProgress: CustomStringConvertible { + init(rustProgress: GrantQrLoginProgress) { + self = switch rustProgress { + case .starting: .starting + case .establishingSecureChannel(let checkCode, let checkCodeString): .establishingSecureChannel(checkCode: checkCode, checkCodeString: checkCodeString) + case .waitingForAuth(let verificationUri): .waitingForAuth(verificationURI: verificationUri) + case .syncingSecrets: .syncingSecrets + case .done: .done + } + } + + var description: String { + switch self { + case .starting: "starting" + case .establishingSecureChannel: "establishingSecureChannel" + case .waitingForAuth: "waitingForAuth" + case .syncingSecrets: "syncingSecrets" + case .done: "done" + } + } +} + +private extension QRCodeLoginError { + init(rustError: HumanQrGrantLoginError) { + self = switch rustError { + case .InvalidCheckCode: + .connectionInsecure + case .UnsupportedProtocol: + .linkingNotSupported + case .Unknown, .NotFound, .MissingSecretsBackup, .DeviceIdAlreadyInUse, .UnableToCreateDevice: + .unknown + } + } +} diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index 481ec89b2..fba9ac56b 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -715,13 +715,9 @@ class ClientProxy: ClientProxyProtocol { return .failure(.sdkError(error)) } } - - func logout() async { - do { - try await client.logout() - } catch { - MXLog.error("Failed logging out with error: \(error)") - } + + func linkNewDeviceService() -> LinkNewDeviceService { + LinkNewDeviceService(handler: client.newGrantLoginWithQrCodeHandler()) } func deactivateAccount(password: String?, eraseData: Bool) async -> Result { @@ -734,6 +730,14 @@ class ClientProxy: ClientProxyProtocol { } } + func logout() async { + do { + try await client.logout() + } catch { + MXLog.error("Failed logging out with error: \(error)") + } + } + func setPusher(with configuration: PusherConfiguration) async throws { try await client.setPusher(identifiers: configuration.identifiers, kind: configuration.kind, diff --git a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift index a03091fb2..b9368845e 100644 --- a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift +++ b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift @@ -194,7 +194,9 @@ protocol ClientProxyProtocol: AnyObject { func setUserAvatar(media: MediaInfo) async -> Result func removeUserAvatar() async -> Result - + + func linkNewDeviceService() -> LinkNewDeviceService + func deactivateAccount(password: String?, eraseData: Bool) async -> Result func logout() async diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index 477f71686..216374790 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -734,6 +734,40 @@ class MockScreen: Identifiable { coordinator.start() return navigationStackCoordinator + case .linkNewDevice: + let navigationStackCoordinator = NavigationStackCoordinator() + let flowCoordinator = LinkNewDeviceFlowCoordinator(navigationStackCoordinator: navigationStackCoordinator, + flowParameters: CommonFlowParameters(userSession: UserSessionMock(.init()), + bugReportService: BugReportServiceMock(.init()), + elementCallService: ElementCallServiceMock(.init()), + timelineControllerFactory: TimelineControllerFactoryMock(.init()), + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), + appMediator: AppMediatorMock.default, + appSettings: ServiceLocator.shared.settings, + appHooks: AppHooks(), + analytics: ServiceLocator.shared.analytics, + userIndicatorController: UserIndicatorControllerMock(), + notificationManager: NotificationManagerMock(), + stateMachineFactory: StateMachineFactory())) + flowCoordinator.actionsPublisher + .sink { [weak self] action in + guard let self else { return } + switch action { + case .dismiss: + navigationRootCoordinator.setSheetCoordinator(nil) + case .requestOIDCAuthorisation: + break + } + } + .store(in: &cancellables) + + retainedState.append(flowCoordinator) + flowCoordinator.start() + + // Use a sheet on top the the placeholder so we can test the dismissal. + navigationRootCoordinator.setSheetCoordinator(navigationStackCoordinator) + return PlaceholderScreenCoordinator(hideBrandChrome: false) case .autoUpdatingTimeline: let appSettings: AppSettings = ServiceLocator.shared.settings appSettings.hasRunIdentityConfirmationOnboarding = true diff --git a/ElementX/Sources/UITests/UITestsScreenIdentifier.swift b/ElementX/Sources/UITests/UITestsScreenIdentifier.swift index c1d6aafbb..85971e6d2 100644 --- a/ElementX/Sources/UITests/UITestsScreenIdentifier.swift +++ b/ElementX/Sources/UITests/UITestsScreenIdentifier.swift @@ -25,6 +25,7 @@ enum UITestsScreenIdentifier: String { case encryptionSettings case encryptionSettingsOutOfSync case encryptionReset + case linkNewDevice case roomLayoutBottom case roomLayoutMiddle case roomLayoutTop diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Generating-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Generating-iPad-en-GB.png index 5b1d0df28..bec214660 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Generating-iPad-en-GB.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Generating-iPad-en-GB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ad18b20c43c8f8127c485c8b434064de0b237d25ee177c98a989eb65113248e4 -size 109374 +oid sha256:46aea5299c049f35dae8f1600616c6223a596fd2f8679f92989c16664cf4dbc1 +size 111944 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Generating-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Generating-iPad-pseudo.png index 5f5caa9e6..63e48b4d2 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Generating-iPad-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Generating-iPad-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5645b5827fdf244280e80f6fc16b421a7dc0635b16b6f80b2b50a1907844b4cf -size 123395 +oid sha256:6a186ec9227165649b719f4dd798e3ac890369b308e1dd96bd21fa433d2af9b2 +size 126198 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Generating-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Generating-iPhone-en-GB.png index 9baac765a..1199f37d4 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Generating-iPhone-en-GB.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Generating-iPhone-en-GB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cc447ed7e8194814387d698437d3adaeb4b7b6f687842598fb84240d58d4e0fe -size 61504 +oid sha256:6a4f19b2fe16544a545520570eb2015aab26546ec8050a3b282715e2ec38826e +size 63808 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Generating-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Generating-iPhone-pseudo.png index 6e601d3e0..204a55582 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Generating-iPhone-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Generating-iPhone-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8510dfa5fe61fc0fe236ccf4bf69d3fec03a3cd5fb14182c93da27fb93b94356 -size 82818 +oid sha256:61b9690e8d8521fcf9d64303caab7d6daf08bafb6d4b175cfdb3ec000d151ffb +size 85214 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Loading-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Loading-iPad-en-GB.png index 850e6a748..13e59bb36 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Loading-iPad-en-GB.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Loading-iPad-en-GB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e5a9223f99dfb8209fc21042a2656ca4a050a4f8f1a1d7723ef613bcf54706b5 -size 98823 +oid sha256:8dfcf396e71ca8a417e88b2c27d452030b1ba092f7e91b2b20737394ca74e7db +size 101393 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Loading-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Loading-iPad-pseudo.png index b58187b63..d19f6760f 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Loading-iPad-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Loading-iPad-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:79aba2f30b36dc42149b05844746297c3959cce54108fb6e9043d22a1e441b55 -size 109776 +oid sha256:f95f8fe398860d1fad8b18babed1e1806c524af15cb9d9685c221cf1a0fb00a0 +size 112579 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Loading-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Loading-iPhone-pseudo.png index 19cc921c8..df2f9a814 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Loading-iPhone-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Loading-iPhone-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c49ca4038efc3d6059b8f8cd715b67741c4c45e4b817ff61aa5569ea3d76ed38 -size 65128 +oid sha256:ed16c902c68752356fa5ee71aa684988a4b4bdcf4e505f75d970cb1f750aba96 +size 64763 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Ready-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Ready-iPad-en-GB.png index 6f47f38c3..8b35292ab 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Ready-iPad-en-GB.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Ready-iPad-en-GB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f94dff4155601b73d57df0294c051463ff330fe431a2c46a5a5b198cdc372ea -size 109664 +oid sha256:cc4f1056724de24ad5aaf7181a831e8b8c16e42b5e8771915dde72b957c98fbb +size 112234 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Ready-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Ready-iPad-pseudo.png index 08beba709..86230d3bf 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Ready-iPad-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Ready-iPad-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a049866c334facc710309e02999e4869a46839cf0448b68660d21e0cb6842a43 -size 123411 +oid sha256:a152660e6fa14a8ae66af91ce8e43020fc4718dd300a8049e4375b697e705264 +size 126214 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Ready-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Ready-iPhone-en-GB.png index aa3fb8946..e15693649 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Ready-iPhone-en-GB.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Ready-iPhone-en-GB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c0d7ce68e28cf45d36e04d4f6b9e62722ed67e7fc5be42287b43e1bc68924a67 -size 61909 +oid sha256:844e98b0b406dafa543d47f773b5aef1367fa6e7bca1ed0b0735f26c55ffec6f +size 64213 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Ready-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Ready-iPhone-pseudo.png index 228bc85a5..3f99f2214 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Ready-iPhone-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Ready-iPhone-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c637c11e208a467b4e4d77a2fb9f1ea0fe6602dcc0788934659328074ed4f3bc -size 81263 +oid sha256:37a8a52edf87070a3c325822dac1c0a29be41ed46f9d803ad5bbe6e2d4bd3015 +size 83659 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Unsupported-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Unsupported-iPad-en-GB.png index b42205d25..a3f4b5cd1 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Unsupported-iPad-en-GB.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Unsupported-iPad-en-GB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d1959686b35a78a6c49ab16e9eb96f5ca879151ecf95969e13acf31d8c61f461 -size 106107 +oid sha256:5ee9d2b8142ec642ea6f8d606769aaf2b4c1742855e5d67370f8d1b270a6e15a +size 108670 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Unsupported-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Unsupported-iPad-pseudo.png index 974cf1f4a..d61d153a9 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Unsupported-iPad-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Unsupported-iPad-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f15c62942085467d9e450c48bd5233ba51a3ae3b2c7f7c4cce2428ef66675b87 -size 120137 +oid sha256:67621aa88efeeb7c589bc91087970211de6fa1ff35794c26245af3e483712f31 +size 123533 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Unsupported-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Unsupported-iPhone-en-GB.png index 00d3249f1..c7af69346 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Unsupported-iPhone-en-GB.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Unsupported-iPhone-en-GB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d85de510b52b116591c0e24b0ba647e3aa404a77b5e645f512a3cbf47d5291dd -size 60430 +oid sha256:a958c859edf66c6e11503fd590c04dc6937e8c322ade159bdaa4c1158f237cb2 +size 63031 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Unsupported-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Unsupported-iPhone-pseudo.png index fa1dd7b7e..028390911 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Unsupported-iPhone-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkNewDeviceScreen.Unsupported-iPhone-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a504b62f6c259b91a9a0a6d8a7b1cf18b513e86a69f5f5f9399aedf69053f65d -size 80204 +oid sha256:ddb567a2b6e080169771b6dece8ad4b47d80f83b207d6c0147531a8882fac7cb +size 82338 diff --git a/UITests/Sources/LinkNewDeviceTests.swift b/UITests/Sources/LinkNewDeviceTests.swift new file mode 100644 index 000000000..33ff89523 --- /dev/null +++ b/UITests/Sources/LinkNewDeviceTests.swift @@ -0,0 +1,26 @@ +// +// Copyright 2025 Element Creations Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. +// + +import XCTest + +@MainActor +class LinkNewDeviceTests: XCTestCase { + enum Step { + static let selectDevice = 1 + static let dismissed = 99 + } + + func testFlow() async throws { + let app = Application.launch(.linkNewDevice) + try await app.assertScreenshot(step: Step.selectDevice) + + let cancelButton = app.buttons[A11yIdentifiers.linkNewDeviceScreen.cancel] + cancelButton.tap() + + try await app.assertScreenshot(step: Step.dismissed) + } +} diff --git a/UITests/Sources/__Snapshots__/Application/linkNewDevice.testFlow-iPad-en-GB-1.png b/UITests/Sources/__Snapshots__/Application/linkNewDevice.testFlow-iPad-en-GB-1.png new file mode 100644 index 000000000..a423d966f --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/linkNewDevice.testFlow-iPad-en-GB-1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9dbfadaaf361b79c626e35ab3cf5f42149e18de2538175e796a704cc21dc8411 +size 158261 diff --git a/UITests/Sources/__Snapshots__/Application/linkNewDevice.testFlow-iPad-en-GB-99.png b/UITests/Sources/__Snapshots__/Application/linkNewDevice.testFlow-iPad-en-GB-99.png new file mode 100644 index 000000000..85cd35443 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/linkNewDevice.testFlow-iPad-en-GB-99.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:048a07df37bc493d91f67b5e1379ef571db047ab35780b28c23ee1990429846f +size 222243 diff --git a/UITests/Sources/__Snapshots__/Application/linkNewDevice.testFlow-iPhone-en-GB-1.png b/UITests/Sources/__Snapshots__/Application/linkNewDevice.testFlow-iPhone-en-GB-1.png new file mode 100644 index 000000000..a4bf12518 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/linkNewDevice.testFlow-iPhone-en-GB-1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da689e9ce1dc5b5fb809eda3e1886a9cec9d4b7c04271b9c8652a9893f910674 +size 115287 diff --git a/UITests/Sources/__Snapshots__/Application/linkNewDevice.testFlow-iPhone-en-GB-99.png b/UITests/Sources/__Snapshots__/Application/linkNewDevice.testFlow-iPhone-en-GB-99.png new file mode 100644 index 000000000..cd49d748f --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/linkNewDevice.testFlow-iPhone-en-GB-99.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47d79566040b9e990cd222dc056d22de86723171364d60a0fdc3592dff9bb48e +size 374177