@@ -12,9 +12,11 @@ import kotlinx.coroutines.SupervisorJob
1212import kotlinx.coroutines.async
1313import kotlinx.coroutines.awaitAll
1414import kotlinx.coroutines.coroutineScope
15+ import kotlinx.coroutines.flow.MutableSharedFlow
1516import kotlinx.coroutines.flow.MutableStateFlow
1617import kotlinx.coroutines.flow.SharingStarted
1718import kotlinx.coroutines.flow.StateFlow
19+ import kotlinx.coroutines.flow.asSharedFlow
1820import kotlinx.coroutines.flow.asStateFlow
1921import kotlinx.coroutines.flow.combine
2022import kotlinx.coroutines.flow.map
@@ -33,23 +35,33 @@ import to.bitkit.models.PubkyProfile
3335import to.bitkit.models.PubkyProfileData
3436import to.bitkit.models.PubkyProfileLink
3537import to.bitkit.models.PubkyPublicKeyFormat
38+ import to.bitkit.models.PubkyRingAuthCallback
39+ import to.bitkit.models.PubkyRingAuthCallbackHandlingResult
3640import to.bitkit.models.PubkySessionBackupKind
3741import to.bitkit.models.PubkySessionBackupV1
3842import to.bitkit.services.PubkyService
3943import to.bitkit.utils.AppError
4044import to.bitkit.utils.Logger
4145import java.io.ByteArrayOutputStream
46+ import java.util.UUID
4247import javax.inject.Inject
4348import javax.inject.Singleton
4449import kotlin.math.min
4550
4651enum class PubkyAuthState { Idle , Authenticating , Authenticated }
4752
53+ data class PubkyRingAuthRequest (
54+ val authUrl : String ,
55+ val callbackNonce : String ,
56+ )
57+
4858sealed class PubkyContactError (message : String ) : AppError(message) {
4959 data object CannotAddSelf : PubkyContactError (" Cannot add your own pubky as a contact" )
5060 data object InvalidFormat : PubkyContactError (" Invalid pubky key format" )
5161}
5262
63+ private class PubkyAuthAttemptInactive : AppError (" Auth attempt is no longer active" )
64+
5365@Suppress(" TooManyFunctions" , " LargeClass" )
5466@Singleton
5567class PubkyRepo @Inject constructor(
@@ -76,6 +88,9 @@ class PubkyRepo @Inject constructor(
7688 private var isServiceInitialized = false
7789
7890 private val _authState = MutableStateFlow (PubkyAuthState .Idle )
91+ private val _activeAuthAttemptId = MutableStateFlow <String ?>(null )
92+ private val _authCancelEvents = MutableSharedFlow <Unit >(extraBufferCapacity = 1 )
93+ val authCancelEvents = _authCancelEvents .asSharedFlow()
7994
8095 private val _profile = MutableStateFlow <PubkyProfile ?>(null )
8196 val profile: StateFlow <PubkyProfile ?> = _profile .asStateFlow()
@@ -104,7 +119,7 @@ class PubkyRepo @Inject constructor(
104119 private val _backupStateVersion = MutableStateFlow (0L )
105120 val backupStateVersion: StateFlow <Long > = _backupStateVersion .asStateFlow()
106121
107- val isAuthenticated: StateFlow <Boolean > = _authState .map { it == PubkyAuthState . Authenticated }
122+ val isAuthenticated: StateFlow <Boolean > = _publicKey .map { it != null }
108123 .stateIn(scope, SharingStarted .Eagerly , false )
109124
110125 val displayName: StateFlow <String ?> = combine(_profile , pubkyStore.data) { profile, cached ->
@@ -236,20 +251,27 @@ class PubkyRepo @Inject constructor(
236251
237252 // region Ring auth flow
238253
239- suspend fun startAuthentication (): Result <String > {
254+ suspend fun startAuthentication (): Result <PubkyRingAuthRequest > {
255+ val attemptId = UUID .randomUUID().toString()
256+ _activeAuthAttemptId .update { attemptId }
240257 _authState .update { PubkyAuthState .Authenticating }
241258 return runCatching {
242- withContext(ioDispatcher) { pubkyService.startAuth() }
259+ val authUrl = withContext(ioDispatcher) { pubkyService.startAuth() }
260+ PubkyRingAuthRequest (authUrl = authUrl, callbackNonce = attemptId)
243261 }.onFailure {
244- _authState .update { PubkyAuthState .Idle }
262+ _activeAuthAttemptId .update { null }
263+ restoreAuthStateAfterAuthFlow()
245264 }
246265 }
247266
248267 suspend fun completeAuthentication (): Result <Unit > {
268+ val attemptId = _activeAuthAttemptId .value
249269 return runCatching {
250270 withContext(ioDispatcher) {
251271 val sessionSecret = pubkyService.completeAuth()
272+ ensureAuthAttemptActive(attemptId)
252273 val pk = pubkyService.importSession(sessionSecret).ensurePubkyPrefix()
274+ ensureAuthAttemptActive(attemptId)
253275
254276 runCatching { keychain.delete(Keychain .Key .PUBKY_SECRET_KEY .name) }
255277 keychain.upsertString(Keychain .Key .PAYKIT_SESSION .name, sessionSecret)
@@ -258,8 +280,14 @@ class PubkyRepo @Inject constructor(
258280 pk
259281 }
260282 }.onFailure {
261- _authState .update { PubkyAuthState .Idle }
283+ if (_activeAuthAttemptId .value == attemptId) {
284+ _activeAuthAttemptId .update { null }
285+ }
286+ restoreAuthStateAfterAuthFlow()
262287 }.onSuccess { pk ->
288+ if (_activeAuthAttemptId .value == attemptId) {
289+ _activeAuthAttemptId .update { null }
290+ }
263291 _publicKey .update { pk }
264292 _authState .update { PubkyAuthState .Authenticated }
265293 Logger .info(" Completed pubky auth for '$pk '" , context = TAG )
@@ -271,13 +299,84 @@ class PubkyRepo @Inject constructor(
271299 runCatching {
272300 withContext(ioDispatcher) { pubkyService.cancelAuth() }
273301 }.onFailure { Logger .warn(" Failed to cancel auth" , it, context = TAG ) }
274- _authState .update { PubkyAuthState . Idle }
302+ endAuthAttempt()
275303 }
276304
277305 fun cancelAuthenticationSync () {
278306 scope.launch { cancelAuthentication() }
279307 }
280308
309+ suspend fun handleAuthCallback (callback : PubkyRingAuthCallback ): PubkyRingAuthCallbackHandlingResult {
310+ if (! isCurrentAuthCallback(callback)) {
311+ return handleInvalidAuthCallback(callback)
312+ }
313+
314+ return when (callback) {
315+ is PubkyRingAuthCallback .Success -> {
316+ Logger .info(" Received Pubky Ring auth success callback" , context = TAG )
317+ PubkyRingAuthCallbackHandlingResult .Handled
318+ }
319+ is PubkyRingAuthCallback .Cancel -> {
320+ Logger .info(" Received Pubky Ring auth cancel callback" , context = TAG )
321+ cancelAuthentication()
322+ PubkyRingAuthCallbackHandlingResult .Handled
323+ }
324+ is PubkyRingAuthCallback .Error -> {
325+ Logger .warn(" Received Pubky Ring auth error callback" , context = TAG )
326+ cancelAuthentication()
327+ PubkyRingAuthCallbackHandlingResult .TrustedError (callback.message)
328+ }
329+ }
330+ }
331+
332+ private fun handleInvalidAuthCallback (
333+ callback : PubkyRingAuthCallback ,
334+ ): PubkyRingAuthCallbackHandlingResult {
335+ if (_activeAuthAttemptId .value == null ) {
336+ Logger .warn(" Ignoring Pubky Ring auth callback with missing or invalid nonce" , context = TAG )
337+ return PubkyRingAuthCallbackHandlingResult .Ignored
338+ }
339+
340+ return when (callback) {
341+ is PubkyRingAuthCallback .Success -> {
342+ Logger .warn(" Ignoring Pubky Ring auth success callback with missing or invalid nonce" , context = TAG )
343+ PubkyRingAuthCallbackHandlingResult .Ignored
344+ }
345+ is PubkyRingAuthCallback .Cancel -> {
346+ Logger .warn(" Received Pubky Ring auth cancel callback with missing or invalid nonce" , context = TAG )
347+ endAuthAttempt()
348+ PubkyRingAuthCallbackHandlingResult .Handled
349+ }
350+ is PubkyRingAuthCallback .Error -> {
351+ Logger .warn(" Received Pubky Ring auth error callback with missing or invalid nonce" , context = TAG )
352+ endAuthAttempt()
353+ PubkyRingAuthCallbackHandlingResult .UntrustedError
354+ }
355+ }
356+ }
357+
358+ private fun isCurrentAuthCallback (callback : PubkyRingAuthCallback ): Boolean {
359+ val activeAuthAttemptId = _activeAuthAttemptId .value ? : return false
360+ return callback.nonce == activeAuthAttemptId
361+ }
362+
363+ private fun ensureAuthAttemptActive (attemptId : String? ) {
364+ if (attemptId == null ) return
365+ if (_activeAuthAttemptId .value == attemptId) return
366+
367+ throw PubkyAuthAttemptInactive ()
368+ }
369+
370+ private fun endAuthAttempt () {
371+ _activeAuthAttemptId .update { null }
372+ _authCancelEvents .tryEmit(Unit )
373+ restoreAuthStateAfterAuthFlow()
374+ }
375+
376+ private fun restoreAuthStateAfterAuthFlow () {
377+ _authState .update { if (_publicKey .value == null ) PubkyAuthState .Idle else PubkyAuthState .Authenticated }
378+ }
379+
281380 // endregion
282381
283382 // region Profile loading
0 commit comments