feat(ui): leaving one profile among several
Phase 7 of docs/multiple-profiles.md. Small, because the forget sequence was built right. Sign out of a read-only identity and "forget this key" for a bare key both run ForgetIdentity and then the tail -- re-list, clear the default since Phase 3, show the selector with what remains or Landing if nothing does -- and that is the right behaviour with several profiles as well; nothing in the sequence changes. A profile with a wallet attached answers WalletAttached from Phase 1 and is offered neither exit: its sign-out row stays the pending route, for the reason both sign-in plans gave, and it is now also the only way such a profile can leave, since the repair would write its credential back. What changes is that the user is no longer stuck behind it -- "switch profile" is the row above. The not-found screen learns about the others. It offers "try again" and, for a read-only identity, "use a different key", which forgets it; with another profile on the device it now offers "switch profile" as well, for every kind, pushing the switcher -- which has a back button, so the screen is still there if the user changes their mind. The identity that was not found stays listed; forgetting it remains a separate decision. The nav host passes the callback only when the listing holds more than one. Tests: ReadOnlyEntrancesJvmTest -- with another profile on the device the not-found state offers the switch beside the read-only exit and it calls through once; a signing identity that was not found gets it above the set-up form; with one profile there is nothing to switch to. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Pulled-From: curated/curated@9bb340042d
This commit is contained in:
@@ -43,6 +43,7 @@ import mantra.composeapp.generated.resources.set_up_a_profile
|
||||
import mantra.composeapp.generated.resources.try_again
|
||||
import mantra.composeapp.generated.resources.use_a_different_key
|
||||
import mantra.composeapp.generated.resources.use_a_different_key_question
|
||||
import mantra.composeapp.generated.resources.switch_profile
|
||||
import mantra.composeapp.generated.resources.could_not_sign_out_please_try_again
|
||||
import press.mantra.compose.ui.composable.widgets.dialogs.SignOutOfReadOnlyDialog
|
||||
import press.mantra.compose.ui.composable.widgets.rememberNotifier
|
||||
@@ -82,6 +83,10 @@ fun UnsyncedProfileScreen(
|
||||
// act as signing out, reached from the other end of the identity's life. Null where
|
||||
// there is no identity to leave (previews), in which case that kind has no exit here.
|
||||
signOut: SignOutDependencies? = null,
|
||||
// The third exit, for every kind: another profile on the device to go to instead.
|
||||
// Null when there is none, in which case the row is not drawn. The identity that was
|
||||
// not found stays listed; forgetting it remains a separate decision.
|
||||
onSwitchProfile: (() -> Unit)? = null,
|
||||
) {
|
||||
val viewModel: UnsyncedProfileViewModel = viewModel(
|
||||
factory = UnsyncedProfileViewModel.factory(
|
||||
@@ -132,6 +137,7 @@ fun UnsyncedProfileScreen(
|
||||
viewModel = viewModel,
|
||||
unsignedNostrEventId = state.unsignedNostrEventId,
|
||||
onUseADifferentKey = signOutViewModel?.let { { it.askToSignOut() } },
|
||||
onSwitchProfile = onSwitchProfile,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -190,6 +196,8 @@ private fun NotFound(
|
||||
unsignedNostrEventId: Long,
|
||||
/** Shown only where the set-up form is not; null when there is no identity to leave. */
|
||||
onUseADifferentKey: (() -> Unit)?,
|
||||
/** Shown for every kind, when there is another profile to go to. */
|
||||
onSwitchProfile: (() -> Unit)?,
|
||||
) {
|
||||
val nameField = viewModel.formState.nameField.textFieldState
|
||||
val biographyField = viewModel.formState.biographyField.textFieldState
|
||||
@@ -222,6 +230,19 @@ private fun NotFound(
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
|
||||
// With other profiles on the device, a way to one of them -- for every kind, since
|
||||
// the wait is the same whichever kind is waiting. See docs/multiple-profiles.md,
|
||||
// Phase 7.
|
||||
if (onSwitchProfile != null) {
|
||||
TextButton(
|
||||
modifier = Modifier.align(Alignment.End),
|
||||
enabled = !isActionPending,
|
||||
onClick = onSwitchProfile,
|
||||
) {
|
||||
Text(text = stringResource(Res.string.switch_profile))
|
||||
}
|
||||
}
|
||||
|
||||
// Setting a profile up is signing a kind 0, which a read-only identity cannot do.
|
||||
// The form is not offered to it -- and then this state would have no exit: the
|
||||
// profile tab is not reachable before ProfileLoaded, so a user whose npub was not
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -757,10 +758,16 @@ fun MantraNavHost(
|
||||
}
|
||||
composable<UnsyncedProfileRoute> { backStackEntry ->
|
||||
val route = backStackEntry.toRoute<UnsyncedProfileRoute>()
|
||||
val identities by sovereignWalletViewModel.availableIdentities.collectAsState()
|
||||
UnsyncedProfileScreen(
|
||||
unsyncedProfilePublicKey = route.publicKey,
|
||||
nostrRepository = databaseNostrRepository,
|
||||
signOut = signOutOfReadOnlyIdentity,
|
||||
// Another profile to go to, when there is one; the switcher has a back
|
||||
// button, so this screen is still there if the user changes their mind.
|
||||
onSwitchProfile = if (identities.size > 1) {
|
||||
{ navController.navigate(route = ProfilesRoute) }
|
||||
} else null,
|
||||
)
|
||||
}
|
||||
composable<UnqueuedProfileSynchronizationRoute> { backStackEntry ->
|
||||
|
||||
@@ -45,6 +45,7 @@ import press.mantra.compose.ui.theme.MantraTheme
|
||||
import press.mantra.compose.ui.view.state.ActiveProfileUIState
|
||||
import press.mantra.compose.ui.view.state.HomeScreenUIState
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
/**
|
||||
* The inventory in Phase 5 of docs/npub-sign-in.md, as assertions.
|
||||
@@ -154,6 +155,36 @@ class ReadOnlyEntrancesJvmTest {
|
||||
onNodeWithText("Use a different key?", useUnmergedTree = true).assertIsDisplayed()
|
||||
}
|
||||
|
||||
// --- Not found: the third exit, when there is another profile to go to ---
|
||||
|
||||
@Test
|
||||
fun `with another profile on the device the not-found state offers to switch, for either kind`() = runDesktopComposeUiTest(400, 1200) {
|
||||
var switched = 0
|
||||
setContent { AsIdentity(canSign = false) { NotFound(onSwitchProfile = { switched++ }) } }
|
||||
|
||||
onNodeWithText("Switch profile", useUnmergedTree = true).performClick()
|
||||
assertEquals(1, switched)
|
||||
// The read-only exit is still there beside it; switching does not forget.
|
||||
onNodeWithText("Use a different key", useUnmergedTree = true).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a signing identity that was not found is offered the switch above the set-up form`() = runDesktopComposeUiTest(400, 1200) {
|
||||
setContent { AsIdentity(canSign = true) { NotFound(onSwitchProfile = {}) } }
|
||||
|
||||
onNodeWithText("Switch profile", useUnmergedTree = true).assertIsDisplayed()
|
||||
// The heading and the button both say it; either is enough to know the form is there.
|
||||
onAllNodesWithText("Set up a profile", useUnmergedTree = true).onFirst().assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `with one profile on the device there is nothing to switch to`() = runDesktopComposeUiTest(400, 1200) {
|
||||
setContent { AsIdentity(canSign = false) { NotFound(onSwitchProfile = null) } }
|
||||
|
||||
onNodeWithText("Use a different key", useUnmergedTree = true).assertIsDisplayed()
|
||||
onNodeWithText("Switch profile", useUnmergedTree = true).assertDoesNotExist()
|
||||
}
|
||||
|
||||
// --- harness ---
|
||||
|
||||
private val readOnlyIdentity = MutableStateFlow<Identity?>(
|
||||
@@ -174,11 +205,12 @@ class ReadOnlyEntrancesJvmTest {
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun NotFound() {
|
||||
private fun NotFound(onSwitchProfile: (() -> Unit)? = null) {
|
||||
UnsyncedProfileScreen(
|
||||
unsyncedProfilePublicKey = publicKey,
|
||||
nostrRepository = NotFoundAccount,
|
||||
signOut = signOutDependencies,
|
||||
onSwitchProfile = onSwitchProfile,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user