Skip to content

Commit 154f92c

Browse files
authored
Merge branch 'master' into feat/send-sheet-redesign
2 parents 5ae9878 + c086a6c commit 154f92c

File tree

5 files changed

+141
-91
lines changed

5 files changed

+141
-91
lines changed

app/src/main/java/to/bitkit/ui/ContentView.kt

Lines changed: 28 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
package to.bitkit.ui
22

33
import android.content.Intent
4-
import androidx.compose.animation.AnimatedVisibility
54
import androidx.compose.foundation.layout.Box
65
import androidx.compose.foundation.layout.fillMaxSize
7-
import androidx.compose.foundation.layout.fillMaxWidth
86
import androidx.compose.material3.DrawerState
97
import androidx.compose.material3.DrawerValue
108
import androidx.compose.material3.rememberDrawerState
@@ -447,39 +445,36 @@ fun ContentView(
447445
}
448446
}
449447
) {
450-
RootNavHost(
451-
navController = navController,
452-
drawerState = drawerState,
453-
walletViewModel = walletViewModel,
454-
appViewModel = appViewModel,
455-
activityListViewModel = activityListViewModel,
456-
settingsViewModel = settingsViewModel,
457-
currencyViewModel = currencyViewModel,
458-
transferViewModel = transferViewModel,
459-
)
460-
}
461-
462-
val navBackStackEntry by navController.currentBackStackEntryAsState()
463-
464-
val currentRoute = navBackStackEntry?.destination?.route
448+
Box(modifier = Modifier.fillMaxSize()) {
449+
RootNavHost(
450+
navController = navController,
451+
drawerState = drawerState,
452+
walletViewModel = walletViewModel,
453+
appViewModel = appViewModel,
454+
activityListViewModel = activityListViewModel,
455+
settingsViewModel = settingsViewModel,
456+
currencyViewModel = currencyViewModel,
457+
transferViewModel = transferViewModel,
458+
)
465459

466-
val showTabBar = currentRoute in listOf(
467-
Routes.Home::class.qualifiedName,
468-
Routes.AllActivity::class.qualifiedName,
469-
)
460+
val navBackStackEntry by navController.currentBackStackEntryAsState()
461+
val currentRoute = navBackStackEntry?.destination?.route
462+
val showTabBar = currentRoute in listOf(
463+
Routes.Home::class.qualifiedName,
464+
Routes.AllActivity::class.qualifiedName,
465+
)
470466

471-
AnimatedVisibility(
472-
visible = showTabBar && currentSheet == null,
473-
modifier = Modifier
474-
.fillMaxWidth()
475-
.align(Alignment.BottomCenter)
476-
) {
477-
TabBar(
478-
hazeState = hazeState,
479-
onSendClick = { appViewModel.showSheet(Sheet.Send()) },
480-
onReceiveClick = { appViewModel.showSheet(Sheet.Receive) },
481-
onScanClick = { navController.navigateToScanner() },
482-
)
467+
if (showTabBar) {
468+
TabBar(
469+
hazeState = hazeState,
470+
onSendClick = { appViewModel.showSheet(Sheet.Send()) },
471+
onReceiveClick = { appViewModel.showSheet(Sheet.Receive) },
472+
onScanClick = { navController.navigateToScanner() },
473+
modifier = Modifier
474+
.align(Alignment.BottomCenter)
475+
)
476+
}
477+
}
483478
}
484479

485480
DrawerMenu(

app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -488,7 +488,7 @@ private fun Content(
488488
onEmptyActivityRowClick = onClickEmptyActivityRow,
489489
)
490490

491-
VerticalSpacer(120.dp) // scrollable empty space behind footer
491+
VerticalSpacer(150.dp) // scrollable empty space behind footer
492492
}
493493
}
494494
if (homeUiState.showEmptyState) {
@@ -497,6 +497,7 @@ private fun Content(
497497
onClose = onDismissEmptyState,
498498
modifier = Modifier
499499
.align(Alignment.BottomCenter)
500+
.padding(bottom = 24.dp)
500501
)
501502
}
502503
}

app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ fun getInvoiceForTab(
2727
}
2828

2929
ReceiveTab.AUTO -> {
30-
bip21.takeIf { isNodeRunning }.orEmpty()
30+
bip21.takeIf { isNodeRunning && containsLightningParameter(bip21) }.orEmpty()
3131
}
3232

3333
ReceiveTab.SPENDING -> {
@@ -57,6 +57,16 @@ fun removeLightningFromBip21(bip21: String, fallbackAddress: String): String {
5757
return withoutLightning.ifBlank { fallbackAddress }
5858
}
5959

60+
/**
61+
* Checks if a BIP21 URI contains a lightning parameter.
62+
*
63+
* @param bip21 The BIP21 URI to check
64+
* @return true if the URI contains a lightning parameter, false otherwise
65+
*/
66+
private fun containsLightningParameter(bip21: String): Boolean {
67+
return Regex("[?&]lightning=[^&]*").containsMatchIn(bip21)
68+
}
69+
6070
/**
6171
* Returns the appropriate QR code logo resource for the selected tab.
6272
*

app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt

Lines changed: 50 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
88
import androidx.compose.foundation.layout.Arrangement
99
import androidx.compose.foundation.layout.Box
1010
import androidx.compose.foundation.layout.Column
11+
import androidx.compose.foundation.layout.PaddingValues
1112
import androidx.compose.foundation.layout.Row
1213
import androidx.compose.foundation.layout.Spacer
1314
import androidx.compose.foundation.layout.fillMaxHeight
@@ -27,12 +28,13 @@ import androidx.compose.material3.ExperimentalMaterial3Api
2728
import androidx.compose.material3.Icon
2829
import androidx.compose.material3.rememberTooltipState
2930
import androidx.compose.runtime.Composable
30-
import androidx.compose.runtime.derivedStateOf
31+
import androidx.compose.runtime.LaunchedEffect
3132
import androidx.compose.runtime.getValue
3233
import androidx.compose.runtime.mutableStateOf
3334
import androidx.compose.runtime.remember
3435
import androidx.compose.runtime.rememberCoroutineScope
3536
import androidx.compose.runtime.setValue
37+
import androidx.compose.runtime.snapshotFlow
3638
import androidx.compose.ui.Alignment
3739
import androidx.compose.ui.Modifier
3840
import androidx.compose.ui.draw.clip
@@ -47,6 +49,8 @@ import androidx.compose.ui.res.stringResource
4749
import androidx.compose.ui.tooling.preview.Devices.NEXUS_5
4850
import androidx.compose.ui.tooling.preview.Preview
4951
import androidx.compose.ui.unit.dp
52+
import kotlinx.coroutines.FlowPreview
53+
import kotlinx.coroutines.flow.distinctUntilChanged
5054
import kotlinx.coroutines.launch
5155
import org.lightningdevkit.ldknode.ChannelDetails
5256
import to.bitkit.R
@@ -65,6 +69,9 @@ import to.bitkit.ui.components.Tooltip
6569
import to.bitkit.ui.components.VerticalSpacer
6670
import to.bitkit.ui.scaffold.SheetTopBar
6771
import to.bitkit.ui.screens.wallets.activity.components.CustomTabRowWithSpacing
72+
import to.bitkit.ui.screens.wallets.receive.ReceiveTab.AUTO
73+
import to.bitkit.ui.screens.wallets.receive.ReceiveTab.SAVINGS
74+
import to.bitkit.ui.screens.wallets.receive.ReceiveTab.SPENDING
6875
import to.bitkit.ui.shared.effects.SetMaxBrightness
6976
import to.bitkit.ui.shared.modifiers.sheetHeight
7077
import to.bitkit.ui.shared.util.gradientBackground
@@ -76,6 +83,7 @@ import to.bitkit.ui.theme.Colors
7683
import to.bitkit.ui.utils.withAccent
7784
import to.bitkit.viewmodels.MainUiState
7885

86+
@OptIn(FlowPreview::class)
7987
@Composable
8088
fun ReceiveQrScreen(
8189
cjitInvoice: String?,
@@ -122,58 +130,39 @@ fun ReceiveQrScreen(
122130
}
123131
}
124132

125-
// Determine initial tab index
126-
val initialTabIndex = remember(initialTab, visibleTabs) {
127-
if (initialTab != null) {
128-
visibleTabs.indexOf(initialTab).coerceAtLeast(0)
129-
} else {
130-
when {
131-
!cjitInvoice.isNullOrEmpty() -> visibleTabs.indexOf(ReceiveTab.SPENDING)
132-
hasUsableChannels -> visibleTabs.indexOf(ReceiveTab.AUTO)
133-
else -> visibleTabs.indexOf(ReceiveTab.SAVINGS)
134-
}.coerceAtLeast(0)
135-
}
136-
}
137-
138133
// LazyRow state with snap behavior
139134
val scope = rememberCoroutineScope()
140-
val lazyListState = rememberLazyListState(
141-
initialFirstVisibleItemIndex = initialTabIndex
142-
)
135+
val lazyListState = rememberLazyListState()
143136

144137
val snapBehavior = rememberSnapFlingBehavior(
145138
lazyListState = lazyListState,
146139
snapPosition = SnapPosition.Center
147140
)
148141

149142
// Calculate current tab based on scroll position for smooth indicator and color updates
150-
val selectedTab by remember {
151-
derivedStateOf {
152-
val layoutInfo = lazyListState.layoutInfo
153-
val currentIndex = if (layoutInfo.visibleItemsInfo.isEmpty()) {
154-
lazyListState.firstVisibleItemIndex
155-
} else {
156-
val viewportMidpoint = layoutInfo.viewportStartOffset +
157-
(layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset) / 2
158-
159-
layoutInfo.visibleItemsInfo
160-
.minByOrNull { item ->
161-
val itemMidpoint = item.offset + item.size / 2
162-
kotlin.math.abs(itemMidpoint - viewportMidpoint)
163-
}
164-
?.index ?: lazyListState.firstVisibleItemIndex
165-
}
143+
var selectedTab by remember {
144+
mutableStateOf(initialTab ?: ReceiveTab.SAVINGS)
145+
}
166146

167-
visibleTabs.getOrNull(currentIndex)
168-
?: visibleTabs.firstOrNull()
169-
?: ReceiveTab.SAVINGS
170-
}
147+
LaunchedEffect(lazyListState, visibleTabs.size) {
148+
snapshotFlow { lazyListState.firstVisibleItemIndex }
149+
.distinctUntilChanged()
150+
.collect { index ->
151+
if (index < visibleTabs.size && index > -1) {
152+
val tab = visibleTabs[index]
153+
selectedTab = tab
154+
}
155+
}
171156
}
172157

173-
// Derive index from selectedTab for tab row indicator
174-
val currentTabIndex by remember {
175-
derivedStateOf {
176-
visibleTabs.indexOf(selectedTab).coerceAtLeast(0)
158+
// Auto-switch to AUTO tab when it becomes available for the first time
159+
LaunchedEffect(hasUsableChannels) {
160+
if (hasUsableChannels && visibleTabs.contains(ReceiveTab.AUTO)) {
161+
val autoIndex = visibleTabs.indexOf(ReceiveTab.AUTO)
162+
if (autoIndex != -1) {
163+
lazyListState.animateScrollToItem(autoIndex)
164+
selectedTab = ReceiveTab.AUTO
165+
}
177166
}
178167
}
179168

@@ -191,23 +180,27 @@ fun ReceiveQrScreen(
191180
.keepScreenOn()
192181
) {
193182
SheetTopBar(stringResource(R.string.wallet__receive_bitcoin))
194-
Column(
195-
modifier = Modifier.padding(horizontal = 16.dp)
196-
) {
183+
Column {
197184
Spacer(Modifier.height(16.dp))
198185

199186
// Tab row
200187
CustomTabRowWithSpacing(
201188
tabs = visibleTabs,
202-
currentTabIndex = currentTabIndex,
203-
selectedColor = selectedTab.accentColor,
189+
currentTabIndex = visibleTabs.indexOf(selectedTab),
190+
selectedColor = when (selectedTab) {
191+
SAVINGS -> Colors.Brand
192+
AUTO -> Colors.White
193+
SPENDING -> Colors.Purple
194+
},
204195
onTabChange = { tab ->
205196
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
206197
val newIndex = visibleTabs.indexOf(tab)
198+
selectedTab = tab
207199
scope.launch {
208200
lazyListState.animateScrollToItem(newIndex)
209201
}
210-
}
202+
},
203+
modifier = Modifier.padding(horizontal = 16.dp)
211204
)
212205

213206
Spacer(Modifier.height(24.dp))
@@ -217,6 +210,7 @@ fun ReceiveQrScreen(
217210
state = lazyListState,
218211
flingBehavior = snapBehavior,
219212
horizontalArrangement = Arrangement.spacedBy(16.dp),
213+
contentPadding = PaddingValues(horizontal = 16.dp),
220214
userScrollEnabled = true,
221215
modifier = Modifier
222216
.weight(1f)
@@ -299,13 +293,15 @@ fun ReceiveQrScreen(
299293
}
300294
},
301295
fullWidth = true,
302-
modifier = Modifier.testTag(
303-
if (showDetails) {
304-
"QRCode"
305-
} else {
306-
"ShowDetails"
307-
}
308-
)
296+
modifier = Modifier
297+
.padding(horizontal = 16.dp)
298+
.testTag(
299+
if (showDetails) {
300+
"QRCode"
301+
} else {
302+
"ShowDetails"
303+
}
304+
)
309305
)
310306
}
311307

app/src/test/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtilsTest.kt

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ class ReceiveInvoiceUtilsTest {
8888
}
8989

9090
@Test
91-
fun `getInvoiceForTab AUTO returns full BIP21 when node is running`() {
91+
fun `getInvoiceForTab AUTO returns full BIP21 when node running and has lightning`() {
9292
val bip21 = "bitcoin:$testAddress?amount=0.001&lightning=$testBolt11"
9393

9494
val result = getInvoiceForTab(
@@ -104,7 +104,7 @@ class ReceiveInvoiceUtilsTest {
104104
}
105105

106106
@Test
107-
fun `getInvoiceForTab AUTO returns empty when node is not running`() {
107+
fun `getInvoiceForTab AUTO returns empty when has lightning but node not running`() {
108108
val bip21 = "bitcoin:$testAddress?amount=0.001&lightning=$testBolt11"
109109

110110
val result = getInvoiceForTab(
@@ -119,6 +119,54 @@ class ReceiveInvoiceUtilsTest {
119119
assertEquals("", result)
120120
}
121121

122+
@Test
123+
fun `getInvoiceForTab AUTO returns empty when BIP21 has no lightning even if node running`() {
124+
val bip21WithoutLightning = "bitcoin:$testAddress?amount=0.001&message=Test"
125+
126+
val result = getInvoiceForTab(
127+
tab = ReceiveTab.AUTO,
128+
bip21 = bip21WithoutLightning,
129+
bolt11 = testBolt11,
130+
cjitInvoice = null,
131+
isNodeRunning = true,
132+
onchainAddress = testAddress
133+
)
134+
135+
assertEquals("", result)
136+
}
137+
138+
@Test
139+
fun `getInvoiceForTab AUTO returns empty when no lightning and node not running`() {
140+
val bip21WithoutLightning = "bitcoin:$testAddress?amount=0.001&message=Test"
141+
142+
val result = getInvoiceForTab(
143+
tab = ReceiveTab.AUTO,
144+
bip21 = bip21WithoutLightning,
145+
bolt11 = testBolt11,
146+
cjitInvoice = null,
147+
isNodeRunning = false,
148+
onchainAddress = testAddress
149+
)
150+
151+
assertEquals("", result)
152+
}
153+
154+
@Test
155+
fun `getInvoiceForTab AUTO detects lightning when it is the first parameter`() {
156+
val bip21LightningFirst = "bitcoin:$testAddress?lightning=$testBolt11&amount=0.001"
157+
158+
val result = getInvoiceForTab(
159+
tab = ReceiveTab.AUTO,
160+
bip21 = bip21LightningFirst,
161+
bolt11 = testBolt11,
162+
cjitInvoice = null,
163+
isNodeRunning = true,
164+
onchainAddress = testAddress
165+
)
166+
167+
assertEquals(bip21LightningFirst, result)
168+
}
169+
122170
@Test
123171
fun `getInvoiceForTab SPENDING returns CJIT invoice when available and node running`() {
124172
val bip21 = "bitcoin:$testAddress?lightning=$testBolt11"

0 commit comments

Comments
 (0)