Add a service and flow coordinator for the LinkNewDevice feature. (#4859)

* Add a LinkNewDeviceService that exposes the SDK's grant QR code login methods.

* Add a flow coordinator for linking a new device.

Changes the presentation too.
This commit is contained in:
Doug
2025-12-15 14:44:26 +00:00
committed by GitHub
parent 2de55f98b0
commit 8df57abc1e
38 changed files with 536 additions and 73 deletions

View File

@@ -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 = "<group>"; };
00245D40CD90FD71D6A05239 /* EmojiPickerScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreen.swift; sourceTree = "<group>"; };
007C16779FDCF10DA4F1A510 /* LinkNewDeviceService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkNewDeviceService.swift; sourceTree = "<group>"; };
00AFC5F08734C2EA4EE79C59 /* IdentityConfirmationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmationScreen.swift; sourceTree = "<group>"; };
00E5B2CBEF8F96424F095508 /* RoomDetailsEditScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenViewModelTests.swift; sourceTree = "<group>"; };
011AFA4990C585D157829679 /* DeclineAndBlockScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeclineAndBlockScreenViewModel.swift; sourceTree = "<group>"; };
@@ -1719,6 +1724,7 @@
22730A30C50AC2E3D5BA8642 /* InviteUsersScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenViewModelProtocol.swift; sourceTree = "<group>"; };
227AC5D71A4CE43512062243 /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = "<group>"; };
22DB19219E6CC4D002E15D48 /* GlobalSearchScreenCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSearchScreenCell.swift; sourceTree = "<group>"; };
233D5F7E5E9F49ABF3413291 /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = hr; path = hr.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
2355398E4A55DA5A89128AD1 /* EncryptionKeyProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionKeyProvider.swift; sourceTree = "<group>"; };
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 = "<group>"; };
@@ -1985,6 +1991,7 @@
5445FCE0CE15E634FDC1A2E2 /* AnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsService.swift; sourceTree = "<group>"; };
5484457C81B325660901B161 /* AppLockSetupSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupSettingsScreen.swift; sourceTree = "<group>"; };
54A5E6F398C269AD52C9AE21 /* EncryptionResetPasswordScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetPasswordScreenModels.swift; sourceTree = "<group>"; };
54A7923F0115CF17ABC8047F /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/SAS.strings; sourceTree = "<group>"; };
54AD70D6E03D2031AE1B5A52 /* TimelineReactionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReactionsView.swift; sourceTree = "<group>"; };
54C4E7B46099462F12000C91 /* DeveloperOptionsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreenViewModelProtocol.swift; sourceTree = "<group>"; };
5557DDA438841AF5DC003D0B /* SpaceListScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceListScreenViewModelTests.swift; sourceTree = "<group>"; };
@@ -2198,6 +2205,7 @@
7FDF541AE914059942B575B4 /* IdentityConfirmationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmationScreenModels.swift; sourceTree = "<group>"; };
8063E65441E771200108C558 /* ReadReceiptsSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadReceiptsSummaryView.swift; sourceTree = "<group>"; };
80E815FF3CC5E5A355E3A25E /* RoomMessageEventStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMessageEventStringBuilder.swift; sourceTree = "<group>"; };
80F04B12FA231E797B7151A8 /* LinkNewDeviceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkNewDeviceTests.swift; sourceTree = "<group>"; };
810133CF215075C285FC6F3A /* test_image.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = test_image.png; sourceTree = "<group>"; };
8112846C9D9D3817689CBAF8 /* TimelineTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewController.swift; sourceTree = "<group>"; };
811E8BF34E931D51552C9C13 /* EncryptionResetScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetScreen.swift; sourceTree = "<group>"; };
@@ -2317,6 +2325,7 @@
94028A227645FA880B966211 /* WaveformSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaveformSource.swift; sourceTree = "<group>"; };
9475FD81B13D50103E5290EB /* SpaceSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceSettingsScreen.swift; sourceTree = "<group>"; };
94D670124FC3E84F23A62CCF /* APNSPayload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APNSPayload.swift; sourceTree = "<group>"; };
94EE2C5F0A06F146BBE3A1B1 /* LinkNewDeviceFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkNewDeviceFlowCoordinator.swift; sourceTree = "<group>"; };
9501D11B4258DFA33BA3B40F /* ServerSelectionScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenModels.swift; sourceTree = "<group>"; };
951F277E0585E50AC91987C8 /* DeclineAndBlockScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeclineAndBlockScreenViewModelProtocol.swift; sourceTree = "<group>"; };
955336CBD5ED73C792D1F580 /* EncryptionAuthenticity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionAuthenticity.swift; sourceTree = "<group>"; };
@@ -2389,6 +2398,7 @@
A1C22B1B5FA3A765EADB2CC9 /* SessionVerificationStateMachineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationStateMachineTests.swift; sourceTree = "<group>"; };
A232D9156D225BD9FD1D0C43 /* PhotoLibraryPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoLibraryPicker.swift; sourceTree = "<group>"; };
A243A6E6207297123E60DE48 /* TimelineItemMacContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMacContextMenu.swift; sourceTree = "<group>"; };
A2723A4AF3AB9F5E18D26E49 /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/InfoPlist.strings; sourceTree = "<group>"; };
A3B4B58B79A6FA250B24A1EC /* HomeScreenContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenContent.swift; sourceTree = "<group>"; };
A3FBD9C2B9A5479526920399 /* BugReportScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreenCoordinator.swift; sourceTree = "<group>"; };
A40C19719687984FD9478FBE /* Task.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Task.swift; sourceTree = "<group>"; };
@@ -2668,6 +2678,7 @@
D653265D006E708E4E51AD64 /* HomeScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenCoordinator.swift; sourceTree = "<group>"; };
D66B5D86A9AB95E0E01BED82 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/InfoPlist.strings; sourceTree = "<group>"; };
D67CBAFA48ED0B6FCE74F88F /* lt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lt; path = lt.lproj/Localizable.strings; sourceTree = "<group>"; };
D6D094C15E8DB424F1C6FC94 /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/Localizable.strings; sourceTree = "<group>"; };
D6DC38E64A5ED3FDB201029A /* BugReportService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportService.swift; sourceTree = "<group>"; };
D7149BDDE47F8AD104E644E2 /* TraceLogPack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceLogPack.swift; sourceTree = "<group>"; };
D751AA05AD2182BFC4608DE6 /* ur */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ur; path = ur.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
@@ -2864,6 +2875,7 @@
FB9EABCA9348DFA27439A809 /* WaveformCursorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaveformCursorView.swift; sourceTree = "<group>"; };
FBB0328F2887BF0A65BC5D49 /* NotificationSettingsEditScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreen.swift; sourceTree = "<group>"; };
FBC776F301D374A3298C69DA /* AppCoordinatorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinatorProtocol.swift; sourceTree = "<group>"; };
FBD288168C48D3F76177FCBF /* GrantLoginWithQrCodeHandlerSDKMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GrantLoginWithQrCodeHandlerSDKMock.swift; sourceTree = "<group>"; };
FC3797A2325BE44FFB478BE9 /* LeaveSpaceRoomDetailsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeaveSpaceRoomDetailsCell.swift; sourceTree = "<group>"; };
FC83F47D2173B7538AA72E0E /* RoomSummaryProviderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProviderMock.swift; sourceTree = "<group>"; };
FC9044BE0E4A66F5B963E834 /* AudioFileEventsTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioFileEventsTimelineView.swift; sourceTree = "<group>"; };
@@ -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 = "<group>";
@@ -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 */,

View File

@@ -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<AnyCancellable>()
private let actionsSubject: PassthroughSubject<LinkNewDeviceFlowCoordinatorAction, Never> = .init()
var actionsPublisher: AnyPublisher<LinkNewDeviceFlowCoordinatorAction, Never> {
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)
}
}

View File

@@ -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<AnyCancellable>()
@@ -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)
}
}

View File

@@ -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

View File

@@ -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)))
}
}
}
}

View File

@@ -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"
}
}

View File

@@ -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? {

View File

@@ -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:

View File

@@ -8,7 +8,7 @@
import Foundation
enum LinkNewDeviceScreenViewModelAction {
case linkMobileDevice
case linkMobileDevice(LinkNewDeviceService.GenerateProgressPublisher)
case linkDesktopComputer
case dismiss
}

View File

@@ -6,6 +6,7 @@
//
import Combine
import MatrixRustSDK
import SwiftUI
typealias LinkNewDeviceScreenViewModelType = StateStoreViewModelV2<LinkNewDeviceScreenViewState, LinkNewDeviceScreenViewAction>
@@ -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
}
}
}

View File

@@ -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)

View File

@@ -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)
}
}

View File

@@ -105,6 +105,7 @@ enum QRCodeLoginError: Error, Equatable {
case expired
case deviceNotSupported
case deviceNotSignedIn
case deviceAlreadySignedIn
case unknown
}

View File

@@ -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<GenerateProgress, QRCodeLoginError>
typealias ScanProgressPublisher = CurrentValuePublisher<ScanProgress, QRCodeLoginError>
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<GenerateProgress, QRCodeLoginError>(.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<ScanProgress, QRCodeLoginError>(.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
}
}
}

View File

@@ -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<Void, ClientProxyError> {
@@ -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,

View File

@@ -194,7 +194,9 @@ protocol ClientProxyProtocol: AnyObject {
func setUserAvatar(media: MediaInfo) async -> Result<Void, ClientProxyError>
func removeUserAvatar() async -> Result<Void, ClientProxyError>
func linkNewDeviceService() -> LinkNewDeviceService
func deactivateAccount(password: String?, eraseData: Bool) async -> Result<Void, ClientProxyError>
func logout() async

View File

@@ -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

View File

@@ -25,6 +25,7 @@ enum UITestsScreenIdentifier: String {
case encryptionSettings
case encryptionSettingsOutOfSync
case encryptionReset
case linkNewDevice
case roomLayoutBottom
case roomLayoutMiddle
case roomLayoutTop

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ad18b20c43c8f8127c485c8b434064de0b237d25ee177c98a989eb65113248e4
size 109374
oid sha256:46aea5299c049f35dae8f1600616c6223a596fd2f8679f92989c16664cf4dbc1
size 111944

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5645b5827fdf244280e80f6fc16b421a7dc0635b16b6f80b2b50a1907844b4cf
size 123395
oid sha256:6a186ec9227165649b719f4dd798e3ac890369b308e1dd96bd21fa433d2af9b2
size 126198

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cc447ed7e8194814387d698437d3adaeb4b7b6f687842598fb84240d58d4e0fe
size 61504
oid sha256:6a4f19b2fe16544a545520570eb2015aab26546ec8050a3b282715e2ec38826e
size 63808

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8510dfa5fe61fc0fe236ccf4bf69d3fec03a3cd5fb14182c93da27fb93b94356
size 82818
oid sha256:61b9690e8d8521fcf9d64303caab7d6daf08bafb6d4b175cfdb3ec000d151ffb
size 85214

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e5a9223f99dfb8209fc21042a2656ca4a050a4f8f1a1d7723ef613bcf54706b5
size 98823
oid sha256:8dfcf396e71ca8a417e88b2c27d452030b1ba092f7e91b2b20737394ca74e7db
size 101393

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:79aba2f30b36dc42149b05844746297c3959cce54108fb6e9043d22a1e441b55
size 109776
oid sha256:f95f8fe398860d1fad8b18babed1e1806c524af15cb9d9685c221cf1a0fb00a0
size 112579

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c49ca4038efc3d6059b8f8cd715b67741c4c45e4b817ff61aa5569ea3d76ed38
size 65128
oid sha256:ed16c902c68752356fa5ee71aa684988a4b4bdcf4e505f75d970cb1f750aba96
size 64763

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7f94dff4155601b73d57df0294c051463ff330fe431a2c46a5a5b198cdc372ea
size 109664
oid sha256:cc4f1056724de24ad5aaf7181a831e8b8c16e42b5e8771915dde72b957c98fbb
size 112234

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a049866c334facc710309e02999e4869a46839cf0448b68660d21e0cb6842a43
size 123411
oid sha256:a152660e6fa14a8ae66af91ce8e43020fc4718dd300a8049e4375b697e705264
size 126214

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c0d7ce68e28cf45d36e04d4f6b9e62722ed67e7fc5be42287b43e1bc68924a67
size 61909
oid sha256:844e98b0b406dafa543d47f773b5aef1367fa6e7bca1ed0b0735f26c55ffec6f
size 64213

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c637c11e208a467b4e4d77a2fb9f1ea0fe6602dcc0788934659328074ed4f3bc
size 81263
oid sha256:37a8a52edf87070a3c325822dac1c0a29be41ed46f9d803ad5bbe6e2d4bd3015
size 83659

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d1959686b35a78a6c49ab16e9eb96f5ca879151ecf95969e13acf31d8c61f461
size 106107
oid sha256:5ee9d2b8142ec642ea6f8d606769aaf2b4c1742855e5d67370f8d1b270a6e15a
size 108670

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f15c62942085467d9e450c48bd5233ba51a3ae3b2c7f7c4cce2428ef66675b87
size 120137
oid sha256:67621aa88efeeb7c589bc91087970211de6fa1ff35794c26245af3e483712f31
size 123533

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d85de510b52b116591c0e24b0ba647e3aa404a77b5e645f512a3cbf47d5291dd
size 60430
oid sha256:a958c859edf66c6e11503fd590c04dc6937e8c322ade159bdaa4c1158f237cb2
size 63031

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a504b62f6c259b91a9a0a6d8a7b1cf18b513e86a69f5f5f9399aedf69053f65d
size 80204
oid sha256:ddb567a2b6e080169771b6dece8ad4b47d80f83b207d6c0147531a8882fac7cb
size 82338

View File

@@ -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)
}
}

View File

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

View File

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