feat: move lightning address and payment targets to wallet screen

- Add lightning address field with save button to WalletScreen
- Add payment targets navigation row to WalletScreen
- Remove lightning address field from profile editor (NewUserMetadataScreen)
- Remove payment targets entry from AllSettings
- Extend WalletViewModel to load/save the lightning address via userMetadata

https://claude.ai/code/session_017NPhwCnSzCuMn9sTX3ca37
This commit is contained in:
Claude
2026-04-08 18:01:13 +00:00
parent 74d3d9e65c
commit d2cf09e5e1
5 changed files with 138 additions and 57 deletions
@@ -227,22 +227,6 @@ fun NewUserMetadataScreen(
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringRes(R.string.lightning_address)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.lnAddress.value,
onValueChange = { postViewModel.lnAddress.value = it },
placeholder = {
Text(
text = "me@mylightningnode.com",
color = MaterialTheme.colorScheme.placeholderText,
)
},
singleLine = true,
)
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringRes(R.string.nip_05) + " (NIP-05)") },
modifier = Modifier.fillMaxWidth(),
@@ -57,8 +57,6 @@ class NewUserMetadataViewModel : ViewModel() {
val website = mutableStateOf("")
val pronouns = mutableStateOf("")
val nip05 = mutableStateOf("")
val lnAddress = mutableStateOf("")
val lnURL = mutableStateOf("")
val twitter = mutableStateOf("")
val github = mutableStateOf("")
@@ -82,8 +80,6 @@ class NewUserMetadataViewModel : ViewModel() {
website.value = it.info.website ?: ""
pronouns.value = it.info.pronouns ?: ""
nip05.value = it.info.nip05 ?: ""
lnAddress.value = it.info.lud16 ?: ""
lnURL.value = it.info.lud06 ?: ""
}
twitter.value = ""
@@ -121,8 +117,6 @@ class NewUserMetadataViewModel : ViewModel() {
pronouns = pronouns.value,
about = about.value,
nip05 = nip05.value,
lnAddress = lnAddress.value,
lnURL = lnURL.value,
)
val identities =
@@ -146,8 +140,6 @@ class NewUserMetadataViewModel : ViewModel() {
banner.value = ""
website.value = ""
nip05.value = ""
lnAddress.value = ""
lnURL.value = ""
twitter.value = ""
github.value = ""
mastodon.value = ""
@@ -29,7 +29,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.AccountBalanceWallet
import androidx.compose.material.icons.outlined.Bolt
import androidx.compose.material.icons.outlined.CloudUpload
import androidx.compose.material.icons.outlined.DeleteForever
@@ -122,13 +121,6 @@ fun AllSettingsScreen(
onClick = { nav.nav(Route.EditMediaServers) },
)
HorizontalDivider()
SettingsNavigationRow(
title = R.string.payment_targets,
icon = Icons.Outlined.AccountBalanceWallet,
tint = tint,
onClick = { nav.nav(Route.EditPaymentTargets) },
)
HorizontalDivider()
SettingsNavigationRow(
title = R.string.reactions,
icon = Icons.Outlined.FavoriteBorder,
@@ -30,22 +30,28 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material.icons.filled.ArrowDownward
import androidx.compose.material.icons.filled.ArrowUpward
import androidx.compose.material.icons.outlined.AccountBalanceWallet
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -64,6 +70,7 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import java.text.NumberFormat
@OptIn(ExperimentalMaterial3Api::class)
@@ -73,10 +80,14 @@ fun WalletScreen(
nav: INav,
) {
val walletViewModel: WalletViewModel = viewModel()
walletViewModel.init(accountViewModel.account)
walletViewModel.init(accountViewModel)
val hasWallet by walletViewModel.hasWalletSetup.collectAsState()
LaunchedEffect(accountViewModel) {
walletViewModel.loadLnAddress()
}
Scaffold(
topBar = {
TopAppBar(
@@ -92,30 +103,36 @@ fun WalletScreen(
)
},
) { padding ->
if (!hasWallet) {
NoWalletSetup(
modifier = Modifier.padding(padding),
nav = nav,
)
} else {
WalletHomeContent(
walletViewModel = walletViewModel,
modifier = Modifier.padding(padding),
nav = nav,
)
Column(
modifier =
Modifier
.padding(padding)
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
if (!hasWallet) {
NoWalletSetup(nav = nav)
} else {
WalletHomeContent(
walletViewModel = walletViewModel,
nav = nav,
)
}
HorizontalDivider()
LightningAddressSection(walletViewModel = walletViewModel)
HorizontalDivider()
PaymentTargetsRow(nav = nav)
}
}
}
@Composable
private fun NoWalletSetup(
modifier: Modifier,
nav: INav,
) {
private fun NoWalletSetup(nav: INav) {
Column(
modifier =
modifier
.fillMaxSize()
Modifier
.fillMaxWidth()
.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
@@ -142,7 +159,6 @@ private fun NoWalletSetup(
@Composable
private fun WalletHomeContent(
walletViewModel: WalletViewModel,
modifier: Modifier,
nav: INav,
) {
val balance by walletViewModel.balanceSats.collectAsState()
@@ -157,8 +173,8 @@ private fun WalletHomeContent(
Column(
modifier =
modifier
.fillMaxSize()
Modifier
.fillMaxWidth()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
@@ -206,7 +222,7 @@ private fun WalletHomeContent(
)
}
Spacer(modifier = Modifier.weight(1f))
Spacer(modifier = Modifier.height(32.dp))
// Action buttons
Row(
@@ -276,3 +292,68 @@ private fun WalletHomeContent(
Spacer(modifier = Modifier.height(24.dp))
}
}
@Composable
private fun LightningAddressSection(walletViewModel: WalletViewModel) {
val lnAddress by walletViewModel.lnAddress.collectAsState()
Column(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
text = stringRes(R.string.lightning_address),
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold,
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
OutlinedTextField(
value = lnAddress,
onValueChange = { walletViewModel.updateLnAddress(it) },
modifier = Modifier.weight(1f),
placeholder = {
Text(
text = "me@mylightningnode.com",
color = MaterialTheme.colorScheme.placeholderText,
)
},
singleLine = true,
)
TextButton(onClick = { walletViewModel.saveLnAddress() }) {
Text(text = stringRes(R.string.save))
}
}
}
}
@Composable
private fun PaymentTargetsRow(nav: INav) {
Row(
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = Icons.Outlined.AccountBalanceWallet,
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.onBackground,
)
Spacer(modifier = Modifier.width(16.dp))
Text(
text = stringRes(R.string.payment_targets),
style = MaterialTheme.typography.bodyLarge,
)
}
TextButton(onClick = { nav.nav(Route.EditPaymentTargets) }) {
Text(text = stringRes(R.string.manage))
}
}
}
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBalanceMethod
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBalanceSuccessResponse
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetInfoMethod
@@ -86,10 +87,14 @@ private const val PAYMENT_FAILED = "Payment failed"
class WalletViewModel : ViewModel() {
private var account: Account? = null
private var accountViewModel: AccountViewModel? = null
private val _hasWalletSetup = MutableStateFlow(false)
val hasWalletSetup = _hasWalletSetup.asStateFlow()
private val _lnAddress = MutableStateFlow("")
val lnAddress = _lnAddress.asStateFlow()
private val _balanceSats = MutableStateFlow<Long?>(null)
val balanceSats = _balanceSats.asStateFlow()
@@ -137,15 +142,42 @@ class WalletViewModel : ViewModel() {
onTimeout()
}
fun init(account: Account) {
this.account = account
_hasWalletSetup.value = account.nip47SignerState.hasWalletConnectSetup()
fun init(accountViewModel: AccountViewModel) {
this.accountViewModel = accountViewModel
this.account = accountViewModel.account
_hasWalletSetup.value = accountViewModel.account.nip47SignerState.hasWalletConnectSetup()
}
fun refreshWalletSetup() {
_hasWalletSetup.value = account?.nip47SignerState?.hasWalletConnectSetup() == true
}
fun loadLnAddress() {
viewModelScope.launch(Dispatchers.IO) {
val info =
account
?.userProfile()
?.metadataOrNull()
?.flow
?.value
?.info
_lnAddress.value = info?.lud16 ?: ""
}
}
fun updateLnAddress(address: String) {
_lnAddress.value = address
}
fun saveLnAddress() {
val accountVm = accountViewModel ?: return
val acc = account ?: return
accountVm.launchSigner {
val event = acc.userMetadata.sendNewUserMetadata(lnAddress = _lnAddress.value)
acc.sendLiterallyEverywhere(event)
}
}
fun fetchBalance() {
val acc = account ?: return
viewModelScope.launch(Dispatchers.IO) {