-
Notifications
You must be signed in to change notification settings - Fork 4
feat: add deep links to every screen #1119
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
54fce34
ec09077
1862da0
a31add7
2846c61
511250c
09239fb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| package to.bitkit.ui.utils | ||
|
|
||
| import android.net.Uri | ||
| import androidx.navigation.NavDeepLink | ||
| import androidx.navigation.navDeepLink | ||
| import to.bitkit.ui.Routes | ||
| import kotlin.reflect.KClass | ||
|
|
||
| object ScreenDeepLinks { | ||
| const val SCHEME = "bitkit" | ||
| const val HOST = "screen" | ||
|
|
||
| private const val BASE_URI = "$SCHEME://$HOST" | ||
|
|
||
| private val CAMEL_HUMP = Regex("(?<=[a-z0-9])(?=[A-Z])") | ||
|
|
||
| private val DENIED: Set<KClass<out Routes>> = setOf( | ||
| Routes.AuthCheck::class, | ||
| Routes.CriticalUpdate::class, | ||
| Routes.ExternalAmount::class, | ||
| Routes.ExternalConfirm::class, | ||
| Routes.ExternalSuccess::class, | ||
| Routes.LegacyRnRecovery::class, | ||
| Routes.LnurlChannel::class, | ||
| Routes.RecoveryMnemonic::class, | ||
| Routes.RecoveryMode::class, | ||
| ) | ||
|
|
||
| fun isDenied(route: KClass<*>): Boolean = route in DENIED | ||
|
|
||
| fun screenId(route: KClass<*>): String? { | ||
| if (!isScreenRoute(route)) return null | ||
| if (isDenied(route)) return null | ||
|
|
||
| return kebabId(route) | ||
| } | ||
|
|
||
| fun kebabId(route: KClass<*>): String? { | ||
| val name = route.simpleName ?: return null | ||
| return CAMEL_HUMP.split(name).joinToString("-") { it.lowercase() } | ||
| } | ||
|
|
||
| fun basePath(route: KClass<*>): String? = screenId(route)?.let { "$BASE_URI/$it" } | ||
|
|
||
| fun <T : Any> linksFor(route: KClass<T>): List<NavDeepLink> { | ||
| val basePath = basePath(route) ?: return emptyList() | ||
| return listOf(navDeepLink(route = route, basePath = basePath) {}) | ||
| } | ||
|
|
||
| fun isScreenDeepLink(uri: Uri): Boolean = | ||
| uri.scheme?.lowercase() == SCHEME && uri.host?.lowercase() == HOST | ||
|
|
||
| private fun isScreenRoute(route: KClass<*>): Boolean = Routes::class.java.isAssignableFrom(route.java) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| package to.bitkit.ui.utils | ||
|
|
||
| import android.net.Uri | ||
| import to.bitkit.ui.components.Sheet | ||
| import to.bitkit.ui.screens.wallets.receive.ReceiveRoute | ||
| import to.bitkit.ui.sheets.BackupRoute | ||
| import to.bitkit.ui.sheets.SendRoute | ||
| import to.bitkit.ui.sheets.WidgetsRoute | ||
| import to.bitkit.ui.sheets.hardware.HardwareRoute | ||
|
|
||
| object SheetDeepLinks { | ||
| private val SHEETS: List<Sheet> = listOf( | ||
| Sheet.Send(SendRoute.Recipient), | ||
| Sheet.Send(SendRoute.Address), | ||
| Sheet.Send(SendRoute.ContactSelect), | ||
| Sheet.Send(SendRoute.Amount), | ||
| Sheet.Send(SendRoute.QrScanner), | ||
| Sheet.Send(SendRoute.CoinSelection), | ||
| Sheet.Send(SendRoute.AddTag), | ||
| Sheet.Send(SendRoute.ComingSoon), | ||
| Sheet.Send(SendRoute.Support), | ||
|
|
||
| Sheet.Receive(ReceiveRoute.QR), | ||
| Sheet.Receive(ReceiveRoute.Amount), | ||
| Sheet.Receive(ReceiveRoute.EditInvoice), | ||
| Sheet.Receive(ReceiveRoute.AddTag), | ||
| Sheet.Receive(ReceiveRoute.GeoBlock), | ||
|
|
||
| Sheet.Backup(BackupRoute.Intro), | ||
| Sheet.Backup(BackupRoute.MultipleDevices), | ||
| Sheet.Backup(BackupRoute.Metadata), | ||
|
|
||
| Sheet.Widgets(WidgetsRoute.Gallery), | ||
| Sheet.Widgets(WidgetsRoute.PricePreview), | ||
| Sheet.Widgets(WidgetsRoute.PriceEdit), | ||
| Sheet.Widgets(WidgetsRoute.WeatherPreview), | ||
| Sheet.Widgets(WidgetsRoute.WeatherEdit), | ||
| Sheet.Widgets(WidgetsRoute.BlocksPreview), | ||
| Sheet.Widgets(WidgetsRoute.BlocksEdit), | ||
| Sheet.Widgets(WidgetsRoute.HeadlinesPreview), | ||
| Sheet.Widgets(WidgetsRoute.HeadlinesEdit), | ||
| Sheet.Widgets(WidgetsRoute.FactsPreview), | ||
| Sheet.Widgets(WidgetsRoute.CalculatorPreview), | ||
| Sheet.Widgets(WidgetsRoute.SuggestionsPreview), | ||
|
|
||
| Sheet.Hardware(HardwareRoute.Intro), | ||
|
|
||
| Sheet.ActivityDateRangeSelector, | ||
| Sheet.ActivityTagSelector, | ||
| Sheet.QrScanner, | ||
| ) | ||
|
|
||
| private val BY_PATH: Map<String, Sheet> = buildMap { | ||
| SHEETS.forEach { sheet -> | ||
| val sheetId = ScreenDeepLinks.kebabId(sheet::class) ?: return@forEach | ||
| putIfAbsent(sheetId, sheet) | ||
|
|
||
| val route = routeOf(sheet) ?: return@forEach | ||
| val routeId = ScreenDeepLinks.kebabId(route::class) ?: return@forEach | ||
| put("$sheetId/$routeId", sheet) | ||
| } | ||
| } | ||
|
|
||
| val paths: Set<String> get() = BY_PATH.keys | ||
|
|
||
| fun sheetFor(uri: Uri): Sheet? { | ||
| if (!ScreenDeepLinks.isScreenDeepLink(uri)) return null | ||
|
|
||
| val path = uri.pathSegments.orEmpty().joinToString("/").lowercase() | ||
| return BY_PATH[path] | ||
| } | ||
|
|
||
| private fun routeOf(sheet: Sheet): Any? = when (sheet) { | ||
| is Sheet.Send -> sheet.route | ||
| is Sheet.Receive -> sheet.route | ||
| is Sheet.Backup -> sheet.route | ||
| is Sheet.Widgets -> sheet.route | ||
| is Sheet.Hardware -> sheet.route | ||
| else -> null | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -75,7 +75,7 @@ object Transitions { | |
| inline fun <reified T : Any> NavGraphBuilder.navigationWithDefaultTransitions( | ||
| startDestination: Any, | ||
| typeMap: Map<KType, @JvmSuppressWildcards NavType<*>> = emptyMap(), | ||
| deepLinks: List<NavDeepLink> = emptyList(), | ||
| deepLinks: List<NavDeepLink> = ScreenDeepLinks.linksFor(T::class), | ||
| noinline enterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?)? = defaultEnterTrans, | ||
| noinline exitTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?)? = defaultExitTrans, | ||
| noinline popEnterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?)? = defaultPopEnterTrans, | ||
|
|
@@ -100,7 +100,7 @@ inline fun <reified T : Any> NavGraphBuilder.navigationWithDefaultTransitions( | |
| @Suppress("LongParameterList", "MaxLineLength") | ||
| inline fun <reified T : Any> NavGraphBuilder.composableWithDefaultTransitions( | ||
| typeMap: Map<KType, NavType<*>> = emptyMap(), | ||
| deepLinks: List<NavDeepLink> = emptyList(), | ||
| deepLinks: List<NavDeepLink> = ScreenDeepLinks.linksFor(T::class), | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I found that
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, Home is the last destination still on raw composable, so it never picked up the default and nothing was attached to the graph. Passed the links explicitly at that call site and confirmed it resolves from another screen now. Resolved in 09239fb |
||
| noinline enterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?)? = defaultEnterTrans, | ||
| noinline exitTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?)? = defaultExitTrans, | ||
| noinline popEnterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?)? = defaultPopEnterTrans, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I found that
bitkit://screen/external-confirmcreates a freshExternalNav-scoped view model withpeer = null. Its enabled swipe reachesonConfirm(), which callsrequireNotNull(peer)and crashes;external-amountcan advance into the same state. Please deny these cold-start routes or restore and validate the required peer state before enabling confirmation, and cover both URIs from fresh state.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Denied both, and external-success too, since it reports a completed channel open over the same empty state. Restoring the peer instead would mean re-running the connect flow validation from a URI, which belongs with that flow.
Resolved in 09239fb