@@ -13,9 +13,11 @@ import kotlinx.coroutines.SupervisorJob
1313import kotlinx.coroutines.async
1414import kotlinx.coroutines.awaitAll
1515import kotlinx.coroutines.coroutineScope
16+ import kotlinx.coroutines.flow.MutableSharedFlow
1617import kotlinx.coroutines.flow.MutableStateFlow
1718import kotlinx.coroutines.flow.SharingStarted
1819import kotlinx.coroutines.flow.StateFlow
20+ import kotlinx.coroutines.flow.asSharedFlow
1921import kotlinx.coroutines.flow.asStateFlow
2022import kotlinx.coroutines.flow.combine
2123import kotlinx.coroutines.flow.map
@@ -35,24 +37,34 @@ import to.bitkit.models.PubkyProfile
3537import to.bitkit.models.PubkyProfileData
3638import to.bitkit.models.PubkyProfileLink
3739import to.bitkit.models.PubkyPublicKeyFormat
40+ import to.bitkit.models.PubkyRingAuthCallback
41+ import to.bitkit.models.PubkyRingAuthCallbackHandlingResult
3842import to.bitkit.models.PubkySessionBackupKind
3943import to.bitkit.models.PubkySessionBackupV1
4044import to.bitkit.services.PubkyService
4145import to.bitkit.utils.AppError
4246import to.bitkit.utils.Logger
4347import java.io.ByteArrayOutputStream
48+ import java.util.UUID
4449import javax.inject.Inject
4550import javax.inject.Singleton
4651import kotlin.math.min
4752
4853enum class PubkyAuthState { Idle , Authenticating , Authenticated }
4954
55+ data class PubkyRingAuthRequest (
56+ val authUrl : String ,
57+ val callbackNonce : String ,
58+ )
59+
5060sealed class PubkyContactError (message : String ) : AppError(message) {
5161 data object AlreadyExists : PubkyContactError (" Contact already exists" )
5262 data object CannotAddSelf : PubkyContactError (" Cannot add your own pubky as a contact" )
5363 data object InvalidFormat : PubkyContactError (" Invalid pubky key format" )
5464}
5565
66+ private class PubkyAuthAttemptInactive : AppError (" Auth attempt is no longer active" )
67+
5668@Suppress(" TooManyFunctions" , " LargeClass" , " LongParameterList" )
5769@Singleton
5870class PubkyRepo @Inject constructor(
@@ -80,6 +92,9 @@ class PubkyRepo @Inject constructor(
8092 private var isServiceInitialized = false
8193
8294 private val _authState = MutableStateFlow (PubkyAuthState .Idle )
95+ private val _activeAuthAttemptId = MutableStateFlow <String ?>(null )
96+ private val _authCancelEvents = MutableSharedFlow <Unit >(extraBufferCapacity = 1 )
97+ val authCancelEvents = _authCancelEvents .asSharedFlow()
8398
8499 private val _profile = MutableStateFlow <PubkyProfile ?>(null )
85100 val profile: StateFlow <PubkyProfile ?> = _profile .asStateFlow()
@@ -108,7 +123,7 @@ class PubkyRepo @Inject constructor(
108123 private val _backupStateVersion = MutableStateFlow (0L )
109124 val backupStateVersion: StateFlow <Long > = _backupStateVersion .asStateFlow()
110125
111- val isAuthenticated: StateFlow <Boolean > = _authState .map { it == PubkyAuthState . Authenticated }
126+ val isAuthenticated: StateFlow <Boolean > = _publicKey .map { it != null }
112127 .stateIn(scope, SharingStarted .Eagerly , false )
113128
114129 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 ? : return Result .failure(PubkyAuthAttemptInactive ())
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 )
@@ -272,13 +300,82 @@ class PubkyRepo @Inject constructor(
272300 runCatching {
273301 withContext(ioDispatcher) { pubkyService.cancelAuth() }
274302 }.onFailure { Logger .warn(" Failed to cancel auth" , it, context = TAG ) }
275- _authState .update { PubkyAuthState . Idle }
303+ endAuthAttempt()
276304 }
277305
278306 fun cancelAuthenticationSync () {
279307 scope.launch { cancelAuthentication() }
280308 }
281309
310+ suspend fun handleAuthCallback (callback : PubkyRingAuthCallback ): PubkyRingAuthCallbackHandlingResult {
311+ if (! isCurrentAuthCallback(callback)) {
312+ return handleInvalidAuthCallback(callback)
313+ }
314+
315+ return when (callback) {
316+ is PubkyRingAuthCallback .Success -> {
317+ Logger .info(" Received Pubky Ring auth success callback" , context = TAG )
318+ PubkyRingAuthCallbackHandlingResult .Handled
319+ }
320+ is PubkyRingAuthCallback .Cancel -> {
321+ Logger .info(" Received Pubky Ring auth cancel callback" , context = TAG )
322+ cancelAuthentication()
323+ PubkyRingAuthCallbackHandlingResult .Handled
324+ }
325+ is PubkyRingAuthCallback .Error -> {
326+ Logger .warn(" Received Pubky Ring auth error callback" , context = TAG )
327+ cancelAuthentication()
328+ PubkyRingAuthCallbackHandlingResult .TrustedError (callback.message)
329+ }
330+ }
331+ }
332+
333+ private fun handleInvalidAuthCallback (
334+ callback : PubkyRingAuthCallback ,
335+ ): PubkyRingAuthCallbackHandlingResult {
336+ if (_activeAuthAttemptId .value == null ) {
337+ Logger .warn(" Ignoring Pubky Ring auth callback with missing or invalid nonce" , context = TAG )
338+ return PubkyRingAuthCallbackHandlingResult .Ignored
339+ }
340+
341+ return when (callback) {
342+ is PubkyRingAuthCallback .Success -> {
343+ Logger .warn(" Ignoring Pubky Ring auth success callback with missing or invalid nonce" , context = TAG )
344+ PubkyRingAuthCallbackHandlingResult .Ignored
345+ }
346+ is PubkyRingAuthCallback .Cancel -> {
347+ Logger .warn(" Ignoring Pubky Ring auth cancel callback with missing or invalid nonce" , context = TAG )
348+ PubkyRingAuthCallbackHandlingResult .Ignored
349+ }
350+ is PubkyRingAuthCallback .Error -> {
351+ Logger .warn(" Ignoring Pubky Ring auth error callback with missing or invalid nonce" , context = TAG )
352+ PubkyRingAuthCallbackHandlingResult .Ignored
353+ }
354+ }
355+ }
356+
357+ private fun isCurrentAuthCallback (callback : PubkyRingAuthCallback ): Boolean {
358+ val activeAuthAttemptId = _activeAuthAttemptId .value ? : return false
359+ return callback.nonce == activeAuthAttemptId
360+ }
361+
362+ private fun ensureAuthAttemptActive (attemptId : String? ) {
363+ if (attemptId == null ) return
364+ if (_activeAuthAttemptId .value == attemptId) return
365+
366+ throw PubkyAuthAttemptInactive ()
367+ }
368+
369+ private fun endAuthAttempt () {
370+ _activeAuthAttemptId .update { null }
371+ _authCancelEvents .tryEmit(Unit )
372+ restoreAuthStateAfterAuthFlow()
373+ }
374+
375+ private fun restoreAuthStateAfterAuthFlow () {
376+ _authState .update { if (_publicKey .value == null ) PubkyAuthState .Idle else PubkyAuthState .Authenticated }
377+ }
378+
282379 // endregion
283380
284381 // region Payment endpoints
0 commit comments