Refining the Payment via Intent
This commit is contained in:
@@ -0,0 +1,207 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.model.User
|
||||||
|
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
||||||
|
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||||
|
import com.vitorpamplona.quartz.events.PayInvoiceErrorResponse
|
||||||
|
import com.vitorpamplona.quartz.events.ZapSplitSetup
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlin.math.round
|
||||||
|
|
||||||
|
class ZapPaymentHandler(val account: Account) {
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class Payable(
|
||||||
|
val info: ZapSplitSetup,
|
||||||
|
val user: User?,
|
||||||
|
val amountMilliSats: Long,
|
||||||
|
val invoice: String
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun zap(
|
||||||
|
note: Note,
|
||||||
|
amountMilliSats: Long,
|
||||||
|
pollOption: Int?,
|
||||||
|
message: String,
|
||||||
|
context: Context,
|
||||||
|
onError: (String) -> Unit,
|
||||||
|
onProgress: (percent: Float) -> Unit,
|
||||||
|
onPayViaIntent: (ImmutableList<Payable>) -> Unit,
|
||||||
|
zapType: LnZapEvent.ZapType
|
||||||
|
) = withContext(Dispatchers.IO) {
|
||||||
|
val zapSplitSetup = note.event?.zapSplitSetup()
|
||||||
|
|
||||||
|
val zapsToSend = if (!zapSplitSetup.isNullOrEmpty()) {
|
||||||
|
zapSplitSetup
|
||||||
|
} else {
|
||||||
|
val lud16 = note.author?.info?.lud16?.trim() ?: note.author?.info?.lud06?.trim()
|
||||||
|
|
||||||
|
if (lud16.isNullOrBlank()) {
|
||||||
|
onError(context.getString(R.string.user_does_not_have_a_lightning_address_setup_to_receive_sats))
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
|
||||||
|
listOf(ZapSplitSetup(lud16, null, weight = 1.0, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
val totalWeight = zapsToSend.sumOf { it.weight }
|
||||||
|
|
||||||
|
val invoicesToPayOnIntent = mutableListOf<Payable>()
|
||||||
|
|
||||||
|
zapsToSend.forEachIndexed { index, value ->
|
||||||
|
val outerProgressMin = index / zapsToSend.size.toFloat()
|
||||||
|
val outerProgressMax = (index + 1) / zapsToSend.size.toFloat()
|
||||||
|
|
||||||
|
val zapValue =
|
||||||
|
round((amountMilliSats * value.weight / totalWeight) / 1000f).toLong() * 1000
|
||||||
|
|
||||||
|
if (value.isLnAddress) {
|
||||||
|
innerZap(
|
||||||
|
lud16 = value.lnAddressOrPubKeyHex,
|
||||||
|
note = note,
|
||||||
|
amount = zapValue,
|
||||||
|
pollOption = pollOption,
|
||||||
|
message = message,
|
||||||
|
context = context,
|
||||||
|
onError = onError,
|
||||||
|
onProgress = {
|
||||||
|
onProgress((it * (outerProgressMax - outerProgressMin)) + outerProgressMin)
|
||||||
|
},
|
||||||
|
zapType = zapType,
|
||||||
|
onPayInvoiceThroughIntent = {
|
||||||
|
invoicesToPayOnIntent.add(
|
||||||
|
Payable(
|
||||||
|
info = value,
|
||||||
|
user = null,
|
||||||
|
amountMilliSats = zapValue,
|
||||||
|
invoice = it
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val user = LocalCache.getUserIfExists(value.lnAddressOrPubKeyHex)
|
||||||
|
val lud16 = user?.info?.lnAddress()
|
||||||
|
|
||||||
|
if (lud16 != null) {
|
||||||
|
innerZap(
|
||||||
|
lud16 = lud16,
|
||||||
|
note = note,
|
||||||
|
amount = zapValue,
|
||||||
|
pollOption = pollOption,
|
||||||
|
message = message,
|
||||||
|
context = context,
|
||||||
|
onError = onError,
|
||||||
|
onProgress = {
|
||||||
|
onProgress((it * (outerProgressMax - outerProgressMin)) + outerProgressMin)
|
||||||
|
},
|
||||||
|
zapType = zapType,
|
||||||
|
overrideUser = user,
|
||||||
|
onPayInvoiceThroughIntent = {
|
||||||
|
invoicesToPayOnIntent.add(
|
||||||
|
Payable(
|
||||||
|
info = value,
|
||||||
|
user = user,
|
||||||
|
amountMilliSats = zapValue,
|
||||||
|
invoice = it
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
onError(
|
||||||
|
context.getString(
|
||||||
|
R.string.user_x_does_not_have_a_lightning_address_setup_to_receive_sats,
|
||||||
|
user?.toBestDisplayName() ?: value.lnAddressOrPubKeyHex
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invoicesToPayOnIntent.isNotEmpty()) {
|
||||||
|
onPayViaIntent(invoicesToPayOnIntent.toImmutableList())
|
||||||
|
onProgress(1f)
|
||||||
|
} else {
|
||||||
|
launch(Dispatchers.IO) {
|
||||||
|
// Awaits for the event to come back to LocalCache.
|
||||||
|
delay(5000)
|
||||||
|
onProgress(1f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun innerZap(
|
||||||
|
lud16: String,
|
||||||
|
note: Note,
|
||||||
|
amount: Long,
|
||||||
|
pollOption: Int?,
|
||||||
|
message: String,
|
||||||
|
context: Context,
|
||||||
|
onError: (String) -> Unit,
|
||||||
|
onProgress: (percent: Float) -> Unit,
|
||||||
|
onPayInvoiceThroughIntent: (String) -> Unit,
|
||||||
|
zapType: LnZapEvent.ZapType,
|
||||||
|
overrideUser: User? = null
|
||||||
|
) {
|
||||||
|
var zapRequestJson = ""
|
||||||
|
|
||||||
|
if (zapType != LnZapEvent.ZapType.NONZAP) {
|
||||||
|
val zapRequest = account.createZapRequestFor(note, pollOption, message, zapType, overrideUser)
|
||||||
|
if (zapRequest != null) {
|
||||||
|
zapRequestJson = zapRequest.toJson()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onProgress(0.10f)
|
||||||
|
|
||||||
|
LightningAddressResolver().lnAddressInvoice(
|
||||||
|
lud16,
|
||||||
|
amount,
|
||||||
|
message,
|
||||||
|
zapRequestJson,
|
||||||
|
onSuccess = {
|
||||||
|
onProgress(0.7f)
|
||||||
|
if (account.hasWalletConnectSetup()) {
|
||||||
|
account.sendZapPaymentRequestFor(
|
||||||
|
bolt11 = it,
|
||||||
|
note,
|
||||||
|
onResponse = { response ->
|
||||||
|
if (response is PayInvoiceErrorResponse) {
|
||||||
|
onProgress(0.0f)
|
||||||
|
onError(
|
||||||
|
response.error?.message
|
||||||
|
?: response.error?.code?.toString()
|
||||||
|
?: "Error parsing error message"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
onProgress(1f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
onProgress(0.8f)
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
onPayInvoiceThroughIntent(it)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
onError(context.getString(R.string.lightning_wallets_not_found2))
|
||||||
|
}
|
||||||
|
onProgress(0f)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError = onError,
|
||||||
|
onProgress = onProgress
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ import androidx.compose.ui.window.Popup
|
|||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
|
||||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||||
@@ -35,6 +36,8 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
|||||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||||
import com.vitorpamplona.quartz.events.toImmutableListOfLists
|
import com.vitorpamplona.quartz.events.toImmutableListOfLists
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -283,6 +286,12 @@ fun ZapVote(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var wantsToZap by remember { mutableStateOf(false) }
|
var wantsToZap by remember { mutableStateOf(false) }
|
||||||
|
var wantsToPay by remember {
|
||||||
|
mutableStateOf<ImmutableList<ZapPaymentHandler.Payable>>(
|
||||||
|
persistentListOf()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
var zappingProgress by remember { mutableStateOf(0f) }
|
var zappingProgress by remember { mutableStateOf(0f) }
|
||||||
var showErrorMessageDialog by remember { mutableStateOf<String?>(null) }
|
var showErrorMessageDialog by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
@@ -361,6 +370,9 @@ fun ZapVote(
|
|||||||
scope.launch(Dispatchers.Main) {
|
scope.launch(Dispatchers.Main) {
|
||||||
zappingProgress = it
|
zappingProgress = it
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onPayViaIntent = {
|
||||||
|
|
||||||
},
|
},
|
||||||
zapType = accountViewModel.account.defaultZapType
|
zapType = accountViewModel.account.defaultZapType
|
||||||
)
|
)
|
||||||
@@ -393,10 +405,19 @@ fun ZapVote(
|
|||||||
scope.launch(Dispatchers.Main) {
|
scope.launch(Dispatchers.Main) {
|
||||||
zappingProgress = it
|
zappingProgress = it
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onPayViaIntent = {
|
||||||
|
wantsToPay = it
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (wantsToPay.isNotEmpty()) {
|
||||||
|
PayViaIntentDialog(payingInvoices = wantsToPay, accountViewModel = accountViewModel) {
|
||||||
|
wantsToPay = persistentListOf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (showErrorMessageDialog != null) {
|
if (showErrorMessageDialog != null) {
|
||||||
ErrorMessageDialog(
|
ErrorMessageDialog(
|
||||||
title = stringResource(id = R.string.error_dialog_zap_error),
|
title = stringResource(id = R.string.error_dialog_zap_error),
|
||||||
@@ -463,7 +484,8 @@ fun FilteredZapAmountChoicePopup(
|
|||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onChangeAmount: () -> Unit,
|
onChangeAmount: () -> Unit,
|
||||||
onError: (text: String) -> Unit,
|
onError: (text: String) -> Unit,
|
||||||
onProgress: (percent: Float) -> Unit
|
onProgress: (percent: Float) -> Unit,
|
||||||
|
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
@@ -502,6 +524,7 @@ fun FilteredZapAmountChoicePopup(
|
|||||||
context,
|
context,
|
||||||
onError,
|
onError,
|
||||||
onProgress,
|
onProgress,
|
||||||
|
onPayViaIntent,
|
||||||
defaultZapType
|
defaultZapType
|
||||||
)
|
)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
@@ -526,6 +549,7 @@ fun FilteredZapAmountChoicePopup(
|
|||||||
context,
|
context,
|
||||||
onError,
|
onError,
|
||||||
onProgress,
|
onProgress,
|
||||||
|
onPayViaIntent,
|
||||||
defaultZapType
|
defaultZapType
|
||||||
)
|
)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ import coil.request.CachePolicy
|
|||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
|
||||||
import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
||||||
import com.vitorpamplona.amethyst.ui.components.ImageUrlType
|
import com.vitorpamplona.amethyst.ui.components.ImageUrlType
|
||||||
import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
|
import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
|
||||||
@@ -107,8 +108,11 @@ import com.vitorpamplona.amethyst.ui.theme.TinyBorders
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
|
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
|
||||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
import com.vitorpamplona.amethyst.ui.theme.placeholderTextColorFilter
|
import com.vitorpamplona.amethyst.ui.theme.placeholderTextColorFilter
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableMap
|
import kotlinx.collections.immutable.toImmutableMap
|
||||||
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -907,6 +911,11 @@ fun ZapReaction(
|
|||||||
var wantsToChangeZapAmount by remember { mutableStateOf(false) }
|
var wantsToChangeZapAmount by remember { mutableStateOf(false) }
|
||||||
var wantsToSetCustomZap by remember { mutableStateOf(false) }
|
var wantsToSetCustomZap by remember { mutableStateOf(false) }
|
||||||
var showErrorMessageDialog by remember { mutableStateOf<String?>(null) }
|
var showErrorMessageDialog by remember { mutableStateOf<String?>(null) }
|
||||||
|
var wantsToPay by remember(baseNote) {
|
||||||
|
mutableStateOf<ImmutableList<ZapPaymentHandler.Payable>>(
|
||||||
|
persistentListOf()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
@@ -938,6 +947,9 @@ fun ZapReaction(
|
|||||||
zappingProgress = 0f
|
zappingProgress = 0f
|
||||||
showErrorMessageDialog = it
|
showErrorMessageDialog = it
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onPayViaIntent = {
|
||||||
|
wantsToPay = it
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -971,6 +983,9 @@ fun ZapReaction(
|
|||||||
scope.launch(Dispatchers.Main) {
|
scope.launch(Dispatchers.Main) {
|
||||||
zappingProgress = it
|
zappingProgress = it
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onPayViaIntent = {
|
||||||
|
wantsToPay = it
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -981,7 +996,10 @@ fun ZapReaction(
|
|||||||
textContent = showErrorMessageDialog ?: "",
|
textContent = showErrorMessageDialog ?: "",
|
||||||
onClickStartMessage = {
|
onClickStartMessage = {
|
||||||
baseNote.author?.let {
|
baseNote.author?.let {
|
||||||
nav(routeToMessage(it, showErrorMessageDialog, accountViewModel))
|
scope.launch(Dispatchers.IO) {
|
||||||
|
val route = routeToMessage(it, showErrorMessageDialog, accountViewModel)
|
||||||
|
nav(route)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDismiss = { showErrorMessageDialog = null }
|
onDismiss = { showErrorMessageDialog = null }
|
||||||
@@ -995,6 +1013,12 @@ fun ZapReaction(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (wantsToPay.isNotEmpty()) {
|
||||||
|
PayViaIntentDialog(payingInvoices = wantsToPay, accountViewModel = accountViewModel) {
|
||||||
|
wantsToPay = persistentListOf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (wantsToSetCustomZap) {
|
if (wantsToSetCustomZap) {
|
||||||
ZapCustomDialog(
|
ZapCustomDialog(
|
||||||
onClose = { wantsToSetCustomZap = false },
|
onClose = { wantsToSetCustomZap = false },
|
||||||
@@ -1009,6 +1033,9 @@ fun ZapReaction(
|
|||||||
zappingProgress = it
|
zappingProgress = it
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onPayViaIntent = {
|
||||||
|
wantsToPay = it
|
||||||
|
},
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
baseNote = baseNote
|
baseNote = baseNote
|
||||||
)
|
)
|
||||||
@@ -1045,7 +1072,8 @@ private fun zapClick(
|
|||||||
context: Context,
|
context: Context,
|
||||||
onZappingProgress: (Float) -> Unit,
|
onZappingProgress: (Float) -> Unit,
|
||||||
onMultipleChoices: () -> Unit,
|
onMultipleChoices: () -> Unit,
|
||||||
onError: (String) -> Unit
|
onError: (String) -> Unit,
|
||||||
|
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit
|
||||||
) {
|
) {
|
||||||
if (accountViewModel.account.zapAmountChoices.isEmpty()) {
|
if (accountViewModel.account.zapAmountChoices.isEmpty()) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -1080,7 +1108,8 @@ private fun zapClick(
|
|||||||
onZappingProgress(it)
|
onZappingProgress(it)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
zapType = accountViewModel.account.defaultZapType
|
zapType = accountViewModel.account.defaultZapType,
|
||||||
|
onPayViaIntent = onPayViaIntent
|
||||||
)
|
)
|
||||||
} else if (accountViewModel.account.zapAmountChoices.size > 1) {
|
} else if (accountViewModel.account.zapAmountChoices.size > 1) {
|
||||||
onMultipleChoices()
|
onMultipleChoices()
|
||||||
@@ -1391,7 +1420,8 @@ fun ZapAmountChoicePopup(
|
|||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onChangeAmount: () -> Unit,
|
onChangeAmount: () -> Unit,
|
||||||
onError: (text: String) -> Unit,
|
onError: (text: String) -> Unit,
|
||||||
onProgress: (percent: Float) -> Unit
|
onProgress: (percent: Float) -> Unit,
|
||||||
|
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
@@ -1417,6 +1447,7 @@ fun ZapAmountChoicePopup(
|
|||||||
context,
|
context,
|
||||||
onError,
|
onError,
|
||||||
onProgress,
|
onProgress,
|
||||||
|
onPayViaIntent,
|
||||||
account.defaultZapType
|
account.defaultZapType
|
||||||
)
|
)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
@@ -1441,7 +1472,8 @@ fun ZapAmountChoicePopup(
|
|||||||
context,
|
context,
|
||||||
onError,
|
onError,
|
||||||
onProgress,
|
onProgress,
|
||||||
account.defaultZapType
|
onPayViaIntent,
|
||||||
|
account.defaultZapType,
|
||||||
)
|
)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.note
|
package com.vitorpamplona.amethyst.ui.note
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
@@ -10,25 +12,41 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.service.NostrHomeDataSource
|
||||||
|
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
|
||||||
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.PostButton
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
|
||||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
class ZapOptionstViewModel : ViewModel() {
|
class ZapOptionstViewModel : ViewModel() {
|
||||||
@@ -62,6 +80,7 @@ fun ZapCustomDialog(
|
|||||||
onClose: () -> Unit,
|
onClose: () -> Unit,
|
||||||
onError: (text: String) -> Unit,
|
onError: (text: String) -> Unit,
|
||||||
onProgress: (percent: Float) -> Unit,
|
onProgress: (percent: Float) -> Unit,
|
||||||
|
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
baseNote: Note
|
baseNote: Note
|
||||||
) {
|
) {
|
||||||
@@ -113,6 +132,7 @@ fun ZapCustomDialog(
|
|||||||
context,
|
context,
|
||||||
onError = onError,
|
onError = onError,
|
||||||
onProgress = onProgress,
|
onProgress = onProgress,
|
||||||
|
onPayViaIntent = onPayViaIntent,
|
||||||
zapType = selectedZapType
|
zapType = selectedZapType
|
||||||
)
|
)
|
||||||
onClose()
|
onClose()
|
||||||
@@ -266,3 +286,115 @@ fun ErrorMessageDialog(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PayViaIntentDialog(
|
||||||
|
payingInvoices: ImmutableList<ZapPaymentHandler.Payable>,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
onClose: () -> Unit,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = onClose,
|
||||||
|
properties = DialogProperties(
|
||||||
|
dismissOnClickOutside = false,
|
||||||
|
usePlatformDefaultWidth = false
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Surface() {
|
||||||
|
Column(modifier = Modifier.padding(10.dp)) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
CloseButton(onPress = onClose)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = DoubleVertSpacer)
|
||||||
|
|
||||||
|
payingInvoices.forEachIndexed { index, it ->
|
||||||
|
val paid = remember {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = Size10dp)) {
|
||||||
|
if (it.user != null) {
|
||||||
|
BaseUserPicture(it.user, Size55dp, accountViewModel = accountViewModel)
|
||||||
|
} else {
|
||||||
|
DisplayBlankAuthor(size = Size55dp)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = DoubleHorzSpacer)
|
||||||
|
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
if (it.user != null) {
|
||||||
|
UsernameDisplay(it.user, showPlayButton = false)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.wallet_number, index+1),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 18.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row() {
|
||||||
|
Text(
|
||||||
|
text = showAmount((it.amountMilliSats/1000.0f).toBigDecimal()),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 18.sp
|
||||||
|
)
|
||||||
|
Spacer(modifier = StdHorzSpacer)
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.sats),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 18.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = DoubleHorzSpacer)
|
||||||
|
|
||||||
|
PayButton(isActive = !paid.value) {
|
||||||
|
paid.value = true
|
||||||
|
|
||||||
|
val uri = "lightning:" + it.invoice
|
||||||
|
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(uri))
|
||||||
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
|
||||||
|
ContextCompat.startActivity(context, intent, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PayButton(isActive: Boolean, modifier: Modifier = Modifier, onPost: () -> Unit = {}) {
|
||||||
|
Button(
|
||||||
|
modifier = modifier,
|
||||||
|
onClick = {
|
||||||
|
onPost()
|
||||||
|
},
|
||||||
|
shape = ButtonBorder,
|
||||||
|
colors = ButtonDefaults
|
||||||
|
.buttonColors(
|
||||||
|
backgroundColor = if (isActive) MaterialTheme.colors.primary else Color.Gray
|
||||||
|
),
|
||||||
|
contentPadding = PaddingValues(0.dp)
|
||||||
|
) {
|
||||||
|
if (isActive)
|
||||||
|
Text(text = stringResource(R.string.pay), color = Color.White)
|
||||||
|
else
|
||||||
|
Text(text = stringResource(R.string.paid), color = Color.White)
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-155
@@ -27,6 +27,7 @@ import com.vitorpamplona.amethyst.service.Nip05Verifier
|
|||||||
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
|
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
|
||||||
import com.vitorpamplona.amethyst.service.Nip11Retriever
|
import com.vitorpamplona.amethyst.service.Nip11Retriever
|
||||||
import com.vitorpamplona.amethyst.service.OnlineChecker
|
import com.vitorpamplona.amethyst.service.OnlineChecker
|
||||||
|
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
|
||||||
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
||||||
import com.vitorpamplona.amethyst.ui.components.UrlPreviewState
|
import com.vitorpamplona.amethyst.ui.components.UrlPreviewState
|
||||||
import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification
|
import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification
|
||||||
@@ -43,6 +44,7 @@ import com.vitorpamplona.quartz.events.SealedGossipEvent
|
|||||||
import com.vitorpamplona.quartz.events.UserMetadata
|
import com.vitorpamplona.quartz.events.UserMetadata
|
||||||
import com.vitorpamplona.quartz.events.ZapSplitSetup
|
import com.vitorpamplona.quartz.events.ZapSplitSetup
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.ImmutableSet
|
import kotlinx.collections.immutable.ImmutableSet
|
||||||
import kotlinx.collections.immutable.persistentSetOf
|
import kotlinx.collections.immutable.persistentSetOf
|
||||||
import kotlinx.collections.immutable.toImmutableSet
|
import kotlinx.collections.immutable.toImmutableSet
|
||||||
@@ -215,111 +217,7 @@ class AccountViewModel(val account: Account) : ViewModel() {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun zap(note: Note, amount: Long, pollOption: Int?, message: String, context: Context, onError: (String) -> Unit, onProgress: (percent: Float) -> Unit, zapType: LnZapEvent.ZapType) {
|
fun zap(
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
innerZap(note, amount, pollOption, message, context, onError, onProgress, zapType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun innerZap(note: Note, amountMilliSats: Long, pollOption: Int?, message: String, context: Context, onError: (String) -> Unit, onProgress: (percent: Float) -> Unit, zapType: LnZapEvent.ZapType) {
|
|
||||||
val zapSplitSetup = note.event?.zapSplitSetup()
|
|
||||||
|
|
||||||
val zapsToSend = if (!zapSplitSetup.isNullOrEmpty()) {
|
|
||||||
zapSplitSetup
|
|
||||||
} else {
|
|
||||||
val lud16 = note.author?.info?.lud16?.trim() ?: note.author?.info?.lud06?.trim()
|
|
||||||
|
|
||||||
if (lud16.isNullOrBlank()) {
|
|
||||||
onError(context.getString(R.string.user_does_not_have_a_lightning_address_setup_to_receive_sats))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
listOf(ZapSplitSetup(lud16, null, weight = 1.0, true))
|
|
||||||
}
|
|
||||||
|
|
||||||
val totalWeight = zapsToSend.sumOf { it.weight }
|
|
||||||
|
|
||||||
val invoicesToPayOnIntent = mutableListOf<String>()
|
|
||||||
|
|
||||||
zapsToSend.forEachIndexed { index, value ->
|
|
||||||
val outerProgressMin = index / zapsToSend.size.toFloat()
|
|
||||||
val outerProgressMax = (index + 1) / zapsToSend.size.toFloat()
|
|
||||||
|
|
||||||
val zapValue =
|
|
||||||
round((amountMilliSats * value.weight / totalWeight) / 1000f).toLong() * 1000
|
|
||||||
|
|
||||||
if (value.isLnAddress) {
|
|
||||||
innerZap(
|
|
||||||
lud16 = value.lnAddressOrPubKeyHex,
|
|
||||||
note = note,
|
|
||||||
amount = zapValue,
|
|
||||||
pollOption = pollOption,
|
|
||||||
message = message,
|
|
||||||
context = context,
|
|
||||||
onError = onError,
|
|
||||||
onProgress = {
|
|
||||||
onProgress((it * (outerProgressMax - outerProgressMin)) + outerProgressMin)
|
|
||||||
},
|
|
||||||
zapType = zapType,
|
|
||||||
onPayInvoiceThroughIntent = {
|
|
||||||
invoicesToPayOnIntent.add(it)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
val user = LocalCache.getUserIfExists(value.lnAddressOrPubKeyHex)
|
|
||||||
val lud16 = user?.info?.lnAddress()
|
|
||||||
|
|
||||||
if (lud16 != null) {
|
|
||||||
innerZap(
|
|
||||||
lud16 = lud16,
|
|
||||||
note = note,
|
|
||||||
amount = zapValue,
|
|
||||||
pollOption = pollOption,
|
|
||||||
message = message,
|
|
||||||
context = context,
|
|
||||||
onError = onError,
|
|
||||||
onProgress = {
|
|
||||||
onProgress((it * (outerProgressMax - outerProgressMin)) + outerProgressMin)
|
|
||||||
},
|
|
||||||
zapType = zapType,
|
|
||||||
overrideUser = user,
|
|
||||||
onPayInvoiceThroughIntent = {
|
|
||||||
invoicesToPayOnIntent.add(it)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
onError(
|
|
||||||
context.getString(
|
|
||||||
R.string.user_x_does_not_have_a_lightning_address_setup_to_receive_sats,
|
|
||||||
user?.toBestDisplayName() ?: value.lnAddressOrPubKeyHex
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (invoicesToPayOnIntent.isNotEmpty()) {
|
|
||||||
payInvoices(bolt11s = invoicesToPayOnIntent, context = context)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Awaits for the event to come back to LocalCache.
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
delay(5000)
|
|
||||||
onProgress(1f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun payInvoices(bolt11s: List<String>, context: Context) {
|
|
||||||
val uri = "lightning:" + bolt11s.joinToString("&")
|
|
||||||
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(uri))
|
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
|
||||||
|
|
||||||
ContextCompat.startActivity(context, intent, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun innerZap(
|
|
||||||
lud16: String,
|
|
||||||
note: Note,
|
note: Note,
|
||||||
amount: Long,
|
amount: Long,
|
||||||
pollOption: Int?,
|
pollOption: Int?,
|
||||||
@@ -327,58 +225,12 @@ class AccountViewModel(val account: Account) : ViewModel() {
|
|||||||
context: Context,
|
context: Context,
|
||||||
onError: (String) -> Unit,
|
onError: (String) -> Unit,
|
||||||
onProgress: (percent: Float) -> Unit,
|
onProgress: (percent: Float) -> Unit,
|
||||||
onPayInvoiceThroughIntent: (String) -> Unit,
|
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
|
||||||
zapType: LnZapEvent.ZapType,
|
zapType: LnZapEvent.ZapType
|
||||||
overrideUser: User? = null
|
|
||||||
) {
|
) {
|
||||||
var zapRequestJson = ""
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
ZapPaymentHandler(account).zap(note, amount, pollOption, message, context, onError, onProgress, onPayViaIntent, zapType)
|
||||||
if (zapType != LnZapEvent.ZapType.NONZAP) {
|
|
||||||
val zapRequest = account.createZapRequestFor(note, pollOption, message, zapType, overrideUser)
|
|
||||||
if (zapRequest != null) {
|
|
||||||
zapRequestJson = zapRequest.toJson()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onProgress(0.10f)
|
|
||||||
|
|
||||||
LightningAddressResolver().lnAddressInvoice(
|
|
||||||
lud16,
|
|
||||||
amount,
|
|
||||||
message,
|
|
||||||
zapRequestJson,
|
|
||||||
onSuccess = {
|
|
||||||
onProgress(0.7f)
|
|
||||||
if (account.hasWalletConnectSetup()) {
|
|
||||||
account.sendZapPaymentRequestFor(
|
|
||||||
bolt11 = it,
|
|
||||||
note,
|
|
||||||
onResponse = { response ->
|
|
||||||
if (response is PayInvoiceErrorResponse) {
|
|
||||||
onProgress(0.0f)
|
|
||||||
onError(
|
|
||||||
response.error?.message
|
|
||||||
?: response.error?.code?.toString()
|
|
||||||
?: "Error parsing error message"
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
onProgress(1f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
onProgress(0.8f)
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
onPayInvoiceThroughIntent(it)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
onError(context.getString(R.string.lightning_wallets_not_found2))
|
|
||||||
}
|
|
||||||
onProgress(0f)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError = onError,
|
|
||||||
onProgress = onProgress
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun report(note: Note, type: ReportEvent.ReportType, content: String = "") {
|
fun report(note: Note, type: ReportEvent.ReportType, content: String = "") {
|
||||||
|
|||||||
@@ -582,4 +582,6 @@
|
|||||||
<string name="forwarding_zaps_to">Forwarding zaps to</string>
|
<string name="forwarding_zaps_to">Forwarding zaps to</string>
|
||||||
|
|
||||||
<string name="lightning_wallets_not_found2">Lightning wallets not found</string>
|
<string name="lightning_wallets_not_found2">Lightning wallets not found</string>
|
||||||
|
<string name="paid">Paid</string>
|
||||||
|
<string name="wallet_number">Wallet %1$s</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user