Repository: element-hq/element-x-ios
Pull request #3875: ClientProxy refactor
Author: login: Velin92
Labels: name: pr-misc
description: for other changes
Created: 2025-03-06T11:19:38Z | Merged: 2025-03-06T13:13:55Z

Refactored the client proxy to:
- throw an error during the init
- to have its init directly instantiate the sync service and the providers



Diff:
diff --git a/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift
index bcd0e1300d..ab63e8e743 100644
--- a/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift
+++ b/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift
@@ -127,9 +127,7 @@ class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol {
     }
     
     private func presentMessageForwarding(with forwardingItem: MessageForwardingItem) {
-        guard let roomSummaryProvider = userSession.clientProxy.alternateRoomSummaryProvider else {
-            fatalError()
-        }
+        let roomSummaryProvider = userSession.clientProxy.alternateRoomSummaryProvider
         
         let stackCoordinator = NavigationStackCoordinator()
         
diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift
index db8c04b3b7..7abb8f1000 100644
--- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift
+++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift
@@ -696,7 +696,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
         self.timelineController = timelineController
         
         let completionSuggestionService = CompletionSuggestionService(roomProxy: roomProxy,
-                                                                      roomListPublisher: userSession.clientProxy.staticRoomSummaryProvider?.roomListPublisher.eraseToAnyPublisher() ?? Empty().replaceEmpty(with: []).eraseToAnyPublisher())
+                                                                      roomListPublisher: userSession.clientProxy.staticRoomSummaryProvider.roomListPublisher.eraseToAnyPublisher())
         let composerDraftService = ComposerDraftService(roomProxy: roomProxy, timelineItemfactory: timelineItemFactory)
         
         let parameters = RoomScreenCoordinatorParameters(clientProxy: userSession.clientProxy,
@@ -1295,9 +1295,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
     }
     
     private func presentMessageForwarding(with forwardingItem: MessageForwardingItem) {
-        guard let roomSummaryProvider = userSession.clientProxy.alternateRoomSummaryProvider else {
-            fatalError()
-        }
+        let roomSummaryProvider = userSession.clientProxy.alternateRoomSummaryProvider
         
         let stackCoordinator = NavigationStackCoordinator()
         
diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift
index 43bee11372..97073e939c 100644
--- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift
+++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift
@@ -831,9 +831,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
     // MARK: Global search
     
     private func presentGlobalSearch() {
-        guard let roomSummaryProvider = userSession.clientProxy.alternateRoomSummaryProvider else {
-            fatalError("Global search room summary provider unavailable")
-        }
+        let roomSummaryProvider = userSession.clientProxy.alternateRoomSummaryProvider
         
         let coordinator = GlobalSearchScreenCoordinator(parameters: .init(roomSummaryProvider: roomSummaryProvider,
                                                                           mediaProvider: userSession.mediaProvider))
@@ -935,9 +933,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
     // MARK: Sharing
     
     private func presentRoomSelectionScreen(sharePayload: ShareExtensionPayload, animated: Bool) {
-        guard let roomSummaryProvider = userSession.clientProxy.alternateRoomSummaryProvider else {
-            fatalError()
-        }
+        let roomSummaryProvider = userSession.clientProxy.alternateRoomSummaryProvider
         
         let stackCoordinator = NavigationStackCoordinator()
         
diff --git a/ElementX/Sources/Mocks/ClientProxyMock.swift b/ElementX/Sources/Mocks/ClientProxyMock.swift
index 615ee090c3..907029451b 100644
--- a/ElementX/Sources/Mocks/ClientProxyMock.swift
+++ b/ElementX/Sources/Mocks/ClientProxyMock.swift
@@ -13,7 +13,7 @@ struct ClientProxyMockConfiguration {
     var userIDServerName: String?
     var userID: String = RoomMemberProxyMock.mockMe.userID
     var deviceID: String?
-    var roomSummaryProvider: RoomSummaryProviderProtocol? = RoomSummaryProviderMock(.init())
+    var roomSummaryProvider: RoomSummaryProviderProtocol = RoomSummaryProviderMock(.init())
     var roomDirectorySearchProxy: RoomDirectorySearchProxyProtocol?
     
     var recoveryState: SecureBackupRecoveryState = .enabled
@@ -86,7 +86,7 @@ extension ClientProxyMock {
         resetIdentityReturnValue = .success(IdentityResetHandleSDKMock(.init()))
         
         roomForIdentifierClosure = { [weak self] identifier in
-            guard let room = self?.roomSummaryProvider?.roomListPublisher.value.first(where: { $0.id == identifier }) else {
+            guard let room = self?.roomSummaryProvider.roomListPublisher.value.first(where: { $0.id == identifier }) else {
                 return nil
             }
             
diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift
index 5bd03c2eb6..bb5db1e657 100644
--- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift
+++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift
@@ -2267,9 +2267,21 @@ class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable {
     }
     var underlyingIgnoredUsersPublisher: CurrentValuePublisher<[String]?, Never>!
     var pusherNotificationClientIdentifier: String?
-    var roomSummaryProvider: RoomSummaryProviderProtocol?
-    var alternateRoomSummaryProvider: RoomSummaryProviderProtocol?
-    var staticRoomSummaryProvider: StaticRoomSummaryProviderProtocol?
+    var roomSummaryProvider: RoomSummaryProviderProtocol {
+        get { return underlyingRoomSummaryProvider }
+        set(value) { underlyingRoomSummaryProvider = value }
+    }
+    var underlyingRoomSummaryProvider: RoomSummaryProviderProtocol!
+    var alternateRoomSummaryProvider: RoomSummaryProviderProtocol {
+        get { return underlyingAlternateRoomSummaryProvider }
+        set(value) { underlyingAlternateRoomSummaryProvider = value }
+    }
+    var underlyingAlternateRoomSummaryProvider: RoomSummaryProviderProtocol!
+    var staticRoomSummaryProvider: StaticRoomSummaryProviderProtocol {
+        get { return underlyingStaticRoomSummaryProvider }
+        set(value) { underlyingStaticRoomSummaryProvider = value }
+    }
+    var underlyingStaticRoomSummaryProvider: StaticRoomSummaryProviderProtocol!
     var roomsToAwait: Set<String> {
         get { return underlyingRoomsToAwait }
         set(value) { underlyingRoomsToAwait = value }
diff --git a/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenViewModel.swift b/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenViewModel.swift
index fb83838d5a..b7c7413155 100644
--- a/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenViewModel.swift
+++ b/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenViewModel.swift
@@ -140,19 +140,19 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo
             roomInfo = invitedRoomProxy.info
         case .knocked(let knockedRoomProxy):
             roomInfo = knockedRoomProxy.info
-            if let roomSummaryProvider = clientProxy.staticRoomSummaryProvider {
-                membershipStateChangeCancellable = roomSummaryProvider.roomListPublisher
-                    .compactMap { summaries -> Void? in
-                        guard let roomSummary = summaries.first(where: { $0.id == roomInfo?.id }),
-                              roomSummary.roomListItem.membership() != .knocked else {
-                            return nil
-                        }
-                        return ()
+            membershipStateChangeCancellable = clientProxy
+                .staticRoomSummaryProvider
+                .roomListPublisher
+                .compactMap { summaries -> Void? in
+                    guard let roomSummary = summaries.first(where: { $0.id == roomInfo?.id }),
+                          roomSummary.roomListItem.membership() != .knocked else {
+                        return nil
                     }
-                    .sink { [weak self] in
-                        Task { await self?.loadRoomDetails() }
-                    }
-            }
+                    return ()
+                }
+                .sink { [weak self] in
+                    Task { await self?.loadRoomDetails() }
+                }
         case .banned(let bannedRoomProxy):
             roomInfo = bannedRoomProxy.info
         default:
diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift
index 4f716cbb1a..85dea85d02 100644
--- a/ElementX/Sources/Services/Client/ClientProxy.swift
+++ b/ElementX/Sources/Services/Client/ClientProxy.swift
@@ -20,13 +20,13 @@ class ClientProxy: ClientProxyProtocol {
     private let mediaLoader: MediaLoaderProtocol
     private let clientQueue: DispatchQueue
     
-    private var roomListService: RoomListService?
+    private var roomListService: RoomListService
     // periphery: ignore - only for retain
     private var roomListStateUpdateTaskHandle: TaskHandle?
     // periphery: ignore - only for retain
     private var roomListStateLoadingStateUpdateTaskHandle: TaskHandle?
 
-    private var syncService: SyncService?
+    private var syncService: SyncService
     // periphery: ignore - only for retain
     private var syncServiceStateUpdateTaskHandle: TaskHandle?
     
@@ -43,10 +43,10 @@ class ClientProxy: ClientProxyProtocol {
     
     // These following summary providers both operate on the same allRooms() list but
     // can apply their own filtering and pagination
-    private(set) var roomSummaryProvider: RoomSummaryProviderProtocol?
-    private(set) var alternateRoomSummaryProvider: RoomSummaryProviderProtocol?
+    private(set) var roomSummaryProvider: RoomSummaryProviderProtocol
+    private(set) var alternateRoomSummaryProvider: RoomSummaryProviderProtocol
     
-    private(set) var staticRoomSummaryProvider: StaticRoomSummaryProviderProtocol?
+    private(set) var staticRoomSummaryProvider: StaticRoomSummaryProviderProtocol
     
     let notificationSettings: NotificationSettingsProxyProtocol
 
@@ -139,7 +139,7 @@ class ClientProxy: ClientProxyProtocol {
     init(client: ClientProtocol,
          needsSlidingSyncMigration: Bool,
          networkMonitor: NetworkMonitorProtocol,
-         appSettings: AppSettings) async {
+         appSettings: AppSettings) async throws {
         self.client = client
         self.networkMonitor = networkMonitor
         self.appSettings = appSettings
@@ -153,7 +153,22 @@ class ClientProxy: ClientProxyProtocol {
         secureBackupController = SecureBackupController(encryption: client.encryption())
         
         self.needsSlidingSyncMigration = needsSlidingSyncMigration
-
+        
+        let configuredAppService = try await ClientProxyServices(client: client,
+                                                                 actionsSubject: actionsSubject,
+                                                                 notificationSettings: notificationSettings,
+                                                                 appSettings: appSettings)
+        
+        syncService = configuredAppService.syncService
+        roomListService = configuredAppService.roomListService
+        roomSummaryProvider = configuredAppService.roomSummaryProvider
+        alternateRoomSummaryProvider = configuredAppService.alternateRoomSummaryProvider
+        staticRoomSummaryProvider = configuredAppService.staticRoomSummaryProvider
+        
+        syncServiceStateUpdateTaskHandle = createSyncServiceStateObserver(syncService)
+        roomListStateUpdateTaskHandle = createRoomListServiceObserver(roomListService)
+        roomListStateLoadingStateUpdateTaskHandle = createRoomListLoadingStateUpdateObserver(roomListService)
+        
         delegateHandle = client.setDelegate(delegate: ClientDelegateWrapper { [weak self] isSoftLogout in
             self?.hasEncounteredAuthError = true
             self?.actionsSubject.send(.receivedAuthError(isSoftLogout: isSoftLogout))
@@ -168,8 +183,6 @@ class ClientProxy: ClientProxyProtocol {
                 }
             }
             .store(in: &cancellables)
-        
-        await configureAppService()
 
         loadUserAvatarURLFromCache()
         
@@ -281,7 +294,7 @@ class ClientProxy: ClientProxyProtocol {
         MXLog.info("Starting sync")
         
         Task {
-            await syncService?.start()
+            await syncService.start()
             
             // If we are using OIDC we want to cache the account management URL in volatile memory on the SDK side.
             // To avoid the cache being invalidated while the app is backgrounded, we cache at every sync start.
@@ -322,12 +335,6 @@ class ClientProxy: ClientProxyProtocol {
             restartTask = nil
         }
         
-        guard let syncService else {
-            MXLog.warning("No sync service to stop.")
-            completion?()
-            return
-        }
-        
         // Capture the sync service strongly as this method is called on deinit and so the
         // existence of self when the Task executes is questionable and would sometimes crash.
         // Note: This isn't strictly necessary now given the unwrap above, but leaving the code as
@@ -493,12 +500,6 @@ class ClientProxy: ClientProxyProtocol {
             return room
         }
         
-        // Else wait for the visible rooms list to go into fully loaded
-        guard let roomSummaryProvider else {
-            MXLog.error("Rooms summary provider not setup yet")
-            return nil
-        }
-        
         if !roomSummaryProvider.statePublisher.value.isLoaded {
             _ = await roomSummaryProvider.statePublisher.values.first { $0.isLoaded }
         }
@@ -524,33 +525,11 @@ class ClientProxy: ClientProxyProtocol {
     }
     
     func roomSummaryForIdentifier(_ identifier: String) -> RoomSummary? {
-        // the alternate room summary provider is not impacted by filtering
-        guard let provider = staticRoomSummaryProvider else {
-            MXLog.verbose("Missing room summary provider")
-            return nil
-        }
-        
-        guard let roomSummary = provider.roomListPublisher.value.first(where: { $0.id == identifier }) else {
-            MXLog.verbose("Missing room summary, count: \(provider.roomListPublisher.value.count)")
-            return nil
-        }
-        
-        return roomSummary
+        staticRoomSummaryProvider.roomListPublisher.value.first(where: { $0.id == identifier })
     }
     
     func roomSummaryForAlias(_ alias: String) -> RoomSummary? {
-        // the alternate room summary provider is not impacted by filtering
-        guard let provider = staticRoomSummaryProvider else {
-            MXLog.verbose("Missing room summary provider")
-            return nil
-        }
-        
-        guard let roomSummary = provider.roomListPublisher.value.first(where: { $0.canonicalAlias == alias || $0.alternativeAliases.contains(alias) }) else {
-            MXLog.verbose("Missing room summary, count: \(provider.roomListPublisher.value.count)")
-            return nil
-        }
-        
-        return roomSummary
+        staticRoomSummaryProvider.roomListPublisher.value.first(where: { $0.canonicalAlias == alias || $0.alternativeAliases.contains(alias) })
     }
 
     func loadUserDisplayName() async -> Result<Void, ClientProxyError> {
@@ -839,63 +818,7 @@ class ClientProxy: ClientProxyProtocol {
             self.userAvatarURLSubject.value = urlString.flatMap(URL.init)
         }
     }
-
-    private func configureAppService() async {
-        guard syncService == nil else {
-            fatalError("This shouldn't be called more than once")
-        }
-        
-        do {
-            let syncService = try await client
-                .syncService()
-                .withCrossProcessLock()
-                .withUtdHook(delegate: ClientDecryptionErrorDelegate(actionsSubject: actionsSubject))
-                .finish()
-            
-            let roomListService = syncService.roomListService()
-            
-            let roomMessageEventStringBuilder = RoomMessageEventStringBuilder(attributedStringBuilder: AttributedStringBuilder(cacheKey: "roomList",
-                                                                                                                               mentionBuilder: PlainMentionBuilder()), destination: .roomList)
-            let eventStringBuilder = RoomEventStringBuilder(stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID, shouldDisambiguateDisplayNames: false),
-                                                            messageEventStringBuilder: roomMessageEventStringBuilder,
-                                                            shouldDisambiguateDisplayNames: false,
-                                                            shouldPrefixSenderName: true)
-            
-            roomSummaryProvider = RoomSummaryProvider(roomListService: roomListService,
-                                                      eventStringBuilder: eventStringBuilder,
-                                                      name: "AllRooms",
-                                                      shouldUpdateVisibleRange: true,
-                                                      notificationSettings: notificationSettings,
-                                                      appSettings: appSettings)
-            try await roomSummaryProvider?.setRoomList(roomListService.allRooms())
-            
-            alternateRoomSummaryProvider = RoomSummaryProvider(roomListService: roomListService,
-                                                               eventStringBuilder: eventStringBuilder,
-                                                               name: "AlternateAllRooms",
-                                                               notificationSettings: notificationSettings,
-                                                               appSettings: appSettings)
-            try await alternateRoomSummaryProvider?.setRoomList(roomListService.allRooms())
-            
-            staticRoomSummaryProvider = RoomSummaryProvider(roomListService: roomListService,
-                                                            eventStringBuilder: eventStringBuilder,
-                                                            name: "StaticAllRooms",
-                                                            roomListPageSize: .max,
-                                                            notificationSettings: notificationSettings,
-                                                            appSettings: appSettings)
-            try await staticRoomSummaryProvider?.setRoomList(roomListService.allRooms())
-                        
-            self.syncService = syncService
-            self.roomListService = roomListService
-
-            syncServiceStateUpdateTaskHandle = createSyncServiceStateObserver(syncService)
-            roomListStateUpdateTaskHandle = createRoomListServiceObserver(roomListService)
-            roomListStateLoadingStateUpdateTaskHandle = createRoomListLoadingStateUpdateObserver(roomListService)
-
-        } catch {
-            MXLog.error("Failed building room list service with error: \(error)")
-        }
-    }
-
+    
     private func createSyncServiceStateObserver(_ syncService: SyncService) -> TaskHandle {
         syncService.state(listener: SyncServiceStateObserverProxy { [weak self] state in
             guard let self else { return }
@@ -967,11 +890,6 @@ class ClientProxy: ClientProxyProtocol {
     }()
     
     private func buildRoomForIdentifier(_ roomID: String) async -> RoomProxyType? {
-        guard let roomListService else {
-            MXLog.error("Failed retrieving room: \(roomID), room list service not set up")
-            return nil
-        }
-                
         do {
             let roomListItem = try roomListService.room(roomId: roomID)
             
@@ -1205,3 +1123,57 @@ private class SendQueueRoomErrorListenerProxy: SendQueueRoomErrorListener {
         onErrorClosure(roomId, error)
     }
 }
+
+private struct ClientProxyServices {
+    let syncService: SyncService
+    let roomListService: RoomListService
+    let roomSummaryProvider: RoomSummaryProviderProtocol
+    let alternateRoomSummaryProvider: RoomSummaryProviderProtocol
+    let staticRoomSummaryProvider: StaticRoomSummaryProviderProtocol
+    
+    init(client: ClientProtocol,
+         actionsSubject: PassthroughSubject<ClientProxyAction, Never>,
+         notificationSettings: NotificationSettingsProxyProtocol,
+         appSettings: AppSettings) async throws {
+        let syncService = try await client
+            .syncService()
+            .withCrossProcessLock()
+            .withUtdHook(delegate: ClientDecryptionErrorDelegate(actionsSubject: actionsSubject))
+            .finish()
+        
+        let roomListService = syncService.roomListService()
+        
+        let roomMessageEventStringBuilder = RoomMessageEventStringBuilder(attributedStringBuilder: AttributedStringBuilder(cacheKey: "roomList",
+                                                                                                                           mentionBuilder: PlainMentionBuilder()), destination: .roomList)
+        let eventStringBuilder = try RoomEventStringBuilder(stateEventStringBuilder: RoomStateEventStringBuilder(userID: client.userId(), shouldDisambiguateDisplayNames: false),
+                                                            messageEventStringBuilder: roomMessageEventStringBuilder,
+                                                            shouldDisambiguateDisplayNames: false,
+                                                            shouldPrefixSenderName: true)
+        
+        roomSummaryProvider = RoomSummaryProvider(roomListService: roomListService,
+                                                  eventStringBuilder: eventStringBuilder,
+                                                  name: "AllRooms",
+                                                  shouldUpdateVisibleRange: true,
+                                                  notificationSettings: notificationSettings,
+                                                  appSettings: appSettings)
+        try await roomSummaryProvider.setRoomList(roomListService.allRooms())
+        
+        alternateRoomSummaryProvider = RoomSummaryProvider(roomListService: roomListService,
+                                                           eventStringBuilder: eventStringBuilder,
+                                                           name: "AlternateAllRooms",
+                                                           notificationSettings: notificationSettings,
+                                                           appSettings: appSettings)
+        try await alternateRoomSummaryProvider.setRoomList(roomListService.allRooms())
+        
+        staticRoomSummaryProvider = RoomSummaryProvider(roomListService: roomListService,
+                                                        eventStringBuilder: eventStringBuilder,
+                                                        name: "StaticAllRooms",
+                                                        roomListPageSize: .max,
+                                                        notificationSettings: notificationSettings,
+                                                        appSettings: appSettings)
+        try await staticRoomSummaryProvider.setRoomList(roomListService.allRooms())
+        
+        self.syncService = syncService
+        self.roomListService = roomListService
+    }
+}
diff --git a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift
index 26bfbe787d..eb659caf32 100644
--- a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift
+++ b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift
@@ -94,15 +94,15 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
     
     var pusherNotificationClientIdentifier: String? { get }
     
-    var roomSummaryProvider: RoomSummaryProviderProtocol? { get }
+    var roomSummaryProvider: RoomSummaryProviderProtocol { get }
     
     /// Used for listing rooms that shouldn't be affected by the main `roomSummaryProvider` filtering
     /// But can still be filtered by queries, since this may be shared across multiple views, remember to reset
     /// The filtering state when you are done with it
-    var alternateRoomSummaryProvider: RoomSummaryProviderProtocol? { get }
+    var alternateRoomSummaryProvider: RoomSummaryProviderProtocol { get }
     
     /// Used for listing rooms, can't be filtered nor its state observed
-    var staticRoomSummaryProvider: StaticRoomSummaryProviderProtocol? { get }
+    var staticRoomSummaryProvider: StaticRoomSummaryProviderProtocol { get }
     
     var roomsToAwait: Set<String> { get set }
     
diff --git a/ElementX/Sources/Services/UserSession/UserSessionStore.swift b/ElementX/Sources/Services/UserSession/UserSessionStore.swift
index c8967df1b4..2e39c7da7f 100644
--- a/ElementX/Sources/Services/UserSession/UserSessionStore.swift
+++ b/ElementX/Sources/Services/UserSession/UserSessionStore.swift
@@ -64,7 +64,7 @@ class UserSessionStore: UserSessionStoreProtocol {
         do {
             let session = try client.session()
             let userID = try client.userId()
-            let clientProxy = await setupProxyForClient(client, needsSlidingSyncMigration: false)
+            let clientProxy = try await setupProxyForClient(client, needsSlidingSyncMigration: false)
             
             keychainController.setRestorationToken(RestorationToken(session: session,
                                                                     sessionDirectories: sessionDirectories,
@@ -138,17 +138,26 @@ class UserSessionStore: UserSessionStoreProtocol {
             
             MXLog.info("Set up session for user \(credentials.userID) at: \(credentials.restorationToken.sessionDirectories)")
             
-            return await .success(setupProxyForClient(client, needsSlidingSyncMigration: credentials.restorationToken.needsSlidingSyncMigration))
+            return try await .success(setupProxyForClient(client, needsSlidingSyncMigration: credentials.restorationToken.needsSlidingSyncMigration))
+        } catch UserSessionStoreError.failedSettingUpClientProxy(let error) {
+            // If this has failed, there is likely something wrong with the creation of the sync service
+            // There is nothing we can do, but at the same time we don't want the user to the get logged out
+            // So it's better to crash here and let the app restart
+            fatalError("Failed setting up the client proxy with error: \(error)")
         } catch {
             MXLog.error("Failed restoring login with error: \(error)")
             return .failure(.failedRestoringLogin)
         }
     }
     
-    private func setupProxyForClient(_ client: ClientProtocol, needsSlidingSyncMigration: Bool) async -> ClientProxyProtocol {
-        await ClientProxy(client: client,
-                          needsSlidingSyncMigration: needsSlidingSyncMigration,
-                          networkMonitor: networkMonitor,
-                          appSettings: appSettings)
+    private func setupProxyForClient(_ client: ClientProtocol, needsSlidingSyncMigration: Bool) async throws -> ClientProxyProtocol {
+        do {
+            return try await ClientProxy(client: client,
+                                         needsSlidingSyncMigration: needsSlidingSyncMigration,
+                                         networkMonitor: networkMonitor,
+                                         appSettings: appSettings)
+        } catch {
+            throw UserSessionStoreError.failedSettingUpClientProxy(error)
+        }
     }
 }
diff --git a/ElementX/Sources/Services/UserSession/UserSessionStoreProtocol.swift b/ElementX/Sources/Services/UserSession/UserSessionStoreProtocol.swift
index c099d597e1..d834226d10 100644
--- a/ElementX/Sources/Services/UserSession/UserSessionStoreProtocol.swift
+++ b/ElementX/Sources/Services/UserSession/UserSessionStoreProtocol.swift
@@ -12,6 +12,7 @@ enum UserSessionStoreError: Error {
     case missingCredentials
     case failedRestoringLogin
     case failedSettingUpSession
+    case failedSettingUpClientProxy(Error)
 }
 
 // sourcery: AutoMockable


Reviews:
user: login: pixlwave
body: Nice, LGTM 👍
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-06T11:49:28Z

Review comments:
diff_hunk: @@ -1205,3 +1123,57 @@ private class SendQueueRoomErrorListenerProxy: SendQueueRoomErrorListener {
         onErrorClosure(roomId, error)
     }
 }
+
+private struct ConfiguredAppService {
path: ElementX/Sources/Services/Client/ClientProxy.swift
user: login: pixlwave
body: Maybe this would be better called `AppServices` or `ClientProxyServices`. The fact that they're configured in the init probably doesn't need to be called out. (Not important, just thinking out loud as I see this).
created_at: 2025-03-06T11:48:29Z
author_association: MEMBER
diff_hunk: @@ -138,17 +138,26 @@ class UserSessionStore: UserSessionStoreProtocol {
             
             MXLog.info("Set up session for user \(credentials.userID) at: \(credentials.restorationToken.sessionDirectories)")
             
-            return await .success(setupProxyForClient(client, needsSlidingSyncMigration: credentials.restorationToken.needsSlidingSyncMigration))
+            return try await .success(setupProxyForClient(client, needsSlidingSyncMigration: credentials.restorationToken.needsSlidingSyncMigration))
+        } catch UserSessionStoreError.failedSettingUpClientProxy(let error) {
+            // If this has failed, there is likely something wrong with the creationg of the sync service
+            // There is nothing we can do, but at the same time we don't want the user the get logged out
+            // So it's better to crash here and let the app restart
path: ElementX/Sources/Services/UserSession/UserSessionStore.swift
user: login: pixlwave
body: Thank you!

Also
```suggestion
            // If this has failed, there is likely something wrong with the creation of the sync service
            // There is nothing we can do, but at the same time we don't want the user to the get logged out
            // So it's better to crash here and let the app restart
```
created_at: 2025-03-06T11:49:12Z
author_association: MEMBER

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-06T11:21:58Z
body: 
<!--
  0 failure: 
  3 warning:  Trailing closure ..., Trailing closure ..., TODOs should be r...
  
  
  DangerID: danger-id-Danger;
-->


<table>
  <thead>
    <tr>
      <th width="50"></th>
      <th width="100%" data-danger-table="true">Warnings</th>
    </tr>
  </thead>
  <tbody><tr>
      <td>:warning:</td>
      <td>

  **ElementX/Sources/Services/Client/ClientProxy.swift#L528** - Trailing closure syntax should be used whenever possible (`trailing_closure`)
  </td>
    </tr>
  
<tr>
      <td>:warning:</td>
      <td>

  **ElementX/Sources/Services/Client/ClientProxy.swift#L532** - Trailing closure syntax should be used whenever possible (`trailing_closure`)
  </td>
    </tr>
  
<tr>
      <td>:warning:</td>
      <td>

  **ElementX/Sources/Services/Client/ClientProxyProtocol.swift#L79** - TODOs should be resolved (This is a temporary value, in ...) (`todo`)
  </td>
    </tr>
  </tbody>
</table>



<p align="right">
  Generated by :no_entry_sign: <a href="https://danger.systems/swift">Danger Swift</a> against ebd5457ccfe94354b4ff1fc4b4e7fb86533710c7
</p>

author_association: NONE
user: login: codecov[bot]
created_at: 2025-03-06T11:57:19Z
body: ## [Codecov](https://app.codecov.io/gh/element-hq/element-x-ios/pull/3875?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=element-hq) Report
Attention: Patch coverage is `33.33333%` with `22 lines` in your changes missing coverage. Please review.
> Project coverage is 79.15%. Comparing base [(`4d01d2a`)](https://app.codecov.io/gh/element-hq/element-x-ios/commit/4d01d2aa0b9207677af3d7fd65cccc471d7948a3?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=element-hq) to head [(`ebd5457`)](https://app.codecov.io/gh/element-hq/element-x-ios/commit/ebd5457ccfe94354b4ff1fc4b4e7fb86533710c7?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=element-hq).
> Report is 1 commits behind head on develop.

:white_check_mark: All tests successful. No failed tests found.

| [Files with missing lines](https://app.codecov.io/gh/element-hq/element-x-ios/pull/3875?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=element-hq) | Patch % | Lines |
|---|---|---|
| [...ources/Services/UserSession/UserSessionStore.swift](https://app.codecov.io/gh/element-hq/element-x-ios/pull/3875?src=pr&el=tree&filepath=ElementX%2FSources%2FServices%2FUserSession%2FUserSessionStore.swift&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=element-hq#diff-RWxlbWVudFgvU291cmNlcy9TZXJ2aWNlcy9Vc2VyU2Vzc2lvbi9Vc2VyU2Vzc2lvblN0b3JlLnN3aWZ0) | 0.00% | [16 Missing :warning: ](https://app.codecov.io/gh/element-hq/element-x-ios/pull/3875?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=element-hq) |
| [...reens/JoinRoomScreen/JoinRoomScreenViewModel.swift](https://app.codecov.io/gh/element-hq/element-x-ios/pull/3875?src=pr&el=tree&filepath=ElementX%2FSources%2FScreens%2FJoinRoomScreen%2FJoinRoomScreenViewModel.swift&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=element-hq#diff-RWxlbWVudFgvU291cmNlcy9TY3JlZW5zL0pvaW5Sb29tU2NyZWVuL0pvaW5Sb29tU2NyZWVuVmlld01vZGVsLnN3aWZ0) | 75.00% | [3 Missing :warning: ](https://app.codecov.io/gh/element-hq/element-x-ios/pull/3875?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=element-hq) |
| [...dinators/PinnedEventsTimelineFlowCoordinator.swift](https://app.codecov.io/gh/element-hq/element-x-ios/pull/3875?src=pr&el=tree&filepath=ElementX%2FSources%2FFlowCoordinators%2FPinnedEventsTimelineFlowCoordinator.swift&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=element-hq#diff-RWxlbWVudFgvU291cmNlcy9GbG93Q29vcmRpbmF0b3JzL1Bpbm5lZEV2ZW50c1RpbWVsaW5lRmxvd0Nvb3JkaW5hdG9yLnN3aWZ0) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/element-hq/element-x-ios/pull/3875?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=element-hq) |
| [...Sources/FlowCoordinators/RoomFlowCoordinator.swift](https://app.codecov.io/gh/element-hq/element-x-ios/pull/3875?src=pr&el=tree&filepath=ElementX%2FSources%2FFlowCoordinators%2FRoomFlowCoordinator.swift&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=element-hq#diff-RWxlbWVudFgvU291cmNlcy9GbG93Q29vcmRpbmF0b3JzL1Jvb21GbG93Q29vcmRpbmF0b3Iuc3dpZnQ=) | 50.00% | [1 Missing :warning: ](https://app.codecov.io/gh/element-hq/element-x-ios/pull/3875?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=element-hq) |
| [.../FlowCoordinators/UserSessionFlowCoordinator.swift](https://app.codecov.io/gh/element-hq/element-x-ios/pull/3875?src=pr&el=tree&filepath=ElementX%2FSources%2FFlowCoordinators%2FUserSessionFlowCoordinator.swift&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=element-hq#diff-RWxlbWVudFgvU291cmNlcy9GbG93Q29vcmRpbmF0b3JzL1VzZXJTZXNzaW9uRmxvd0Nvb3JkaW5hdG9yLnN3aWZ0) | 50.00% | [1 Missing :warning: ](https://app.codecov.io/gh/element-hq/element-x-ios/pull/3875?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=element-hq) |

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##           develop    #3875      +/-   ##
===========================================
- Coverage    79.16%   79.15%   -0.02%     
===========================================
  Files          793      793              
  Lines        70760    70761       +1     
===========================================
- Hits         56020    56013       -7     
- Misses       14740    14748       +8     
```

| [Flag](https://app.codecov.io/gh/element-hq/element-x-ios/pull/3875/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=element-hq) | Coverage Δ | |
|---|---|---|
| [unittests](https://app.codecov.io/gh/element-hq/element-x-ios/pull/3875/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=element-hq) | `71.07% <33.33%> (-0.01%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=element-hq#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/element-hq/element-x-ios/pull/3875?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=element-hq).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=element-hq).

author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-03-06T13:19:04Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=element-x-ios&pullRequest=3875) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=element-x-ios&pullRequest=3875&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=element-x-ios&pullRequest=3875&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=element-x-ios&pullRequest=3875&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=element-x-ios&pullRequest=3875&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1.9% Duplication on New Code](https://sonarcloud.io/component_measures?id=element-x-ios&pullRequest=3875&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=element-x-ios&pullRequest=3875)


author_association: NONE