Skip to content

Commit

Permalink
(android) Add UI to spend from final wallet (#633)
Browse files Browse the repository at this point in the history
User can only spend confirmed funds.

Also show a notification in the home when there are
funds on the final wallet, as it's easy to miss.
  • Loading branch information
dpad85 authored Oct 9, 2024
1 parent 8628bb6 commit 76413ed
Show file tree
Hide file tree
Showing 21 changed files with 686 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ import fr.acinq.phoenix.android.settings.displayseed.DisplaySeedView
import fr.acinq.phoenix.android.settings.fees.AdvancedIncomingFeePolicy
import fr.acinq.phoenix.android.settings.fees.LiquidityPolicyView
import fr.acinq.phoenix.android.settings.walletinfo.FinalWalletInfo
import fr.acinq.phoenix.android.settings.walletinfo.FinalWalletRefundView
import fr.acinq.phoenix.android.settings.walletinfo.SendSwapInRefundView
import fr.acinq.phoenix.android.settings.walletinfo.SwapInAddresses
import fr.acinq.phoenix.android.settings.walletinfo.SwapInSignerView
Expand Down Expand Up @@ -246,7 +247,8 @@ fun AppView(
onPaymentsHistoryClick = { navController.navigate(Screen.PaymentsHistory.route) },
onTorClick = { navController.navigate(Screen.TorConfig) },
onElectrumClick = { navController.navigate(Screen.ElectrumServer) },
onShowSwapInWallet = { navController.navigate(Screen.WalletInfo.SwapInWallet) },
onNavigateToSwapInWallet = { navController.navigate(Screen.WalletInfo.SwapInWallet) },
onNavigateToFinalWallet = { navController.navigate(Screen.WalletInfo.FinalWallet) },
onShowNotifications = { navController.navigate(Screen.Notifications) },
onRequestLiquidityClick = { navController.navigate(Screen.LiquidityRequest.route) },
)
Expand Down Expand Up @@ -455,7 +457,10 @@ fun AppView(
SendSwapInRefundView(onBackClick = { navController.popBackStack() })
}
composable(Screen.WalletInfo.FinalWallet.route) {
FinalWalletInfo(onBackClick = { navController.popBackStack() })
FinalWalletInfo(onBackClick = { navController.popBackStack() }, onSpendClick = { navController.navigate(Screen.WalletInfo.FinalWalletRefund.route) })
}
composable(Screen.WalletInfo.FinalWalletRefund.route) {
FinalWalletRefundView(onBackClick = { navController.popBackStack() })
}
composable(Screen.LiquidityPolicy.route, deepLinks = listOf(navDeepLink { uriPattern ="phoenix:liquiditypolicy" })) {
LiquidityPolicyView(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ sealed class Screen(val route: String) {
data object SwapInAddresses: Screen("settings/walletinfo/swapinaddresses")
data object SwapInSigner: Screen("settings/walletinfo/swapinsigner")
data object FinalWallet: Screen("settings/walletinfo/final")
data object FinalWalletRefund: Screen("settings/walletinfo/finalrefund")
data object SwapInRefund: Screen("settings/walletinfo/swapinrefund")
}
data object LiquidityPolicy: Screen("settings/liquiditypolicy")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import fr.acinq.phoenix.android.utils.datastore.InternalDataRepository
import fr.acinq.phoenix.data.WalletNotice
import fr.acinq.phoenix.managers.AppConfigurationManager
import fr.acinq.phoenix.managers.PeerManager
import fr.acinq.phoenix.utils.extensions.confirmed
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch
Expand All @@ -49,6 +50,7 @@ sealed class Notice() {
data object BackupSeedReminder : ShowInHome(5)
data object MempoolFull : ShowInHome(10)
data object UpdateAvailable : ShowInHome(20)
data object FundsInFinalWallet : ShowInHome(21)
data object NotificationPermission : ShowInHome(30)

// less important notices
Expand Down Expand Up @@ -80,6 +82,7 @@ class NoticesViewModel(
viewModelScope.launch { monitorWalletContext() }
viewModelScope.launch { monitorSwapInCloseToTimeout() }
viewModelScope.launch { monitorWalletNotice() }
viewModelScope.launch { monitorFinalWallet() }
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
isPowerSaverModeOn = powerManager.isPowerSaveMode
context.registerReceiver(receiver, IntentFilter(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED))
Expand Down Expand Up @@ -148,6 +151,16 @@ class NoticesViewModel(
}
}

private suspend fun monitorFinalWallet() {
peerManager.finalWallet.filterNotNull().collect {
if (it.all.isNotEmpty()) {
addNotice(Notice.FundsInFinalWallet)
} else {
removeNotice<Notice.FundsInFinalWallet>()
}
}
}

class Factory(
private val appConfigurationManager: AppConfigurationManager,
private val peerManager: PeerManager,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
Expand Down Expand Up @@ -59,14 +60,16 @@ fun HomeNotices(
notices: List<Notice>,
notifications: List<Pair<Set<UUID>, Notification>>,
onNavigateToSwapInWallet: () -> Unit,
onNavigateToFinalWallet: () -> Unit,
onNavigateToNotificationsList: () -> Unit,
) {
val filteredNotices = notices.filterIsInstance<Notice.ShowInHome>().sortedBy { it.priority }
val now = currentTimestampMillis()
val recentRejectedOffchainCount = notifications.map { it.second }
.filterIsInstance<Notification.PaymentRejected>()
.filter { it.source == LiquidityEvents.Source.OffChainPayment && (now - it.createdAt) < 15 * DateUtils.HOUR_IN_MILLIS }
.size
val filteredNotices = remember(notices) { notices.filterIsInstance<Notice.ShowInHome>().sortedBy { it.priority } }
val recentRejectedOffchainCount = remember (notifications) {
notifications.map { it.second }
.filterIsInstance<Notification.PaymentRejected>()
.filter { it.source == LiquidityEvents.Source.OffChainPayment && (currentTimestampMillis() - it.createdAt) < 15 * DateUtils.HOUR_IN_MILLIS }
.size
}

Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(12.dp)) {
if (recentRejectedOffchainCount > 0) {
Expand All @@ -77,6 +80,7 @@ fun HomeNotices(
notice = it,
messagesCount = notices.size + notifications.size,
onNavigateToSwapInWallet = onNavigateToSwapInWallet,
onNavigateToFinalWallet = onNavigateToFinalWallet,
onNavigateToNotificationsList = onNavigateToNotificationsList
)
}
Expand All @@ -90,6 +94,7 @@ private fun FirstNoticeView(
notice: Notice.ShowInHome,
messagesCount: Int,
onNavigateToSwapInWallet: () -> Unit,
onNavigateToFinalWallet: () -> Unit,
onNavigateToNotificationsList: () -> Unit,
) {
val context = LocalContext.current
Expand Down Expand Up @@ -130,6 +135,8 @@ private fun FirstNoticeView(
is Notice.MempoolFull -> onNavigateToNotificationsList

is Notice.RemoteMessage -> onNavigateToNotificationsList

is Notice.FundsInFinalWallet -> onNavigateToFinalWallet
}
} else {
onNavigateToNotificationsList
Expand Down Expand Up @@ -184,6 +191,9 @@ private fun FirstNoticeView(
NoticeTextView(text = notice.notice.message, icon = R.drawable.ic_info)
}

is Notice.FundsInFinalWallet -> {
NoticeTextView(text = stringResource(id = R.string.inappnotif_final_wallet_message), icon = R.drawable.ic_info)
}
}

if (messagesCount > 1) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ fun HomeView(
onPaymentsHistoryClick: () -> Unit,
onTorClick: () -> Unit,
onElectrumClick: () -> Unit,
onShowSwapInWallet: () -> Unit,
onNavigateToSwapInWallet: () -> Unit,
onNavigateToFinalWallet: () -> Unit,
onShowNotifications: () -> Unit,
onRequestLiquidityClick: () -> Unit,
) {
Expand Down Expand Up @@ -238,14 +239,15 @@ fun HomeView(
balanceDisplayMode = balanceDisplayMode,
swapInBalance = swapInBalance.value,
unconfirmedChannelsBalance = pendingChannelsBalance.value,
onShowSwapInWallet = onShowSwapInWallet,
onShowSwapInWallet = onNavigateToSwapInWallet,
)
PrimarySeparator(modifier = Modifier.layoutId("separator"))
HomeNotices(
modifier = Modifier.layoutId("notices"),
notices = notices,
notices = notices.toList(),
notifications = notifications,
onNavigateToSwapInWallet = onShowSwapInWallet,
onNavigateToSwapInWallet = onNavigateToSwapInWallet,
onNavigateToFinalWallet = onNavigateToFinalWallet,
onNavigateToNotificationsList = onShowNotifications,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,15 @@ private fun PermamentNotice(
},
)
}

is Notice.FundsInFinalWallet -> {
ImportantNotification(
icon = R.drawable.ic_chain,
message = stringResource(id = R.string.inappnotif_final_wallet_message),
actionText = stringResource(id = R.string.inappnotif_final_wallet_action),
onActionClick = { nc?.navigate(Screen.WalletInfo.FinalWallet.route) },
)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
Expand All @@ -31,46 +29,62 @@ import androidx.compose.ui.unit.dp
import fr.acinq.lightning.MilliSatoshi
import fr.acinq.lightning.blockchain.electrum.WalletState
import fr.acinq.lightning.blockchain.electrum.balance
import fr.acinq.lightning.utils.msat
import fr.acinq.lightning.utils.sat
import fr.acinq.lightning.utils.toMilliSatoshi
import fr.acinq.phoenix.android.R
import fr.acinq.phoenix.android.business
import fr.acinq.phoenix.android.components.*
import fr.acinq.phoenix.android.components.Card
import fr.acinq.phoenix.android.components.CardHeader
import fr.acinq.phoenix.android.components.DefaultScreenHeader
import fr.acinq.phoenix.android.components.DefaultScreenLayout
import fr.acinq.phoenix.android.components.FilledButton
import fr.acinq.phoenix.utils.extensions.confirmed

@Composable
fun FinalWalletInfo(
onBackClick: () -> Unit
onBackClick: () -> Unit,
onSpendClick: () -> Unit,
) {
val finalWallet by business.peerManager.finalWallet.collectAsState()

DefaultScreenLayout(isScrollable = false) {
DefaultScreenHeader(onBackClick = onBackClick, title = stringResource(id = R.string.walletinfo_onchain_final), helpMessage = stringResource(id = R.string.walletinfo_onchain_final_about))
ConfirmedBalanceView(balance = finalWallet?.all?.balance?.toMilliSatoshi())
UnconfirmedWalletView(wallet = finalWallet)
ConfirmedBalanceView(balance = finalWallet?.confirmed?.balance?.toMilliSatoshi(), onSpendClick = onSpendClick)
UnconfirmedWalletView(utxos = finalWallet?.unconfirmed.orEmpty())
}
}

@Composable
private fun ConfirmedBalanceView(
balance: MilliSatoshi?
balance: MilliSatoshi?,
onSpendClick: () -> Unit,
) {
CardHeader(text = stringResource(id = R.string.walletinfo_confirmed_title))
Card(
internalPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp),
modifier = Modifier.fillMaxWidth()
) {
BalanceRow(balance = balance)
if (balance != null && balance > 0.msat) {
Spacer(modifier = Modifier.height(12.dp))
FilledButton(
text = "Spend",
icon = R.drawable.ic_send,
onClick = onSpendClick,
)
}
}
}

@Composable
private fun UnconfirmedWalletView(
wallet: WalletState.WalletWithConfirmations?
utxos: List<WalletState.Utxo>
) {
if (wallet != null && wallet.unconfirmed.balance > 0.sat) {
if (utxos.balance > 0.sat) {
CardHeader(text = stringResource(id = R.string.walletinfo_unconfirmed_title))
Card {
wallet.unconfirmed.forEach {
utxos.forEach {
UtxoRow(it, null)
}
}
Expand Down
Loading

0 comments on commit 76413ed

Please sign in to comment.