Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -112,16 +112,16 @@ fun AmountHeroInput(
val rate = rates[unit]
if (rate != null) {
initialAmount?.toFiat(rate.price).toPlainString(limitDecimal = true)
} else "?!"
} else context.getString(R.string.utils_unknown_amount)
}
is BitcoinUnit -> {
initialAmount?.toUnit(u).toPlainString()
}
else -> "?!"
else -> context.getString(R.string.utils_unknown_amount)
}
))
}
var inputAmount by remember { mutableStateOf(initialAmount) }
var inputAmount by remember(initialAmount) { mutableStateOf(initialAmount) }
val convertedValue: String by remember(inputAmount, unit) {
val s = when (unit) {
is FiatCurrency -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ fun ColumnScope.LightningInvoiceView(
if (state is LightningInvoiceState.Done) {
Column(modifier = Modifier.padding(top = 2.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
if (state.description.isNullOrBlank() && state.amount == null) {
Text(text = "Add amount and description", style = MaterialTheme.typography.subtitle2)
Text(text = stringResource(id = R.string.receive_lightning_edit_button), style = MaterialTheme.typography.subtitle2)
}
state.description?.takeIf { it.isNotBlank() }?.let { desc ->
QRCodeLabel(label = stringResource(R.string.receive_lightning_desc_label)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ sealed class LightningInvoiceState {
data class Bolt12(val offer: OfferTypes.Offer) : Done() {
override val paymentData by lazy { offer.encode() }
override val description: String? by lazy { offer.description }
override val amount: MilliSatoshi? by lazy { offer.amount }
override val amount: MilliSatoshi? by lazy { offer.amountMsat }
}
}
data class Error(val e: Throwable) : LightningInvoiceState()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
Expand All @@ -42,27 +43,63 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import fr.acinq.bitcoin.utils.Either
import fr.acinq.lightning.MilliSatoshi
import fr.acinq.lightning.wire.OfferTypes
import fr.acinq.phoenix.PhoenixBusiness
import fr.acinq.phoenix.android.LocalBitcoinUnits
import fr.acinq.phoenix.android.R
import fr.acinq.phoenix.android.WalletId
import fr.acinq.phoenix.android.application
import fr.acinq.phoenix.android.components.inputs.AmountHeroInput
import fr.acinq.phoenix.android.components.AmountWithFiatRowView
import fr.acinq.phoenix.android.components.buttons.BackButtonWithActiveWallet
import fr.acinq.phoenix.android.components.buttons.Clickable
import fr.acinq.phoenix.android.components.buttons.FilledButton
import fr.acinq.phoenix.android.components.ProgressView
import fr.acinq.phoenix.android.components.TextWithIcon
import fr.acinq.phoenix.android.components.layouts.SplashLabelRow
import fr.acinq.phoenix.android.components.layouts.SplashLayout
import fr.acinq.phoenix.android.components.inputs.TextInput
import fr.acinq.phoenix.android.components.buttons.SmartSpendButton
import fr.acinq.phoenix.android.components.contact.ContactOrOfferView
import fr.acinq.phoenix.android.components.dialogs.Dialog
import fr.acinq.phoenix.android.components.dialogs.ModalBottomSheet
import fr.acinq.phoenix.android.components.feedback.ErrorMessage
import fr.acinq.phoenix.android.payments.details.splash.translatePaymentError
import fr.acinq.phoenix.android.utils.annotatedStringResource
import fr.acinq.phoenix.android.utils.converters.AmountConverter.toMilliSatoshi
import fr.acinq.phoenix.android.utils.converters.AmountFormatter.toPlainString
import fr.acinq.phoenix.android.utils.converters.AmountFormatter.toPrettyString
import fr.acinq.phoenix.android.utils.converters.DateFormatter.toAbsoluteDateTimeString
import fr.acinq.phoenix.data.ExchangeRate
import fr.acinq.phoenix.data.FiatCurrency
import kotlinx.coroutines.launch


sealed class InitialAmount {
abstract val amount: MilliSatoshi?

data object None : InitialAmount() { override val amount = null }

data class MilliSat(override val amount: MilliSatoshi): InitialAmount()

sealed class Fiat : InitialAmount() {
abstract val value: Long
abstract val currencyCode: String?
data class ResolvingToMsat(override val value: Long, override val currencyCode: String) : Fiat() {
override val amount = null
}
data class ConvertedToMsat(override val value: Long, override val currencyCode: String, override val amount: MilliSatoshi, val rate: ExchangeRate.BitcoinPriceRate): Fiat()
data class CurrencyCodeUnsupported(override val value: Long, override val currencyCode: String): Fiat() {
override val amount = null
}
data class NoCurrencyInOffer(override val value: Long) : Fiat() {
override val amount = null
override val currencyCode = null
}
}
}

@Composable
fun SendToOfferView(
Expand All @@ -79,16 +116,48 @@ fun SendToOfferView(
val balance = business.balanceManager.balance.collectAsState(null).value
val peer by business.peerManager.peerState.collectAsState()
val trampolineFees = peer?.walletParams?.trampolineFees?.firstOrNull()
val currencyManager = application.phoenixGlobal.currencyManager

val vm = viewModel<SendOfferViewModel>(factory = SendOfferViewModel.Factory(offer, business.peerManager, business.nodeParamsManager, business.databaseManager), key = offer.encode())
val requestedAmount = offer.amount
var amount by remember { mutableStateOf(requestedAmount) }
val requestedAmountState = produceState<InitialAmount?>(initialValue = null, key1 = offer) {
value = when (val amountInOffer = offer.amount) {
is Either.Left -> InitialAmount.MilliSat(amountInOffer.value)
is Either.Right -> {
val currencyCode = offer.currency
if (currencyCode == null) {
InitialAmount.Fiat.NoCurrencyInOffer(value = amountInOffer.value)
} else {
val fiatCurrency = FiatCurrency.valueOfOrNull(currencyCode)
if (fiatCurrency == null) {
InitialAmount.Fiat.CurrencyCodeUnsupported(value = amountInOffer.value, currencyCode = currencyCode)
} else {
launch {
currencyManager.fetchRateForCurrency(fiatCurrency)?.let { rate ->
value = InitialAmount.Fiat.ConvertedToMsat(
value = amountInOffer.value,
currencyCode = currencyCode,
amount = amountInOffer.value.toDouble().toMilliSatoshi(rate.price),
rate = rate,
)
}
}
InitialAmount.Fiat.ResolvingToMsat(value = amountInOffer.value, currencyCode = currencyCode)
}
}
}
null -> InitialAmount.None
}
}
val requestedAmount = requestedAmountState.value?.amount

// the requested amount may be in fiat and needs to be converted first
var amount by remember(requestedAmount) { mutableStateOf(requestedAmount) }
val amountErrorMessage: String = remember(amount) {
val currentAmount = amount
when {
currentAmount == null -> ""
balance != null && currentAmount > balance -> context.getString(R.string.send_error_amount_over_balance)
requestedAmount != null && currentAmount < requestedAmount -> context.getString(
requestedAmount != null && currentAmount < requestedAmount && requestedAmountState.value is InitialAmount.MilliSat -> context.getString(
R.string.send_error_amount_below_requested,
(requestedAmount).toPrettyString(prefBitcoinUnit, withUnit = true)
)
Expand All @@ -112,6 +181,10 @@ fun SendToOfferView(
validationErrorMessage = amountErrorMessage,
inputTextSize = 42.sp,
)
when (val state = requestedAmountState.value) {
is InitialAmount.Fiat -> ConvertingFiatOfferAmount(state)
else -> Unit
}
}
) {
offer.description?.takeIf { it.isNotBlank() }?.let {
Expand Down Expand Up @@ -248,3 +321,44 @@ private fun PayerNoteInput(
Spacer(modifier = Modifier.height(80.dp))
}
}

@Composable
private fun ConvertingFiatOfferAmount(state: InitialAmount.Fiat) {
var showOfferInFiatDialog by remember { mutableStateOf(false) }

Clickable(onClick = { showOfferInFiatDialog = true }, internalPadding = PaddingValues(8.dp), shape = RoundedCornerShape(12.dp)) {
TextWithIcon(
text = stringResource(R.string.send_offer_fiat_main_label, "${state.value} ${state.currencyCode}"),
textStyle = MaterialTheme.typography.caption.copy(fontSize = 14.sp),
icon = R.drawable.ic_refresh,
iconTint = MaterialTheme.typography.caption.color,
)
}

if (showOfferInFiatDialog) {
Dialog(onDismiss = { showOfferInFiatDialog = false }, buttons = null) {
Column(modifier = Modifier.padding(horizontal = 20.dp, vertical = 16.dp)) {
Text(text = stringResource(id = R.string.send_offer_fiat_main_label, "${state.value} ${state.currencyCode}"), style = MaterialTheme.typography.h4)
Spacer(Modifier.height(12.dp))
when (state) {
is InitialAmount.Fiat.ResolvingToMsat -> {
Text(text = stringResource(id = R.string.send_offer_fiat_converting_desc, state.currencyCode))
}
is InitialAmount.Fiat.CurrencyCodeUnsupported -> {
Text(text = stringResource(id = R.string.send_offer_fiat_err_unsupported, state.currencyCode))
}
is InitialAmount.Fiat.NoCurrencyInOffer -> {
Text(text = stringResource(id = R.string.send_offer_fiat_err_malformed_invoice))
}
is InitialAmount.Fiat.ConvertedToMsat -> {
Text(text = annotatedStringResource(id = R.string.send_offer_fiat_converted_to, state.amount.toPrettyString(unit = LocalBitcoinUnits.current.primary, withUnit = true)))
Spacer(Modifier.height(12.dp))
Text(text = stringResource(id = R.string.send_offer_fiat_converted_rate_details, state.rate.price.toPlainString(), state.rate.fiatCurrency.name), style = MaterialTheme.typography.subtitle2)
Spacer(Modifier.height(2.dp))
Text(text = stringResource(id = R.string.send_offer_fiat_converted_rate_timestamp, state.rate.timestampMillis.toAbsoluteDateTimeString()), style = MaterialTheme.typography.subtitle2)
}
}
}
}
}
}
34 changes: 34 additions & 0 deletions phoenix-android/src/main/res/drawable/ic_refresh.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#00000000"
android:pathData="M21,12a9,9 0,0 0,-9 -9,9.75 9.75,0 0,0 -6.74,2.74L3,8"
android:strokeWidth="1.5"
android:strokeColor="?attr/colorPrimary"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M3,3v5h5"
android:strokeWidth="1.5"
android:strokeColor="?attr/colorPrimary"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M3,12a9,9 0,0 0,9 9,9.75 9.75,0 0,0 6.74,-2.74L21,16"
android:strokeWidth="1.5"
android:strokeColor="?attr/colorPrimary"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M16,16h5v5"
android:strokeWidth="1.5"
android:strokeColor="?attr/colorPrimary"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
9 changes: 9 additions & 0 deletions phoenix-android/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@
<string name="receive_bip353_info">This Lightning address uses the modern Bip353 standard that works with Bolt12 payment requests.\n\nIt is more private than LNURL-based Lightning addresses, and can even be self-hosted.\n\nHowever, it\'s is bleeding edge tech ; some wallets or services do not understand it yet and won\'t be able to pay you.</string>
<string name="receive_bip353_link">Learn more.</string>

<string name="receive_lightning_edit_button">Add amount and description</string>
<string name="receive_lightning_edit_title_bolt11">Customise this Bolt11 invoice</string>
<string name="receive_lightning_edit_title_bolt12">Customise this Bolt12 invoice</string>
<string name="receive_lightning_edit_amount_label">Amount (optional)</string>
Expand Down Expand Up @@ -181,6 +182,14 @@
<string name="send_offer_failure_title">Payment has failed</string>
<string name="send_offer_failure_timeout">Could not retrieve payment details within a reasonable time.\n\nThe recipient may be offline or unreachable.</string>

<string name="send_offer_fiat_main_label">Invoice requests %1$s</string>
<string name="send_offer_fiat_converting_desc">Retrieving %1$s/BTC feerate…</string>
<string name="send_offer_fiat_err_unsupported">%1$s is not supported by Phoenix. You must convert the amount to BTC manually.</string>
<string name="send_offer_fiat_err_malformed_invoice">This invoice is malformed and misses a fiat currency code.</string>
<string name="send_offer_fiat_converted_to">Phoenix converted that to <b>%1$s</b>, but you may adjust that value.</string>
<string name="send_offer_fiat_converted_rate_details">Rate: %1$s %2$s/BTC</string>
<string name="send_offer_fiat_converted_rate_timestamp">Refreshed: %1$s</string>

<string name="send_paymentmode_onchain">Pay on-chain</string>
<string name="send_paymentmode_onchain_desc">On-chain transactions are typically slower and best suited for large payments.</string>
<string name="send_paymentmode_lightning">Pay with Lightning</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class PaymentMetadataQueue(
internal fun enrichPaymentMetadata(metadata: WalletPaymentMetadata?): WalletPaymentMetadata {
val metadataOrDefault = metadata ?: WalletPaymentMetadata()
return if (metadataOrDefault.originalFiat == null) {
val currentFiatRate = appConfigurationManager.preferredFiatCurrencies.value?.let { currencyManager.calculateOriginalFiat(it.primary) }
val currentFiatRate = appConfigurationManager.preferredFiatCurrencies.value?.let { currencyManager.getSnapshotRateForCurrency(it.primary) }
metadataOrDefault.copy(originalFiat = currentFiatRate)
} else metadataOrDefault
}
Expand Down
Loading