Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
65 changes: 65 additions & 0 deletions app/src/debug/java/to/bitkit/ui/utils/ScreenDeepLinkRuntime.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package to.bitkit.ui.utils

import android.net.Uri
import androidx.navigation.NavDeepLink
import androidx.navigation.navDeepLink
import to.bitkit.ui.Routes
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
import kotlin.reflect.KClass

internal object ScreenDeepLinkRuntime {
val isEnabled = true

val sheetIds: Set<String>
get() {
val families = FAMILIES.mapNotNull { ScreenDeepLinks.kebabId(it.type) }
val standalone = STANDALONE.mapNotNull { ScreenDeepLinks.kebabId(it::class) }
return (families + standalone).toSet()
}

fun <T : Routes.DeepLinkable> linksFor(route: KClass<T>): List<NavDeepLink> {
val basePath = ScreenDeepLinks.basePath(route) ?: return emptyList()
return listOf(navDeepLink(route = route, basePath = basePath) {})
}

fun sheetFor(uri: Uri): Sheet? {
if (!ScreenDeepLinks.isScreenDeepLink(uri)) return null

val segments = uri.pathSegments.orEmpty()
if (segments.isEmpty() || segments.size > 2) return null

val id = segments.first().lowercase()
val child = segments.getOrElse(1) { "" }

FAMILIES.firstOrNull { ScreenDeepLinks.kebabId(it.type) == id }
?.let { return it.fromDeepLink(child) }

if (child.isNotEmpty()) return null

return STANDALONE.firstOrNull { ScreenDeepLinks.kebabId(it::class) == id }
}
}

private class SheetDeepLinkFamily(
val type: KClass<out Sheet>,
val fromDeepLink: (String) -> Sheet?,
)

private val FAMILIES: List<SheetDeepLinkFamily> = listOf(
SheetDeepLinkFamily(Sheet.Send::class) { path -> SendRoute.fromDeepLink(path)?.let { Sheet.Send(it) } },
SheetDeepLinkFamily(Sheet.Receive::class) { path -> ReceiveRoute.fromDeepLink(path)?.let { Sheet.Receive(it) } },
SheetDeepLinkFamily(Sheet.Backup::class) { path -> BackupRoute.fromDeepLink(path)?.let { Sheet.Backup(it) } },
SheetDeepLinkFamily(Sheet.Widgets::class) { path -> WidgetsRoute.fromDeepLink(path)?.let { Sheet.Widgets(it) } },
SheetDeepLinkFamily(Sheet.Hardware::class) { path -> HardwareRoute.fromDeepLink(path)?.let { Sheet.Hardware(it) } },
)

private val STANDALONE: List<Sheet> = listOf(
Sheet.ActivityDateRangeSelector,
Sheet.ActivityTagSelector,
Sheet.QrScanner(),
)
11 changes: 6 additions & 5 deletions app/src/main/java/to/bitkit/ui/utils/ScreenDeepLinks.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package to.bitkit.ui.utils
import android.content.Intent
import android.net.Uri
import androidx.navigation.NavDeepLink
import androidx.navigation.navDeepLink
import to.bitkit.ui.Routes
import kotlin.reflect.KClass

Expand All @@ -15,6 +14,10 @@ object ScreenDeepLinks {

private val CAMEL_HUMP = Regex("(?<=[a-z0-9])(?=[A-Z])")

val isEnabled: Boolean get() = ScreenDeepLinkRuntime.isEnabled

fun shouldQueue(isDevModeEnabled: Boolean): Boolean = isEnabled && isDevModeEnabled

fun screenId(route: KClass<out Routes.DeepLinkable>): String? = kebabId(route)

fun kebabId(route: KClass<*>): String? {
Expand All @@ -24,10 +27,8 @@ object ScreenDeepLinks {

fun basePath(route: KClass<out Routes.DeepLinkable>): String? = screenId(route)?.let { "$BASE_URI/$it" }

fun <T : Routes.DeepLinkable> linksFor(route: KClass<T>): List<NavDeepLink> {
val basePath = basePath(route) ?: return emptyList()
return listOf(navDeepLink(route = route, basePath = basePath) {})
}
fun <T : Routes.DeepLinkable> linksFor(route: KClass<T>): List<NavDeepLink> =
ScreenDeepLinkRuntime.linksFor(route)

fun <T : Any> matchStart(path: String, default: T, starts: List<T>): T? = when {
path.isEmpty() -> default
Expand Down
48 changes: 2 additions & 46 deletions app/src/main/java/to/bitkit/ui/utils/SheetDeepLinks.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,54 +2,10 @@ 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
import kotlin.reflect.KClass

object SheetDeepLinks {
private class Family(
val type: KClass<out Sheet>,
val fromDeepLink: (String) -> Sheet?,
)

private val FAMILIES: List<Family> = listOf(
Family(Sheet.Send::class) { path -> SendRoute.fromDeepLink(path)?.let { Sheet.Send(it) } },
Family(Sheet.Receive::class) { path -> ReceiveRoute.fromDeepLink(path)?.let { Sheet.Receive(it) } },
Family(Sheet.Backup::class) { path -> BackupRoute.fromDeepLink(path)?.let { Sheet.Backup(it) } },
Family(Sheet.Widgets::class) { path -> WidgetsRoute.fromDeepLink(path)?.let { Sheet.Widgets(it) } },
Family(Sheet.Hardware::class) { path -> HardwareRoute.fromDeepLink(path)?.let { Sheet.Hardware(it) } },
)

private val STANDALONE: List<Sheet> = listOf(
Sheet.ActivityDateRangeSelector,
Sheet.ActivityTagSelector,
Sheet.QrScanner(),
)

val sheetIds: Set<String>
get() {
val families = FAMILIES.mapNotNull { ScreenDeepLinks.kebabId(it.type) }
val standalone = STANDALONE.mapNotNull { ScreenDeepLinks.kebabId(it::class) }
return (families + standalone).toSet()
}

fun sheetFor(uri: Uri): Sheet? {
if (!ScreenDeepLinks.isScreenDeepLink(uri)) return null

val segments = uri.pathSegments.orEmpty()
if (segments.isEmpty() || segments.size > 2) return null

val id = segments.first().lowercase()
val child = segments.getOrElse(1) { "" }

FAMILIES.firstOrNull { ScreenDeepLinks.kebabId(it.type) == id }
?.let { return it.fromDeepLink(child) }

if (child.isNotEmpty()) return null
get() = ScreenDeepLinkRuntime.sheetIds

return STANDALONE.firstOrNull { ScreenDeepLinks.kebabId(it::class) == id }
}
fun sheetFor(uri: Uri): Sheet? = ScreenDeepLinkRuntime.sheetFor(uri)
}
4 changes: 2 additions & 2 deletions app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3718,8 +3718,8 @@ class AppViewModel @Inject constructor(
}

if (ScreenDeepLinks.isScreenDeepLink(uri)) {
if (!settingsStore.data.first().isDevModeEnabled) {
Logger.warn("Ignoring screen deeplink, dev mode is off", context = TAG)
if (!ScreenDeepLinks.shouldQueue(settingsStore.data.first().isDevModeEnabled)) {
Comment thread
ovitrif marked this conversation as resolved.
Logger.warn("Ignoring screen deeplink, not queued", context = TAG)
return@launch
}

Expand Down
18 changes: 18 additions & 0 deletions app/src/release/java/to/bitkit/ui/utils/ScreenDeepLinkRuntime.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package to.bitkit.ui.utils

import android.net.Uri
import androidx.navigation.NavDeepLink
import to.bitkit.ui.Routes
import to.bitkit.ui.components.Sheet
import kotlin.reflect.KClass

internal object ScreenDeepLinkRuntime {
val isEnabled = false
val sheetIds: Set<String> = emptySet()

@Suppress("UNUSED_PARAMETER")
fun <T : Routes.DeepLinkable> linksFor(route: KClass<T>): List<NavDeepLink> = emptyList()

@Suppress("UNUSED_PARAMETER")
fun sheetFor(uri: Uri): Sheet? = null
}
39 changes: 16 additions & 23 deletions app/src/test/java/to/bitkit/ui/utils/ScreenDeepLinksTest.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package to.bitkit.ui.utils

import android.content.Intent
import android.net.Uri
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import to.bitkit.test.BaseUnitTest
import to.bitkit.ui.Routes
import kotlin.reflect.KClass
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
Expand All @@ -17,26 +17,6 @@ import kotlin.test.assertTrue
@Config(sdk = [34])
@RunWith(RobolectricTestRunner::class)
class ScreenDeepLinksTest : BaseUnitTest() {
private companion object {
val SENSITIVE_ROUTES: List<KClass<out Routes>> = listOf(
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,
Routes.SavingsProgress::class,
Routes.SettingUp::class,
Routes.SpendingAdvanced::class,
Routes.SpendingConfirm::class,
Routes.SpendingHwSign::class,
Routes.SpendingHwSigned::class,
)
}

@Test
fun `screen id is derived from the route name in kebab-case`() {
val home = ScreenDeepLinks.screenId(Routes.Home::class)
Expand All @@ -50,6 +30,7 @@ class ScreenDeepLinksTest : BaseUnitTest() {

@Test
fun `routes without arguments produce a bare pattern`() {
if (!ScreenDeepLinks.isEnabled) return
val links = ScreenDeepLinks.linksFor(Routes.Settings::class)

assertEquals(1, links.size)
Expand All @@ -58,20 +39,23 @@ class ScreenDeepLinksTest : BaseUnitTest() {

@Test
fun `required arguments are appended as path segments`() {
if (!ScreenDeepLinks.isEnabled) return
val links = ScreenDeepLinks.linksFor(Routes.ActivityAssignContact::class)

assertEquals("bitkit://screen/activity-assign-contact/{id}", links.single().uriPattern)
}

@Test
fun `a route with both argument kinds keeps the required one in the path`() {
if (!ScreenDeepLinks.isEnabled) return
val links = ScreenDeepLinks.linksFor(Routes.ActivityDetail::class)

assertEquals("bitkit://screen/activity-detail/{id}?walletId={walletId}", links.single().uriPattern)
}

@Test
fun `arguments with defaults are appended as query parameters`() {
if (!ScreenDeepLinks.isEnabled) return
val links = ScreenDeepLinks.linksFor(Routes.Contacts::class)

assertEquals(
Expand All @@ -90,7 +74,7 @@ class ScreenDeepLinksTest : BaseUnitTest() {
}

@Test
fun `every deep-linkable route has a unique screen id and one link`() {
fun `every deep-linkable route has a unique screen id`() {
val ids = mutableMapOf<String, String>()

Routes.DeepLinkable::class.sealedSubclasses.forEach { route ->
Expand All @@ -100,7 +84,6 @@ class ScreenDeepLinksTest : BaseUnitTest() {
assertNotNull(id, "route $name has no screen id")
val clash = ids.put(id, name)
assertNull(clash, "screen id '$id' is used by both $clash and $name")
assertEquals(1, ScreenDeepLinks.linksFor(route).size, "route $name has no deep link")
}
}

Expand Down Expand Up @@ -132,4 +115,14 @@ class ScreenDeepLinksTest : BaseUnitTest() {
assertFalse(pubkyAuth)
assertFalse(lightning)
}

@Test
fun `screen uris are detached from the activity intent`() {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("bitkit://screen/settings"))

val detached = ScreenDeepLinks.detachScreenUri(intent)

assertTrue(detached)
assertNull(intent.data)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ import to.bitkit.ui.shared.toast.ToastQueueManager
import to.bitkit.ui.sheets.SendRoute
import to.bitkit.ui.sheets.hardware.HardwareRoute
import to.bitkit.ui.theme.TRANSITION_SCREEN_MS
import to.bitkit.ui.utils.ScreenDeepLinks
import to.bitkit.usecases.FormatMoneyValue
import to.bitkit.usecases.RefreshContactPaykitReceiversUseCase
import to.bitkit.utils.AppError
Expand Down Expand Up @@ -1134,13 +1135,17 @@ class AppViewModelSendFlowTest : BaseUnitTest() {
}

@Test
fun `screen deeplink is held for replay when dev mode is on`() = test {
fun `screen deeplink is held for replay only on debug when dev mode is on`() = test {
settingsData.value = SettingsData(isDevModeEnabled = true)

sut.handleDeeplinkIntent(screenIntent("settings"))
advanceUntilIdle()

assertNotNull(sut.pendingScreenDeepLink.value)
if (ScreenDeepLinks.isEnabled) {
Comment thread
ovitrif marked this conversation as resolved.
assertNotNull(sut.pendingScreenDeepLink.value)
} else {
assertNull(sut.pendingScreenDeepLink.value)
}
}

@Test
Expand Down
4 changes: 2 additions & 2 deletions journeys/deeplinks/screen-deeplink.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<journey name="screen deeplink">
<description>Precondition: onboarded dev wallet, dev mode on, at least one log file present.</description>
<description>Precondition: onboarded debug build, dev mode on, at least one log file present.</description>
<actions>
<action>Run `adb shell am start -W -a android.intent.action.VIEW -d "bitkit://screen/settings" to.bitkit.dev`</action>
<action>Verify that the Settings screen is visible with the "General", "Security" and "Advanced" tabs</action>
Expand All @@ -18,7 +18,7 @@
<action>Run `adb shell am force-stop to.bitkit.dev`</action>
<action>Run `adb shell am start -a android.intent.action.VIEW -d "bitkit://screen/settings" to.bitkit.dev`</action>
<action>Verify that the wallet overview is visible and Settings is not, on a cold start with dev mode off</action>
<action>Verify that logcat contains "Ignoring screen deeplink, dev mode is off"</action>
<action>Verify that logcat contains "Ignoring screen deeplink, not queued"</action>
<action>Open Settings ▸ Support and tap the version row five times to turn dev mode back on</action>
</actions>
</journey>
Loading