feat: localisation with lyricist
This commit is contained in:
@@ -110,7 +110,7 @@ val EnHomeStrings = AppStrings(
|
|||||||
receiveVmWithdrawRejected = "Withdraw service rejected the invoice",
|
receiveVmWithdrawRejected = "Withdraw service rejected the invoice",
|
||||||
receiveVmWithdrawFailed = "Withdraw failed",
|
receiveVmWithdrawFailed = "Withdraw failed",
|
||||||
|
|
||||||
// ── SendScreen — general ──────────────────────────────────────────────────
|
// ── SendScreen — general ──────────────────────────────────────────────────
|
||||||
send = "Send",
|
send = "Send",
|
||||||
paste = "Paste",
|
paste = "Paste",
|
||||||
continueButton = "Continue",
|
continueButton = "Continue",
|
||||||
@@ -176,6 +176,20 @@ val EnHomeStrings = AppStrings(
|
|||||||
routeHintHopsHeader = "Private route hint hops",
|
routeHintHopsHeader = "Private route hint hops",
|
||||||
routeHintHopFees = { ppm, baseLabel -> "$ppm ppm · $baseLabel base" },
|
routeHintHopFees = { ppm, baseLabel -> "$ppm ppm · $baseLabel base" },
|
||||||
|
|
||||||
|
// ── SendViewModel ─────────────────────────────────────────────────────────
|
||||||
|
sendVmLnurlAuthNotSupported = "Lightning Login (LNURL-auth) is not yet supported",
|
||||||
|
sendVmUnrecognisedInput = "Unrecognised input — paste a BOLT-11 invoice, LNURL, or Lightning Address",
|
||||||
|
sendVmPaymentFailed = "Payment failed",
|
||||||
|
sendVmCouldNotFetchInvoice = "Could not fetch invoice from recipient",
|
||||||
|
sendVmCouldNotDecodeInvoice = "Could not decode invoice",
|
||||||
|
sendVmAuthFailed = { msg -> "Authentication failed: $msg" },
|
||||||
|
sendVmOnChainNotSupported = "On-chain Bitcoin payments are not supported — share a BIP-21 URI with a lightning= parameter",
|
||||||
|
sendVmChannelRequestNotSupported = "Channel requests (lnurlc) are not supported",
|
||||||
|
sendVmCouldNotResolveAddress = "Could not resolve address",
|
||||||
|
sendVmEmptyResponse = "Empty response from server",
|
||||||
|
sendVmUnsupportedType = { tag -> "Unsupported type: $tag" },
|
||||||
|
sendVmPaymentFailedRescan = "Payment failed — could not re-fetch address",
|
||||||
|
|
||||||
// ── HistoryScreen ─────────────────────────────────────────────────────────
|
// ── HistoryScreen ─────────────────────────────────────────────────────────
|
||||||
historyTitle = "History",
|
historyTitle = "History",
|
||||||
historyClose = "Close",
|
historyClose = "Close",
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ val EsHomeStrings = AppStrings(
|
|||||||
receiveVmWithdrawRejected = "El servicio de retiro rechazó la factura",
|
receiveVmWithdrawRejected = "El servicio de retiro rechazó la factura",
|
||||||
receiveVmWithdrawFailed = "Error al retirar",
|
receiveVmWithdrawFailed = "Error al retirar",
|
||||||
|
|
||||||
// ── SendScreen — general ──────────────────────────────────────────────────
|
// ── SendScreen — general ──────────────────────────────────────────────────
|
||||||
send = "Enviar",
|
send = "Enviar",
|
||||||
paste = "Pegar",
|
paste = "Pegar",
|
||||||
continueButton = "Continuar",
|
continueButton = "Continuar",
|
||||||
@@ -176,7 +176,21 @@ val EsHomeStrings = AppStrings(
|
|||||||
routeHintHopsHeader = "Saltos de ruta privada",
|
routeHintHopsHeader = "Saltos de ruta privada",
|
||||||
routeHintHopFees = { ppm, baseLabel -> "$ppm ppm · $baseLabel base" },
|
routeHintHopFees = { ppm, baseLabel -> "$ppm ppm · $baseLabel base" },
|
||||||
|
|
||||||
// ── HistoryScreen ─────────────────────────────────────────────────────────
|
// ── SendViewModel ─────────────────────────────────────────────────────────
|
||||||
|
sendVmLnurlAuthNotSupported = "Lightning Login (LNURL-auth) aún no está disponible",
|
||||||
|
sendVmUnrecognisedInput = "Entrada no reconocida — pega una factura BOLT-11, LNURL o dirección Lightning",
|
||||||
|
sendVmPaymentFailed = "Pago fallido",
|
||||||
|
sendVmCouldNotFetchInvoice = "No se pudo obtener la factura del destinatario",
|
||||||
|
sendVmCouldNotDecodeInvoice = "No se pudo decodificar la factura",
|
||||||
|
sendVmAuthFailed = { msg -> "Autenticación fallida: $msg" },
|
||||||
|
sendVmOnChainNotSupported = "Los pagos Bitcoin en cadena no están disponibles — comparte un URI BIP-21 con el parámetro lightning=",
|
||||||
|
sendVmChannelRequestNotSupported = "Las solicitudes de canal (lnurlc) no están disponibles",
|
||||||
|
sendVmCouldNotResolveAddress = "No se pudo resolver la dirección",
|
||||||
|
sendVmEmptyResponse = "Respuesta vacía del servidor",
|
||||||
|
sendVmUnsupportedType = { tag -> "Tipo no compatible: $tag" },
|
||||||
|
sendVmPaymentFailedRescan = "Pago fallido — no se pudo volver a obtener la dirección",
|
||||||
|
|
||||||
|
// ── HistoryScreen ─────────────────────────────────────────────────────────
|
||||||
historyTitle = "Historial",
|
historyTitle = "Historial",
|
||||||
historyClose = "Cerrar",
|
historyClose = "Cerrar",
|
||||||
historySearchPayments = "Buscar pagos",
|
historySearchPayments = "Buscar pagos",
|
||||||
|
|||||||
@@ -185,6 +185,20 @@ data class AppStrings(
|
|||||||
val routeHintHopsHeader : String,
|
val routeHintHopsHeader : String,
|
||||||
val routeHintHopFees : (ppm: Long, baseLabel: String) -> String,
|
val routeHintHopFees : (ppm: Long, baseLabel: String) -> String,
|
||||||
|
|
||||||
|
// ── SendViewModel ─────────────────────────────────────────────────────────
|
||||||
|
val sendVmLnurlAuthNotSupported : String,
|
||||||
|
val sendVmUnrecognisedInput : String,
|
||||||
|
val sendVmPaymentFailed : String,
|
||||||
|
val sendVmCouldNotFetchInvoice : String,
|
||||||
|
val sendVmCouldNotDecodeInvoice : String,
|
||||||
|
val sendVmAuthFailed : (msg: CharSequence) -> String,
|
||||||
|
val sendVmOnChainNotSupported : String,
|
||||||
|
val sendVmChannelRequestNotSupported : String,
|
||||||
|
val sendVmCouldNotResolveAddress : String,
|
||||||
|
val sendVmEmptyResponse : String,
|
||||||
|
val sendVmUnsupportedType : (tag: String) -> String,
|
||||||
|
val sendVmPaymentFailedRescan : String,
|
||||||
|
|
||||||
// ── HistoryScreen ─────────────────────────────────────────────────────────
|
// ── HistoryScreen ─────────────────────────────────────────────────────────
|
||||||
val historyTitle : String,
|
val historyTitle : String,
|
||||||
val historyClose : String,
|
val historyClose : String,
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ import com.bitcointxoko.gudariwallet.ui.history.PaymentDetailScreen
|
|||||||
import com.bitcointxoko.gudariwallet.ui.nfc.NfcViewModel
|
import com.bitcointxoko.gudariwallet.ui.nfc.NfcViewModel
|
||||||
import com.bitcointxoko.gudariwallet.ui.receive.ReceiveScreen
|
import com.bitcointxoko.gudariwallet.ui.receive.ReceiveScreen
|
||||||
import com.bitcointxoko.gudariwallet.ui.send.SendScreen
|
import com.bitcointxoko.gudariwallet.ui.send.SendScreen
|
||||||
|
import com.bitcointxoko.gudariwallet.ui.send.SendStrings
|
||||||
import com.bitcointxoko.gudariwallet.util.SendInputType
|
import com.bitcointxoko.gudariwallet.util.SendInputType
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
@@ -90,6 +91,22 @@ fun WalletScreen(
|
|||||||
lyricist : Lyricist<AppStrings>
|
lyricist : Lyricist<AppStrings>
|
||||||
) {
|
) {
|
||||||
val strings = LocalAppStrings.current
|
val strings = LocalAppStrings.current
|
||||||
|
val sendStrings = remember(strings) {
|
||||||
|
SendStrings(
|
||||||
|
lnurlAuthNotSupported = strings.sendVmLnurlAuthNotSupported,
|
||||||
|
unrecognisedInput = strings.sendVmUnrecognisedInput,
|
||||||
|
paymentFailed = strings.sendVmPaymentFailed,
|
||||||
|
couldNotFetchInvoice = strings.sendVmCouldNotFetchInvoice,
|
||||||
|
couldNotDecodeInvoice = strings.sendVmCouldNotDecodeInvoice,
|
||||||
|
onChainNotSupported = strings.sendVmOnChainNotSupported,
|
||||||
|
channelRequestNotSupported = strings.sendVmChannelRequestNotSupported,
|
||||||
|
couldNotResolveAddress = strings.sendVmCouldNotResolveAddress,
|
||||||
|
emptyResponse = strings.sendVmEmptyResponse,
|
||||||
|
unsupportedType = strings.sendVmUnsupportedType,
|
||||||
|
paymentFailedRescan = strings.sendVmPaymentFailedRescan,
|
||||||
|
authFailed = strings.sendVmAuthFailed,
|
||||||
|
)
|
||||||
|
}
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val historyFactory = remember {
|
val historyFactory = remember {
|
||||||
@@ -153,7 +170,7 @@ fun WalletScreen(
|
|||||||
if (intentUri != null) {
|
if (intentUri != null) {
|
||||||
Log.d("WalletScreen", "INTENT [URI] deep link received: $intentUri")
|
Log.d("WalletScreen", "INTENT [URI] deep link received: $intentUri")
|
||||||
pendingPaymentUri.value = null
|
pendingPaymentUri.value = null
|
||||||
vm.scan(intentUri)
|
vm.scan(intentUri, sendStrings)
|
||||||
navController.navigate(TabItem.Send.route) {
|
navController.navigate(TabItem.Send.route) {
|
||||||
launchSingleTop = true
|
launchSingleTop = true
|
||||||
}
|
}
|
||||||
@@ -316,7 +333,8 @@ fun WalletScreen(
|
|||||||
navController.navigate("payment_detail/$checkingId") {
|
navController.navigate("payment_detail/$checkingId") {
|
||||||
launchSingleTop = true
|
launchSingleTop = true
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
sendStrings
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,7 +342,7 @@ fun WalletScreen(
|
|||||||
composable(ROUTE_SCANNER) {
|
composable(ROUTE_SCANNER) {
|
||||||
QrScannerScreen(
|
QrScannerScreen(
|
||||||
onScanned = { scanned ->
|
onScanned = { scanned ->
|
||||||
vm.scan(scanned)
|
vm.scan(scanned, sendStrings)
|
||||||
val wentBack = navController.popBackStack(TabItem.Send.route, inclusive = false)
|
val wentBack = navController.popBackStack(TabItem.Send.route, inclusive = false)
|
||||||
if (!wentBack) {
|
if (!wentBack) {
|
||||||
navController.navigate(TabItem.Send.route) {
|
navController.navigate(TabItem.Send.route) {
|
||||||
@@ -418,7 +436,7 @@ fun WalletScreen(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable {
|
.clickable {
|
||||||
vm.dismissClipboardOffer()
|
vm.dismissClipboardOffer()
|
||||||
vm.scan(offer.raw)
|
vm.scan(offer.raw, sendStrings)
|
||||||
navController.navigate(TabItem.Send.route) {
|
navController.navigate(TabItem.Send.route) {
|
||||||
launchSingleTop = true
|
launchSingleTop = true
|
||||||
}
|
}
|
||||||
@@ -474,7 +492,7 @@ fun WalletScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable {
|
.clickable {
|
||||||
vm.scan(offer.raw)
|
vm.scan(offer.raw, sendStrings)
|
||||||
nfcVm.dismissNfcOffer()
|
nfcVm.dismissNfcOffer()
|
||||||
navController.navigate(TabItem.Send.route) {
|
navController.navigate(TabItem.Send.route) {
|
||||||
launchSingleTop = true
|
launchSingleTop = true
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import com.bitcointxoko.gudariwallet.ui.balance.BalanceViewModel
|
|||||||
import com.bitcointxoko.gudariwallet.ui.clipboard.ClipboardViewModel
|
import com.bitcointxoko.gudariwallet.ui.clipboard.ClipboardViewModel
|
||||||
import com.bitcointxoko.gudariwallet.ui.fiat.FiatViewModel
|
import com.bitcointxoko.gudariwallet.ui.fiat.FiatViewModel
|
||||||
import com.bitcointxoko.gudariwallet.ui.receive.ReceiveViewModel
|
import com.bitcointxoko.gudariwallet.ui.receive.ReceiveViewModel
|
||||||
|
import com.bitcointxoko.gudariwallet.ui.send.SendStrings
|
||||||
import com.bitcointxoko.gudariwallet.ui.send.SendViewModel
|
import com.bitcointxoko.gudariwallet.ui.send.SendViewModel
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
@@ -75,15 +76,38 @@ class WalletViewModel(
|
|||||||
)
|
)
|
||||||
val sendState: StateFlow<SendState> get() = sendVm.sendState
|
val sendState: StateFlow<SendState> get() = sendVm.sendState
|
||||||
fun refreshBalance() = balanceVm.refreshBalance()
|
fun refreshBalance() = balanceVm.refreshBalance()
|
||||||
fun scan(rawInput: String) = sendVm.scan(rawInput)
|
fun scan(rawInput: String, strings: SendStrings) =
|
||||||
fun payBolt11(bolt11: String) = sendVm.payBolt11(bolt11)
|
sendVm.scan(rawInput, strings)
|
||||||
fun payLnurl(rawRes: LnurlScanResponse, lnurl: String, amountMsat: Long, comment: String?) =
|
|
||||||
sendVm.payLnurl(rawRes, lnurl, amountMsat, comment)
|
fun payBolt11(bolt11: String, strings: SendStrings) =
|
||||||
fun fetchLnurlInvoice(rawRes: LnurlScanResponse, lnurl: String, domain: String, amountMsat: Long, comment: String?) =
|
sendVm.payBolt11(bolt11, strings)
|
||||||
sendVm.fetchLnurlInvoice(rawRes, lnurl, domain, amountMsat, comment)
|
|
||||||
fun payLnurlInvoice(state: SendState.LnurlInvoiceReady) = sendVm.payLnurlInvoice(state)
|
fun payLnurl(
|
||||||
fun requestPayment(activity: FragmentActivity, subtitle: String, pay: () -> Unit) =
|
rawRes : LnurlScanResponse,
|
||||||
sendVm.requestPayment(activity, subtitle, pay)
|
lnurl : String,
|
||||||
|
amountMsat: Long,
|
||||||
|
comment : String?,
|
||||||
|
strings : SendStrings,
|
||||||
|
) = sendVm.payLnurl(rawRes, lnurl, amountMsat, comment, strings)
|
||||||
|
|
||||||
|
fun fetchLnurlInvoice(
|
||||||
|
rawRes : LnurlScanResponse,
|
||||||
|
lnurl : String,
|
||||||
|
domain : String,
|
||||||
|
amountMsat: Long,
|
||||||
|
comment : String?,
|
||||||
|
strings : SendStrings,
|
||||||
|
) = sendVm.fetchLnurlInvoice(rawRes, lnurl, domain, amountMsat, comment, strings)
|
||||||
|
|
||||||
|
fun payLnurlInvoice(state: SendState.LnurlInvoiceReady, strings: SendStrings) =
|
||||||
|
sendVm.payLnurlInvoice(state, strings)
|
||||||
|
|
||||||
|
fun requestPayment(
|
||||||
|
activity : FragmentActivity,
|
||||||
|
subtitle : String,
|
||||||
|
strings : SendStrings,
|
||||||
|
pay : () -> Unit,
|
||||||
|
) = sendVm.requestPayment(activity, subtitle, strings, pay)
|
||||||
fun resetSendState() = sendVm.resetSendState()
|
fun resetSendState() = sendVm.resetSendState()
|
||||||
|
|
||||||
val lightningAddress: StateFlow<String?> = lnAddressStore.lightningAddressFlow
|
val lightningAddress: StateFlow<String?> = lnAddressStore.lightningAddressFlow
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ internal fun Bolt11ConfirmCard(
|
|||||||
activity : FragmentActivity,
|
activity : FragmentActivity,
|
||||||
vm : WalletViewModel,
|
vm : WalletViewModel,
|
||||||
fiatRate : Double?,
|
fiatRate : Double?,
|
||||||
fiatCurrency: String?
|
fiatCurrency: String?,
|
||||||
|
sendStrings : SendStrings,
|
||||||
) {
|
) {
|
||||||
val strings = LocalAppStrings.current
|
val strings = LocalAppStrings.current
|
||||||
val fiatLbl = fiatLabel(state.amountSats, fiatRate, fiatCurrency)
|
val fiatLbl = fiatLabel(state.amountSats, fiatRate, fiatCurrency)
|
||||||
@@ -54,8 +55,9 @@ internal fun Bolt11ConfirmCard(
|
|||||||
onPay = {
|
onPay = {
|
||||||
vm.requestPayment(
|
vm.requestPayment(
|
||||||
activity,
|
activity,
|
||||||
strings.sendBiometricPrompt(state.amountSats) // ← "Send X sats"
|
strings.sendBiometricPrompt(state.amountSats),
|
||||||
) { vm.payBolt11(state.bolt11) }
|
strings = sendStrings, // ← "Send X sats"
|
||||||
|
) { vm.payBolt11(state.bolt11, sendStrings) }
|
||||||
},
|
},
|
||||||
onCancel = { vm.resetSendState() }
|
onCancel = { vm.resetSendState() }
|
||||||
)
|
)
|
||||||
@@ -67,7 +69,8 @@ internal fun LnurlInvoiceConfirmCard(
|
|||||||
activity : FragmentActivity,
|
activity : FragmentActivity,
|
||||||
vm : WalletViewModel,
|
vm : WalletViewModel,
|
||||||
fiatRate : Double?,
|
fiatRate : Double?,
|
||||||
fiatCurrency: String?
|
fiatCurrency: String?,
|
||||||
|
sendStrings : SendStrings,
|
||||||
) {
|
) {
|
||||||
val strings = LocalAppStrings.current
|
val strings = LocalAppStrings.current
|
||||||
val label = payingToLabel(state.lnurl, state.domain)
|
val label = payingToLabel(state.lnurl, state.domain)
|
||||||
@@ -79,8 +82,9 @@ internal fun LnurlInvoiceConfirmCard(
|
|||||||
onPay = {
|
onPay = {
|
||||||
vm.requestPayment(
|
vm.requestPayment(
|
||||||
activity,
|
activity,
|
||||||
strings.sendBiometricPromptTo(state.amountSats, label) // ← "Send X sats to Y"
|
strings.sendBiometricPromptTo(state.amountSats, label),
|
||||||
) { vm.payLnurlInvoice(state) }
|
strings = sendStrings, // ← "Send X sats to Y"
|
||||||
|
) { vm.payLnurlInvoice(state, sendStrings) }
|
||||||
},
|
},
|
||||||
onCancel = { vm.resetSendState() }
|
onCancel = { vm.resetSendState() }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ internal fun LnurlPayForm(
|
|||||||
state : SendState.LnurlReady,
|
state : SendState.LnurlReady,
|
||||||
vm : WalletViewModel,
|
vm : WalletViewModel,
|
||||||
fiatRate : Double?,
|
fiatRate : Double?,
|
||||||
fiatCurrency: String?
|
fiatCurrency: String?,
|
||||||
|
sendStrings: SendStrings
|
||||||
) {
|
) {
|
||||||
val strings = LocalAppStrings.current
|
val strings = LocalAppStrings.current
|
||||||
|
|
||||||
@@ -115,7 +116,8 @@ internal fun LnurlPayForm(
|
|||||||
lnurl = state.lnurl,
|
lnurl = state.lnurl,
|
||||||
domain = state.domain,
|
domain = state.domain,
|
||||||
amountMsat = sats * WalletConstants.MSAT_PER_SAT,
|
amountMsat = sats * WalletConstants.MSAT_PER_SAT,
|
||||||
comment = comment.takeIf { it.isNotBlank() }
|
comment = comment.takeIf { it.isNotBlank() },
|
||||||
|
strings = sendStrings
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ import kotlinx.coroutines.launch
|
|||||||
fun SendScreen(
|
fun SendScreen(
|
||||||
vm : WalletViewModel,
|
vm : WalletViewModel,
|
||||||
nfcVm : NfcViewModel,
|
nfcVm : NfcViewModel,
|
||||||
onViewDetails: (paymentHash: String, record: PaymentRecord) -> Unit = { _, _ -> }
|
onViewDetails: (paymentHash: String, record: PaymentRecord) -> Unit = { _, _ -> },
|
||||||
|
sendStrings: SendStrings
|
||||||
) {
|
) {
|
||||||
val strings = LocalAppStrings.current
|
val strings = LocalAppStrings.current
|
||||||
val sendState : SendState by vm.sendState.collectAsStateWithLifecycle()
|
val sendState : SendState by vm.sendState.collectAsStateWithLifecycle()
|
||||||
@@ -63,7 +64,7 @@ fun SendScreen(
|
|||||||
val offer = nfcOffer
|
val offer = nfcOffer
|
||||||
if (offer is NfcOfferState.Detected && sendState is SendState.Idle) {
|
if (offer is NfcOfferState.Detected && sendState is SendState.Idle) {
|
||||||
input = offer.raw
|
input = offer.raw
|
||||||
vm.scan(offer.raw)
|
vm.scan(offer.raw, sendStrings)
|
||||||
nfcVm.dismissNfcOffer()
|
nfcVm.dismissNfcOffer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,7 +125,7 @@ fun SendScreen(
|
|||||||
imeAction = ImeAction.Go
|
imeAction = ImeAction.Go
|
||||||
),
|
),
|
||||||
keyboardActions = KeyboardActions(
|
keyboardActions = KeyboardActions(
|
||||||
onGo = { if (input.isNotBlank()) vm.scan(input) }
|
onGo = { if (input.isNotBlank()) vm.scan(input, sendStrings) }
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
@@ -143,7 +144,7 @@ fun SendScreen(
|
|||||||
?.toString()
|
?.toString()
|
||||||
if (!text.isNullOrBlank()) {
|
if (!text.isNullOrBlank()) {
|
||||||
input = SendInputDetector.normalize(text)
|
input = SendInputDetector.normalize(text)
|
||||||
vm.scan(text)
|
vm.scan(text, sendStrings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -159,7 +160,7 @@ fun SendScreen(
|
|||||||
}
|
}
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = { if (input.isNotBlank()) vm.scan(input) },
|
onClick = { if (input.isNotBlank()) vm.scan(input, sendStrings) },
|
||||||
enabled = input.isNotBlank(),
|
enabled = input.isNotBlank(),
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) { Text(strings.continueButton) } // ← "Continue"
|
) { Text(strings.continueButton) } // ← "Continue"
|
||||||
@@ -178,7 +179,8 @@ fun SendScreen(
|
|||||||
activity = activity,
|
activity = activity,
|
||||||
vm = vm,
|
vm = vm,
|
||||||
fiatRate = fiatRate,
|
fiatRate = fiatRate,
|
||||||
fiatCurrency = fiatCurrency
|
fiatCurrency = fiatCurrency,
|
||||||
|
sendStrings
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +189,8 @@ fun SendScreen(
|
|||||||
state = state,
|
state = state,
|
||||||
vm = vm,
|
vm = vm,
|
||||||
fiatRate = fiatRate,
|
fiatRate = fiatRate,
|
||||||
fiatCurrency = fiatCurrency
|
fiatCurrency = fiatCurrency,
|
||||||
|
sendStrings
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,7 +200,8 @@ fun SendScreen(
|
|||||||
activity = activity,
|
activity = activity,
|
||||||
vm = vm,
|
vm = vm,
|
||||||
fiatRate = fiatRate,
|
fiatRate = fiatRate,
|
||||||
fiatCurrency = fiatCurrency
|
fiatCurrency = fiatCurrency,
|
||||||
|
sendStrings
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,7 +288,7 @@ fun SendScreen(
|
|||||||
)
|
)
|
||||||
Spacer(Modifier.height(12.dp))
|
Spacer(Modifier.height(12.dp))
|
||||||
Button(
|
Button(
|
||||||
onClick = { if (input.isNotBlank()) vm.scan(input) },
|
onClick = { if (input.isNotBlank()) vm.scan(input, sendStrings) },
|
||||||
enabled = input.isNotBlank(),
|
enabled = input.isNotBlank(),
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) { Text(strings.tryAgain) } // ← "Try Again"
|
) { Text(strings.tryAgain) } // ← "Try Again"
|
||||||
|
|||||||
@@ -26,13 +26,32 @@ import kotlinx.coroutines.launch
|
|||||||
|
|
||||||
private const val TAG = "SendViewModel"
|
private const val TAG = "SendViewModel"
|
||||||
|
|
||||||
|
// ── String bundle injected at the call site ───────────────────────────────────
|
||||||
|
/**
|
||||||
|
* All user-visible strings produced by [SendViewModel].
|
||||||
|
* Populate from [LocalAppStrings.current] in the owning Composable and pass
|
||||||
|
* to [SendViewModel.scan], [SendViewModel.payBolt11], etc.
|
||||||
|
*/
|
||||||
|
data class SendStrings(
|
||||||
|
val lnurlAuthNotSupported : String,
|
||||||
|
val unrecognisedInput : String,
|
||||||
|
val paymentFailed : String,
|
||||||
|
val couldNotFetchInvoice : String,
|
||||||
|
val couldNotDecodeInvoice : String,
|
||||||
|
val onChainNotSupported : String,
|
||||||
|
val channelRequestNotSupported : String,
|
||||||
|
val couldNotResolveAddress : String,
|
||||||
|
val emptyResponse : String,
|
||||||
|
val unsupportedType : (tag: String) -> String,
|
||||||
|
val paymentFailedRescan : String,
|
||||||
|
val authFailed : (msg: String) -> String,
|
||||||
|
)
|
||||||
|
|
||||||
class SendViewModel(
|
class SendViewModel(
|
||||||
private val repo : WalletRepository,
|
private val repo : WalletRepository,
|
||||||
private val aliasRepo : NodeAliasRepository,
|
private val aliasRepo : NodeAliasRepository,
|
||||||
private val lnurlCache : LnurlCacheRepository,
|
private val lnurlCache : LnurlCacheRepository,
|
||||||
private val balanceVm : BalanceViewModel,
|
private val balanceVm : BalanceViewModel,
|
||||||
// Called when a withdrawRequest is scanned — hands off to ReceiveViewModel
|
|
||||||
// without creating a hard dependency between the two.
|
|
||||||
private val onWithdrawScanned: (raw: LnurlScanResponse, lnurl: String) -> Unit,
|
private val onWithdrawScanned: (raw: LnurlScanResponse, lnurl: String) -> Unit,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
@@ -41,33 +60,32 @@ class SendViewModel(
|
|||||||
|
|
||||||
// ── Entry point ──────────────────────────────────────────────────────────
|
// ── Entry point ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fun scan(rawInput: String) {
|
fun scan(rawInput: String, strings: SendStrings) {
|
||||||
val normalized = SendInputDetector.normalize(rawInput)
|
val normalized = SendInputDetector.normalize(rawInput)
|
||||||
val type = SendInputDetector.detect(rawInput)
|
val type = SendInputDetector.detect(rawInput)
|
||||||
Log.d(TAG, "SCAN [DETECT] input='$rawInput' → type=$type")
|
Log.d(TAG, "SCAN [DETECT] input='$rawInput' → type=$type")
|
||||||
|
|
||||||
when (type) {
|
when (type) {
|
||||||
SendInputType.Unknown -> {
|
SendInputType.Unknown -> {
|
||||||
val msg = if (rawInput.trim().lowercase().startsWith("keyauth")) {
|
val msg = if (rawInput.trim().lowercase().startsWith("keyauth"))
|
||||||
"Lightning Login (LNURL-auth) is not yet supported"
|
strings.lnurlAuthNotSupported // ← "Lightning Login (LNURL-auth) is not yet supported"
|
||||||
} else {
|
else
|
||||||
"Unrecognised input — paste a BOLT-11 invoice, LNURL, or Lightning Address"
|
strings.unrecognisedInput // ← "Unrecognised input — paste a BOLT-11 invoice, LNURL, or Lightning Address"
|
||||||
}
|
|
||||||
_sendState.value = SendState.Error(msg)
|
_sendState.value = SendState.Error(msg)
|
||||||
}
|
}
|
||||||
SendInputType.Bip21 -> handleBip21Scan(rawInput)
|
SendInputType.Bip21 -> handleBip21Scan(rawInput, strings)
|
||||||
SendInputType.Lud17Url -> handleLud17Scan(rawInput)
|
SendInputType.Lud17Url -> handleLud17Scan(rawInput, strings)
|
||||||
SendInputType.Bolt11 -> handleBolt11Scan(normalized)
|
SendInputType.Bolt11 -> handleBolt11Scan(normalized, strings)
|
||||||
else -> handleLnurlScan(normalized)
|
else -> handleLnurlScan(normalized, strings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Payment ──────────────────────────────────────────────────────────────
|
// ── Payment ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fun payBolt11(bolt11: String) {
|
fun payBolt11(bolt11: String, strings: SendStrings) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val decoded = _sendState.value as? SendState.Bolt11Decoded
|
val decoded = _sendState.value as? SendState.Bolt11Decoded
|
||||||
val amountSats = (_sendState.value as? SendState.Bolt11Decoded)?.amountSats ?: 0L
|
val amountSats = decoded?.amountSats ?: 0L
|
||||||
_sendState.value = SendState.Paying
|
_sendState.value = SendState.Paying
|
||||||
runCatching { repo.payBolt11(bolt11) }
|
runCatching { repo.payBolt11(bolt11) }
|
||||||
.onSuccess { response ->
|
.onSuccess { response ->
|
||||||
@@ -92,7 +110,9 @@ class SendViewModel(
|
|||||||
}
|
}
|
||||||
.onFailure { e ->
|
.onFailure { e ->
|
||||||
Log.e(TAG, "SEND [PAY BOLT11 ERROR] ${e.message}")
|
Log.e(TAG, "SEND [PAY BOLT11 ERROR] ${e.message}")
|
||||||
_sendState.value = SendState.Error(parseApiError(e, "Payment failed"))
|
_sendState.value = SendState.Error(
|
||||||
|
parseApiError(e, strings.paymentFailed) // ← "Payment failed"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,23 +121,26 @@ class SendViewModel(
|
|||||||
rawRes : LnurlScanResponse,
|
rawRes : LnurlScanResponse,
|
||||||
lnurl : String,
|
lnurl : String,
|
||||||
amountMsat: Long,
|
amountMsat: Long,
|
||||||
comment : String?
|
comment : String?,
|
||||||
|
strings : SendStrings,
|
||||||
) {
|
) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_sendState.value = SendState.Paying
|
_sendState.value = SendState.Paying
|
||||||
|
|
||||||
// ── 1. Initial attempt (direct → proxy)
|
// ── 1. Initial attempt (direct → proxy)
|
||||||
val initial = tryPayLnurlWithFallback(rawRes, lnurl, amountMsat, comment, "INITIAL")
|
val initial = tryPayLnurlWithFallback(rawRes, lnurl, amountMsat, comment, "INITIAL", strings)
|
||||||
if (initial.isSuccess) {
|
if (initial.isSuccess) {
|
||||||
recordPaymentSuccess(initial.getOrThrow(), amountMsat)
|
recordPaymentSuccess(initial.getOrThrow(), amountMsat)
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 2. Both paths failed — check if retrying is worthwhile
|
// ── 2. Both paths failed — check if retrying is worthwhile
|
||||||
val initialError = initial.exceptionOrNull() ?: Exception("Payment failed")
|
val initialError = initial.exceptionOrNull() ?: Exception(strings.paymentFailed)
|
||||||
if (isTerminalPayError(initialError)) {
|
if (isTerminalPayError(initialError)) {
|
||||||
Log.w(TAG, "LNURL [PAY TERMINAL] ${initialError.message} — not retrying")
|
Log.w(TAG, "LNURL [PAY TERMINAL] ${initialError.message} — not retrying")
|
||||||
_sendState.value = SendState.Error(parseApiError(initialError, "Payment failed"))
|
_sendState.value = SendState.Error(
|
||||||
|
parseApiError(initialError, strings.paymentFailed) // ← "Payment failed"
|
||||||
|
)
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +148,7 @@ class SendViewModel(
|
|||||||
lnurlCache.invalidate(lnurl)
|
lnurlCache.invalidate(lnurl)
|
||||||
|
|
||||||
// ── 3. Re-fetch metadata
|
// ── 3. Re-fetch metadata
|
||||||
val freshRaw = rescanLnurl(lnurl) ?: return@launch // error already set
|
val freshRaw = rescanLnurl(lnurl, strings) ?: return@launch // error already set
|
||||||
|
|
||||||
val scanProtocolError = lnurlProtocolError(freshRaw)
|
val scanProtocolError = lnurlProtocolError(freshRaw)
|
||||||
if (scanProtocolError != null) {
|
if (scanProtocolError != null) {
|
||||||
@@ -136,29 +159,28 @@ class SendViewModel(
|
|||||||
lnurlCache.put(lnurl, freshRaw)
|
lnurlCache.put(lnurl, freshRaw)
|
||||||
|
|
||||||
// ── 4. Final attempt (direct → proxy) with fresh metadata
|
// ── 4. Final attempt (direct → proxy) with fresh metadata
|
||||||
val retry = tryPayLnurlWithFallback(freshRaw, lnurl, amountMsat, comment, "RETRY")
|
val retry = tryPayLnurlWithFallback(freshRaw, lnurl, amountMsat, comment, "RETRY", strings)
|
||||||
if (retry.isSuccess) {
|
if (retry.isSuccess) {
|
||||||
recordPaymentSuccess(retry.getOrThrow(), amountMsat)
|
recordPaymentSuccess(retry.getOrThrow(), amountMsat)
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 5. Final failure
|
// ── 5. Final failure
|
||||||
val retryError = retry.exceptionOrNull() ?: Exception("Payment failed")
|
val retryError = retry.exceptionOrNull() ?: Exception(strings.paymentFailed)
|
||||||
Log.e(TAG, "LNURL [PAY FINAL ERR] ${retryError.message}")
|
Log.e(TAG, "LNURL [PAY FINAL ERR] ${retryError.message}")
|
||||||
_sendState.value = SendState.Error(parseApiError(retryError, "Payment failed"))
|
_sendState.value = SendState.Error(
|
||||||
|
parseApiError(retryError, strings.paymentFailed) // ← "Payment failed"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Step 1: fetch the LNURL callback invoice, decode it, analyse route hints.
|
|
||||||
* Called after the user taps "Pay" in LnurlPayForm and passes biometric auth.
|
|
||||||
*/
|
|
||||||
fun fetchLnurlInvoice(
|
fun fetchLnurlInvoice(
|
||||||
rawRes : LnurlScanResponse,
|
rawRes : LnurlScanResponse,
|
||||||
lnurl : String,
|
lnurl : String,
|
||||||
domain : String,
|
domain : String,
|
||||||
amountMsat: Long,
|
amountMsat: Long,
|
||||||
comment : String?
|
comment : String?,
|
||||||
|
strings : SendStrings,
|
||||||
) {
|
) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_sendState.value = SendState.Scanning
|
_sendState.value = SendState.Scanning
|
||||||
@@ -168,7 +190,7 @@ class SendViewModel(
|
|||||||
}.getOrElse { e ->
|
}.getOrElse { e ->
|
||||||
Log.e(TAG, "LNURL [FETCH INVOICE ERR] ${e.message}")
|
Log.e(TAG, "LNURL [FETCH INVOICE ERR] ${e.message}")
|
||||||
_sendState.value = SendState.Error(
|
_sendState.value = SendState.Error(
|
||||||
parseApiError(e, "Could not fetch invoice from recipient")
|
parseApiError(e, strings.couldNotFetchInvoice) // ← "Could not fetch invoice from recipient"
|
||||||
)
|
)
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
@@ -177,7 +199,9 @@ class SendViewModel(
|
|||||||
repo.decodeBolt11(bolt11)
|
repo.decodeBolt11(bolt11)
|
||||||
}.getOrElse { e ->
|
}.getOrElse { e ->
|
||||||
Log.e(TAG, "LNURL [DECODE ERR] ${e.message}")
|
Log.e(TAG, "LNURL [DECODE ERR] ${e.message}")
|
||||||
_sendState.value = SendState.Error(parseApiError(e, "Could not decode invoice"))
|
_sendState.value = SendState.Error(
|
||||||
|
parseApiError(e, strings.couldNotDecodeInvoice) // ← "Could not decode invoice"
|
||||||
|
)
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,18 +249,14 @@ class SendViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
fun payLnurlInvoice(state: SendState.LnurlInvoiceReady, strings: SendStrings) {
|
||||||
* Step 2: pay the bolt11 fetched in fetchLnurlInvoice.
|
|
||||||
* Tries payBolt11 directly first; falls back to the full payLnurl path.
|
|
||||||
*/
|
|
||||||
fun payLnurlInvoice(state: SendState.LnurlInvoiceReady) {
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_sendState.value = SendState.Paying
|
_sendState.value = SendState.Paying
|
||||||
|
|
||||||
val directResult = runCatching { repo.payBolt11(state.bolt11) }
|
val directResult = runCatching { repo.payBolt11(state.bolt11) }
|
||||||
val paymentHash = directResult.getOrThrow().paymentHash
|
|
||||||
|
|
||||||
if (directResult.isSuccess) {
|
if (directResult.isSuccess) {
|
||||||
|
val paymentHash = directResult.getOrThrow().paymentHash
|
||||||
val feeSats = runCatching {
|
val feeSats = runCatching {
|
||||||
repo.getPaymentDetail(paymentHash).details?.feeSat
|
repo.getPaymentDetail(paymentHash).details?.feeSat
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
@@ -265,7 +285,8 @@ class SendViewModel(
|
|||||||
rawRes = state.rawRes,
|
rawRes = state.rawRes,
|
||||||
lnurl = state.lnurl,
|
lnurl = state.lnurl,
|
||||||
amountMsat = state.amountMsat,
|
amountMsat = state.amountMsat,
|
||||||
comment = state.comment
|
comment = state.comment,
|
||||||
|
strings = strings,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -274,8 +295,9 @@ class SendViewModel(
|
|||||||
* Biometric gate. Shows prompt if device supports it; calls [pay] directly if not.
|
* Biometric gate. Shows prompt if device supports it; calls [pay] directly if not.
|
||||||
*/
|
*/
|
||||||
fun requestPayment(
|
fun requestPayment(
|
||||||
activity: FragmentActivity,
|
activity : FragmentActivity,
|
||||||
subtitle: String,
|
subtitle : String,
|
||||||
|
strings : SendStrings,
|
||||||
pay : () -> Unit
|
pay : () -> Unit
|
||||||
) {
|
) {
|
||||||
if (!BiometricHelper.canAuthenticate(activity)) {
|
if (!BiometricHelper.canAuthenticate(activity)) {
|
||||||
@@ -287,7 +309,9 @@ class SendViewModel(
|
|||||||
subtitle = subtitle,
|
subtitle = subtitle,
|
||||||
onSuccess = pay,
|
onSuccess = pay,
|
||||||
onError = { _, msg ->
|
onError = { _, msg ->
|
||||||
_sendState.value = SendState.Error("Authentication failed: $msg")
|
_sendState.value = SendState.Error(
|
||||||
|
strings.authFailed(msg.toString()) // ← "Authentication failed: $msg"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -296,24 +320,25 @@ class SendViewModel(
|
|||||||
|
|
||||||
// ── Scan handlers ────────────────────────────────────────────────────────
|
// ── Scan handlers ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private fun handleBip21Scan(rawInput: String) {
|
private fun handleBip21Scan(rawInput: String, strings: SendStrings) {
|
||||||
val params = parseBip21Params(rawInput)
|
val params = parseBip21Params(rawInput)
|
||||||
val bolt11 = params["lightning"]
|
val bolt11 = params["lightning"]
|
||||||
val lnurl = params["lnurl"] ?: params["lnurlp"]
|
val lnurl = params["lnurl"] ?: params["lnurlp"]
|
||||||
when {
|
when {
|
||||||
bolt11 != null -> scan(bolt11)
|
bolt11 != null -> scan(bolt11, strings)
|
||||||
lnurl != null -> scan(lnurl)
|
lnurl != null -> scan(lnurl, strings)
|
||||||
else -> _sendState.value = SendState.Error(
|
else -> _sendState.value = SendState.Error(
|
||||||
"On-chain Bitcoin payments are not supported — " +
|
strings.onChainNotSupported // ← "On-chain Bitcoin payments are not supported…"
|
||||||
"share a BIP-21 URI with a lightning= parameter"
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleLud17Scan(rawInput: String) {
|
private fun handleLud17Scan(rawInput: String, strings: SendStrings) {
|
||||||
val scheme = rawInput.trim().lowercase().substringBefore("://")
|
val scheme = rawInput.trim().lowercase().substringBefore("://")
|
||||||
if (scheme == "lnurlc") {
|
if (scheme == "lnurlc") {
|
||||||
_sendState.value = SendState.Error("Channel requests (lnurlc) are not supported")
|
_sendState.value = SendState.Error(
|
||||||
|
strings.channelRequestNotSupported // ← "Channel requests (lnurlc) are not supported"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val httpsUrl = "https://" + rawInput.trim().substringAfter("://")
|
val httpsUrl = "https://" + rawInput.trim().substringAfter("://")
|
||||||
@@ -333,18 +358,20 @@ class SendViewModel(
|
|||||||
_sendState.value = buildLnurlReadyState(raw, httpsUrl)
|
_sendState.value = buildLnurlReadyState(raw, httpsUrl)
|
||||||
}
|
}
|
||||||
else -> _sendState.value = SendState.Error(
|
else -> _sendState.value = SendState.Error(
|
||||||
"Unsupported LNURL type: ${raw.tag ?: "unknown"}"
|
strings.unsupportedType(raw.tag ?: "unknown") // ← "Unsupported type: $tag"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onFailure { e ->
|
.onFailure { e ->
|
||||||
Log.e(TAG, "LUD17 [ERROR] ${e.message}")
|
Log.e(TAG, "LUD17 [ERROR] ${e.message}")
|
||||||
_sendState.value = SendState.Error(parseApiError(e, "Could not resolve address"))
|
_sendState.value = SendState.Error(
|
||||||
|
parseApiError(e, strings.couldNotResolveAddress) // ← "Could not resolve address"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleBolt11Scan(normalized: String) {
|
private fun handleBolt11Scan(normalized: String, strings: SendStrings) {
|
||||||
_sendState.value = SendState.Scanning
|
_sendState.value = SendState.Scanning
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
runCatching { repo.decodeBolt11(normalized) }
|
runCatching { repo.decodeBolt11(normalized) }
|
||||||
@@ -383,12 +410,14 @@ class SendViewModel(
|
|||||||
}
|
}
|
||||||
.onFailure { e ->
|
.onFailure { e ->
|
||||||
Log.e(TAG, "SEND [DECODE ERROR] ${e.message}")
|
Log.e(TAG, "SEND [DECODE ERROR] ${e.message}")
|
||||||
_sendState.value = SendState.Error(parseApiError(e, "Could not decode invoice"))
|
_sendState.value = SendState.Error(
|
||||||
|
parseApiError(e, strings.couldNotDecodeInvoice) // ← "Could not decode invoice"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleLnurlScan(normalized: String) {
|
private fun handleLnurlScan(normalized: String, strings: SendStrings) {
|
||||||
_sendState.value = SendState.Scanning
|
_sendState.value = SendState.Scanning
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
// 1. Cache hit
|
// 1. Cache hit
|
||||||
@@ -424,8 +453,8 @@ class SendViewModel(
|
|||||||
_sendState.value = SendState.Idle
|
_sendState.value = SendState.Idle
|
||||||
onWithdrawScanned(scanned.rawRes, normalized)
|
onWithdrawScanned(scanned.rawRes, normalized)
|
||||||
}
|
}
|
||||||
null -> _sendState.value = SendState.Error("Empty response from server")
|
null -> _sendState.value = SendState.Error(strings.emptyResponse) // ← "Empty response from server"
|
||||||
else -> _sendState.value = SendState.Error("Unsupported type: ${scanned.tag}")
|
else -> _sendState.value = SendState.Error(strings.unsupportedType(scanned.tag)) // ← "Unsupported type: $tag"
|
||||||
}
|
}
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
@@ -448,12 +477,14 @@ class SendViewModel(
|
|||||||
_sendState.value = SendState.Idle
|
_sendState.value = SendState.Idle
|
||||||
onWithdrawScanned(scanned.rawRes, normalized)
|
onWithdrawScanned(scanned.rawRes, normalized)
|
||||||
}
|
}
|
||||||
null -> _sendState.value = SendState.Error("Empty response from server")
|
null -> _sendState.value = SendState.Error(strings.emptyResponse) // ← "Empty response from server"
|
||||||
else -> _sendState.value = SendState.Error("Unsupported type: ${scanned.tag}")
|
else -> _sendState.value = SendState.Error(strings.unsupportedType(scanned.tag)) // ← "Unsupported type: $tag"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onFailure { e ->
|
.onFailure { e ->
|
||||||
_sendState.value = SendState.Error(parseApiError(e, "Could not resolve address"))
|
_sendState.value = SendState.Error(
|
||||||
|
parseApiError(e, strings.couldNotResolveAddress) // ← "Could not resolve address"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -474,16 +505,14 @@ class SendViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun recordPaymentSuccess(
|
private suspend fun recordPaymentSuccess(
|
||||||
paymentHash: String,
|
paymentHash : String,
|
||||||
amountMsat: Long,
|
amountMsat : Long,
|
||||||
bolt11 : String? = null,
|
bolt11 : String? = null,
|
||||||
memo : String? = null,
|
memo : String? = null,
|
||||||
pubkey : String? = null,
|
pubkey : String? = null,
|
||||||
alias : String? = null
|
alias : String? = null
|
||||||
) {
|
) {
|
||||||
val feeSats = runCatching {
|
val feeSats = runCatching { repo.getPaymentDetail(paymentHash).details?.feeSat }.getOrNull()
|
||||||
repo.getPaymentDetail(paymentHash).details?.feeSat
|
|
||||||
}.getOrNull()
|
|
||||||
val amountSats = amountMsat / WalletConstants.MSAT_PER_SAT
|
val amountSats = amountMsat / WalletConstants.MSAT_PER_SAT
|
||||||
_sendState.value = SendState.PaymentSent(
|
_sendState.value = SendState.PaymentSent(
|
||||||
amountSats = amountSats,
|
amountSats = amountSats,
|
||||||
@@ -507,7 +536,8 @@ class SendViewModel(
|
|||||||
lnurl : String,
|
lnurl : String,
|
||||||
amountMsat: Long,
|
amountMsat: Long,
|
||||||
comment : String?,
|
comment : String?,
|
||||||
label : String
|
label : String,
|
||||||
|
strings : SendStrings,
|
||||||
): Result<String> {
|
): Result<String> {
|
||||||
val direct = runCatching { repo.payLnurlDirect(rawRes, amountMsat, comment) }
|
val direct = runCatching { repo.payLnurlDirect(rawRes, amountMsat, comment) }
|
||||||
if (direct.isSuccess) {
|
if (direct.isSuccess) {
|
||||||
@@ -522,10 +552,12 @@ class SendViewModel(
|
|||||||
return Result.success(proxy.getOrThrow().paymentHash)
|
return Result.success(proxy.getOrThrow().paymentHash)
|
||||||
}
|
}
|
||||||
Log.w(TAG, "LNURL [PAY $label PROXY FAIL] ${proxy.exceptionOrNull()?.message}")
|
Log.w(TAG, "LNURL [PAY $label PROXY FAIL] ${proxy.exceptionOrNull()?.message}")
|
||||||
return Result.failure(proxy.exceptionOrNull() ?: Exception("Payment failed"))
|
return Result.failure(
|
||||||
|
proxy.exceptionOrNull() ?: Exception(strings.paymentFailed) // ← "Payment failed"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun rescanLnurl(lnurl: String): LnurlScanResponse? {
|
private suspend fun rescanLnurl(lnurl: String, strings: SendStrings): LnurlScanResponse? {
|
||||||
val direct = runCatching { repo.scanLnurlDirect(lnurl) }
|
val direct = runCatching { repo.scanLnurlDirect(lnurl) }
|
||||||
if (direct.isSuccess) {
|
if (direct.isSuccess) {
|
||||||
Log.d(TAG, "LNURL [RESCAN DIRECT] success")
|
Log.d(TAG, "LNURL [RESCAN DIRECT] success")
|
||||||
@@ -538,7 +570,10 @@ class SendViewModel(
|
|||||||
|
|
||||||
Log.e(TAG, "LNURL [RESCAN ERR] ${proxy.exceptionOrNull()?.message}")
|
Log.e(TAG, "LNURL [RESCAN ERR] ${proxy.exceptionOrNull()?.message}")
|
||||||
_sendState.value = SendState.Error(
|
_sendState.value = SendState.Error(
|
||||||
parseApiError(proxy.exceptionOrNull() ?: Exception(), "Payment failed — could not re-fetch address")
|
parseApiError(
|
||||||
|
proxy.exceptionOrNull() ?: Exception(),
|
||||||
|
strings.paymentFailedRescan // ← "Payment failed — could not re-fetch address"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -589,7 +624,7 @@ class SendViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun buildPendingRecord(
|
private fun buildPendingRecord(
|
||||||
paymentHash: String,
|
paymentHash : String,
|
||||||
amountSats : Long,
|
amountSats : Long,
|
||||||
feeSats : Long?,
|
feeSats : Long?,
|
||||||
bolt11 : String?,
|
bolt11 : String?,
|
||||||
@@ -599,7 +634,7 @@ class SendViewModel(
|
|||||||
): PaymentRecord = PaymentRecord(
|
): PaymentRecord = PaymentRecord(
|
||||||
paymentHash = paymentHash,
|
paymentHash = paymentHash,
|
||||||
checkingId = paymentHash,
|
checkingId = paymentHash,
|
||||||
amountMsat = -(amountSats * 1_000L), // negative = outgoing
|
amountMsat = -(amountSats * 1_000L),
|
||||||
feeMsat = (feeSats ?: 0L) * 1_000L,
|
feeMsat = (feeSats ?: 0L) * 1_000L,
|
||||||
memo = memo,
|
memo = memo,
|
||||||
time = (System.currentTimeMillis() / 1_000L).toString(),
|
time = (System.currentTimeMillis() / 1_000L).toString(),
|
||||||
|
|||||||
Reference in New Issue
Block a user