From c0de81513401090dc5974a73556940750c5249cd Mon Sep 17 00:00:00 2001 From: benk10 Date: Tue, 28 Apr 2026 08:20:56 -0700 Subject: [PATCH 1/4] fix: add pubky ring callbacks --- .../commands/NotifyPaymentReceivedHandler.kt | 2 +- .../main/java/to/bitkit/fcm/WakeNodeWorker.kt | 2 +- .../to/bitkit/models/PubkyRingAuthCallback.kt | 70 +++++++++++ .../java/to/bitkit/repositories/PubkyRepo.kt | 111 +++++++++++++++++- .../java/to/bitkit/repositories/WalletRepo.kt | 2 +- .../java/to/bitkit/services/CoreService.kt | 2 +- .../main/java/to/bitkit/ui/NodeInfoScreen.kt | 2 +- .../screens/profile/PubkyChoiceViewModel.kt | 19 ++- .../settings/lightning/ChannelDetailScreen.kt | 2 +- .../lightning/LightningConnectionsScreen.kt | 2 +- .../java/to/bitkit/viewmodels/AppViewModel.kt | 28 +++++ .../models/PubkyRingAuthCallbackTest.kt | 77 ++++++++++++ .../to/bitkit/repositories/PubkyRepoTest.kt | 68 ++++++++++- .../profile/PubkyChoiceViewModelTest.kt | 3 + changelog.d/next/917.added.md | 1 + 15 files changed, 375 insertions(+), 16 deletions(-) create mode 100644 app/src/main/java/to/bitkit/models/PubkyRingAuthCallback.kt create mode 100644 app/src/test/java/to/bitkit/models/PubkyRingAuthCallbackTest.kt create mode 100644 changelog.d/next/917.added.md diff --git a/app/src/main/java/to/bitkit/domain/commands/NotifyPaymentReceivedHandler.kt b/app/src/main/java/to/bitkit/domain/commands/NotifyPaymentReceivedHandler.kt index 1992e08cd..629b84842 100644 --- a/app/src/main/java/to/bitkit/domain/commands/NotifyPaymentReceivedHandler.kt +++ b/app/src/main/java/to/bitkit/domain/commands/NotifyPaymentReceivedHandler.kt @@ -17,8 +17,8 @@ import to.bitkit.models.NewTransactionSheetType import to.bitkit.models.NotificationDetails import to.bitkit.models.PrimaryDisplay import to.bitkit.models.formatToModernDisplay -import to.bitkit.repositories.ActivityRepo import to.bitkit.models.msatCeilOf +import to.bitkit.repositories.ActivityRepo import to.bitkit.repositories.CurrencyRepo import to.bitkit.utils.Logger import javax.inject.Inject diff --git a/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt b/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt index fe9a86efc..6572848a8 100644 --- a/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt +++ b/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt @@ -26,7 +26,6 @@ import to.bitkit.ext.amountOnClose import to.bitkit.ext.toUserMessage import to.bitkit.models.BITCOIN_SYMBOL import to.bitkit.models.BlocktankNotificationType -import to.bitkit.models.msatCeilOf import to.bitkit.models.BlocktankNotificationType.cjitPaymentArrived import to.bitkit.models.BlocktankNotificationType.incomingHtlc import to.bitkit.models.BlocktankNotificationType.mutualClose @@ -36,6 +35,7 @@ import to.bitkit.models.NewTransactionSheetDetails import to.bitkit.models.NewTransactionSheetDirection import to.bitkit.models.NewTransactionSheetType import to.bitkit.models.NotificationDetails +import to.bitkit.models.msatCeilOf import to.bitkit.repositories.ActivityRepo import to.bitkit.repositories.BlocktankRepo import to.bitkit.repositories.LightningRepo diff --git a/app/src/main/java/to/bitkit/models/PubkyRingAuthCallback.kt b/app/src/main/java/to/bitkit/models/PubkyRingAuthCallback.kt new file mode 100644 index 000000000..1e454fc00 --- /dev/null +++ b/app/src/main/java/to/bitkit/models/PubkyRingAuthCallback.kt @@ -0,0 +1,70 @@ +package to.bitkit.models + +import android.net.Uri + +sealed interface PubkyRingAuthCallback { + companion object { + private const val BITKIT_SCHEME = "bitkit" + private const val PUBKY_AUTH_HOST = "pubky-auth" + private const val SUCCESS_PATH = "/success" + private const val CANCEL_PATH = "/cancel" + private const val ERROR_PATH = "/error" + private const val NONCE_PARAM = "nonce" + private const val ERROR_MESSAGE_PARAM = "errorMessage" + + fun parse(uri: Uri): PubkyRingAuthCallback? { + if (uri.scheme != BITKIT_SCHEME || uri.host != PUBKY_AUTH_HOST) return null + + val nonce = uri.getQueryParameter(NONCE_PARAM) + return when (uri.path) { + SUCCESS_PATH -> Success(nonce) + CANCEL_PATH -> Cancel(nonce) + ERROR_PATH -> Error(uri.getQueryParameter(ERROR_MESSAGE_PARAM), nonce) + else -> null + } + } + } + + val nonce: String? + + data class Success(override val nonce: String?) : PubkyRingAuthCallback + data class Cancel(override val nonce: String?) : PubkyRingAuthCallback + data class Error(val message: String?, override val nonce: String?) : PubkyRingAuthCallback +} + +sealed interface PubkyRingAuthCallbackHandlingResult { + data object Ignored : PubkyRingAuthCallbackHandlingResult + data object Handled : PubkyRingAuthCallbackHandlingResult + data class TrustedError(val message: String?) : PubkyRingAuthCallbackHandlingResult + data object UntrustedError : PubkyRingAuthCallbackHandlingResult +} + +object PubkyRingAuthUrlBuilder { + const val SUCCESS_CALLBACK = "bitkit://pubky-auth/success" + const val CANCEL_CALLBACK = "bitkit://pubky-auth/cancel" + const val ERROR_CALLBACK = "bitkit://pubky-auth/error" + const val SOURCE = "Bitkit" + + fun addCallbacks(authUrl: String, nonce: String? = null): String? { + val uri = Uri.parse(authUrl) + if (uri.scheme.isNullOrBlank()) return null + + return uri.buildUpon() + .appendQueryParameter("x-success", callbackUrl(SUCCESS_CALLBACK, nonce)) + .appendQueryParameter("x-cancel", callbackUrl(CANCEL_CALLBACK, nonce)) + .appendQueryParameter("x-error", callbackUrl(ERROR_CALLBACK, nonce)) + .appendQueryParameter("x-source", SOURCE) + .build() + .toString() + } + + private fun callbackUrl(baseUrl: String, nonce: String?): String { + if (nonce.isNullOrBlank()) return baseUrl + + return Uri.parse(baseUrl) + .buildUpon() + .appendQueryParameter("nonce", nonce) + .build() + .toString() + } +} diff --git a/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt b/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt index 6cf2104c2..c223dbfda 100644 --- a/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt @@ -12,9 +12,11 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map @@ -33,23 +35,33 @@ import to.bitkit.models.PubkyProfile import to.bitkit.models.PubkyProfileData import to.bitkit.models.PubkyProfileLink import to.bitkit.models.PubkyPublicKeyFormat +import to.bitkit.models.PubkyRingAuthCallback +import to.bitkit.models.PubkyRingAuthCallbackHandlingResult import to.bitkit.models.PubkySessionBackupKind import to.bitkit.models.PubkySessionBackupV1 import to.bitkit.services.PubkyService import to.bitkit.utils.AppError import to.bitkit.utils.Logger import java.io.ByteArrayOutputStream +import java.util.UUID import javax.inject.Inject import javax.inject.Singleton import kotlin.math.min enum class PubkyAuthState { Idle, Authenticating, Authenticated } +data class PubkyRingAuthRequest( + val authUrl: String, + val callbackNonce: String, +) + sealed class PubkyContactError(message: String) : AppError(message) { data object CannotAddSelf : PubkyContactError("Cannot add your own pubky as a contact") data object InvalidFormat : PubkyContactError("Invalid pubky key format") } +private class PubkyAuthAttemptInactive : AppError("Auth attempt is no longer active") + @Suppress("TooManyFunctions", "LargeClass") @Singleton class PubkyRepo @Inject constructor( @@ -76,6 +88,9 @@ class PubkyRepo @Inject constructor( private var isServiceInitialized = false private val _authState = MutableStateFlow(PubkyAuthState.Idle) + private val _activeAuthAttemptId = MutableStateFlow(null) + private val _authCancelEvents = MutableSharedFlow(extraBufferCapacity = 1) + val authCancelEvents = _authCancelEvents.asSharedFlow() private val _profile = MutableStateFlow(null) val profile: StateFlow = _profile.asStateFlow() @@ -104,7 +119,7 @@ class PubkyRepo @Inject constructor( private val _backupStateVersion = MutableStateFlow(0L) val backupStateVersion: StateFlow = _backupStateVersion.asStateFlow() - val isAuthenticated: StateFlow = _authState.map { it == PubkyAuthState.Authenticated } + val isAuthenticated: StateFlow = _publicKey.map { it != null } .stateIn(scope, SharingStarted.Eagerly, false) val displayName: StateFlow = combine(_profile, pubkyStore.data) { profile, cached -> @@ -236,20 +251,27 @@ class PubkyRepo @Inject constructor( // region Ring auth flow - suspend fun startAuthentication(): Result { + suspend fun startAuthentication(): Result { + val attemptId = UUID.randomUUID().toString() + _activeAuthAttemptId.update { attemptId } _authState.update { PubkyAuthState.Authenticating } return runCatching { - withContext(ioDispatcher) { pubkyService.startAuth() } + val authUrl = withContext(ioDispatcher) { pubkyService.startAuth() } + PubkyRingAuthRequest(authUrl = authUrl, callbackNonce = attemptId) }.onFailure { - _authState.update { PubkyAuthState.Idle } + _activeAuthAttemptId.update { null } + restoreAuthStateAfterAuthFlow() } } suspend fun completeAuthentication(): Result { + val attemptId = _activeAuthAttemptId.value return runCatching { withContext(ioDispatcher) { val sessionSecret = pubkyService.completeAuth() + ensureAuthAttemptActive(attemptId) val pk = pubkyService.importSession(sessionSecret).ensurePubkyPrefix() + ensureAuthAttemptActive(attemptId) runCatching { keychain.delete(Keychain.Key.PUBKY_SECRET_KEY.name) } keychain.upsertString(Keychain.Key.PAYKIT_SESSION.name, sessionSecret) @@ -258,8 +280,14 @@ class PubkyRepo @Inject constructor( pk } }.onFailure { - _authState.update { PubkyAuthState.Idle } + if (_activeAuthAttemptId.value == attemptId) { + _activeAuthAttemptId.update { null } + } + restoreAuthStateAfterAuthFlow() }.onSuccess { pk -> + if (_activeAuthAttemptId.value == attemptId) { + _activeAuthAttemptId.update { null } + } _publicKey.update { pk } _authState.update { PubkyAuthState.Authenticated } Logger.info("Completed pubky auth for '$pk'", context = TAG) @@ -271,13 +299,84 @@ class PubkyRepo @Inject constructor( runCatching { withContext(ioDispatcher) { pubkyService.cancelAuth() } }.onFailure { Logger.warn("Failed to cancel auth", it, context = TAG) } - _authState.update { PubkyAuthState.Idle } + endAuthAttempt() } fun cancelAuthenticationSync() { scope.launch { cancelAuthentication() } } + suspend fun handleAuthCallback(callback: PubkyRingAuthCallback): PubkyRingAuthCallbackHandlingResult { + if (!isCurrentAuthCallback(callback)) { + return handleInvalidAuthCallback(callback) + } + + return when (callback) { + is PubkyRingAuthCallback.Success -> { + Logger.info("Received Pubky Ring auth success callback", context = TAG) + PubkyRingAuthCallbackHandlingResult.Handled + } + is PubkyRingAuthCallback.Cancel -> { + Logger.info("Received Pubky Ring auth cancel callback", context = TAG) + cancelAuthentication() + PubkyRingAuthCallbackHandlingResult.Handled + } + is PubkyRingAuthCallback.Error -> { + Logger.warn("Received Pubky Ring auth error callback", context = TAG) + cancelAuthentication() + PubkyRingAuthCallbackHandlingResult.TrustedError(callback.message) + } + } + } + + private fun handleInvalidAuthCallback( + callback: PubkyRingAuthCallback, + ): PubkyRingAuthCallbackHandlingResult { + if (_activeAuthAttemptId.value == null) { + Logger.warn("Ignoring Pubky Ring auth callback with missing or invalid nonce", context = TAG) + return PubkyRingAuthCallbackHandlingResult.Ignored + } + + return when (callback) { + is PubkyRingAuthCallback.Success -> { + Logger.warn("Ignoring Pubky Ring auth success callback with missing or invalid nonce", context = TAG) + PubkyRingAuthCallbackHandlingResult.Ignored + } + is PubkyRingAuthCallback.Cancel -> { + Logger.warn("Received Pubky Ring auth cancel callback with missing or invalid nonce", context = TAG) + endAuthAttempt() + PubkyRingAuthCallbackHandlingResult.Handled + } + is PubkyRingAuthCallback.Error -> { + Logger.warn("Received Pubky Ring auth error callback with missing or invalid nonce", context = TAG) + endAuthAttempt() + PubkyRingAuthCallbackHandlingResult.UntrustedError + } + } + } + + private fun isCurrentAuthCallback(callback: PubkyRingAuthCallback): Boolean { + val activeAuthAttemptId = _activeAuthAttemptId.value ?: return false + return callback.nonce == activeAuthAttemptId + } + + private fun ensureAuthAttemptActive(attemptId: String?) { + if (attemptId == null) return + if (_activeAuthAttemptId.value == attemptId) return + + throw PubkyAuthAttemptInactive() + } + + private fun endAuthAttempt() { + _activeAuthAttemptId.update { null } + _authCancelEvents.tryEmit(Unit) + restoreAuthStateAfterAuthFlow() + } + + private fun restoreAuthStateAfterAuthFlow() { + _authState.update { if (_publicKey.value == null) PubkyAuthState.Idle else PubkyAuthState.Authenticated } + } + // endregion // region Profile loading diff --git a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt index 199e2ff3e..dde2ee0a4 100644 --- a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt @@ -32,8 +32,8 @@ import to.bitkit.ext.toHex import to.bitkit.models.ALL_ADDRESS_TYPE_STRINGS import to.bitkit.models.AddressModel import to.bitkit.models.BalanceState -import to.bitkit.models.msatFloorOf import to.bitkit.models.DEFAULT_ADDRESS_TYPE_STRING +import to.bitkit.models.msatFloorOf import to.bitkit.models.toDerivationPath import to.bitkit.services.CoreService import to.bitkit.usecases.DeriveBalanceStateUseCase diff --git a/app/src/main/java/to/bitkit/services/CoreService.kt b/app/src/main/java/to/bitkit/services/CoreService.kt index 4349eced4..527dff93d 100644 --- a/app/src/main/java/to/bitkit/services/CoreService.kt +++ b/app/src/main/java/to/bitkit/services/CoreService.kt @@ -72,11 +72,11 @@ import to.bitkit.data.CacheStore import to.bitkit.data.SettingsStore import to.bitkit.env.Env import to.bitkit.ext.amountSats -import to.bitkit.models.msatFloorOf import to.bitkit.ext.channelId import to.bitkit.ext.create import to.bitkit.ext.latestSpendingTxid import to.bitkit.models.addressTypeFromAddress +import to.bitkit.models.msatFloorOf import to.bitkit.models.toCoreNetwork import to.bitkit.utils.AppError import to.bitkit.utils.Logger diff --git a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt index 1b6f70840..43b87cd91 100644 --- a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt +++ b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt @@ -50,11 +50,11 @@ import to.bitkit.ext.createChannelDetails import to.bitkit.ext.ellipsisMiddle import to.bitkit.ext.formatToString import to.bitkit.ext.uri -import to.bitkit.models.msatFloorOf import to.bitkit.models.NodeLifecycleState import to.bitkit.models.NodePeer import to.bitkit.models.alias import to.bitkit.models.formatToModernDisplay +import to.bitkit.models.msatFloorOf import to.bitkit.repositories.LightningState import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BodyMSB diff --git a/app/src/main/java/to/bitkit/ui/screens/profile/PubkyChoiceViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/profile/PubkyChoiceViewModel.kt index 4c352d2fe..1328d2205 100644 --- a/app/src/main/java/to/bitkit/ui/screens/profile/PubkyChoiceViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/profile/PubkyChoiceViewModel.kt @@ -17,6 +17,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import to.bitkit.R +import to.bitkit.models.PubkyRingAuthUrlBuilder import to.bitkit.models.Toast import to.bitkit.repositories.PubkyRepo import to.bitkit.ui.shared.toast.ToastEventBus @@ -41,6 +42,16 @@ class PubkyChoiceViewModel @Inject constructor( private var approvalJob: Job? = null + init { + viewModelScope.launch { + pubkyRepo.authCancelEvents.collect { + approvalJob?.cancel() + approvalJob = null + _uiState.update { it.copy(isWaitingForRing = false, isLoadingAfterAuth = false) } + } + } + } + override fun onCleared() { super.onCleared() if (_uiState.value.isWaitingForRing) { @@ -63,8 +74,12 @@ class PubkyChoiceViewModel @Inject constructor( } pubkyRepo.startAuthentication() - .onSuccess { authUrl -> - val ringIntent = createRingAuthIntent(authUrl) + .onSuccess { authRequest -> + val callbackAuthUrl = PubkyRingAuthUrlBuilder.addCallbacks( + authUrl = authRequest.authUrl, + nonce = authRequest.callbackNonce, + ) ?: authRequest.authUrl + val ringIntent = createRingAuthIntent(callbackAuthUrl) if (!canOpenWithRing(ringIntent)) { cancelAuthAndShowRingDialog() return@launch diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt index 0b93407ac..a41243741 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt @@ -60,8 +60,8 @@ import to.bitkit.ext.DatePattern import to.bitkit.ext.amountOnClose import to.bitkit.ext.createChannelDetails import to.bitkit.ext.setClipboardText -import to.bitkit.models.msatFloorOf import to.bitkit.models.Toast +import to.bitkit.models.msatFloorOf import to.bitkit.ui.Routes import to.bitkit.ui.appViewModel import to.bitkit.ui.components.Caption13Up diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsScreen.kt index 89f42eb0f..4c1913954 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsScreen.kt @@ -47,8 +47,8 @@ import kotlinx.collections.immutable.toImmutableList import to.bitkit.R import to.bitkit.ext.amountOnClose import to.bitkit.ext.createChannelDetails -import to.bitkit.models.msatFloorOf import to.bitkit.models.formatToModernDisplay +import to.bitkit.models.msatFloorOf import to.bitkit.ui.Routes import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BodyMSB diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 7c1ad3b4e..991a68272 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -97,6 +97,8 @@ import to.bitkit.models.NewTransactionSheetType import to.bitkit.models.NodeLifecycleState import to.bitkit.models.PubkyProfile import to.bitkit.models.PubkyPublicKeyFormat +import to.bitkit.models.PubkyRingAuthCallback +import to.bitkit.models.PubkyRingAuthCallbackHandlingResult import to.bitkit.models.Suggestion import to.bitkit.models.Toast import to.bitkit.models.TransactionSpeed @@ -2527,6 +2529,11 @@ class AppViewModel @Inject constructor( return@launch } + PubkyRingAuthCallback.parse(uri)?.let { + handlePubkyRingAuthCallback(it) + return@launch + } + if (uri.scheme == PUBKYAUTH_SCHEME) { handlePubkyAuth(uri.toString()) return@launch @@ -2548,6 +2555,27 @@ class AppViewModel @Inject constructor( showSheet(Sheet.PubkyAuth(authUrl)) } + private suspend fun handlePubkyRingAuthCallback(callback: PubkyRingAuthCallback) { + when (val result = pubkyRepo.handleAuthCallback(callback)) { + is PubkyRingAuthCallbackHandlingResult.TrustedError -> { + ToastEventBus.send( + type = Toast.ToastType.ERROR, + title = context.getString(R.string.profile__auth_error_title), + description = result.message ?: context.getString(R.string.other__qr_error_text), + ) + } + PubkyRingAuthCallbackHandlingResult.UntrustedError -> { + ToastEventBus.send( + type = Toast.ToastType.ERROR, + title = context.getString(R.string.profile__auth_error_title), + ) + } + PubkyRingAuthCallbackHandlingResult.Handled, + PubkyRingAuthCallbackHandlingResult.Ignored, + -> Unit + } + } + // TODO Temporary fix while these schemes can't be decoded https://github.com/synonymdev/bitkit-core/issues/70 private fun String.removeLightningSchemes(): String = LIGHTNING_SCHEME_PATTERNS.fold(this) { acc, regex -> acc.replace(regex, "") diff --git a/app/src/test/java/to/bitkit/models/PubkyRingAuthCallbackTest.kt b/app/src/test/java/to/bitkit/models/PubkyRingAuthCallbackTest.kt new file mode 100644 index 000000000..8e753f7b4 --- /dev/null +++ b/app/src/test/java/to/bitkit/models/PubkyRingAuthCallbackTest.kt @@ -0,0 +1,77 @@ +package to.bitkit.models + +import androidx.core.net.toUri +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [34]) +class PubkyRingAuthCallbackTest { + @Test + fun `addCallbacks adds Ring x-callback params`() { + val url = checkNotNull( + PubkyRingAuthUrlBuilder.addCallbacks( + authUrl = "pubkyauth://auth?relay=https%3A%2F%2Frelay.example", + nonce = "12345678-1234-1234-1234-123456789ABC", + ), + ) { "Auth URL should be valid" } + val uri = url.toUri() + + assertEquals("https://relay.example", uri.getQueryParameter("relay")) + assertEquals( + "bitkit://pubky-auth/success?nonce=12345678-1234-1234-1234-123456789ABC", + uri.getQueryParameter("x-success"), + ) + assertEquals( + "bitkit://pubky-auth/cancel?nonce=12345678-1234-1234-1234-123456789ABC", + uri.getQueryParameter("x-cancel"), + ) + assertEquals( + "bitkit://pubky-auth/error?nonce=12345678-1234-1234-1234-123456789ABC", + uri.getQueryParameter("x-error"), + ) + assertEquals(PubkyRingAuthUrlBuilder.SOURCE, uri.getQueryParameter("x-source")) + } + + @Test + fun `parse returns success cancel and error callbacks`() { + assertEquals( + PubkyRingAuthCallback.Success(nonce = null), + PubkyRingAuthCallback.parse("bitkit://pubky-auth/success".toUri()), + ) + assertEquals( + PubkyRingAuthCallback.Cancel(nonce = null), + PubkyRingAuthCallback.parse("bitkit://pubky-auth/cancel".toUri()), + ) + assertEquals( + PubkyRingAuthCallback.Error(message = "Denied", nonce = null), + PubkyRingAuthCallback.parse("bitkit://pubky-auth/error?errorMessage=Denied".toUri()), + ) + } + + @Test + fun `parse returns nonce when callback includes value`() { + assertEquals( + PubkyRingAuthCallback.Error(message = "Denied", nonce = "abc"), + PubkyRingAuthCallback.parse("bitkit://pubky-auth/error?nonce=abc&errorMessage=Denied".toUri()), + ) + } + + @Test + fun `parse treats bare nonce as missing`() { + assertEquals( + PubkyRingAuthCallback.Cancel(nonce = null), + PubkyRingAuthCallback.parse("bitkit://pubky-auth/cancel?nonce".toUri()), + ) + } + + @Test + fun `parse rejects other deeplinks`() { + assertNull(PubkyRingAuthCallback.parse("bitkit://wallet/success".toUri())) + assertNull(PubkyRingAuthCallback.parse("https://pubky-auth/success".toUri())) + } +} diff --git a/app/src/test/java/to/bitkit/repositories/PubkyRepoTest.kt b/app/src/test/java/to/bitkit/repositories/PubkyRepoTest.kt index 67acbd04f..c5a72c124 100644 --- a/app/src/test/java/to/bitkit/repositories/PubkyRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/PubkyRepoTest.kt @@ -22,6 +22,8 @@ import to.bitkit.data.PubkyStoreData import to.bitkit.data.keychain.Keychain import to.bitkit.env.Env import to.bitkit.models.PubkyProfile +import to.bitkit.models.PubkyRingAuthCallback +import to.bitkit.models.PubkyRingAuthCallbackHandlingResult import to.bitkit.models.PubkySessionBackupKind import to.bitkit.models.PubkySessionBackupV1 import to.bitkit.services.PubkyService @@ -80,7 +82,8 @@ class PubkyRepoTest : BaseUnitTest() { val result = sut.startAuthentication() assertTrue(result.isSuccess) - assertEquals(authUri, result.getOrNull()) + assertEquals(authUri, result.getOrNull()?.authUrl) + assertNotNull(result.getOrNull()?.callbackNonce) } @Test @@ -165,6 +168,69 @@ class PubkyRepoTest : BaseUnitTest() { assertFalse(sut.isAuthenticated.value) } + @Test + fun `cancelAuthentication should keep restored profile authenticated`() = test { + authenticateForTesting() + whenever(pubkyService.startAuth()).thenReturn("auth_uri") + sut.startAuthentication() + + sut.cancelAuthentication() + + assertTrue(sut.isAuthenticated.value) + assertNotNull(sut.publicKey.value) + } + + @Test + fun `handleAuthCallback should reject invalid success nonce`() = test { + whenever(pubkyService.startAuth()).thenReturn("auth_uri") + sut.startAuthentication() + + val result = sut.handleAuthCallback(PubkyRingAuthCallback.Success(nonce = "invalid")) + + assertEquals(PubkyRingAuthCallbackHandlingResult.Ignored, result) + verifyBlocking(pubkyService, never()) { cancelAuth() } + } + + @Test + fun `handleAuthCallback should clear invalid cancel nonce`() = test { + whenever(pubkyService.startAuth()).thenReturn("auth_uri") + sut.startAuthentication() + + val result = sut.handleAuthCallback(PubkyRingAuthCallback.Cancel(nonce = "invalid")) + + assertEquals(PubkyRingAuthCallbackHandlingResult.Handled, result) + assertFalse(sut.isAuthenticated.value) + verifyBlocking(pubkyService, never()) { cancelAuth() } + } + + @Test + fun `handleAuthCallback should not trust invalid error message`() = test { + whenever(pubkyService.startAuth()).thenReturn("auth_uri") + sut.startAuthentication() + + val result = sut.handleAuthCallback( + PubkyRingAuthCallback.Error(message = "Forged error", nonce = "invalid"), + ) + + assertEquals(PubkyRingAuthCallbackHandlingResult.UntrustedError, result) + verifyBlocking(pubkyService, never()) { cancelAuth() } + } + + @Test + fun `handleAuthCallback should trust matching error nonce`() = test { + whenever(pubkyService.startAuth()).thenReturn("auth_uri") + val authRequest = checkNotNull(sut.startAuthentication().getOrNull()) { + "Auth request should be returned" + } + + val result = sut.handleAuthCallback( + PubkyRingAuthCallback.Error(message = "Ring failed", nonce = authRequest.callbackNonce), + ) + + assertEquals(PubkyRingAuthCallbackHandlingResult.TrustedError("Ring failed"), result) + verifyBlocking(pubkyService) { cancelAuth() } + } + @Test fun `loadProfile should update profile on success`() = test { authenticateForTesting() diff --git a/app/src/test/java/to/bitkit/ui/screens/profile/PubkyChoiceViewModelTest.kt b/app/src/test/java/to/bitkit/ui/screens/profile/PubkyChoiceViewModelTest.kt index 3678e1fb3..afb09beae 100644 --- a/app/src/test/java/to/bitkit/ui/screens/profile/PubkyChoiceViewModelTest.kt +++ b/app/src/test/java/to/bitkit/ui/screens/profile/PubkyChoiceViewModelTest.kt @@ -3,6 +3,7 @@ package to.bitkit.ui.screens.profile import android.content.Context import android.content.pm.PackageManager import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.test.advanceUntilIdle @@ -28,6 +29,7 @@ class PubkyChoiceViewModelTest : BaseUnitTest() { private val packageManager: PackageManager = mock() private val pubkyRepo: PubkyRepo = mock() private val pendingImportContacts = MutableStateFlow>(emptyList()) + private val authCancelEvents = MutableSharedFlow(extraBufferCapacity = 1) private lateinit var sut: PubkyChoiceViewModel @@ -37,6 +39,7 @@ class PubkyChoiceViewModelTest : BaseUnitTest() { whenever(context.getString(R.string.common__error)).thenReturn("Error") whenever(context.getString(R.string.profile__auth_error_title)).thenReturn("Authorization Failed") whenever(pubkyRepo.pendingImportContacts).thenReturn(pendingImportContacts) + whenever(pubkyRepo.authCancelEvents).thenReturn(authCancelEvents) sut = PubkyChoiceViewModel( context = context, pubkyRepo = pubkyRepo, diff --git a/changelog.d/next/917.added.md b/changelog.d/next/917.added.md new file mode 100644 index 000000000..e8da260e7 --- /dev/null +++ b/changelog.d/next/917.added.md @@ -0,0 +1 @@ +- Return to Bitkit after Pubky Ring approval, cancellation, or error callbacks #917 From b805610ac9872e02b6f0e33c4c2adaf8f2f9ee7f Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 5 May 2026 21:29:06 +0200 Subject: [PATCH 2/4] fix: ignore stale pubky callbacks --- .../to/bitkit/models/PubkyRingAuthCallback.kt | 3 +- .../java/to/bitkit/repositories/PubkyRepo.kt | 10 +-- .../java/to/bitkit/viewmodels/AppViewModel.kt | 6 -- .../to/bitkit/repositories/PubkyRepoTest.kt | 88 ++++++++++++++++++- changelog.d/next/917.added.md | 2 +- 5 files changed, 90 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/to/bitkit/models/PubkyRingAuthCallback.kt b/app/src/main/java/to/bitkit/models/PubkyRingAuthCallback.kt index 1e454fc00..35457a844 100644 --- a/app/src/main/java/to/bitkit/models/PubkyRingAuthCallback.kt +++ b/app/src/main/java/to/bitkit/models/PubkyRingAuthCallback.kt @@ -15,7 +15,7 @@ sealed interface PubkyRingAuthCallback { fun parse(uri: Uri): PubkyRingAuthCallback? { if (uri.scheme != BITKIT_SCHEME || uri.host != PUBKY_AUTH_HOST) return null - val nonce = uri.getQueryParameter(NONCE_PARAM) + val nonce = uri.getQueryParameter(NONCE_PARAM)?.takeIf { it.isNotBlank() } return when (uri.path) { SUCCESS_PATH -> Success(nonce) CANCEL_PATH -> Cancel(nonce) @@ -36,7 +36,6 @@ sealed interface PubkyRingAuthCallbackHandlingResult { data object Ignored : PubkyRingAuthCallbackHandlingResult data object Handled : PubkyRingAuthCallbackHandlingResult data class TrustedError(val message: String?) : PubkyRingAuthCallbackHandlingResult - data object UntrustedError : PubkyRingAuthCallbackHandlingResult } object PubkyRingAuthUrlBuilder { diff --git a/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt b/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt index c223dbfda..865506416 100644 --- a/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt @@ -343,14 +343,12 @@ class PubkyRepo @Inject constructor( PubkyRingAuthCallbackHandlingResult.Ignored } is PubkyRingAuthCallback.Cancel -> { - Logger.warn("Received Pubky Ring auth cancel callback with missing or invalid nonce", context = TAG) - endAuthAttempt() - PubkyRingAuthCallbackHandlingResult.Handled + Logger.warn("Ignoring Pubky Ring auth cancel callback with missing or invalid nonce", context = TAG) + PubkyRingAuthCallbackHandlingResult.Ignored } is PubkyRingAuthCallback.Error -> { - Logger.warn("Received Pubky Ring auth error callback with missing or invalid nonce", context = TAG) - endAuthAttempt() - PubkyRingAuthCallbackHandlingResult.UntrustedError + Logger.warn("Ignoring Pubky Ring auth error callback with missing or invalid nonce", context = TAG) + PubkyRingAuthCallbackHandlingResult.Ignored } } } diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 991a68272..89d5c44e9 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -2564,12 +2564,6 @@ class AppViewModel @Inject constructor( description = result.message ?: context.getString(R.string.other__qr_error_text), ) } - PubkyRingAuthCallbackHandlingResult.UntrustedError -> { - ToastEventBus.send( - type = Toast.ToastType.ERROR, - title = context.getString(R.string.profile__auth_error_title), - ) - } PubkyRingAuthCallbackHandlingResult.Handled, PubkyRingAuthCallbackHandlingResult.Ignored, -> Unit diff --git a/app/src/test/java/to/bitkit/repositories/PubkyRepoTest.kt b/app/src/test/java/to/bitkit/repositories/PubkyRepoTest.kt index c5a72c124..d07f3c801 100644 --- a/app/src/test/java/to/bitkit/repositories/PubkyRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/PubkyRepoTest.kt @@ -192,19 +192,19 @@ class PubkyRepoTest : BaseUnitTest() { } @Test - fun `handleAuthCallback should clear invalid cancel nonce`() = test { + fun `handleAuthCallback should ignore invalid cancel nonce`() = test { whenever(pubkyService.startAuth()).thenReturn("auth_uri") sut.startAuthentication() val result = sut.handleAuthCallback(PubkyRingAuthCallback.Cancel(nonce = "invalid")) - assertEquals(PubkyRingAuthCallbackHandlingResult.Handled, result) + assertEquals(PubkyRingAuthCallbackHandlingResult.Ignored, result) assertFalse(sut.isAuthenticated.value) verifyBlocking(pubkyService, never()) { cancelAuth() } } @Test - fun `handleAuthCallback should not trust invalid error message`() = test { + fun `handleAuthCallback should ignore invalid error nonce`() = test { whenever(pubkyService.startAuth()).thenReturn("auth_uri") sut.startAuthentication() @@ -212,7 +212,87 @@ class PubkyRepoTest : BaseUnitTest() { PubkyRingAuthCallback.Error(message = "Forged error", nonce = "invalid"), ) - assertEquals(PubkyRingAuthCallbackHandlingResult.UntrustedError, result) + assertEquals(PubkyRingAuthCallbackHandlingResult.Ignored, result) + verifyBlocking(pubkyService, never()) { cancelAuth() } + } + + @Test + fun `handleAuthCallback should keep active auth after missing cancel nonce`() = test { + val testSecret = "session_secret" + val testPk = VALID_SELF_KEY.removePrefix("pubky") + whenever(pubkyService.startAuth()).thenReturn("auth_uri") + whenever(pubkyService.completeAuth()).thenReturn(testSecret) + whenever(pubkyService.importSession(testSecret)).thenReturn(testPk) + whenever(pubkyService.getProfile(VALID_SELF_KEY)).thenReturn(mock()) + sut.startAuthentication() + + val callbackResult = sut.handleAuthCallback(PubkyRingAuthCallback.Cancel(nonce = null)) + val result = sut.completeAuthentication() + + assertEquals(PubkyRingAuthCallbackHandlingResult.Ignored, callbackResult) + assertTrue(result.isSuccess) + assertTrue(sut.isAuthenticated.value) + verifyBlocking(pubkyService, never()) { cancelAuth() } + } + + @Test + fun `handleAuthCallback should keep active auth after missing error nonce`() = test { + val testSecret = "session_secret" + val testPk = VALID_SELF_KEY.removePrefix("pubky") + whenever(pubkyService.startAuth()).thenReturn("auth_uri") + whenever(pubkyService.completeAuth()).thenReturn(testSecret) + whenever(pubkyService.importSession(testSecret)).thenReturn(testPk) + whenever(pubkyService.getProfile(VALID_SELF_KEY)).thenReturn(mock()) + sut.startAuthentication() + + val callbackResult = sut.handleAuthCallback( + PubkyRingAuthCallback.Error(message = "Forged error", nonce = null), + ) + val result = sut.completeAuthentication() + + assertEquals(PubkyRingAuthCallbackHandlingResult.Ignored, callbackResult) + assertTrue(result.isSuccess) + assertTrue(sut.isAuthenticated.value) + verifyBlocking(pubkyService, never()) { cancelAuth() } + } + + @Test + fun `handleAuthCallback should keep active auth after invalid cancel nonce`() = test { + val testSecret = "session_secret" + val testPk = VALID_SELF_KEY.removePrefix("pubky") + whenever(pubkyService.startAuth()).thenReturn("auth_uri") + whenever(pubkyService.completeAuth()).thenReturn(testSecret) + whenever(pubkyService.importSession(testSecret)).thenReturn(testPk) + whenever(pubkyService.getProfile(VALID_SELF_KEY)).thenReturn(mock()) + sut.startAuthentication() + + val callbackResult = sut.handleAuthCallback(PubkyRingAuthCallback.Cancel(nonce = "invalid")) + val result = sut.completeAuthentication() + + assertEquals(PubkyRingAuthCallbackHandlingResult.Ignored, callbackResult) + assertTrue(result.isSuccess) + assertTrue(sut.isAuthenticated.value) + verifyBlocking(pubkyService, never()) { cancelAuth() } + } + + @Test + fun `handleAuthCallback should keep active auth after invalid error nonce`() = test { + val testSecret = "session_secret" + val testPk = VALID_SELF_KEY.removePrefix("pubky") + whenever(pubkyService.startAuth()).thenReturn("auth_uri") + whenever(pubkyService.completeAuth()).thenReturn(testSecret) + whenever(pubkyService.importSession(testSecret)).thenReturn(testPk) + whenever(pubkyService.getProfile(VALID_SELF_KEY)).thenReturn(mock()) + sut.startAuthentication() + + val callbackResult = sut.handleAuthCallback( + PubkyRingAuthCallback.Error(message = "Forged error", nonce = "invalid"), + ) + val result = sut.completeAuthentication() + + assertEquals(PubkyRingAuthCallbackHandlingResult.Ignored, callbackResult) + assertTrue(result.isSuccess) + assertTrue(sut.isAuthenticated.value) verifyBlocking(pubkyService, never()) { cancelAuth() } } diff --git a/changelog.d/next/917.added.md b/changelog.d/next/917.added.md index e8da260e7..92a1a28f0 100644 --- a/changelog.d/next/917.added.md +++ b/changelog.d/next/917.added.md @@ -1 +1 @@ -- Return to Bitkit after Pubky Ring approval, cancellation, or error callbacks #917 +Return to Bitkit after Pubky Ring approval, cancellation, or error callbacks From 46b372d664d460e8c1904a6611b0006c2ef6b462 Mon Sep 17 00:00:00 2001 From: benk10 Date: Tue, 5 May 2026 14:29:56 -0500 Subject: [PATCH 3/4] fix: guard pubky auth completion --- .../java/to/bitkit/repositories/PubkyRepo.kt | 2 +- .../to/bitkit/repositories/PubkyRepoTest.kt | 32 +++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt b/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt index 865506416..2862c0a8a 100644 --- a/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt @@ -265,7 +265,7 @@ class PubkyRepo @Inject constructor( } suspend fun completeAuthentication(): Result { - val attemptId = _activeAuthAttemptId.value + val attemptId = _activeAuthAttemptId.value ?: return Result.failure(PubkyAuthAttemptInactive()) return runCatching { withContext(ioDispatcher) { val sessionSecret = pubkyService.completeAuth() diff --git a/app/src/test/java/to/bitkit/repositories/PubkyRepoTest.kt b/app/src/test/java/to/bitkit/repositories/PubkyRepoTest.kt index d07f3c801..2fdd555a2 100644 --- a/app/src/test/java/to/bitkit/repositories/PubkyRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/PubkyRepoTest.kt @@ -102,6 +102,7 @@ class PubkyRepoTest : BaseUnitTest() { fun `completeAuthentication should save session and update state`() = test { val testSecret = "session_secret" val testPk = VALID_SELF_KEY.removePrefix("pubky") + whenever(pubkyService.startAuth()).thenReturn("auth_uri") whenever(pubkyService.completeAuth()).thenReturn(testSecret) whenever(pubkyService.importSession(testSecret)).thenReturn(testPk) @@ -109,6 +110,7 @@ class PubkyRepoTest : BaseUnitTest() { whenever(ffiProfile.name).thenReturn("User") whenever(pubkyService.getProfile(VALID_SELF_KEY)).thenReturn(ffiProfile) + sut.startAuthentication() val result = sut.completeAuthentication() assertTrue(result.isSuccess) @@ -121,11 +123,13 @@ class PubkyRepoTest : BaseUnitTest() { fun `completeAuthentication should clear managed secret key`() = test { val testSecret = "session_secret" val testPk = VALID_SELF_KEY.removePrefix("pubky") + whenever(pubkyService.startAuth()).thenReturn("auth_uri") whenever(pubkyService.completeAuth()).thenReturn(testSecret) whenever(pubkyService.importSession(testSecret)).thenReturn(testPk) val ffiProfile = createFfiProfile(name = "User") whenever(pubkyService.getProfile(VALID_SELF_KEY)).thenReturn(ffiProfile) + sut.startAuthentication() val result = sut.completeAuthentication() assertTrue(result.isSuccess) @@ -136,11 +140,13 @@ class PubkyRepoTest : BaseUnitTest() { fun `completeAuthentication should not load contacts automatically`() = test { val testSecret = "session_secret" val testPk = VALID_SELF_KEY.removePrefix("pubky") + whenever(pubkyService.startAuth()).thenReturn("auth_uri") whenever(pubkyService.completeAuth()).thenReturn(testSecret) whenever(pubkyService.importSession(testSecret)).thenReturn(testPk) val ffiProfile = createFfiProfile(name = "User") whenever(pubkyService.getProfile(VALID_SELF_KEY)).thenReturn(ffiProfile) + sut.startAuthentication() val result = sut.completeAuthentication() assertTrue(result.isSuccess) @@ -149,8 +155,10 @@ class PubkyRepoTest : BaseUnitTest() { @Test fun `completeAuthentication should reset state on failure`() = test { + whenever(pubkyService.startAuth()).thenReturn("auth_uri") whenever(pubkyService.completeAuth()).thenAnswer { throw TestAppError("Failed") } + sut.startAuthentication() val result = sut.completeAuthentication() assertTrue(result.isFailure) @@ -158,6 +166,14 @@ class PubkyRepoTest : BaseUnitTest() { assertNull(sut.publicKey.value) } + @Test + fun `completeAuthentication should fail when auth attempt inactive`() = test { + val result = sut.completeAuthentication() + + assertTrue(result.isFailure) + verifyBlocking(pubkyService, never()) { completeAuth() } + } + @Test fun `cancelAuthentication should reset state to idle`() = test { whenever(pubkyService.startAuth()).thenReturn("auth_uri") @@ -760,7 +776,10 @@ class PubkyRepoTest : BaseUnitTest() { whenever(pubkyService.sessionList(newSecret, Env.contactsBasePath)).thenReturn(emptyList()) val staleProfile = createFfiProfile(name = "Stale Old") whenever(pubkyService.getProfile(oldPublicKey.ensurePubkyPrefixForTest())).thenAnswer { - runBlocking { sut.completeAuthentication() } + runBlocking { + startAuthForTesting() + sut.completeAuthentication() + } staleProfile } @@ -803,7 +822,10 @@ class PubkyRepoTest : BaseUnitTest() { whenever(keychain.loadString(Keychain.Key.PAYKIT_SESSION.name)).thenReturn(oldSecret) whenever(pubkyService.sessionList(oldSecret, Env.contactsBasePath)).thenReturn(listOf(staleContactPath)) whenever(pubkyService.fetchFileString(staleContactUri)).thenAnswer { - runBlocking { sut.completeAuthentication() } + runBlocking { + startAuthForTesting() + sut.completeAuthentication() + } """{"name":"Stale Contact","bio":""}""" } @@ -1063,9 +1085,15 @@ class PubkyRepoTest : BaseUnitTest() { whenever(keychain.loadString(Keychain.Key.PAYKIT_SESSION.name)).thenReturn(secret) whenever { pubkyService.sessionList(secret, Env.contactsBasePath) }.thenReturn(emptyList()) + startAuthForTesting() sut.completeAuthentication() } + private suspend fun startAuthForTesting(authUri: String = "auth_uri") { + whenever { pubkyService.startAuth() }.thenReturn(authUri) + sut.startAuthentication() + } + private fun createFfiProfile(name: String): CorePubkyProfile { val ffiProfile = mock() whenever(ffiProfile.name).thenReturn(name) From 20dcbc3e082b36c1573d5b77f8f56fde33a4bc69 Mon Sep 17 00:00:00 2001 From: benk10 Date: Tue, 5 May 2026 14:54:44 -0500 Subject: [PATCH 4/4] fix: reuse pubky nonce constant --- app/src/main/java/to/bitkit/models/PubkyRingAuthCallback.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/models/PubkyRingAuthCallback.kt b/app/src/main/java/to/bitkit/models/PubkyRingAuthCallback.kt index 35457a844..7f57732ac 100644 --- a/app/src/main/java/to/bitkit/models/PubkyRingAuthCallback.kt +++ b/app/src/main/java/to/bitkit/models/PubkyRingAuthCallback.kt @@ -2,6 +2,8 @@ package to.bitkit.models import android.net.Uri +private const val NONCE_PARAM = "nonce" + sealed interface PubkyRingAuthCallback { companion object { private const val BITKIT_SCHEME = "bitkit" @@ -9,7 +11,6 @@ sealed interface PubkyRingAuthCallback { private const val SUCCESS_PATH = "/success" private const val CANCEL_PATH = "/cancel" private const val ERROR_PATH = "/error" - private const val NONCE_PARAM = "nonce" private const val ERROR_MESSAGE_PARAM = "errorMessage" fun parse(uri: Uri): PubkyRingAuthCallback? { @@ -62,7 +63,7 @@ object PubkyRingAuthUrlBuilder { return Uri.parse(baseUrl) .buildUpon() - .appendQueryParameter("nonce", nonce) + .appendQueryParameter(NONCE_PARAM, nonce) .build() .toString() }