Sending Lightning Tips

This commit is contained in:
Vitor Pamplona 2023-02-08 18:26:49 -05:00
parent 00981ef15c
commit 5195ddf63e
5 changed files with 325 additions and 4 deletions

View File

@ -0,0 +1,106 @@
package com.vitorpamplona.amethyst.lnurl
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import kotlin.math.ln
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import nostr.postr.Bech32
import okhttp3.Call
import okhttp3.Callback
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
class LightningAddressResolver {
val client = OkHttpClient.Builder().build()
fun assembleUrl(lnaddress: String): String? {
val parts = lnaddress.split("@")
if (parts.size != 2) {
return null
}
return "https://${parts[1]}/.well-known/lnurlp/${parts[0]}"
}
fun fetchLightningAddressJson(lnaddress: String, onSucess: (String) -> Unit) {
val scope = CoroutineScope(Job() + Dispatchers.IO)
scope.launch {
fetchLightningAddressJsonSuspend(lnaddress, onSucess)
}
}
private suspend fun fetchLightningAddressJsonSuspend(lnaddress: String, onSucess: (String) -> Unit) {
val url = assembleUrl(lnaddress) ?: return
withContext(Dispatchers.IO) {
val request: Request = Request.Builder().url(url).build()
client.newCall(request).enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
response.use {
onSucess(response.body.string())
}
}
override fun onFailure(call: Call, e: java.io.IOException) {
e.printStackTrace()
}
})
}
}
fun fetchLightningInvoice(lnCallback: String, milliSats: Long, message: String, onSucess: (String) -> Unit) {
val scope = CoroutineScope(Job() + Dispatchers.IO)
scope.launch {
fetchLightningInvoiceSuspend(lnCallback, milliSats, message, onSucess)
}
}
private suspend fun fetchLightningInvoiceSuspend(lnCallback: String, milliSats: Long, message: String, onSucess: (String) -> Unit) {
val urlBinder = if (lnCallback.contains("?")) "&" else "?"
val url = "$lnCallback${urlBinder}amount=$milliSats&comment=$message"
withContext(Dispatchers.IO) {
val request: Request = Request.Builder().url(url).build()
client.newCall(request).enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
response.use {
onSucess(response.body.string())
}
}
override fun onFailure(call: Call, e: java.io.IOException) {
e.printStackTrace()
}
})
}
}
fun lnAddressToLnUrl(lnaddress: String, onSucess: (String) -> Unit) {
fetchLightningAddressJson(lnaddress) {
onSucess(Bech32.encodeBytes("lnurl",it.toByteArray(), Bech32.Encoding.Bech32))
}
}
fun lnAddressInvoice(lnaddress: String, milliSats: Long, message: String, onSucess: (String) -> Unit) {
val mapper = jacksonObjectMapper()
fetchLightningAddressJson(lnaddress) {
mapper.readTree(it)?.get("callback")?.asText()?.let { callback ->
fetchLightningInvoice(callback, milliSats, message,
onSucess = {
mapper.readTree(it)?.get("pr")?.asText()?.let { pr ->
onSucess(pr)
}
}
)
}
}
}
}

View File

@ -9,13 +9,20 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -23,11 +30,15 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat.startActivity
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.lnurl.LnInvoiceUtil
import java.text.NumberFormat
@Composable
fun InvoicePreview(lnInvoice: String) {
@ -78,14 +89,15 @@ fun InvoicePreview(lnInvoice: String) {
amount?.let {
Text(
text = "${amount.toInt()} sats",
text = "${NumberFormat.getInstance().format(amount)} sats",
fontSize = 25.sp,
fontWeight = FontWeight.W500,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 10.dp),
)
}
}
Button(
modifier = Modifier.fillMaxWidth().padding(vertical = 10.dp),

View File

@ -0,0 +1,137 @@
package com.vitorpamplona.amethyst.ui.components
import android.content.Intent
import android.net.Uri
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat.startActivity
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.lnurl.LightningAddressResolver
@Composable
fun InvoiceRequest(lud16: String, onClose: () -> Unit ) {
val context = LocalContext.current
Column(modifier = Modifier
.fillMaxWidth()
.padding(start = 30.dp, end = 30.dp)
.clip(shape = RoundedCornerShape(10.dp))
.border(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f), RoundedCornerShape(15.dp))
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(30.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 10.dp)
) {
Icon(
painter = painterResource(R.drawable.lightning),
null,
modifier = Modifier.size(20.dp),
tint = Color.Unspecified
)
Text(
text = "Lightning Invoice",
fontSize = 20.sp,
fontWeight = FontWeight.W500,
modifier = Modifier.padding(start = 10.dp)
)
}
Divider()
var message by remember { mutableStateOf("") }
var amount by remember { mutableStateOf(1000L) }
OutlinedTextField(
label = { Text(text = "Note to Receiver") },
modifier = Modifier.fillMaxWidth(),
value = message,
onValueChange = { message = it },
placeholder = {
Text(
text = "Thank you so much!",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
},
keyboardOptions = KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences
),
singleLine = true
)
OutlinedTextField(
label = { Text(text = "Amount in Sats") },
modifier = Modifier.fillMaxWidth(),
value = amount.toString(),
onValueChange = { amount = it.toLong() },
placeholder = {
Text(
text = "1000",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
},
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number
),
singleLine = true
)
Button(
modifier = Modifier.fillMaxWidth().padding(vertical = 10.dp),
onClick = {
LightningAddressResolver().lnAddressInvoice(lud16, amount * 1000, message) {
runCatching {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("lightning:$it"))
startActivity(context, intent, null)
}
onClose()
}
},
shape = RoundedCornerShape(15.dp),
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.primary
)
) {
Text(text = "Send", color = Color.White, fontSize = 20.sp)
}
}
}
}

View File

@ -1,14 +1,20 @@
package com.vitorpamplona.amethyst.ui.screen
import android.content.Intent
import android.net.Uri
import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bolt
import androidx.compose.material.icons.filled.EditNote
import androidx.compose.material.icons.filled.Key
import androidx.compose.material.icons.filled.Link
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Share
import androidx.compose.runtime.*
@ -25,6 +31,7 @@ import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
@ -32,6 +39,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import coil.compose.AsyncImage
@ -41,6 +49,7 @@ import com.google.accompanist.pager.pagerTabIndicatorOffset
import com.google.accompanist.pager.rememberPagerState
import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.lnurl.LightningAddressResolver
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
@ -48,8 +57,10 @@ import com.vitorpamplona.amethyst.service.NostrUserProfileFollowersDataSource
import com.vitorpamplona.amethyst.service.NostrUserProfileFollowsDataSource
import com.vitorpamplona.amethyst.service.model.ReportEvent
import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataView
import com.vitorpamplona.amethyst.ui.components.InvoiceRequest
import com.vitorpamplona.amethyst.ui.note.UserPicture
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import kotlinx.coroutines.launch
import nostr.postr.toNpub
import nostr.postr.toNsec
@ -301,6 +312,9 @@ private fun DrawAdditionalInfo(baseUser: User) {
val userState by baseUser.liveMetadata.observeAsState()
val user = userState?.user ?: return
val uri = LocalUriHandler.current
val context = LocalContext.current
Text(
user.bestDisplayName() ?: "",
modifier = Modifier.padding(top = 7.dp),
@ -308,9 +322,60 @@ private fun DrawAdditionalInfo(baseUser: User) {
fontSize = 25.sp
)
Text(
" @${user.bestUsername()}",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
"@${user.bestUsername()}",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = 5.dp)
)
val website = user.info.website
if (!website.isNullOrEmpty()) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
imageVector = Icons.Default.Link,
contentDescription = "Website",
modifier = Modifier.size(16.dp)
)
ClickableText(
text = AnnotatedString(website.removePrefix("https://")),
onClick = { user.info.website?.let { uri.openUri(it) } },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary),
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = 5.dp)
)
}
}
var ZapExpanded by remember { mutableStateOf(false) }
val lud16 = user.info.lud16
if (!lud16.isNullOrEmpty()) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
tint = BitcoinOrange,
imageVector = Icons.Default.Bolt,
contentDescription = "Lightning Address",
modifier = Modifier.size(16.dp)
)
ClickableText(
text = AnnotatedString(lud16),
onClick = { ZapExpanded = !ZapExpanded },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary),
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = 5.dp).weight(1f)
)
}
if (ZapExpanded) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 5.dp)) {
InvoiceRequest(lud16) {
ZapExpanded = false
}
}
}
}
Text(
"${user.info.about}",
color = MaterialTheme.colors.onSurface,

View File

@ -6,6 +6,7 @@ val Purple200 = Color(0xFFBB86FC)
val Purple500 = Color(0xFF6200EE)
val Purple700 = Color(0xFF3700B3)
val Teal200 = Color(0xFF03DAC5)
val BitcoinOrange = Color (0xFFF7931A)
val Following = Color(0xFF03DAC5)
val FollowsFollow = Color.Yellow