Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
935599c
feat(totp): let a scanned code choose its item
OffRange Aug 22, 2026
e38fc6a
feat(totp): keep the import on the picker until it is saved
OffRange Aug 23, 2026
a81f5da
feat(totp): add a ViewModel for the item picker
OffRange Aug 23, 2026
076ab37
feat(totp): let the login form take a pending code
OffRange Aug 23, 2026
e1dddd3
feat(totp): give the item picker its own destination
OffRange Aug 23, 2026
2fd8d1d
refactor(totp): drop the overlay item picker
OffRange Aug 23, 2026
bd6e085
fix(totp): apply review fix wave for item picker navigation
OffRange Aug 23, 2026
3d7461d
feat(totp): validate a deep-linked code before the unlock
OffRange Aug 23, 2026
aea2ac0
refactor(totp): move the deep link redirect into the totp feature
OffRange Aug 23, 2026
f8d68ce
test(totp): cover the validated import routing the app performs
OffRange Aug 23, 2026
1e38a4a
refactor(totp): drop the parse error surfaces the gate made dead
OffRange Aug 23, 2026
c42e14b
test(totp): name the picker's unreadable code test for what it checks
OffRange Aug 23, 2026
467901e
fix(totp): let back close the deep link gate and restore the picker t…
OffRange Aug 24, 2026
0e09c50
refactor(totp): move the item picker into the totp feature
OffRange Aug 24, 2026
37e6ca0
refactor(totp): pass the scanned code instead of parking it on the vi…
OffRange Aug 24, 2026
5e3cfbd
doc: remove comments
OffRange Aug 24, 2026
a9f7def
refactor(test): fold the three RegistrableDomainResolver fakes into one
OffRange Aug 24, 2026
0eeb328
fix: leave app when back pressed during totp scanned flow
OffRange Aug 24, 2026
5e5b2ba
fix: create item fab
OffRange Aug 24, 2026
4e182dc
Merge branch 'v2' into feat/totp-item-picker
OffRange Aug 24, 2026
b80ce47
fix: add missing dependencies
OffRange Aug 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 80 additions & 16 deletions app/src/main/kotlin/de/davis/keygo/app/presentation/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package de.davis.keygo.app.presentation

import android.os.Bundle
import android.util.Log
import androidx.activity.compose.LocalActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Box
Expand All @@ -24,8 +26,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.fragment.app.FragmentActivity
import androidx.navigation.NavController
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
Expand All @@ -37,6 +41,7 @@ import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
import de.davis.keygo.R
import de.davis.keygo.app.presentation.component.KeyGoNavigationWrapper
import de.davis.keygo.core.presentation.model.RouteDestination
import de.davis.keygo.core.ui.model.PendingTotpImport
import de.davis.keygo.core.ui.theme.KeyGoTheme
import de.davis.keygo.core.util.domain.snackbar.SnackbarManager
import de.davis.keygo.core.util.presentation.snackbar.LocalSnackbarManager
Expand All @@ -47,15 +52,22 @@ import de.davis.keygo.feature.auth.presentation.AuthRoute
import de.davis.keygo.feature.auth.presentation.authGraph
import de.davis.keygo.feature.backup.presentation.BackupHubRoute
import de.davis.keygo.feature.backup.presentation.backupGraph
import de.davis.keygo.feature.item.create.presentation.totp.AssignTotpRoute
import de.davis.keygo.feature.item.create.presentation.totp.assignTotpGraph
import de.davis.keygo.feature.onboarding.presentation.OnboardingRoute
import de.davis.keygo.feature.onboarding.presentation.onboardingGraph
import de.davis.keygo.feature.settings.presentation.ChangePasswordRoute
import de.davis.keygo.feature.settings.presentation.settingsGraph
import de.davis.keygo.feature.totp.presentation.SelectItemForTotpRoute
import de.davis.keygo.feature.totp.presentation.selectItemForTotpGraph
import de.davis.keygo.feature.totp.presentation.totpImportRedirectGraph
import de.davis.keygo.item.dialog.SelectItemContent
import kotlinx.coroutines.launch
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.compose.koinInject

private const val TAG = "MainActivity"

class MainActivity : FragmentActivity() {

private val viewModel by viewModel<AppViewModel>()
Expand Down Expand Up @@ -86,11 +98,45 @@ class MainActivity : FragmentActivity() {
}
}

private fun destinationAfterUnlock(totpUri: String?): Any =
totpUri?.let { SelectItemForTotpRoute(it) } ?: RouteDestination.TopLevelAppGraph

internal fun NavController.navigateToValidatedImport(
hasAccess: Boolean,
pending: PendingTotpImport
) {
navigate(
if (hasAccess) AuthRoute(
totpInfo = pending.totpInfo,
queries = pending.queries,
)
else OnboardingRoute(
totpInfo = pending.totpInfo,
queries = pending.queries,
),
) {
popUpTo(graph.findStartDestination().id) { inclusive = true }
}
}

/**
* Where the picker's answer goes. The picker stays composed and collecting through its exit
* transition, so a double tap on a row can fire twice before the first navigation leaves it.
* [AssignTotpRoute] is a data class, so launchSingleTop dedupes the repeat instead of pushing it
* twice onto the back stack.
*/
internal fun NavController.navigateToAssignTotp(route: AssignTotpRoute) {
navigate(route) {
launchSingleTop = true
}
}

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
private fun App(hasAccess: Boolean) {
val listNavigator = rememberListDetailPaneScaffoldNavigator<DetailType>()
val navController = rememberNavController()
val activity = LocalActivity.current

val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
Expand Down Expand Up @@ -145,33 +191,51 @@ private fun App(hasAccess: Boolean) {
startDestination = if (hasAccess) AuthRoute() else OnboardingRoute(),
) {
totpImportRedirectGraph(
hasAccess = hasAccess,
navigateAndReplace = { dest ->
navController.navigate(dest) {
popUpTo<TotpImportRedirect> { inclusive = true }
onValidated = { pending ->
navController.navigateToValidatedImport(
hasAccess,
pending
)
},
// The app was launched only to import this code. With nothing left to import, the
// Activity is what closes, and :app is the only module that owns one.
onRejected = {
activity?.finish() ?: Log.w(
TAG,
"No activity to finish after rejecting an invalid TOTP deep link"
)
},
)

selectItemForTotpGraph(
onItemSelected = { totpUri, itemId ->
navController.navigateToAssignTotp(AssignTotpRoute(totpUri, itemId.toString()))
},
onCreateNew = { totpUri ->
navController.navigateToAssignTotp(AssignTotpRoute(totpUri))
},
)

assignTotpGraph(
onImportFinished = {
navController.navigate(RouteDestination.TopLevelAppGraph) {
popUpTo<SelectItemForTotpRoute> { inclusive = true }
}
}
},
navigateUp = { navController.navigateUp() },
)

authGraph(
onSuccess = { totpUri ->
val dest = totpUri?.let {
RouteDestination.Home.Root(it)
} ?: RouteDestination.TopLevelAppGraph

navController.navigate(dest) {
navController.navigate(destinationAfterUnlock(totpUri)) {
popUpTo<AuthRoute> { inclusive = true }
}
}
)

onboardingGraph(
onSuccess = { totpUri ->
val dest = totpUri?.let {
RouteDestination.Home.Root(it)
} ?: RouteDestination.TopLevelAppGraph

navController.navigate(dest) {
navController.navigate(destinationAfterUnlock(totpUri)) {
popUpTo<OnboardingRoute> { inclusive = true }
}
}
Expand All @@ -181,7 +245,7 @@ private fun App(hasAccess: Boolean) {
startDestination = RouteDestination.Home.NavGraph
) {
navigation<RouteDestination.Home.NavGraph>(
startDestination = RouteDestination.Home.Root()
startDestination = RouteDestination.Home.Root
) {
dialog<RouteDestination.Home.SelectItem> {
SelectItemContent(
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,10 @@ fun KeyGoNavigationWrapper(

FloatingActionButtonMenu(
expanded = fabMenuExpanded,
modifier = Modifier.animateFloatingActionButton(
visible = (showChrome && showPrimaryActionButton) || fabMenuExpanded,
alignment = Alignment.BottomEnd,
),
button = {
TooltipBox(
positionProvider =
Expand All @@ -226,10 +230,6 @@ fun KeyGoNavigationWrapper(
.semantics {
traversalIndex = -1f
}
.animateFloatingActionButton(
visible = (showChrome && showPrimaryActionButton) || fabMenuExpanded,
alignment = Alignment.BottomEnd,
)
.focusRequester(focusRequester),
) {
val imageVector by remember {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ sealed interface RouteDestination : UiRouteDestination {
data object NavGraph : Home

@Serializable
data class Root(val totpUri: String? = null) : Home
data object Root : Home

@Serializable
data object SelectItem : Home
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import androidx.navigation.toRoute
import de.davis.keygo.core.presentation.model.RouteDestination
import de.davis.keygo.core.ui.composition.LocalIsInSinglePaneMode
import de.davis.keygo.feature.item.core.presentation.model.NavigationEvent
Expand Down Expand Up @@ -58,16 +57,6 @@ fun NavGraphBuilder.dashboardGraph(
}
}

val route = it.toRoute<RouteDestination.Home.Root>()
LaunchedEffect(route) {
route.totpUri?.let { totpUri ->
listNavigator.navigateTo(
ListDetailPaneScaffoldRole.Detail,
DetailType.Modify.Totp(totpUri)
)
}
}

CompositionLocalProvider(
LocalIsInSinglePaneMode provides isSinglePaneMode,
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import android.os.Parcelable
import de.davis.keygo.core.item.domain.alias.ItemId
import de.davis.keygo.core.item.generated.domain.model.VaultItemType
import de.davis.keygo.feature.item.core.presentation.model.DetailPaneInformation
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize

@Parcelize
Expand All @@ -16,11 +15,6 @@ sealed interface DetailType : Parcelable {

data class CreateNew(override val vaultItemType: VaultItemType) : Modify
data class Edit(override val vaultItemType: VaultItemType, val itemId: ItemId) : Modify
data class Totp(val uri: String) : Modify {

@IgnoredOnParcel
override val vaultItemType: VaultItemType = VaultItemType.Login
}
}

data class View(val itemId: ItemId) : DetailType
Expand All @@ -29,5 +23,4 @@ sealed interface DetailType : Parcelable {
fun DetailType.Modify.asDetailPaneInformation() = when (this) {
is DetailType.Modify.CreateNew -> DetailPaneInformation.Init.New(vaultItemType)
is DetailType.Modify.Edit -> DetailPaneInformation.Init.Existing(vaultItemType, itemId)
is DetailType.Modify.Totp -> DetailPaneInformation.Init.TOTP(vaultItemType, uri)
}
Loading
Loading