mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 08:15:24 +00:00
implemented the LiveLocationManager as part of the user session and setup of permissions in the plist.
This commit is contained in:
@@ -36,6 +36,7 @@
|
||||
02A92F8F4538CECDFB4F2607 /* RoomDirectorySearchScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1562EAF6231151A675BED7A9 /* RoomDirectorySearchScreenCoordinator.swift */; };
|
||||
02D8DF8EB7537EB4E9019DDB /* EventBasedTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 218AB05B4E3889731959C5F1 /* EventBasedTimelineItemProtocol.swift */; };
|
||||
0307469D99B5FE6C7043AE39 /* KnockRequestsListScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9E8A8EC299E12490588B07C /* KnockRequestsListScreenCoordinator.swift */; };
|
||||
03364DE2568860586A713472 /* LiveLocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D17F49E39CC38DAB7B305701 /* LiveLocationManager.swift */; };
|
||||
03631FC946FF0BBAD37E22BF /* preview_avatar_user.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 87FC42213E86E8182CFD3A49 /* preview_avatar_user.jpg */; };
|
||||
037006FB6DF1374F94E4058D /* Dictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDCAC6CAAD65A2C24EA9C4B /* Dictionary.swift */; };
|
||||
038AB2E86960FD240231D4C2 /* GeneratedPreviewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95A2E4BD7C0CAD25EF924A4C /* GeneratedPreviewTests.swift */; };
|
||||
@@ -1188,6 +1189,7 @@
|
||||
CC961529F9F1854BEC3272C9 /* LayoutMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC8AA23D4F37CC26564F63C5 /* LayoutMocks.swift */; };
|
||||
CCBEC2100CAF2EEBE9DB4156 /* TemplateScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA40B98B098B6F0371B750B3 /* TemplateScreenModels.swift */; };
|
||||
CD0088B763CD970CF1CBF8CB /* DateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B5E97E9615A158C76B2AB77 /* DateTests.swift */; };
|
||||
CD077E14FAADC444C5A80068 /* LiveLocationManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33752AE856E93CE62412B7A1 /* LiveLocationManagerProtocol.swift */; };
|
||||
CD6A72B65D3B6076F4045C30 /* PHGPostHogConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6B891A6DA826E2461DBB40F /* PHGPostHogConfiguration.swift */; };
|
||||
CDAE3A37D4DF136F9D07DB61 /* RoomChangeRolesScreenSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF710CB1C31F8938EAA3A7D /* RoomChangeRolesScreenSection.swift */; };
|
||||
CDCA8A559E098503DDE29477 /* AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5C6FBF97B6EED3D4FA5EFF /* AttributedStringBuilder.swift */; };
|
||||
@@ -1865,6 +1867,7 @@
|
||||
33035418BB35754232985871 /* TimelineReadReceiptsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReadReceiptsView.swift; sourceTree = "<group>"; };
|
||||
330AF4D121C3396F7A14B21D /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/SAS.strings; sourceTree = "<group>"; };
|
||||
3339B1DDB1341E833D2555BC /* AVMetadataMachineReadableCodeObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVMetadataMachineReadableCodeObject.swift; sourceTree = "<group>"; };
|
||||
33752AE856E93CE62412B7A1 /* LiveLocationManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveLocationManagerProtocol.swift; sourceTree = "<group>"; };
|
||||
33AE897D86784CCA5E4E9227 /* ElementCallService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallService.swift; sourceTree = "<group>"; };
|
||||
33E49C5C6F802B4D94CA78D1 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
342BEBC3C5FC3F9943C41C4C /* TemplateScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||
@@ -2719,6 +2722,7 @@
|
||||
D071F86CD47582B9196C9D16 /* UserDiscoverySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoverySection.swift; sourceTree = "<group>"; };
|
||||
D09227E671DB30795C43FFFD /* KnockRequestsListScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KnockRequestsListScreenViewModel.swift; sourceTree = "<group>"; };
|
||||
D0C2D52E36AD614B3C003EF6 /* RoomTimelineItemViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemViewState.swift; sourceTree = "<group>"; };
|
||||
D17F49E39CC38DAB7B305701 /* LiveLocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveLocationManager.swift; sourceTree = "<group>"; };
|
||||
D1896F6288D80E1F3EFB3DF8 /* ka */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ka; path = ka.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
D196116D2DD3F2757D45FCB7 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/SAS.strings; sourceTree = "<group>"; };
|
||||
D1BC84BA0AF11C2128D58ABD /* Common.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Common.swift; sourceTree = "<group>"; };
|
||||
@@ -3192,6 +3196,7 @@
|
||||
39557ADF21345E18F3865B9E /* Emojis */,
|
||||
CA555F7C7CA382ACACF0D82B /* Keychain */,
|
||||
DCF22D1E268F1A36EC3431E4 /* LinkMetadata */,
|
||||
90FD376E9373246E4925CE95 /* Location */,
|
||||
79E560F5113ED25D172E550C /* Media */,
|
||||
6709362D60732DED2069AE0F /* MediaPlayer */,
|
||||
6DE13A7AE6587B079F4049D7 /* Notification */,
|
||||
@@ -5388,6 +5393,15 @@
|
||||
path = LoginScreen;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
90FD376E9373246E4925CE95 /* Location */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D17F49E39CC38DAB7B305701 /* LiveLocationManager.swift */,
|
||||
33752AE856E93CE62412B7A1 /* LiveLocationManagerProtocol.swift */,
|
||||
);
|
||||
path = Location;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
92E99C57D7F92ED16F73282C /* ElementCall */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -8293,6 +8307,8 @@
|
||||
A37BFB32EAB8AEF6DD5BA0DC /* LinkNewDeviceService.swift in Sources */,
|
||||
74DF5BC17DE9F51E077FD457 /* LinkNewDeviceServiceMock.swift in Sources */,
|
||||
866FA35E7A2339EF8B6D91CA /* LinkPreviewView.swift in Sources */,
|
||||
03364DE2568860586A713472 /* LiveLocationManager.swift in Sources */,
|
||||
CD077E14FAADC444C5A80068 /* LiveLocationManagerProtocol.swift in Sources */,
|
||||
C8D0AC22E03F652118A2BB73 /* LiveLocationRoomTimelineItem.swift in Sources */,
|
||||
F7977C53B2B1D73030C69761 /* LiveLocationRoomTimelineView.swift in Sources */,
|
||||
6E47D126DD7585E8F8237CE7 /* LoadableAvatarImage.swift in Sources */,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"NSCameraUsageDescription" = "Каб зрабіць фота ці відэа і адправіць іх у выглядзе паведамлення, Element X патрабуе доступу да камеры.";
|
||||
"NSFaceIDUsageDescription" = "Face ID выкарыстоўваецца для доступу да праграмы.";
|
||||
"NSLocationAlwaysAndWhenInUseUsageDescription" = "To share your live location, Element X needs location access when the app is in the background.";
|
||||
"NSLocationAlwaysAndWhenInUseUsageDescription" = "To share your live location, Element X needs location access when the app is in the background.";
|
||||
"NSLocationWhenInUseUsageDescription" = "Дайце доступ да месцазнаходжання, каб Element X мог падзяліцца вашым месцазнаходжаннем.";
|
||||
"NSMicrophoneUsageDescription" = "Каб запісваць і адпраўляць паведамленні з гукам, Element X патрабуе доступу да мікрафона.";
|
||||
"NSPhotoLibraryUsageDescription" = "Дазваляе захоўваць фатаграфіі і відэа ў вашу бібліятэку.";
|
||||
|
||||
@@ -48,6 +48,7 @@ final class AppSettings {
|
||||
case analyticsConsentState
|
||||
case hasRunNotificationPermissionsOnboarding
|
||||
case hasRunIdentityConfirmationOnboarding
|
||||
case hasRequestedAlwaysLocationAuthorization
|
||||
|
||||
case frequentlyUsedSystemEmojis
|
||||
|
||||
@@ -343,6 +344,9 @@ final class AppSettings {
|
||||
@UserPreference(key: UserDefaultsKeys.hasRunIdentityConfirmationOnboarding, defaultValue: false, storageType: .userDefaults(store))
|
||||
var hasRunIdentityConfirmationOnboarding
|
||||
|
||||
@UserPreference(key: UserDefaultsKeys.hasRequestedAlwaysLocationAuthorization, defaultValue: false, storageType: .userDefaults(store))
|
||||
var hasRequestedAlwaysLocationAuthorization
|
||||
|
||||
@UserPreference(key: UserDefaultsKeys.frequentlyUsedSystemEmojis, defaultValue: [FrequentlyUsedEmoji](), storageType: .userDefaults(store))
|
||||
var frequentlyUsedSystemEmojis
|
||||
|
||||
|
||||
@@ -11100,6 +11100,79 @@ class LinkNewDeviceServiceMock: LinkNewDeviceServiceProtocol, @unchecked Sendabl
|
||||
}
|
||||
}
|
||||
}
|
||||
class LiveLocationManagerMock: LiveLocationManagerProtocol, @unchecked Sendable {
|
||||
var authorizationStatus: CurrentValuePublisher<CLAuthorizationStatus, Never> {
|
||||
get { return underlyingAuthorizationStatus }
|
||||
set(value) { underlyingAuthorizationStatus = value }
|
||||
}
|
||||
var underlyingAuthorizationStatus: CurrentValuePublisher<CLAuthorizationStatus, Never>!
|
||||
|
||||
//MARK: - requestAlwaysAuthorizationIfPossible
|
||||
|
||||
var requestAlwaysAuthorizationIfPossibleUnderlyingCallsCount = 0
|
||||
var requestAlwaysAuthorizationIfPossibleCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return requestAlwaysAuthorizationIfPossibleUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = requestAlwaysAuthorizationIfPossibleUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
requestAlwaysAuthorizationIfPossibleUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
requestAlwaysAuthorizationIfPossibleUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var requestAlwaysAuthorizationIfPossibleCalled: Bool {
|
||||
return requestAlwaysAuthorizationIfPossibleCallsCount > 0
|
||||
}
|
||||
|
||||
var requestAlwaysAuthorizationIfPossibleUnderlyingReturnValue: Bool!
|
||||
var requestAlwaysAuthorizationIfPossibleReturnValue: Bool! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return requestAlwaysAuthorizationIfPossibleUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: Bool? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = requestAlwaysAuthorizationIfPossibleUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
requestAlwaysAuthorizationIfPossibleUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
requestAlwaysAuthorizationIfPossibleUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var requestAlwaysAuthorizationIfPossibleClosure: (() -> Bool)?
|
||||
|
||||
@discardableResult
|
||||
func requestAlwaysAuthorizationIfPossible() -> Bool {
|
||||
requestAlwaysAuthorizationIfPossibleCallsCount += 1
|
||||
if let requestAlwaysAuthorizationIfPossibleClosure = requestAlwaysAuthorizationIfPossibleClosure {
|
||||
return requestAlwaysAuthorizationIfPossibleClosure()
|
||||
} else {
|
||||
return requestAlwaysAuthorizationIfPossibleReturnValue
|
||||
}
|
||||
}
|
||||
}
|
||||
class MediaLoaderMock: MediaLoaderProtocol, @unchecked Sendable {
|
||||
|
||||
//MARK: - loadMediaContentForSource
|
||||
@@ -20236,6 +20309,11 @@ class UserSessionMock: UserSessionProtocol, @unchecked Sendable {
|
||||
set(value) { underlyingVoiceMessageMediaManager = value }
|
||||
}
|
||||
var underlyingVoiceMessageMediaManager: VoiceMessageMediaManagerProtocol!
|
||||
var liveLocationManager: LiveLocationManagerProtocol {
|
||||
get { return underlyingLiveLocationManager }
|
||||
set(value) { underlyingLiveLocationManager = value }
|
||||
}
|
||||
var underlyingLiveLocationManager: LiveLocationManagerProtocol!
|
||||
var sessionSecurityStatePublisher: CurrentValuePublisher<SessionSecurityState, Never> {
|
||||
get { return underlyingSessionSecurityStatePublisher }
|
||||
set(value) { underlyingSessionSecurityStatePublisher = value }
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
//
|
||||
// Copyright 2026 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 CoreLocation
|
||||
|
||||
class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationManagerDelegate {
|
||||
private let clientProxy: ClientProxyProtocol
|
||||
private let locationManager: CLLocationManager
|
||||
private let appSettings: AppSettings
|
||||
|
||||
private let authorizationStatusSubject: CurrentValueSubject<CLAuthorizationStatus, Never>
|
||||
|
||||
var authorizationStatus: CurrentValuePublisher<CLAuthorizationStatus, Never> {
|
||||
authorizationStatusSubject.asCurrentValuePublisher()
|
||||
}
|
||||
|
||||
@MainActor
|
||||
init(clientProxy: ClientProxyProtocol,
|
||||
appSettings: AppSettings) {
|
||||
self.clientProxy = clientProxy
|
||||
self.appSettings = appSettings
|
||||
// Very important, the CLLocationManager needs to be initialised on the main thread
|
||||
// or the delegate functions won't be handled!
|
||||
// https://developer.apple.com/documentation/corelocation/cllocationmanagerdelegate
|
||||
locationManager = CLLocationManager()
|
||||
authorizationStatusSubject = CurrentValueSubject(locationManager.authorizationStatus)
|
||||
|
||||
super.init()
|
||||
|
||||
locationManager.delegate = self
|
||||
}
|
||||
|
||||
// MARK: - LiveLocationManagerProtocol
|
||||
|
||||
@discardableResult
|
||||
func requestAlwaysAuthorizationIfPossible() -> Bool {
|
||||
guard !appSettings.hasRequestedAlwaysLocationAuthorization else { return false }
|
||||
appSettings.hasRequestedAlwaysLocationAuthorization = true
|
||||
locationManager.requestAlwaysAuthorization()
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: - CLLocationManagerDelegate
|
||||
|
||||
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
||||
// If the system resets authorization to notDetermined (e.g. after app reinstall or
|
||||
// settings reset), clear the flag so we can request again.
|
||||
if manager.authorizationStatus == .notDetermined {
|
||||
appSettings.hasRequestedAlwaysLocationAuthorization = false
|
||||
}
|
||||
authorizationStatusSubject.send(manager.authorizationStatus)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
// TODO: Add CLLocationManager location update handling to forward updates to rooms
|
||||
// TODO: Track which rooms are currently sharing live location
|
||||
// TODO: Send location updates to all active rooms via clientProxy
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// Copyright 2026 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 CoreLocation
|
||||
import Foundation
|
||||
|
||||
// sourcery: AutoMockable
|
||||
protocol LiveLocationManagerProtocol: AnyObject {
|
||||
/// Publishes the current "Always" location authorization status.
|
||||
var authorizationStatus: CurrentValuePublisher<CLAuthorizationStatus, Never> { get }
|
||||
|
||||
/// Requests "Always" location authorization from the user if the system allows it.
|
||||
///
|
||||
/// - Returns: `true` if the request was forwarded to the system and a prompt will be shown;
|
||||
/// `false` if the request was already made before and iOS would silently ignore it.
|
||||
@discardableResult
|
||||
func requestAlwaysAuthorizationIfPossible() -> Bool
|
||||
}
|
||||
@@ -17,6 +17,7 @@ class UserSession: UserSessionProtocol {
|
||||
let clientProxy: ClientProxyProtocol
|
||||
let mediaProvider: MediaProviderProtocol
|
||||
let voiceMessageMediaManager: VoiceMessageMediaManagerProtocol
|
||||
let liveLocationManager: LiveLocationManagerProtocol
|
||||
|
||||
let callbacks = PassthroughSubject<UserSessionCallback, Never>()
|
||||
|
||||
@@ -25,10 +26,11 @@ class UserSession: UserSessionProtocol {
|
||||
sessionSecurityStateSubject.asCurrentValuePublisher()
|
||||
}
|
||||
|
||||
init(clientProxy: ClientProxyProtocol, mediaProvider: MediaProviderProtocol, voiceMessageMediaManager: VoiceMessageMediaManagerProtocol) {
|
||||
init(clientProxy: ClientProxyProtocol, mediaProvider: MediaProviderProtocol, voiceMessageMediaManager: VoiceMessageMediaManagerProtocol, liveLocationManager: LiveLocationManagerProtocol) {
|
||||
self.clientProxy = clientProxy
|
||||
self.mediaProvider = mediaProvider
|
||||
self.voiceMessageMediaManager = voiceMessageMediaManager
|
||||
self.liveLocationManager = liveLocationManager
|
||||
|
||||
authErrorCancellable = clientProxy.actionsPublisher
|
||||
.receive(on: DispatchQueue.main)
|
||||
|
||||
@@ -23,6 +23,7 @@ protocol UserSessionProtocol {
|
||||
var clientProxy: ClientProxyProtocol { get }
|
||||
var mediaProvider: MediaProviderProtocol { get }
|
||||
var voiceMessageMediaManager: VoiceMessageMediaManagerProtocol { get }
|
||||
var liveLocationManager: LiveLocationManagerProtocol { get }
|
||||
|
||||
var sessionSecurityStatePublisher: CurrentValuePublisher<SessionSecurityState, Never> { get }
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ class UserSessionStore: UserSessionStoreProtocol {
|
||||
|
||||
switch await restorePreviousLogin(credentials) {
|
||||
case .success(let clientProxy):
|
||||
return .success(buildUserSessionWithClient(clientProxy))
|
||||
return await .success(buildUserSessionWithClient(clientProxy))
|
||||
case .failure(let error):
|
||||
MXLog.error("Failed restoring login with error: \(error)")
|
||||
|
||||
@@ -84,7 +84,7 @@ class UserSessionStore: UserSessionStoreProtocol {
|
||||
|
||||
MXLog.info("Set up session for user \(userID) at: \(sessionDirectories)")
|
||||
|
||||
return .success(buildUserSessionWithClient(clientProxy))
|
||||
return await .success(buildUserSessionWithClient(clientProxy))
|
||||
} catch {
|
||||
MXLog.error("Failed creating user session with error: \(error)")
|
||||
return .failure(.failedSettingUpSession)
|
||||
@@ -103,16 +103,22 @@ class UserSessionStore: UserSessionStoreProtocol {
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func buildUserSessionWithClient(_ clientProxy: ClientProxyProtocol) -> UserSessionProtocol {
|
||||
private func buildUserSessionWithClient(_ clientProxy: ClientProxyProtocol) async -> UserSessionProtocol {
|
||||
let mediaProvider = MediaProvider(mediaLoader: clientProxy.mediaLoader,
|
||||
imageCache: .onlyInMemory,
|
||||
homeserverReachabilityPublisher: clientProxy.homeserverReachabilityPublisher)
|
||||
|
||||
let voiceMessageMediaManager = VoiceMessageMediaManager(mediaProvider: mediaProvider)
|
||||
|
||||
let liveLocationManager = await MainActor.run {
|
||||
LiveLocationManager(clientProxy: clientProxy,
|
||||
appSettings: appSettings)
|
||||
}
|
||||
|
||||
return UserSession(clientProxy: clientProxy,
|
||||
mediaProvider: mediaProvider,
|
||||
voiceMessageMediaManager: voiceMessageMediaManager)
|
||||
voiceMessageMediaManager: voiceMessageMediaManager,
|
||||
liveLocationManager: liveLocationManager)
|
||||
}
|
||||
|
||||
private func restorePreviousLogin(_ credentials: KeychainCredentials) async -> Result<ClientProxyProtocol, UserSessionStoreError> {
|
||||
|
||||
Reference in New Issue
Block a user