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
25 changes: 19 additions & 6 deletions app/src/main/java/to/bitkit/ui/ContentView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -315,13 +315,22 @@ fun ContentView(
val uri = pendingScreenDeepLink ?: return@LaunchedEffect

navController.currentBackStackEntryFlow.first()
appViewModel.consumeScreenDeepLink()

SheetDeepLinks.sheetFor(uri)?.let {
appViewModel.showSheet(it)
appViewModel.consumeScreenDeepLink()
return@LaunchedEffect
}

ScreenDeepLinks.spendingHwSignLink(uri)?.let { link ->
val prepared = transferViewModel.prepareSpendingHwSign(link.walletId, link.orderId)
if (!prepared) {
Logger.warn("Unhandled screen deeplink '$uri'", context = "ContentView")
appViewModel.consumeScreenDeepLink()
return@LaunchedEffect
}
}

val request = Intent(Intent.ACTION_VIEW, uri)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
val handled = navController.handleDeepLink(request)
Expand All @@ -332,6 +341,7 @@ fun ContentView(
if (!handled) {
Logger.warn("Unhandled screen deeplink '$uri'", context = "ContentView")
}
appViewModel.consumeScreenDeepLink()
}

LaunchedEffect(appViewModel) {
Expand Down Expand Up @@ -840,13 +850,16 @@ private fun RootNavHost(
viewModel = transferViewModel,
isOffline = connectivityState != ConnectivityState.CONNECTED,
onBackClick = { navController.popBackStack() },
onOrderCreated = { navController.navigateTo(Routes.SpendingHwSign(walletId)) },
onOrderCreated = { orderId ->
navController.navigateTo(Routes.SpendingHwSign(walletId, orderId))
},
)
}
composableWithDefaultTransitions<Routes.SpendingHwSign> { entry ->
val walletId = entry.toRoute<Routes.SpendingHwSign>().walletId
deepLinkableComposable<Routes.SpendingHwSign> { entry ->
val route = entry.toRoute<Routes.SpendingHwSign>()
SpendingHwSignScreen(
walletId = walletId,
walletId = route.walletId,
orderId = route.orderId,
viewModel = transferViewModel,
onBackClick = { navController.popBackStack() },
onCloseClick = { navController.navigateToHome() },
Expand Down Expand Up @@ -2126,7 +2139,7 @@ sealed interface Routes {
data class SpendingAmountHw(val walletId: String) : Routes.DeepLinkable

@Serializable
data class SpendingHwSign(val walletId: String) : Routes.InternalOnly
data class SpendingHwSign(val walletId: String, val orderId: String) : Routes.DeepLinkable

@Serializable
data object SpendingHwSigned : Routes.InternalOnly
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ fun SpendingAdvancedScreen(
LaunchedEffect(Unit) {
viewModel.transferEffects.collect { effect ->
when (effect) {
TransferEffect.OnOrderCreated -> currentOnOrderCreated()
is TransferEffect.OnOrderCreated -> currentOnOrderCreated()
is TransferEffect.ToastException -> {
isLoading = false
app.toast(effect.e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ fun SpendingAmountScreen(
LaunchedEffect(Unit) {
viewModel.transferEffects.collect { effect ->
when (effect) {
TransferEffect.OnOrderCreated -> onOrderCreated()
is TransferEffect.OnOrderCreated -> onOrderCreated()
is TransferEffect.ToastError -> toast(effect.title, effect.description)
is TransferEffect.ToastException -> toastException(effect.e)
else -> Unit
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ fun SpendingAmountHwScreen(
viewModel: TransferViewModel,
isOffline: Boolean,
onBackClick: () -> Unit = {},
onOrderCreated: () -> Unit = {},
onOrderCreated: (String) -> Unit = {},
currencies: CurrencyState = LocalCurrencies.current,
amountInputViewModel: AmountInputViewModel = hiltViewModel(),
) {
Expand All @@ -83,7 +83,7 @@ fun SpendingAmountHwScreen(
LaunchedEffect(Unit) {
viewModel.transferEffects.collect { effect ->
when (effect) {
TransferEffect.OnOrderCreated -> onOrderCreated()
is TransferEffect.OnOrderCreated -> onOrderCreated(effect.orderId)
is TransferEffect.ToastError -> ToastEventBus.send(
type = Toast.ToastType.ERROR,
title = effect.title,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import to.bitkit.viewmodels.TransferViewModel
@Composable
fun SpendingHwSignScreen(
walletId: String,
orderId: String,
viewModel: TransferViewModel,
onBackClick: () -> Unit,
onCloseClick: () -> Unit,
Expand All @@ -50,7 +51,7 @@ fun SpendingHwSignScreen(
) {
val state by viewModel.spendingUiState.collectAsStateWithLifecycle()

val order = state.order ?: run {
val order = state.order?.takeIf { it.id == orderId } ?: run {
onCloseClick()
return
}
Expand Down
18 changes: 18 additions & 0 deletions app/src/main/java/to/bitkit/ui/utils/ScreenDeepLinks.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,19 @@ object ScreenDeepLinks {
fun isScreenDeepLink(uri: Uri): Boolean =
uri.scheme?.lowercase() == SCHEME && uri.host?.lowercase() == HOST

fun spendingHwSignLink(uri: Uri): SpendingHwSignLink? {
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
if (!isScreenDeepLink(uri)) return null
val segments = uri.pathSegments.orEmpty()
val screenId = kebabId(Routes.SpendingHwSign::class)
if (segments.size != 3 || screenId == null || !segments[0].equals(screenId, ignoreCase = true)) {
return null
}
val walletId = segments[1]
val orderId = segments[2]
if (walletId.isBlank() || orderId.isBlank()) return null
return SpendingHwSignLink(walletId = walletId, orderId = orderId)
}

fun detachScreenUri(intent: Intent): Boolean {
val uri = intent.data ?: return false
if (!isScreenDeepLink(uri)) return false
Expand All @@ -46,3 +59,8 @@ object ScreenDeepLinks {
return true
}
}

data class SpendingHwSignLink(
val walletId: String,
val orderId: String,
)
30 changes: 27 additions & 3 deletions app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ class TransferViewModel @Inject constructor(
hwMiningFeeSats = 0uL,
)
}
setTransferEffect(TransferEffect.OnOrderCreated)
setTransferEffect(TransferEffect.OnOrderCreated(newOrder.id))
}.onFailure { e ->
setTransferEffect(TransferEffect.ToastException(e))
}
Expand Down Expand Up @@ -581,6 +581,31 @@ class TransferViewModel @Inject constructor(

private suspend fun onOrderCreated(order: IBtOrder) {
settingsStore.update { it.copy(lightningSetupStep = 0) }
adoptSpendingOrder(order)
setTransferEffect(TransferEffect.OnOrderCreated(order.id))
}

suspend fun prepareSpendingHwSign(walletId: String, orderId: String): Boolean {
if (walletId.isBlank() || orderId.isBlank()) return false
if (hwWalletRepo.wallets.value.none { it.id == walletId }) {
Logger.warn("Refused spending hw sign deeplink, unknown wallet '$walletId'", context = TAG)
return false
}
val current = _spendingUiState.value.order
if (current?.id == orderId) return true

val order = blocktankRepo.getOrder(orderId, refresh = true).getOrNull()
if (order == null) {
Logger.warn("Refused spending hw sign deeplink, missing order '$orderId'", context = TAG)
return false
}

settingsStore.update { it.copy(lightningSetupStep = 0) }
adoptSpendingOrder(order)
return true
}

private fun adoptSpendingOrder(order: IBtOrder) {
pendingHwFundingBroadcast = null
hwFeeEstimateJob?.cancel()
hwFeeEstimateJob = null
Expand All @@ -593,7 +618,6 @@ class TransferViewModel @Inject constructor(
hwMiningFeeSats = 0uL,
)
}
setTransferEffect(TransferEffect.OnOrderCreated)
}

private fun updateAvailableAmount() {
Expand Down Expand Up @@ -1673,7 +1697,7 @@ data class TransferValues(
)

sealed interface TransferEffect {
data object OnOrderCreated : TransferEffect
data class OnOrderCreated(val orderId: String) : TransferEffect
data object OnSpendingFundingPaid : TransferEffect
data object OnHwTxSigned : TransferEffect
data class ToastException(val e: Throwable) : TransferEffect
Expand Down
2 changes: 1 addition & 1 deletion app/src/test/java/to/bitkit/ui/ContentViewTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class ContentViewTest {
fun `transfer effect destinations cover funding paid and hw signed`() {
assertEquals(Routes.SettingUp, transferEffectDestination(TransferEffect.OnSpendingFundingPaid))
assertEquals(Routes.SpendingHwSigned, transferEffectDestination(TransferEffect.OnHwTxSigned))
assertNull(transferEffectDestination(TransferEffect.OnOrderCreated))
assertNull(transferEffectDestination(TransferEffect.OnOrderCreated("order")))
}

@Test
Expand Down
32 changes: 32 additions & 0 deletions app/src/test/java/to/bitkit/ui/utils/ScreenDeepLinksTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,38 @@ class ScreenDeepLinksTest : BaseUnitTest() {
assertEquals("bitkit://screen/activity-assign-contact/{id}", links.single().uriPattern)
}

@Test
fun `SpendingHwSign required arguments are wallet and order path segments`() {
if (!ScreenDeepLinks.isEnabled) return
val links = ScreenDeepLinks.linksFor(Routes.SpendingHwSign::class)

assertEquals(
"bitkit://screen/spending-hw-sign/{walletId}/{orderId}",
links.single().uriPattern,
)
}

@Test
fun `spendingHwSignLink reads wallet and order ids from the path`() {
val screenId = ScreenDeepLinks.kebabId(Routes.SpendingHwSign::class)
val uri = Uri.parse("bitkit://screen/$screenId/hardware-wallet/order-1")

val link = ScreenDeepLinks.spendingHwSignLink(uri)

assertNotNull(link)
assertEquals("hardware-wallet", link.walletId)
assertEquals("order-1", link.orderId)
}

@Test
fun `spendingHwSignLink returns null when the order id is missing`() {
val uri = Uri.parse("bitkit://screen/spending-hw-sign/hardware-wallet")

val link = ScreenDeepLinks.spendingHwSignLink(uri)

assertNull(link)
}

@Test
fun `a route with both argument kinds keeps the required one in the path`() {
if (!ScreenDeepLinks.isEnabled) return
Expand Down
52 changes: 52 additions & 0 deletions app/src/test/java/to/bitkit/viewmodels/TransferViewModelTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,58 @@ class TransferViewModelTest : BaseUnitTest() {
verify(hwWalletRepo, never()).signFunding(any(), any())
}

@Test
fun `prepareSpendingHwSign loads the named order when the wallet is known`() = test {
val order = previewBtOrder()
whenever(hwWalletRepo.wallets)
.thenReturn(MutableStateFlow(persistentListOf(hwWallet(HARDWARE_WALLET_ID, connected = true))))
whenever { blocktankRepo.getOrder(eq(order.id), eq(true)) }.thenReturn(Result.success(order))

val prepared = sut.prepareSpendingHwSign(HARDWARE_WALLET_ID, order.id)

assertTrue(prepared)
assertEquals(order.id, sut.spendingUiState.value.order?.id)
}

@Test
fun `prepareSpendingHwSign refuses an unknown wallet`() = test {
val order = previewBtOrder()
whenever(hwWalletRepo.wallets).thenReturn(MutableStateFlow(persistentListOf()))

val prepared = sut.prepareSpendingHwSign(HARDWARE_WALLET_ID, order.id)

assertFalse(prepared)
assertNull(sut.spendingUiState.value.order)
verify(blocktankRepo, never()).getOrder(any(), any())
}

@Test
fun `prepareSpendingHwSign refuses a missing order`() = test {
val order = previewBtOrder()
whenever(hwWalletRepo.wallets)
.thenReturn(MutableStateFlow(persistentListOf(hwWallet(HARDWARE_WALLET_ID, connected = true))))
whenever { blocktankRepo.getOrder(eq(order.id), eq(true)) }.thenReturn(Result.success(null))

val prepared = sut.prepareSpendingHwSign(HARDWARE_WALLET_ID, order.id)

assertFalse(prepared)
assertNull(sut.spendingUiState.value.order)
}

@Test
fun `prepareSpendingHwSign reuses the in-memory order without fetching`() = test {
val order = previewBtOrder()
whenever(hwWalletRepo.wallets)
.thenReturn(MutableStateFlow(persistentListOf(hwWallet(HARDWARE_WALLET_ID, connected = true))))
whenever { blocktankRepo.getOrder(eq(order.id), eq(true)) }.thenReturn(Result.success(order))
sut.prepareSpendingHwSign(HARDWARE_WALLET_ID, order.id)

val prepared = sut.prepareSpendingHwSign(HARDWARE_WALLET_ID, order.id)

assertTrue(prepared)
verify(blocktankRepo, times(1)).getOrder(eq(order.id), eq(true))
}

@Test
fun `updateHwFundingFeeEstimate ignores superseded estimate`() = test {
val orderA = previewBtOrder()
Expand Down
1 change: 1 addition & 0 deletions changelog.d/next/1176.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Dev-mode deep links can open the hardware-wallet transfer sign screen from a wallet id and Blocktank order id.
Loading