From 09d272c1cadf5500e83cd249d6d57a9412222074 Mon Sep 17 00:00:00 2001 From: ovitrif Date: Fri, 16 Oct 2026 10:35:16 +0200 Subject: [PATCH 1/7] fix: keep pubky sign-in links working through a slow or failed restore --- app/src/main/java/to/bitkit/models/Toast.kt | 4 + .../services/PubkyAuthHandlerRegistrar.kt | 15 +++- .../java/to/bitkit/ui/components/ToastView.kt | 30 ++++++- .../ui/shared/toast/ToastQueueManager.kt | 8 ++ .../java/to/bitkit/viewmodels/AppViewModel.kt | 67 ++++++++++++-- app/src/main/res/values/strings.xml | 2 + .../services/PubkyAuthHandlerRegistrarTest.kt | 21 +++++ .../ui/shared/toast/ToastQueueManagerTest.kt | 33 +++++++ .../viewmodels/AppViewModelSendFlowTest.kt | 90 +++++++++++++------ changelog.d/next/1513.fixed.md | 1 + journeys/README.md | 1 + journeys/pubky-auth/README.md | 7 ++ journeys/pubky-auth/signin-cold-start.xml | 26 ++++++ 13 files changed, 267 insertions(+), 38 deletions(-) create mode 100644 app/src/test/java/to/bitkit/ui/shared/toast/ToastQueueManagerTest.kt create mode 100644 changelog.d/next/1513.fixed.md create mode 100644 journeys/pubky-auth/signin-cold-start.xml diff --git a/app/src/main/java/to/bitkit/models/Toast.kt b/app/src/main/java/to/bitkit/models/Toast.kt index 4ff2cd760d..c4bed3e31d 100644 --- a/app/src/main/java/to/bitkit/models/Toast.kt +++ b/app/src/main/java/to/bitkit/models/Toast.kt @@ -7,11 +7,15 @@ data class Toast( val autoHide: Boolean, val visibilityTime: Long = VISIBILITY_TIME_DEFAULT, val testTag: String? = null, + val isLoading: Boolean = false, ) { enum class ToastType { SUCCESS, INFO, LIGHTNING, WARNING, ERROR } companion object { const val VISIBILITY_TIME_SHORT = 1500L const val VISIBILITY_TIME_DEFAULT = 3000L + + /** Visibility for a toast with an action the user must read before it hides. */ + const val VISIBILITY_TIME_LONG = 6000L } } diff --git a/app/src/main/java/to/bitkit/services/PubkyAuthHandlerRegistrar.kt b/app/src/main/java/to/bitkit/services/PubkyAuthHandlerRegistrar.kt index ba19d236f6..973bdb599f 100644 --- a/app/src/main/java/to/bitkit/services/PubkyAuthHandlerRegistrar.kt +++ b/app/src/main/java/to/bitkit/services/PubkyAuthHandlerRegistrar.kt @@ -21,7 +21,11 @@ import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import javax.inject.Singleton -/** Advertises Pubky signup and authorization handlers when their required identity state is available. */ +/** + * Advertises Pubky signup and authorization handlers when their required identity state is available. + * While a saved session is not restored yet, a stored secret key keeps the authorization handler on, so a sign-in link + * can wait for the restore instead of being dropped. Approval still checks the restored identity's key. + */ @Singleton internal class PubkyAuthHandlerRegistrar @Inject constructor( @ApplicationContext private val context: Context, @@ -59,7 +63,7 @@ internal class PubkyAuthHandlerRegistrar @Inject constructor( .onFailure { Logger.warn("Failed to read saved Pubky identity", it, context = TAG) } .getOrDefault(true) val isPaykitUiEnabled = PaykitFeatureFlags.isUiEnabled(localFlagEnabled) - val hasSecretKey = isPaykitUiEnabled && publicKey != null && pubkyRepo.hasSecretKey() + val hasSecretKey = isPaykitUiEnabled && hasLocalSecretKey(publicKey) val authEnabled = canHandlePubkyAuth(isPaykitUiEnabled, hasIdentity, hasSecretKey) authEnabled to (isPaykitUiEnabled && !hasIdentity) } @@ -71,6 +75,13 @@ internal class PubkyAuthHandlerRegistrar @Inject constructor( } } + private suspend fun hasLocalSecretKey(publicKey: String?): Boolean { + if (publicKey != null) return pubkyRepo.hasSecretKey() + return runSuspendCatching { pubkyRepo.hasStoredSecretKey() } + .onFailure { Logger.warn("Failed to read saved Pubky secret key", it, context = TAG) } + .getOrDefault(false) + } + private fun setAliasEnabled(component: ComponentName, enabled: Boolean) { val state = if (enabled) { diff --git a/app/src/main/java/to/bitkit/ui/components/ToastView.kt b/app/src/main/java/to/bitkit/ui/components/ToastView.kt index 08bbee6c7e..3eee0aa6a9 100644 --- a/app/src/main/java/to/bitkit/ui/components/ToastView.kt +++ b/app/src/main/java/to/bitkit/ui/components/ToastView.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.gestures.detectVerticalDragGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.offset @@ -177,10 +178,21 @@ fun ToastView( .fillMaxWidth() .padding(16.dp) ) { - BodyMSB( - text = toast.title, - color = tintColor, - ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + if (toast.isLoading) { + GradientCircularProgressIndicator( + strokeWidth = 2.dp, + modifier = Modifier.size(16.dp) + ) + } + BodyMSB( + text = toast.title, + color = tintColor, + ) + } toast.description?.let { description -> Caption( text = description, @@ -312,6 +324,16 @@ private fun ToastViewPreview() { ), onDismiss = {}, ) + ToastView( + toast = Toast( + type = Toast.ToastType.INFO, + title = "Loading Your Pubky Profile", + description = "Your sign-in request opens when your profile is ready.", + autoHide = false, + isLoading = true, + ), + onDismiss = {}, + ) ToastView( toast = Toast( type = Toast.ToastType.ERROR, diff --git a/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueueManager.kt b/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueueManager.kt index 94af44aa3d..e485175ce3 100644 --- a/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueueManager.kt +++ b/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueueManager.kt @@ -63,6 +63,14 @@ class ToastQueueManager(private val scope: CoroutineScope) { showNextToastIfAvailable() } + /** + * Hide the given toast, if shown or still queued, and leave any other toast in place. + */ + fun dismiss(toast: Toast) { + if (_currentToast.value === toast) return dismissCurrentToast() + _queue.update { queue -> queue.filterNot { it === toast } } + } + /** * Pause current toast timer (called on drag start). */ diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 61bb8a4436..bac97e6a70 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -199,6 +199,7 @@ import to.bitkit.repositories.PendingPaymentResolution import to.bitkit.repositories.PreActivityMetadataRepo import to.bitkit.repositories.PrivatePaykitPaymentContext import to.bitkit.repositories.PrivatePaykitRepo +import to.bitkit.repositories.PubkyIdentityReadiness import to.bitkit.repositories.PubkyRepo import to.bitkit.repositories.PubkySignIn import to.bitkit.repositories.PublicPaykitPaymentResult @@ -336,6 +337,7 @@ class AppViewModel @Inject constructor( @Volatile private var scheduledScan: ScheduledScan? = null + private var pubkyAuthWaitToast: Toast? = null private val deferredScanLock = Any() private var deferredScan: DeferredScan? = null @@ -3064,11 +3066,15 @@ class AppViewModel @Inject constructor( val nextJob = viewModelScope.launch(start = CoroutineStart.LAZY) { clearPaymentRequestPreparation(previousPreparation) scanMutex.withLock { - if (!awaitPubkyDeeplinkInitialization(source, data, allowPubkyAuth)) return@withLock - if (deferLockedScan(deferrable)) return@withLock - prepareContactPaymentContextForScan(normalized, allowPubkyAuth, contactPaymentContext) - if (startDelay > Duration.ZERO) delay(startDelay) - handleScan(data, routePubkyKeys, contactPaymentContext, allowPubkyAuth, suppressQuickPay) + try { + if (!awaitPubkyDeeplinkInitialization(source, data, allowPubkyAuth)) return@withLock + if (deferLockedScan(deferrable)) return@withLock + prepareContactPaymentContextForScan(normalized, allowPubkyAuth, contactPaymentContext) + if (startDelay > Duration.ZERO) delay(startDelay) + handleScan(data, routePubkyKeys, contactPaymentContext, allowPubkyAuth, suppressQuickPay) + } finally { + hidePubkyAuthWaitToast() + } } } val nextScheduledScan = ScheduledScan( @@ -3115,13 +3121,20 @@ class AppViewModel @Inject constructor( } if (!PubkyAuthRequest.isSignupUrl(data) || PubkyAuthRequest.isGrantSignupUrl(data)) { - val isInitializationReady = withTimeoutOrNull(PubkyService.AUTHORIZATION_TIMEOUT) { + if (!isContactLink) showPubkyAuthWaitToastIfRestoring() + val timeout = if (isContactLink) PubkyService.AUTHORIZATION_TIMEOUT else PUBKY_AUTH_DEEPLINK_TIMEOUT + val isInitializationReady = withTimeoutOrNull(timeout) { pubkyRepo.awaitInitialization() - if (!isContactLink) pubkyRepo.awaitIdentityReady() + if (!isContactLink) awaitPubkyAuthIdentity() awaitContactDataForDeeplink(isContactLink) } ?: false if (!isInitializationReady) { Logger.warn("Failed to initialize Pubky deeplink", context = TAG) + if (!isContactLink) { + hidePubkyAuthWaitToast() + showPubkyIdentityUnavailableToast() + return false + } ToastEventBus.send( type = Toast.ToastType.ERROR, title = context.getString( @@ -3141,6 +3154,36 @@ class AppViewModel @Inject constructor( return isPaykitUiEnabledFromSettings() && walletRepo.walletExists() } + private suspend fun awaitPubkyAuthIdentity() { + while (pubkyRepo.awaitIdentityReady() == PubkyIdentityReadiness.Unavailable) { + delay(PUBKY_AUTH_DEEPLINK_RETRY_INTERVAL) + isOnline.first { it == ConnectivityState.CONNECTED } + Logger.debug("Retrying Pubky session restore for a sign-in link", context = TAG) + } + } + + private suspend fun showPubkyAuthWaitToastIfRestoring() { + if (pubkyRepo.publicKey.value != null) return + if (!isPaykitUiEnabledFromSettings() || !walletRepo.walletExists()) return + if (!runSuspendCatching { pubkyRepo.hasIdentity() }.getOrDefault(false)) return + val waitToast = Toast( + type = Toast.ToastType.INFO, + title = context.getString(R.string.pubky_auth__loading_identity), + description = context.getString(R.string.pubky_auth__loading_identity_desc), + autoHide = false, + testTag = PUBKY_AUTH_WAIT_TOAST_TAG, + isLoading = true, + ) + pubkyAuthWaitToast = waitToast + toastManager.enqueue(waitToast) + } + + private fun hidePubkyAuthWaitToast() { + val waitToast = pubkyAuthWaitToast ?: return + pubkyAuthWaitToast = null + toastManager.dismiss(waitToast) + } + private suspend fun awaitContactDataForDeeplink(isContactLink: Boolean): Boolean { if (!isContactLink || pubkyRepo.publicKey.value == null) return true return pubkyRepo.refreshContacts() @@ -7629,6 +7672,7 @@ class AppViewModel @Inject constructor( type = Toast.ToastType.ERROR, title = context.getString(R.string.pubky_auth__identity_unavailable), description = context.getString(R.string.pubky_auth__identity_unavailable_desc), + visibilityTime = Toast.VISIBILITY_TIME_LONG, ) return } @@ -7725,6 +7769,15 @@ class AppViewModel @Inject constructor( private val PAYKIT_PAYMENT_REQUEST_PRESENTATION_RETRY_INTERVAL = 120.seconds private val PUBLIC_PAYKIT_SYNC_DEBOUNCE = 1.seconds private val PUBLIC_PAYKIT_BOLT11_REFRESH_WINDOW = 30.minutes + /** Longest wait for the saved Pubky session before a sign-in link fails. */ + internal val PUBKY_AUTH_DEEPLINK_TIMEOUT = 60.seconds + + /** Pause between Pubky session restore attempts while a sign-in link waits. */ + private val PUBKY_AUTH_DEEPLINK_RETRY_INTERVAL = 3.seconds + + /** Test tag of the toast shown while a sign-in link waits for the Pubky session. */ + internal const val PUBKY_AUTH_WAIT_TOAST_TAG = "PubkyAuthWaitToast" + private const val BITKIT_SCHEME = "bitkit" private const val RECOVERY_MODE_DEEPLINK = "recovery-mode" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fb93e82778..0076334f3d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -748,6 +748,8 @@ Homeserver Couldn\'t Load Your Pubky Profile Check your connection and try again. + Loading Your Pubky Profile + Your sign-in request opens when your profile is ready. Pubky Identity Required Create a Pubky identity in your profile to approve auth requests. Create a new Pubky identity on this homeserver. Only continue if you trust it. diff --git a/app/src/test/java/to/bitkit/services/PubkyAuthHandlerRegistrarTest.kt b/app/src/test/java/to/bitkit/services/PubkyAuthHandlerRegistrarTest.kt index 8e5f659d6d..a1b501cc36 100644 --- a/app/src/test/java/to/bitkit/services/PubkyAuthHandlerRegistrarTest.kt +++ b/app/src/test/java/to/bitkit/services/PubkyAuthHandlerRegistrarTest.kt @@ -55,6 +55,7 @@ class PubkyAuthHandlerRegistrarTest : BaseUnitTest() { whenever(pubkyRepo.backupStateVersion).thenReturn(backupStateVersion) whenever(pubkyRepo.identityRefreshVersion).thenReturn(identityRefreshVersion) whenever(pubkyRepo.hasIdentity()).thenAnswer { publicKey.value != null } + whenever(pubkyRepo.hasStoredSecretKey()).thenReturn(false) } @Test @@ -150,6 +151,26 @@ class PubkyAuthHandlerRegistrarTest : BaseUnitTest() { verifyComponentStates(authEnabled = false, signupEnabled = true) } + @Test + fun `failed restore keeps the handler on for a saved locally managed identity`() = test { + isPaykitEnabled.value = true + whenever(pubkyRepo.hasIdentity()).thenReturn(true) + whenever(pubkyRepo.hasStoredSecretKey()).thenReturn(true) + + createSut().start(backgroundScope) + runCurrent() + + verifyComponentStates(authEnabled = true, signupEnabled = false) + verify(pubkyRepo, never()).hasSecretKey() + + clearInvocations(packageManager) + whenever(pubkyRepo.hasSecretKey()).thenReturn(true) + publicKey.value = "pubkylocal" + runCurrent() + + verify(packageManager, never()).setComponentEnabledSetting(any(), any(), any()) + } + @Test fun `saved identity suppresses signup during restoration failure until explicitly removed`() = test { isPaykitEnabled.value = true diff --git a/app/src/test/java/to/bitkit/ui/shared/toast/ToastQueueManagerTest.kt b/app/src/test/java/to/bitkit/ui/shared/toast/ToastQueueManagerTest.kt new file mode 100644 index 0000000000..e85cb9a1b0 --- /dev/null +++ b/app/src/test/java/to/bitkit/ui/shared/toast/ToastQueueManagerTest.kt @@ -0,0 +1,33 @@ +package to.bitkit.ui.shared.toast + +import org.junit.Test +import to.bitkit.models.Toast +import to.bitkit.test.BaseUnitTest +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class ToastQueueManagerTest : BaseUnitTest() { + private val waitToast = Toast(type = Toast.ToastType.INFO, title = "Loading", autoHide = false, isLoading = true) + private val errorToast = Toast(type = Toast.ToastType.ERROR, title = "Failed", autoHide = false) + + @Test + fun `dismiss hides the given toast when it is shown`() = test { + val sut = ToastQueueManager(backgroundScope) + sut.enqueue(waitToast) + + sut.dismiss(waitToast) + + assertNull(sut.currentToast.value) + } + + @Test + fun `dismiss keeps a toast that replaced the given one`() = test { + val sut = ToastQueueManager(backgroundScope) + sut.enqueue(waitToast) + sut.enqueue(errorToast) + + sut.dismiss(waitToast) + + assertEquals(errorToast, sut.currentToast.value) + } +} diff --git a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt index a722178112..0c790d892c 100644 --- a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt +++ b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt @@ -206,7 +206,6 @@ import to.bitkit.services.MigrationService import to.bitkit.services.NodeServiceFgState import to.bitkit.services.PaykitSdkOperationLock.Priority import to.bitkit.services.PendingChannelMigration -import to.bitkit.services.PubkyService import to.bitkit.test.BaseUnitTest import to.bitkit.ui.Routes import to.bitkit.ui.components.Sheet @@ -4704,26 +4703,26 @@ class AppViewModelSendFlowTest : BaseUnitTest() { advanceUntilIdle() sut.setIsAuthenticated(true) whenever(pubkyRepo.awaitInitialization()).doSuspendableAnswer { awaitCancellation() } - whenever(context.getString(R.string.profile__auth_error_title)).thenReturn("Authorization failed") - whenever(context.getString(R.string.profile__auth_error_timeout)).thenReturn("Authorization timed out") + whenever(pubkyRepo.hasIdentity()).thenReturn(true) + whenever(context.getString(R.string.pubky_auth__identity_unavailable)) + .thenReturn("Couldn't Load Your Pubky Profile") val authUrl = "pubkyauth://signin_grant?caps=/pub/paykit/v0/:rw" val bolt11 = "lnbcrt1posttimeoutscan" stubLightningScan(bolt11 = bolt11, amountSats = 500u) balanceState.value = BalanceState(maxSendLightningSats = 100_000u) sut.handleDeeplinkIntent(Intent(Intent.ACTION_VIEW, authUrl.toUri())) - advanceTimeBy(PubkyService.AUTHORIZATION_TIMEOUT.inWholeMilliseconds) + advanceTimeBy(AppViewModel.PUBKY_AUTH_DEEPLINK_TIMEOUT.inWholeMilliseconds) runCurrent() assertNull(sut.currentSheet.value) verify(pubkyRepo, never()).hasSecretKey() - verify(toastManager).enqueue( - check { - assertEquals(Toast.ToastType.ERROR, it.type) - assertEquals("Authorization failed", it.title) - assertEquals("Authorization timed out", it.description) - } - ) + val errorToast = argumentCaptor().run { + verify(toastManager, times(2)).enqueue(capture()) + lastValue + } + assertEquals(Toast.ToastType.ERROR, errorToast.type) + assertEquals("Couldn't Load Your Pubky Profile", errorToast.title) sut.onScanResult(bolt11) advanceUntilIdle() @@ -4944,19 +4943,51 @@ class AppViewModelSendFlowTest : BaseUnitTest() { pubkyPublicKey.value = null retry = CompletableDeferred() + clearInvocations(toastManager) sut.handleDeeplinkIntent(Intent(Intent.ACTION_VIEW, authUrl.toUri())) runCurrent() assertNull(sut.currentSheet.value) - verify(toastManager, never()).enqueue(any()) + val waitToast = argumentCaptor().run { + verify(toastManager).enqueue(capture()) + firstValue + } + assertTrue(waitToast.isLoading) + assertFalse(waitToast.autoHide) + assertEquals(AppViewModel.PUBKY_AUTH_WAIT_TOAST_TAG, waitToast.testTag) + verify(toastManager, never()).dismiss(any()) retry.complete(Unit) advanceUntilIdle() assertEquals(Sheet.PubkyAuth(authUrl), sut.currentSheet.value) - verify(toastManager, never()).enqueue(any()) + verify(toastManager).dismiss(waitToast) + verify(toastManager).enqueue(any()) } } + @Test + fun `cold pubky auth deeplink retries a deferred session restore until it succeeds`() = test { + enablePaykitUi() + advanceUntilIdle() + whenever(pubkyRepo.hasIdentity()).thenReturn(true) + whenever(pubkyRepo.hasSecretKey()).thenReturn(true) + var attempts = 0 + whenever(pubkyRepo.awaitIdentityReady()).doSuspendableAnswer { + attempts++ + if (attempts < 3) return@doSuspendableAnswer PubkyIdentityReadiness.Unavailable + pubkyPublicKey.value = testPublicKey + PubkyIdentityReadiness.Ready + } + val authUrl = "pubkyauth://signin_grant?caps=/pub/paykit/v0/:rw" + + sut.handleDeeplinkIntent(Intent(Intent.ACTION_VIEW, authUrl.toUri())) + advanceUntilIdle() + + assertEquals(3, attempts) + assertEquals(Sheet.PubkyAuth(authUrl), sut.currentSheet.value) + verify(toastManager).dismiss(check { assertTrue(it.isLoading) }) + } + @Test fun `pubky auth deeplink shows a retryable error for a saved identity it cannot restore`() = test { enablePaykitUi() @@ -4974,16 +5005,19 @@ class AppViewModelSendFlowTest : BaseUnitTest() { advanceUntilIdle() assertNull(sut.currentSheet.value) - verify(pubkyRepo).awaitIdentityReady() + verify(pubkyRepo, atLeast(2)).awaitIdentityReady() verify(context, never()).getString(R.string.pubky_auth__no_identity) verify(context, never()).getString(R.string.pubky_auth__already_signed_in) - verify(toastManager).enqueue( - check { - assertEquals(Toast.ToastType.ERROR, it.type) - assertEquals("Couldn't Load Your Pubky Profile", it.title) - assertEquals("Check your connection and try again.", it.description) - } - ) + val (waitToast, errorToast) = argumentCaptor().run { + verify(toastManager, times(2)).enqueue(capture()) + allValues + } + assertTrue(waitToast.isLoading) + verify(toastManager).dismiss(waitToast) + assertEquals(Toast.ToastType.ERROR, errorToast.type) + assertEquals("Couldn't Load Your Pubky Profile", errorToast.title) + assertEquals("Check your connection and try again.", errorToast.description) + assertEquals(Toast.VISIBILITY_TIME_LONG, errorToast.visibilityTime) } } @@ -5013,20 +5047,26 @@ class AppViewModelSendFlowTest : BaseUnitTest() { enablePaykitUi() advanceUntilIdle() sut.setIsAuthenticated(true) + whenever(pubkyRepo.hasIdentity()).thenReturn(true) whenever(pubkyRepo.awaitIdentityReady()).doSuspendableAnswer { awaitCancellation() } - whenever(context.getString(R.string.profile__auth_error_title)).thenReturn("Authorization failed") - whenever(context.getString(R.string.profile__auth_error_timeout)).thenReturn("Authorization timed out") + whenever(context.getString(R.string.pubky_auth__identity_unavailable_desc)) + .thenReturn("Check your connection and try again.") val bolt11 = "lnbcrt1postretrytimeoutscan" stubLightningScan(bolt11 = bolt11, amountSats = 500u) balanceState.value = BalanceState(maxSendLightningSats = 100_000u) sut.handleDeeplinkIntent(Intent(Intent.ACTION_VIEW, "pubkyauth://signin_grant".toUri())) - advanceTimeBy(PubkyService.AUTHORIZATION_TIMEOUT.inWholeMilliseconds) + advanceTimeBy(AppViewModel.PUBKY_AUTH_DEEPLINK_TIMEOUT.inWholeMilliseconds) runCurrent() assertNull(sut.currentSheet.value) verify(pubkyRepo, never()).hasSecretKey() - verify(toastManager).enqueue(check { assertEquals("Authorization timed out", it.description) }) + val enqueued = argumentCaptor().run { + verify(toastManager, times(2)).enqueue(capture()) + allValues + } + verify(toastManager).dismiss(enqueued.first()) + assertEquals("Check your connection and try again.", enqueued.last().description) sut.onScanResult(bolt11) advanceUntilIdle() diff --git a/changelog.d/next/1513.fixed.md b/changelog.d/next/1513.fixed.md new file mode 100644 index 0000000000..cfcb089543 --- /dev/null +++ b/changelog.d/next/1513.fixed.md @@ -0,0 +1 @@ +Fixed Pubky sign-in links closing Bitkit or doing nothing while it starts; Bitkit now shows that it is loading your profile until the request opens. diff --git a/journeys/README.md b/journeys/README.md index 8f9d16b3ae..84a4f407e1 100644 --- a/journeys/README.md +++ b/journeys/README.md @@ -172,6 +172,7 @@ Known differences in the corpus, as of the iOS port (synonymdev/bitkit-ios#691): | `onchain-receive/*` | port pending in synonymdev/bitkit-ios#588, which wires `onchainTransactionConfirmed` into the same received-sheet flow but carries no `journeys/` files. Two adaptations when it lands: iOS suppresses replayed historical receives with a `pendingRestoreActivitySeen` flag cleared by the first post-restore on-chain sync, not the one-hour block-timestamp guard used here, so a stale-confirmation step has to drive a restore instead of a clock; and iOS has no foreground-service path, so `confirmed-only-background-notification.xml` has no counterpart | | `backup/show-mnemonic-long-words.xml` | not ported — the long-word fit is an Android-only change (synonymdev/bitkit-android#633); whether iOS wraps long words at larger text sizes is unchecked | | `deeplinks/pubky-contact.xml` | ported — same contact routing and unlock behavior | +| `pubky-auth/signin-cold-start.xml` | not ported — iOS does not register the raw `pubkyauth` scheme for OS link delivery | | `pubky-profile/cached-profile-header.xml` | same file, journey name and steps — one step differs: Android adds a screenshot check of the cached avatar, which comes from the Pubky image disk cache synonymdev/bitkit-android#1399 adds | | `pubky-profile/ring-choice-rows.xml` | same file, journey name and steps — Android names the rows and their lookup spinners differently (see [Identifiers](#identifiers)), checks that rows are disabled by tapping another row instead of reading the snapshot's Targets, needs no second tap on the profile button, and names the import overview by its "Import" title | | `pubky-profile/contact-import-after-leaving.xml` | same file, journey name and steps — Android names the Ring rows differently and has no identifiers on the import overview's profile and friend count, so those steps name the screen text here; see [Identifiers](#identifiers). synonymdev/bitkit-android#1399 adds `ContactImportOverviewImportAll`, the only contact import testTag the journeys use, under its iOS name | diff --git a/journeys/pubky-auth/README.md b/journeys/pubky-auth/README.md index f9c30e1a5c..8f36f71fd4 100644 --- a/journeys/pubky-auth/README.md +++ b/journeys/pubky-auth/README.md @@ -1,5 +1,12 @@ # Pubky authorization +## Sign-in from a link + +- `signin-cold-start.xml` checks a `pubkyauth://signin_grant` link opened while Bitkit is not running: + the loading toast while the saved session restores, the authorization sheet, and a restore that + fails while offline, which keeps Bitkit open and the link handler enabled. Android only; iOS does + not register the raw `pubkyauth` scheme for OS link delivery. + ## Grant signup - `grant-signup.xml` checks consent, cancellation, homeserver registration, and profile setup for a fresh identity. diff --git a/journeys/pubky-auth/signin-cold-start.xml b/journeys/pubky-auth/signin-cold-start.xml new file mode 100644 index 0000000000..422ec88815 --- /dev/null +++ b/journeys/pubky-auth/signin-cold-start.xml @@ -0,0 +1,26 @@ + + + Verifies that a sign-in link opened while Bitkit is not running waits visibly for the saved Pubky + session, survives a failed session restore, and opens the authorization sheet. Precondition: an + unlocked wallet with Paykit enabled and a locally managed Pubky identity, a requesting app (the + Pubky App sign-in page) that shows a fresh pubkyauth://signin_grant link in the device browser, + and a reachable homeserver and relay. Android only: iOS does not register the raw pubkyauth scheme + for OS link delivery. + + + Run adb shell am force-stop to.bitkit.dev + Tap the requesting app's sign-in link in the browser + Unlock Bitkit with PIN or biometrics when prompted + Verify a toast with a spinner reads Loading Your Pubky Profile (screenshot; toasts are not in the layout) + Verify the toast stays until the authorization sheet opens + Verify the authorization sheet shows the requesting app and permissions + Tap Authorize (id "PubkyAuthAuthorize") and complete PIN or biometric approval when enabled + Verify the authorization success state is shown (id "PubkyAuthOK") + Run adb shell am force-stop to.bitkit.dev and turn off the device network + Tap a fresh sign-in link in the browser and unlock Bitkit + Verify Bitkit stays open and the Loading Your Pubky Profile toast is shown + Turn the device network back on within 30 seconds + Verify the authorization sheet opens without tapping the link again + Run adb shell pm dump to.bitkit.dev and verify to.bitkit.ui.MainActivityPubkyAuth is not listed as disabled + + From 8cc76f241582b31fcf7ba5b6e442529eafcce0d8 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Oct 2026 11:03:05 +0200 Subject: [PATCH 2/7] fix: bring the sign-in wait toast back after other toasts --- .../ui/shared/toast/ToastQueueManager.kt | 12 ++++-------- .../ui/shared/toast/ToastQueueManagerTest.kt | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueueManager.kt b/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueueManager.kt index e485175ce3..194f5df3b3 100644 --- a/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueueManager.kt +++ b/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueueManager.kt @@ -37,17 +37,13 @@ class ToastQueueManager(private val scope: CoroutineScope) { private var isPaused = false /** - * Add toast to queue. If queue is full, drops oldest. + * Add toast to queue. If queue is full, drops oldest. A shown loading toast returns after the new one. */ fun enqueue(toast: Toast) { + val interruptedLoading = _currentToast.value?.takeIf { it.isLoading } _queue.update { current -> - val newQueue = if (current.size >= MAX_QUEUE_SIZE) { - // Drop oldest (first item) when queue full - current.drop(1) + toast - } else { - current + toast - } - newQueue + // Drop oldest when queue full + (current + toast + listOfNotNull(interruptedLoading)).takeLast(MAX_QUEUE_SIZE) } dismissCurrentToast() } diff --git a/app/src/test/java/to/bitkit/ui/shared/toast/ToastQueueManagerTest.kt b/app/src/test/java/to/bitkit/ui/shared/toast/ToastQueueManagerTest.kt index e85cb9a1b0..73a70d81a3 100644 --- a/app/src/test/java/to/bitkit/ui/shared/toast/ToastQueueManagerTest.kt +++ b/app/src/test/java/to/bitkit/ui/shared/toast/ToastQueueManagerTest.kt @@ -1,11 +1,14 @@ package to.bitkit.ui.shared.toast +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceTimeBy import org.junit.Test import to.bitkit.models.Toast import to.bitkit.test.BaseUnitTest import kotlin.test.assertEquals import kotlin.test.assertNull +@OptIn(ExperimentalCoroutinesApi::class) class ToastQueueManagerTest : BaseUnitTest() { private val waitToast = Toast(type = Toast.ToastType.INFO, title = "Loading", autoHide = false, isLoading = true) private val errorToast = Toast(type = Toast.ToastType.ERROR, title = "Failed", autoHide = false) @@ -29,5 +32,20 @@ class ToastQueueManagerTest : BaseUnitTest() { sut.dismiss(waitToast) assertEquals(errorToast, sut.currentToast.value) + sut.dismissCurrentToast() + assertNull(sut.currentToast.value) + } + + @Test + fun `loading toast returns after a toast that interrupts it`() = test { + val sut = ToastQueueManager(backgroundScope) + val infoToast = Toast(type = Toast.ToastType.SUCCESS, title = "Back online", autoHide = true) + sut.enqueue(waitToast) + sut.enqueue(infoToast) + assertEquals(infoToast, sut.currentToast.value) + + advanceTimeBy(Toast.VISIBILITY_TIME_DEFAULT + 1) + + assertEquals(waitToast, sut.currentToast.value) } } From 655881b0228d27c66df2d2ceb93454f6d4529772 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Oct 2026 11:03:34 +0200 Subject: [PATCH 3/7] chore: rename changelog fragment --- changelog.d/next/{1513.fixed.md => 1514.fixed.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/next/{1513.fixed.md => 1514.fixed.md} (100%) diff --git a/changelog.d/next/1513.fixed.md b/changelog.d/next/1514.fixed.md similarity index 100% rename from changelog.d/next/1513.fixed.md rename to changelog.d/next/1514.fixed.md From 080913cfebbb4c78636cdea620e6032e34c43bad Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Oct 2026 11:10:32 +0200 Subject: [PATCH 4/7] fix: leave ring-sourced identities out of the pending sign-in handler --- .../main/java/to/bitkit/repositories/PubkyRepo.kt | 5 +++++ .../to/bitkit/services/PubkyAuthHandlerRegistrar.kt | 6 +++--- .../java/to/bitkit/repositories/PubkyRepoTest.kt | 13 +++++++++++++ .../services/PubkyAuthHandlerRegistrarTest.kt | 4 ++-- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt b/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt index 93ed5efc3c..bb5a2360ca 100644 --- a/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt @@ -1527,6 +1527,11 @@ class PubkyRepo @Inject constructor( keychain.exists(Keychain.Key.PUBKY_SECRET_KEY.name) } + /** Whether Bitkit stores its own key for the saved identity, read without restoring the session. */ + suspend fun hasLocalSecretKey(): Boolean = withContext(ioDispatcher) { + keychain.exists(Keychain.Key.PUBKY_SECRET_KEY.name) && !keychain.exists(Keychain.Key.SHARED_PUBKY_SOURCE.name) + } + suspend fun hasIdentity(): Boolean = withContext(ioDispatcher) { _publicKey.value != null || !keychain.loadString(Keychain.Key.PAYKIT_SESSION.name).isNullOrEmpty() || diff --git a/app/src/main/java/to/bitkit/services/PubkyAuthHandlerRegistrar.kt b/app/src/main/java/to/bitkit/services/PubkyAuthHandlerRegistrar.kt index 973bdb599f..4328f3505d 100644 --- a/app/src/main/java/to/bitkit/services/PubkyAuthHandlerRegistrar.kt +++ b/app/src/main/java/to/bitkit/services/PubkyAuthHandlerRegistrar.kt @@ -23,8 +23,8 @@ import javax.inject.Singleton /** * Advertises Pubky signup and authorization handlers when their required identity state is available. - * While a saved session is not restored yet, a stored secret key keeps the authorization handler on, so a sign-in link - * can wait for the restore instead of being dropped. Approval still checks the restored identity's key. + * While a saved session is not restored yet, a secret key Bitkit stores itself keeps the authorization handler on, so a + * sign-in link can wait for the restore instead of being dropped. Approval still checks the restored identity's key. */ @Singleton internal class PubkyAuthHandlerRegistrar @Inject constructor( @@ -77,7 +77,7 @@ internal class PubkyAuthHandlerRegistrar @Inject constructor( private suspend fun hasLocalSecretKey(publicKey: String?): Boolean { if (publicKey != null) return pubkyRepo.hasSecretKey() - return runSuspendCatching { pubkyRepo.hasStoredSecretKey() } + return runSuspendCatching { pubkyRepo.hasLocalSecretKey() } .onFailure { Logger.warn("Failed to read saved Pubky secret key", it, context = TAG) } .getOrDefault(false) } diff --git a/app/src/test/java/to/bitkit/repositories/PubkyRepoTest.kt b/app/src/test/java/to/bitkit/repositories/PubkyRepoTest.kt index 4f379d4058..23aadaf43f 100644 --- a/app/src/test/java/to/bitkit/repositories/PubkyRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/PubkyRepoTest.kt @@ -1772,6 +1772,19 @@ class PubkyRepoTest : BaseUnitTest() { assertNull(result.getOrNull()) } + @Test + fun `hasLocalSecretKey is true only for a stored key without a Ring source`() = test { + whenever(keychain.exists(Keychain.Key.PUBKY_SECRET_KEY.name)).thenReturn(true) + assertTrue(sut.hasLocalSecretKey()) + + whenever(keychain.exists(Keychain.Key.SHARED_PUBKY_SOURCE.name)).thenReturn(true) + assertFalse(sut.hasLocalSecretKey()) + + whenever(keychain.exists(Keychain.Key.PUBKY_SECRET_KEY.name)).thenReturn(false) + whenever(keychain.exists(Keychain.Key.SHARED_PUBKY_SOURCE.name)).thenReturn(false) + assertFalse(sut.hasLocalSecretKey()) + } + @Test fun `adoptRingIdentity should reject a mismatching credential without changing the source`() = test { val ringPubky = VALID_SELF_KEY.removePrefix("pubky") diff --git a/app/src/test/java/to/bitkit/services/PubkyAuthHandlerRegistrarTest.kt b/app/src/test/java/to/bitkit/services/PubkyAuthHandlerRegistrarTest.kt index a1b501cc36..1447fe2eb2 100644 --- a/app/src/test/java/to/bitkit/services/PubkyAuthHandlerRegistrarTest.kt +++ b/app/src/test/java/to/bitkit/services/PubkyAuthHandlerRegistrarTest.kt @@ -55,7 +55,7 @@ class PubkyAuthHandlerRegistrarTest : BaseUnitTest() { whenever(pubkyRepo.backupStateVersion).thenReturn(backupStateVersion) whenever(pubkyRepo.identityRefreshVersion).thenReturn(identityRefreshVersion) whenever(pubkyRepo.hasIdentity()).thenAnswer { publicKey.value != null } - whenever(pubkyRepo.hasStoredSecretKey()).thenReturn(false) + whenever(pubkyRepo.hasLocalSecretKey()).thenReturn(false) } @Test @@ -155,7 +155,7 @@ class PubkyAuthHandlerRegistrarTest : BaseUnitTest() { fun `failed restore keeps the handler on for a saved locally managed identity`() = test { isPaykitEnabled.value = true whenever(pubkyRepo.hasIdentity()).thenReturn(true) - whenever(pubkyRepo.hasStoredSecretKey()).thenReturn(true) + whenever(pubkyRepo.hasLocalSecretKey()).thenReturn(true) createSut().start(backgroundScope) runCurrent() From 0410b34a0ffa360279d3cc6b366deb6c86c0ea35 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Oct 2026 11:10:32 +0200 Subject: [PATCH 5/7] docs: keep the cold-start sign-in journey within journey capabilities --- journeys/pubky-auth/README.md | 7 ++++--- journeys/pubky-auth/signin-cold-start.xml | 18 ++++++------------ 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/journeys/pubky-auth/README.md b/journeys/pubky-auth/README.md index 8f36f71fd4..cb33249896 100644 --- a/journeys/pubky-auth/README.md +++ b/journeys/pubky-auth/README.md @@ -3,9 +3,10 @@ ## Sign-in from a link - `signin-cold-start.xml` checks a `pubkyauth://signin_grant` link opened while Bitkit is not running: - the loading toast while the saved session restores, the authorization sheet, and a restore that - fails while offline, which keeps Bitkit open and the link handler enabled. Android only; iOS does - not register the raw `pubkyauth` scheme for OS link delivery. + the loading toast while the saved session restores, then the authorization sheet. Android only; + iOS does not register the raw `pubkyauth` scheme for OS link delivery. A restore that fails while + the device is offline needs the device network toggled, which the journey environment does not + provide, so that case is a manual test. ## Grant signup diff --git a/journeys/pubky-auth/signin-cold-start.xml b/journeys/pubky-auth/signin-cold-start.xml index 422ec88815..b27d8e55ba 100644 --- a/journeys/pubky-auth/signin-cold-start.xml +++ b/journeys/pubky-auth/signin-cold-start.xml @@ -1,26 +1,20 @@ Verifies that a sign-in link opened while Bitkit is not running waits visibly for the saved Pubky - session, survives a failed session restore, and opens the authorization sheet. Precondition: an - unlocked wallet with Paykit enabled and a locally managed Pubky identity, a requesting app (the - Pubky App sign-in page) that shows a fresh pubkyauth://signin_grant link in the device browser, - and a reachable homeserver and relay. Android only: iOS does not register the raw pubkyauth scheme - for OS link delivery. + session and then opens the authorization sheet. Precondition: an unlocked wallet with Paykit + enabled and a locally managed Pubky identity, and a requesting app that supplies a fresh + pubkyauth://signin_grant URL with a reachable homeserver and relay. Android only: iOS does not + register the raw pubkyauth scheme for OS link delivery. Run adb shell am force-stop to.bitkit.dev - Tap the requesting app's sign-in link in the browser + Run adb shell am start -a android.intent.action.VIEW -d "<the prepared signin_grant URL>" Unlock Bitkit with PIN or biometrics when prompted Verify a toast with a spinner reads Loading Your Pubky Profile (screenshot; toasts are not in the layout) Verify the toast stays until the authorization sheet opens Verify the authorization sheet shows the requesting app and permissions Tap Authorize (id "PubkyAuthAuthorize") and complete PIN or biometric approval when enabled Verify the authorization success state is shown (id "PubkyAuthOK") - Run adb shell am force-stop to.bitkit.dev and turn off the device network - Tap a fresh sign-in link in the browser and unlock Bitkit - Verify Bitkit stays open and the Loading Your Pubky Profile toast is shown - Turn the device network back on within 30 seconds - Verify the authorization sheet opens without tapping the link again - Run adb shell pm dump to.bitkit.dev and verify to.bitkit.ui.MainActivityPubkyAuth is not listed as disabled + Verify the requesting app receives approval from the recorded public key From f8bd4ecd1724414de99454975cb3ab0faaeee483 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Oct 2026 11:10:37 +0200 Subject: [PATCH 6/7] fix: keep the sign-in wait toast behind every new toast --- .../bitkit/ui/shared/toast/ToastQueueManager.kt | 5 +++-- .../ui/shared/toast/ToastQueueManagerTest.kt | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueueManager.kt b/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueueManager.kt index 194f5df3b3..eabf11320d 100644 --- a/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueueManager.kt +++ b/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueueManager.kt @@ -37,13 +37,14 @@ class ToastQueueManager(private val scope: CoroutineScope) { private var isPaused = false /** - * Add toast to queue. If queue is full, drops oldest. A shown loading toast returns after the new one. + * Add toast to queue. If queue is full, drops oldest. A shown or queued loading toast returns after the new one. */ fun enqueue(toast: Toast) { val interruptedLoading = _currentToast.value?.takeIf { it.isLoading } _queue.update { current -> + val (loading, others) = current.partition { it.isLoading } // Drop oldest when queue full - (current + toast + listOfNotNull(interruptedLoading)).takeLast(MAX_QUEUE_SIZE) + (others + toast + loading + listOfNotNull(interruptedLoading)).takeLast(MAX_QUEUE_SIZE) } dismissCurrentToast() } diff --git a/app/src/test/java/to/bitkit/ui/shared/toast/ToastQueueManagerTest.kt b/app/src/test/java/to/bitkit/ui/shared/toast/ToastQueueManagerTest.kt index 73a70d81a3..e3962d86a0 100644 --- a/app/src/test/java/to/bitkit/ui/shared/toast/ToastQueueManagerTest.kt +++ b/app/src/test/java/to/bitkit/ui/shared/toast/ToastQueueManagerTest.kt @@ -48,4 +48,19 @@ class ToastQueueManagerTest : BaseUnitTest() { assertEquals(waitToast, sut.currentToast.value) } + + @Test + fun `loading toast stays behind a second toast that interrupts it`() = test { + val sut = ToastQueueManager(backgroundScope) + val firstToast = Toast(type = Toast.ToastType.SUCCESS, title = "Back online", autoHide = true) + val secondToast = Toast(type = Toast.ToastType.INFO, title = "Payment received", autoHide = true) + sut.enqueue(waitToast) + sut.enqueue(firstToast) + sut.enqueue(secondToast) + assertEquals(secondToast, sut.currentToast.value) + + advanceTimeBy(Toast.VISIBILITY_TIME_DEFAULT + 1) + + assertEquals(waitToast, sut.currentToast.value) + } } From d790f3440f37c5ba3a47bb5943715724e0430664 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Oct 2026 16:51:19 +0200 Subject: [PATCH 7/7] docs: point the cold-start sign-in journey at its ios counterpart --- journeys/README.md | 2 +- journeys/pubky-auth/README.md | 6 ++++-- journeys/pubky-auth/signin-cold-start.xml | 5 +++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/journeys/README.md b/journeys/README.md index 84a4f407e1..ef9fd7367f 100644 --- a/journeys/README.md +++ b/journeys/README.md @@ -172,7 +172,7 @@ Known differences in the corpus, as of the iOS port (synonymdev/bitkit-ios#691): | `onchain-receive/*` | port pending in synonymdev/bitkit-ios#588, which wires `onchainTransactionConfirmed` into the same received-sheet flow but carries no `journeys/` files. Two adaptations when it lands: iOS suppresses replayed historical receives with a `pendingRestoreActivitySeen` flag cleared by the first post-restore on-chain sync, not the one-hour block-timestamp guard used here, so a stale-confirmation step has to drive a restore instead of a clock; and iOS has no foreground-service path, so `confirmed-only-background-notification.xml` has no counterpart | | `backup/show-mnemonic-long-words.xml` | not ported — the long-word fit is an Android-only change (synonymdev/bitkit-android#633); whether iOS wraps long words at larger text sizes is unchecked | | `deeplinks/pubky-contact.xml` | ported — same contact routing and unlock behavior | -| `pubky-auth/signin-cold-start.xml` | not ported — iOS does not register the raw `pubkyauth` scheme for OS link delivery | +| `pubky-auth/signin-cold-start.xml` | `pubky-auth/open-signin-link.xml`, adapted — iOS opens the same sign-in link on a terminated app through `bitkit://pubky-auth/signin`. Android only: the raw `pubkyauth` scheme, the "Loading Your Pubky Profile" toast while the session restores, and Authorize through to `PubkyAuthOK` | | `pubky-profile/cached-profile-header.xml` | same file, journey name and steps — one step differs: Android adds a screenshot check of the cached avatar, which comes from the Pubky image disk cache synonymdev/bitkit-android#1399 adds | | `pubky-profile/ring-choice-rows.xml` | same file, journey name and steps — Android names the rows and their lookup spinners differently (see [Identifiers](#identifiers)), checks that rows are disabled by tapping another row instead of reading the snapshot's Targets, needs no second tap on the profile button, and names the import overview by its "Import" title | | `pubky-profile/contact-import-after-leaving.xml` | same file, journey name and steps — Android names the Ring rows differently and has no identifiers on the import overview's profile and friend count, so those steps name the screen text here; see [Identifiers](#identifiers). synonymdev/bitkit-android#1399 adds `ContactImportOverviewImportAll`, the only contact import testTag the journeys use, under its iOS name | diff --git a/journeys/pubky-auth/README.md b/journeys/pubky-auth/README.md index cb33249896..0917a92dfb 100644 --- a/journeys/pubky-auth/README.md +++ b/journeys/pubky-auth/README.md @@ -3,8 +3,10 @@ ## Sign-in from a link - `signin-cold-start.xml` checks a `pubkyauth://signin_grant` link opened while Bitkit is not running: - the loading toast while the saved session restores, then the authorization sheet. Android only; - iOS does not register the raw `pubkyauth` scheme for OS link delivery. A restore that fails while + the loading toast while the saved session restores, then the authorization sheet. The iOS + counterpart is `open-signin-link.xml`, which opens the same flow through `bitkit://pubky-auth/signin`; + only Android has the raw `pubkyauth` scheme, the loading toast and Authorize through to + `PubkyAuthOK`. A restore that fails while the device is offline needs the device network toggled, which the journey environment does not provide, so that case is a manual test. diff --git a/journeys/pubky-auth/signin-cold-start.xml b/journeys/pubky-auth/signin-cold-start.xml index b27d8e55ba..2c038ee736 100644 --- a/journeys/pubky-auth/signin-cold-start.xml +++ b/journeys/pubky-auth/signin-cold-start.xml @@ -3,8 +3,9 @@ Verifies that a sign-in link opened while Bitkit is not running waits visibly for the saved Pubky session and then opens the authorization sheet. Precondition: an unlocked wallet with Paykit enabled and a locally managed Pubky identity, and a requesting app that supplies a fresh - pubkyauth://signin_grant URL with a reachable homeserver and relay. Android only: iOS does not - register the raw pubkyauth scheme for OS link delivery. + pubkyauth://signin_grant URL with a reachable homeserver and relay. The iOS counterpart is + pubky-auth/open-signin-link.xml (bitkit://pubky-auth/signin); only Android has the raw pubkyauth + scheme, the loading toast and Authorize through to PubkyAuthOK. Run adb shell am force-stop to.bitkit.dev