Skip to content

Commit d1342e1

Browse files
authored
Merge pull request #43 from synonymdev/feat/transfer-from-spending
Transfer from Spending flow
2 parents e05b0a3 + 1cc2e7b commit d1342e1

29 files changed

+1352
-153
lines changed

app/src/androidTest/java/to/bitkit/services/BlocktankTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ class BlocktankTest {
142142
couponCode = "",
143143
source = "bitkit-android",
144144
discountCode = null,
145-
turboChannel = false,
145+
zeroConf = false,
146146
zeroConfPayment = true,
147147
zeroReserve = false,
148148
clientNodeId = null,
@@ -189,7 +189,7 @@ class BlocktankTest {
189189
couponCode = "",
190190
source = "bitkit-android",
191191
discountCode = null,
192-
turboChannel = false,
192+
zeroConf = false,
193193
zeroConfPayment = true,
194194
zeroReserve = false,
195195
clientNodeId = null,

app/src/main/java/to/bitkit/data/SettingsStore.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ class SettingsStore @Inject constructor(
5757
store.edit { it[HAS_SEEN_SPENDING_INTRO] = value }
5858
}
5959

60+
val hasSeenSavingsIntro: Flow<Boolean> = store.data.map { it[HAS_SEEN_SAVINGS_INTRO] ?: false }
61+
suspend fun setHasSeenSavingsIntro(value: Boolean) {
62+
store.edit { it[HAS_SEEN_SAVINGS_INTRO] = value }
63+
}
64+
6065
val lightningSetupStep: Flow<Int> = store.data.map { it[LIGHTNING_SETUP_STEP] ?: 0 }
6166
suspend fun setLightningSetupStep(value: Int) {
6267
store.edit { it[LIGHTNING_SETUP_STEP] = value }
@@ -68,6 +73,7 @@ class SettingsStore @Inject constructor(
6873
private val SELECTED_CURRENCY_KEY = stringPreferencesKey("selected_currency")
6974
private val SHOW_EMPTY_STATE = booleanPreferencesKey("show_empty_state")
7075
private val HAS_SEEN_SPENDING_INTRO = booleanPreferencesKey("has_seen_spending_intro")
76+
private val HAS_SEEN_SAVINGS_INTRO = booleanPreferencesKey("has_seen_savings_intro")
7177
private val LIGHTNING_SETUP_STEP = intPreferencesKey("lightning_setup_step")
7278
}
7379
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package to.bitkit.ext
2+
3+
import org.lightningdevkit.ldknode.ChannelConfig
4+
import org.lightningdevkit.ldknode.ChannelDetails
5+
import org.lightningdevkit.ldknode.MaxDustHtlcExposure
6+
7+
/**
8+
* Calculates the expected amount in sats that will be available upon channel closure.
9+
*/
10+
val ChannelDetails.amountOnClose: ULong
11+
get() {
12+
val outboundCapacitySat = this.outboundCapacityMsat / 1000u
13+
val reserveSats = this.unspendablePunishmentReserve ?: 0u
14+
15+
return outboundCapacitySat + reserveSats
16+
}
17+
18+
fun mockChannelDetails(
19+
channelId: String,
20+
isChannelReady: Boolean = true,
21+
): ChannelDetails {
22+
return ChannelDetails(
23+
channelId = channelId,
24+
counterpartyNodeId = "counterpartyNodeId",
25+
fundingTxo = null,
26+
channelValueSats = 100_000uL,
27+
unspendablePunishmentReserve = 354uL,
28+
userChannelId = "userChannelId",
29+
feerateSatPer1000Weight = 5u,
30+
outboundCapacityMsat = 50_000uL,
31+
inboundCapacityMsat = 50_000uL,
32+
confirmationsRequired = 0u,
33+
confirmations = 12u,
34+
isOutbound = false,
35+
isChannelReady = isChannelReady,
36+
isUsable = true,
37+
isAnnounced = false,
38+
cltvExpiryDelta = null,
39+
counterpartyUnspendablePunishmentReserve = 0uL,
40+
counterpartyOutboundHtlcMinimumMsat = null,
41+
counterpartyOutboundHtlcMaximumMsat = null,
42+
counterpartyForwardingInfoFeeBaseMsat = null,
43+
counterpartyForwardingInfoFeeProportionalMillionths = null,
44+
counterpartyForwardingInfoCltvExpiryDelta = null,
45+
nextOutboundHtlcLimitMsat = 50_000uL,
46+
nextOutboundHtlcMinimumMsat = 0uL,
47+
forceCloseSpendDelay = null,
48+
inboundHtlcMinimumMsat = 0uL,
49+
inboundHtlcMaximumMsat = null,
50+
config = ChannelConfig(
51+
forwardingFeeProportionalMillionths = 0u,
52+
forwardingFeeBaseMsat = 0u,
53+
cltvExpiryDelta = 0u,
54+
maxDustHtlcExposure = MaxDustHtlcExposure.FixedLimit(limitMsat = 0uL),
55+
forceCloseAvoidanceMaxFeeSatoshis = 0uL,
56+
acceptUnderpayingHtlcs = false,
57+
),
58+
)
59+
}

app/src/main/java/to/bitkit/services/LightningService.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import org.lightningdevkit.ldknode.Builder
1818
import org.lightningdevkit.ldknode.ChannelDetails
1919
import org.lightningdevkit.ldknode.EsploraSyncConfig
2020
import org.lightningdevkit.ldknode.Event
21+
import org.lightningdevkit.ldknode.LightningBalance
2122
import org.lightningdevkit.ldknode.LogLevel
2223
import org.lightningdevkit.ldknode.Network
2324
import org.lightningdevkit.ldknode.Node

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

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ import to.bitkit.ui.screens.scanner.QrScanningScreen
4040
import to.bitkit.ui.screens.transfer.FundingAdvancedScreen
4141
import to.bitkit.ui.screens.transfer.FundingScreen
4242
import to.bitkit.ui.screens.transfer.LiquidityScreen
43+
import to.bitkit.ui.screens.transfer.SavingsAdvancedScreen
44+
import to.bitkit.ui.screens.transfer.SavingsAvailabilityScreen
45+
import to.bitkit.ui.screens.transfer.SavingsConfirmScreen
46+
import to.bitkit.ui.screens.transfer.SavingsIntroScreen
47+
import to.bitkit.ui.screens.transfer.SavingsProgressScreen
4348
import to.bitkit.ui.screens.transfer.SettingUpScreen
4449
import to.bitkit.ui.screens.transfer.SpendingAdvancedScreen
4550
import to.bitkit.ui.screens.transfer.SpendingAmountScreen
@@ -220,6 +225,44 @@ fun ContentView(
220225
composable<Routes.TransferIntro> {
221226
TransferIntroScreen()
222227
}
228+
composable<Routes.SavingsIntro> {
229+
SavingsIntroScreen(
230+
onContinueClick = {
231+
navController.navigate(Routes.SavingsAvailability)
232+
appViewModel.setHasSeenSavingsIntro(true)
233+
},
234+
onBackClick = { navController.popBackStack() },
235+
onCloseClick = { navController.popBackStack<Routes.Home>(inclusive = false) },
236+
)
237+
}
238+
composable<Routes.SavingsAvailability> {
239+
SavingsAvailabilityScreen(
240+
onBackClick = { navController.popBackStack() },
241+
onCancelClick = { navController.popBackStack<Routes.Home>(inclusive = false) },
242+
onContinueClick = { navController.navigate(Routes.SavingsConfirm) },
243+
)
244+
}
245+
composable<Routes.SavingsConfirm> {
246+
SavingsConfirmScreen(
247+
onConfirm = { navController.navigate(Routes.SavingsProgress) },
248+
onAdvancedClick = { navController.navigate(Routes.SavingsAdvanced) },
249+
onBackClick = { navController.popBackStack() },
250+
onCloseClick = { navController.popBackStack<Routes.Home>(inclusive = false) },
251+
)
252+
}
253+
composable<Routes.SavingsAdvanced> {
254+
SavingsAdvancedScreen(
255+
onContinueClick = { navController.popBackStack<Routes.SavingsConfirm>(inclusive = false) },
256+
onBackClick = { navController.popBackStack() },
257+
onCloseClick = { navController.popBackStack<Routes.Home>(inclusive = false) },
258+
)
259+
}
260+
composable<Routes.SavingsProgress> {
261+
SavingsProgressScreen(
262+
onContinueClick = { navController.popBackStack<Routes.Home>(inclusive = false) },
263+
onCloseClick = { navController.popBackStack<Routes.Home>(inclusive = false) },
264+
)
265+
}
223266
composable<Routes.SpendingIntro> {
224267
SpendingIntroScreen(
225268
onContinueClick = {
@@ -567,6 +610,14 @@ fun NavController.navigateToRegtestSettings() = navigate(
567610
route = Routes.RegtestSettings,
568611
)
569612

613+
fun NavController.navigateToTransferSavingsIntro() = navigate(
614+
route = Routes.SavingsIntro,
615+
)
616+
617+
fun NavController.navigateToTransferSavingsAvailability() = navigate(
618+
route = Routes.SavingsAvailability,
619+
)
620+
570621
fun NavController.navigateToTransferSpendingIntro() = navigate(
571622
route = Routes.SpendingIntro,
572623
)
@@ -662,6 +713,21 @@ object Routes {
662713
@Serializable
663714
data object SettingUp
664715

716+
@Serializable
717+
data object SavingsIntro
718+
719+
@Serializable
720+
data object SavingsAvailability
721+
722+
@Serializable
723+
data object SavingsConfirm
724+
725+
@Serializable
726+
data object SavingsAdvanced
727+
728+
@Serializable
729+
data object SavingsProgress
730+
665731
@Serializable
666732
data object Funding
667733

app/src/main/java/to/bitkit/ui/components/BalanceHeaderView.kt

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,20 @@
11
package to.bitkit.ui.components
22

3-
import androidx.compose.foundation.gestures.detectTapGestures
43
import androidx.compose.foundation.layout.Arrangement
54
import androidx.compose.foundation.layout.Column
65
import androidx.compose.foundation.layout.Row
76
import androidx.compose.foundation.layout.height
87
import androidx.compose.foundation.layout.padding
98
import androidx.compose.runtime.Composable
10-
import androidx.compose.runtime.getValue
11-
import androidx.compose.runtime.mutableStateOf
12-
import androidx.compose.runtime.remember
13-
import androidx.compose.runtime.setValue
149
import androidx.compose.ui.Alignment
1510
import androidx.compose.ui.Modifier
1611
import androidx.compose.ui.draw.alpha
17-
import androidx.compose.ui.graphics.graphicsLayer
18-
import androidx.compose.ui.input.pointer.pointerInput
1912
import androidx.compose.ui.unit.dp
2013
import to.bitkit.models.ConvertedAmount
2114
import to.bitkit.models.PrimaryDisplay
2215
import to.bitkit.ui.LocalCurrencies
2316
import to.bitkit.ui.currencyViewModel
17+
import to.bitkit.ui.shared.util.clickableAlpha
2418

2519
@Composable
2620
fun BalanceHeaderView(
@@ -33,27 +27,11 @@ fun BalanceHeaderView(
3327
val (rates, _, _, _, displayUnit, primaryDisplay) = LocalCurrencies.current
3428
val converted: ConvertedAmount? = if (rates.isNotEmpty()) currency.convert(sats = sats) else null
3529

36-
var isPressed by remember { mutableStateOf(false) }
37-
3830
Column(
3931
verticalArrangement = Arrangement.spacedBy(4.dp),
4032
horizontalAlignment = Alignment.Start,
4133
modifier = modifier
42-
.graphicsLayer {
43-
this.alpha = if (isPressed) 0.5f else 1f
44-
}
45-
.pointerInput(Unit) {
46-
detectTapGestures(
47-
onPress = {
48-
isPressed = true
49-
tryAwaitRelease()
50-
isPressed = false
51-
},
52-
onTap = {
53-
currency.togglePrimaryDisplay()
54-
}
55-
)
56-
}
34+
.clickableAlpha { currency.togglePrimaryDisplay() }
5735
) {
5836
converted?.let { converted ->
5937
if (primaryDisplay == PrimaryDisplay.BITCOIN) {
Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,35 @@
11
package to.bitkit.ui.components
22

33
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.remember
5+
import androidx.compose.ui.Modifier
46
import androidx.compose.ui.graphics.Color
57
import androidx.compose.ui.platform.LocalInspectionMode
68
import to.bitkit.models.PrimaryDisplay
79
import to.bitkit.models.formatToModernDisplay
810
import to.bitkit.ui.LocalCurrencies
911
import to.bitkit.ui.currencyViewModel
12+
import to.bitkit.ui.shared.util.clickableAlpha
13+
import to.bitkit.ui.theme.Colors
14+
import to.bitkit.ui.utils.withAccent
1015

1116
@Composable
12-
fun MoneySSB(sats: Long) {
13-
val currency = currencyViewModel ?: return
14-
val currencies = LocalCurrencies.current
17+
fun MoneyDisplay(
18+
sats: Long,
19+
onClick: (() -> Unit)? = null,
20+
) {
21+
rememberMoneyText(sats)?.let { text ->
22+
Display(
23+
text = text.withAccent(accentColor = Colors.White64),
24+
modifier = Modifier.clickableAlpha(onClick = onClick)
25+
)
26+
}
27+
}
1528

16-
currency.convert(sats)?.let { converted ->
17-
if (currencies.primaryDisplay == PrimaryDisplay.BITCOIN) {
18-
val btcComponents = converted.bitcoinDisplay(currencies.displayUnit)
19-
BodySSB(text = "${btcComponents.symbol} ${btcComponents.value}")
20-
} else {
21-
BodySSB(text = "${converted.symbol} ${converted.formatted}")
22-
}
29+
@Composable
30+
fun MoneySSB(sats: Long) {
31+
rememberMoneyText(sats)?.let { text ->
32+
BodySSB(text = text.withAccent(accentColor = Colors.White64))
2333
}
2434
}
2535

@@ -31,16 +41,54 @@ fun MoneyCaptionB(
3141
val isPreview = LocalInspectionMode.current
3242
if (isPreview) {
3343
CaptionB(text = sats.formatToModernDisplay(), color = color)
44+
return
3445
}
3546

3647
val currency = currencyViewModel ?: return
3748
val currencies = LocalCurrencies.current
3849

39-
currency.convert(sats)?.let { converted ->
40-
val btcComponents = converted.bitcoinDisplay(currencies.displayUnit)
50+
val displayText = remember(currencies, sats) {
51+
currency.convert(sats)?.let { converted ->
52+
val btcComponents = converted.bitcoinDisplay(currencies.displayUnit)
53+
btcComponents.value
54+
}
55+
}
56+
57+
displayText?.let { text ->
4158
CaptionB(
42-
text = btcComponents.value,
59+
text = text,
4360
color = color,
4461
)
4562
}
4663
}
64+
65+
/**
66+
* Generates a formatted representation of a monetary value based on the provided amount in satoshis
67+
* and the current currency display settings. Can be either in bitcoin or fiat.
68+
*
69+
* @param sats The amount in satoshis to be formatted and displayed.
70+
* @return A formatted string representation of the monetary value, or null if it cannot be generated.
71+
*/
72+
@Composable
73+
fun rememberMoneyText(
74+
sats: Long,
75+
): String? {
76+
val isPreview = LocalInspectionMode.current
77+
if (isPreview) {
78+
return "<accent>₿</accent> ${sats.formatToModernDisplay()}"
79+
}
80+
81+
val currency = currencyViewModel ?: return null
82+
val currencies = LocalCurrencies.current
83+
84+
return remember(currencies, sats) {
85+
val converted = currency.convert(sats) ?: return@remember null
86+
87+
if (currencies.primaryDisplay == PrimaryDisplay.BITCOIN) {
88+
val btcComponents = converted.bitcoinDisplay(currencies.displayUnit)
89+
"<accent>${btcComponents.symbol}</accent> ${btcComponents.value}"
90+
} else {
91+
"<accent>${converted.symbol}</accent> ${converted.formatted}"
92+
}
93+
}
94+
}

0 commit comments

Comments
 (0)