diff --git a/app/src/main/kotlin/com/darkwisp/app/repo/NwcRepository.kt b/app/src/main/kotlin/com/darkwisp/app/repo/NwcRepository.kt index 4a5339c..cdf41a3 100644 --- a/app/src/main/kotlin/com/darkwisp/app/repo/NwcRepository.kt +++ b/app/src/main/kotlin/com/darkwisp/app/repo/NwcRepository.kt @@ -356,15 +356,6 @@ class NwcRepository(private val context: Context, private val relayPool: RelayPo return result.map { (it as Nip47.NwcResponse.MakeInvoiceResult).invoice } } - override suspend fun getDepositAddress(): Result = - Result.failure(UnsupportedOperationException("NWC does not support on-chain receive")) - - override suspend fun prepareOnchainSend(address: String, amountSats: Long): Result> = - Result.failure(UnsupportedOperationException("NWC does not support on-chain send")) - - override suspend fun sendOnchain(prepareData: Any, speed: String): Result = - Result.failure(UnsupportedOperationException("NWC does not support on-chain send")) - suspend fun listNwcTransactions(limit: Int = 50, offset: Int = 0): Result> { val result = sendRequest(Nip47.NwcRequest.ListTransactions(limit = limit, offset = offset)) return result.map { (it as Nip47.NwcResponse.ListTransactionsResult).transactions } diff --git a/app/src/main/kotlin/com/darkwisp/app/repo/SparkRepository.kt b/app/src/main/kotlin/com/darkwisp/app/repo/SparkRepository.kt index 4781daa..8a1fbba 100644 --- a/app/src/main/kotlin/com/darkwisp/app/repo/SparkRepository.kt +++ b/app/src/main/kotlin/com/darkwisp/app/repo/SparkRepository.kt @@ -10,7 +10,6 @@ import breez_sdk_spark.EventListener import breez_sdk_spark.GetInfoRequest import breez_sdk_spark.ListPaymentsRequest import breez_sdk_spark.Network -import breez_sdk_spark.OnchainConfirmationSpeed import breez_sdk_spark.PaymentDetails import breez_sdk_spark.PaymentType import breez_sdk_spark.PrepareSendPaymentRequest @@ -455,57 +454,6 @@ class SparkRepository( } } - override suspend fun getDepositAddress(): Result = withContext(Dispatchers.IO) { - try { - val instance = sdk ?: return@withContext Result.failure(Exception("Not connected")) - val response = instance.receivePayment(ReceivePaymentRequest(ReceivePaymentMethod.BitcoinAddress)) - Result.success(response.paymentRequest) - } catch (e: Exception) { - Result.failure(e) - } - } - - override suspend fun prepareOnchainSend(address: String, amountSats: Long): Result> = - withContext(Dispatchers.IO) { - try { - val instance = sdk ?: return@withContext Result.failure(Exception("Not connected")) - val prepareReq = PrepareSendPaymentRequest( - paymentRequest = address, - amount = java.math.BigInteger.valueOf(amountSats) - ) - val prepareResponse = instance.prepareSendPayment(prepareReq) - val method = prepareResponse.paymentMethod as? SendPaymentMethod.BitcoinAddress - ?: return@withContext Result.failure(Exception("Not a Bitcoin address payment")) - val feeQuote = method.feeQuote - val quote = OnchainFeeQuote( - fastFeeSats = (feeQuote.speedFast.userFeeSat + feeQuote.speedFast.l1BroadcastFeeSat).toLong(), - mediumFeeSats = (feeQuote.speedMedium.userFeeSat + feeQuote.speedMedium.l1BroadcastFeeSat).toLong(), - slowFeeSats = (feeQuote.speedSlow.userFeeSat + feeQuote.speedSlow.l1BroadcastFeeSat).toLong() - ) - Result.success(Pair(quote, prepareResponse as Any)) - } catch (e: Exception) { - Result.failure(e) - } - } - - override suspend fun sendOnchain(prepareData: Any, speed: String): Result = withContext(Dispatchers.IO) { - try { - val instance = sdk ?: return@withContext Result.failure(Exception("Not connected")) - val prepareResponse = prepareData as breez_sdk_spark.PrepareSendPaymentResponse - val confirmationSpeed = when (speed) { - "FAST" -> OnchainConfirmationSpeed.FAST - "SLOW" -> OnchainConfirmationSpeed.SLOW - else -> OnchainConfirmationSpeed.MEDIUM - } - val options = SendPaymentOptions.BitcoinAddress(confirmationSpeed = confirmationSpeed) - val sendResponse = instance.sendPayment(SendPaymentRequest(prepareResponse, options)) - emitStatus("Bitcoin sent") - Result.success(sendResponse.payment.id) - } catch (e: Exception) { - emitStatus("Bitcoin send failed: ${e.message}") - Result.failure(e) - } - } // --- Sync polling --- diff --git a/app/src/main/kotlin/com/darkwisp/app/repo/WalletProvider.kt b/app/src/main/kotlin/com/darkwisp/app/repo/WalletProvider.kt index 4088377..ccd8cfb 100644 --- a/app/src/main/kotlin/com/darkwisp/app/repo/WalletProvider.kt +++ b/app/src/main/kotlin/com/darkwisp/app/repo/WalletProvider.kt @@ -3,12 +3,6 @@ package com.darkwisp.app.repo import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow -data class OnchainFeeQuote( - val fastFeeSats: Long, - val mediumFeeSats: Long, - val slowFeeSats: Long -) - interface WalletProvider { val balance: StateFlow val isConnected: StateFlow @@ -27,15 +21,6 @@ interface WalletProvider { suspend fun payInvoice(bolt11: String): Result suspend fun makeInvoice(amountMsats: Long, description: String, expirySecs: Int = 3600): Result suspend fun listTransactions(limit: Int = 50, offset: Int = 0): Result> - - /** Spark only — returns an on-chain deposit address. NWC returns failure. */ - suspend fun getDepositAddress(): Result - - /** Spark only — prepares an on-chain send and returns fee quote + opaque prepare data. */ - suspend fun prepareOnchainSend(address: String, amountSats: Long): Result> - - /** Spark only — broadcasts a prepared on-chain send. */ - suspend fun sendOnchain(prepareData: Any, speed: String = "MEDIUM"): Result } data class WalletTransaction( diff --git a/app/src/main/kotlin/com/darkwisp/app/ui/screen/WalletScreen.kt b/app/src/main/kotlin/com/darkwisp/app/ui/screen/WalletScreen.kt index dacfec7..5f9618f 100644 --- a/app/src/main/kotlin/com/darkwisp/app/ui/screen/WalletScreen.kt +++ b/app/src/main/kotlin/com/darkwisp/app/ui/screen/WalletScreen.kt @@ -85,7 +85,6 @@ import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.filled.Image import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowUp -import androidx.compose.material.icons.filled.CurrencyBitcoin import androidx.compose.material.icons.filled.QrCode import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.filled.Receipt @@ -103,9 +102,6 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.SegmentedButton -import androidx.compose.material3.SegmentedButtonDefaults -import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.TextFieldDefaults import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions @@ -175,7 +171,6 @@ import com.darkwisp.app.BuildConfig import com.darkwisp.app.R import com.darkwisp.app.nostr.NipA3 import com.darkwisp.app.repo.BalanceUnit -import com.darkwisp.app.repo.OnchainFeeQuote import com.darkwisp.app.repo.WalletBalanceDisplayMode import com.darkwisp.app.repo.FiatPreferences import com.darkwisp.app.repo.WalletMode @@ -510,13 +505,8 @@ fun WalletScreen( amount = viewModel.receiveAmount.collectAsState().value, isLoading = viewModel.isLoading.collectAsState().value, lightningAddress = viewModel.lightningAddress.collectAsState().value, - walletMode = viewModel.walletMode.collectAsState().value, - depositAddress = viewModel.depositAddress.collectAsState().value, - depositAddressLoading = viewModel.depositAddressLoading.collectAsState().value, - depositAddressError = viewModel.depositAddressError.collectAsState().value, onAmountChange = { viewModel.setReceiveAmount(it) }, onGenerate = { sats, note, expirySecs -> viewModel.generateInvoice(sats, note, expirySecs) }, - onLoadDepositAddress = { viewModel.loadDepositAddress() }, onShowAddressQR = { viewModel.navigateTo(WalletPage.LightningAddressQR) }, modifier = Modifier.padding(padding) ) @@ -648,65 +638,6 @@ fun WalletScreen( }, modifier = Modifier.padding(padding) ) - is WalletPage.OnchainSendAmount -> { - val page = currentPage as WalletPage.OnchainSendAmount - val sendAmount by viewModel.sendAmount.collectAsState() - val feeLoading by viewModel.onchainSendLoading.collectAsState() - val error by viewModel.sendError.collectAsState() - val feeQuote by viewModel.onchainFeeQuote.collectAsState() - OnchainSendAmountContent( - address = page.address, - amount = sendAmount, - balanceSats = balanceMsats / 1000, - isLoading = feeLoading, - error = error, - feeQuote = feeQuote, - onAmountChange = { - viewModel.setSendAmount(it) - viewModel.clearOnchainQuote() - }, - onUseAll = { - viewModel.setSendAmount((balanceMsats / 1000).toString()) - viewModel.clearOnchainQuote() - }, - onGetFeeQuote = { - val sats = sendAmount.toLongOrNull() ?: return@OnchainSendAmountContent - viewModel.prepareOnchainSend(page.address, sats) - }, - onContinue = { - val sats = sendAmount.toLongOrNull() ?: return@OnchainSendAmountContent - viewModel.continueToOnchainConfirm(page.address, sats) - }, - onBack = { - viewModel.clearOnchainQuote() - viewModel.navigateBack() - }, - modifier = Modifier.padding(padding) - ) - } - is WalletPage.OnchainSendConfirm -> { - val page = currentPage as WalletPage.OnchainSendConfirm - val sending by viewModel.isLoading.collectAsState() - OnchainSendConfirmContent( - address = page.address, - amountSats = page.amountSats, - feeQuote = page.feeQuote, - isLoading = sending, - onConfirm = { viewModel.sendOnchain(page.prepareData) }, - onBack = { viewModel.navigateBack() }, - modifier = Modifier.padding(padding) - ) - } - is WalletPage.OnchainSendResult -> { - val page = currentPage as WalletPage.OnchainSendResult - OnchainSendResultContent( - success = page.success, - paymentId = page.paymentId, - message = page.message, - onDone = { viewModel.navigateHome() }, - modifier = Modifier.padding(padding) - ) - } else -> { // ModeSelection, NwcSetup, SparkSetup — shouldn't appear while connected val profileKey = viewModel.profileRefreshKey.collectAsState().value @@ -2287,8 +2218,6 @@ private fun SendResultContent( // --- Receive amount --- -private enum class ReceiveTab { LIGHTNING, BITCOIN } - private enum class InvoiceExpiry(val seconds: Int) { ONE_HOUR(3600), ONE_DAY(86400), CUSTOM(0) } @@ -2299,13 +2228,8 @@ private fun ReceiveAmountContent( amount: String, isLoading: Boolean, lightningAddress: String?, - walletMode: WalletMode, - depositAddress: String?, - depositAddressLoading: Boolean, - depositAddressError: String?, onAmountChange: (String) -> Unit, onGenerate: (Long, String, Int) -> Unit, - onLoadDepositAddress: () -> Unit, onShowAddressQR: () -> Unit = {}, modifier: Modifier = Modifier ) { @@ -2333,9 +2257,6 @@ private fun ReceiveAmountContent( val canCreate = (satAmount ?: 0L) > 0L && !isLoading && (selectedExpiry != InvoiceExpiry.CUSTOM || expirySecs > 0) - val isSpark = walletMode == WalletMode.SPARK - var selectedTab by remember { mutableStateOf(ReceiveTab.LIGHTNING) } - val fieldShape = RoundedCornerShape(14.dp) val fieldBg = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f) @@ -2358,250 +2279,219 @@ private fun ReceiveAmountContent( Spacer(Modifier.height(20.dp)) - // Show tab row only for Spark wallets (which support on-chain) - if (isSpark) { - SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { - SegmentedButton( - selected = selectedTab == ReceiveTab.LIGHTNING, - onClick = { selectedTab = ReceiveTab.LIGHTNING }, - shape = SegmentedButtonDefaults.itemShape(index = 0, count = 2), - icon = { Icon(Icons.Outlined.Bolt, contentDescription = null, modifier = Modifier.size(16.dp)) } - ) { Text(stringResource(R.string.wallet_receive_lightning_tab)) } - SegmentedButton( - selected = selectedTab == ReceiveTab.BITCOIN, - onClick = { - selectedTab = ReceiveTab.BITCOIN - onLoadDepositAddress() - }, - shape = SegmentedButtonDefaults.itemShape(index = 1, count = 2), - icon = { Icon(Icons.Default.CurrencyBitcoin, contentDescription = null, modifier = Modifier.size(16.dp)) } - ) { Text(stringResource(R.string.wallet_receive_bitcoin_tab)) } - } - Spacer(Modifier.height(20.dp)) - } + // AMOUNT field + Text( + stringResource(R.string.wallet_receive_amount_label), + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.SemiBold, + letterSpacing = 0.5.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(Modifier.height(8.dp)) - if (selectedTab == ReceiveTab.BITCOIN && isSpark) { - ReceiveBitcoinBlock( - address = depositAddress, - isLoading = depositAddressLoading, - error = depositAddressError, - onRetry = onLoadDepositAddress - ) - } else { - // AMOUNT field - Text( - stringResource(R.string.wallet_receive_amount_label), - style = MaterialTheme.typography.labelMedium, + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .background(fieldBg, fieldShape) + .padding(horizontal = 16.dp, vertical = 16.dp) + ) { + val amountTextStyle = TextStyle( + fontSize = 32.sp, fontWeight = FontWeight.SemiBold, - letterSpacing = 0.5.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant + color = MaterialTheme.colorScheme.onSurface ) - Spacer(Modifier.height(8.dp)) - - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .background(fieldBg, fieldShape) - .padding(horizontal = 16.dp, vertical = 16.dp) - ) { - val amountTextStyle = TextStyle( - fontSize = 32.sp, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface - ) - BasicTextField( - value = amount, - onValueChange = { input -> - val filtered = if (fiatMode) { - val sb = StringBuilder() - var seenDot = false - for (c in input) { - if (c.isDigit()) sb.append(c) - else if (c == '.' && !seenDot) { sb.append(c); seenDot = true } - } - sb.toString() - } else { - input.filter { it.isDigit() } - } - onAmountChange(filtered) - }, - textStyle = amountTextStyle, - singleLine = true, - keyboardOptions = KeyboardOptions( - keyboardType = if (fiatMode) KeyboardType.Decimal else KeyboardType.NumberPassword - ), - cursorBrush = androidx.compose.ui.graphics.SolidColor(MaterialTheme.colorScheme.primary), - modifier = Modifier.weight(1f), - decorationBox = { inner -> - Box(contentAlignment = Alignment.CenterStart) { - if (amount.isEmpty()) { - Text( - "0", - style = amountTextStyle.copy(color = MaterialTheme.colorScheme.onSurfaceVariant) - ) - } - inner() + BasicTextField( + value = amount, + onValueChange = { input -> + val filtered = if (fiatMode) { + val sb = StringBuilder() + var seenDot = false + for (c in input) { + if (c.isDigit()) sb.append(c) + else if (c == '.' && !seenDot) { sb.append(c); seenDot = true } } + sb.toString() + } else { + input.filter { it.isDigit() } } - ) - Spacer(Modifier.width(8.dp)) - Text( - if (fiatMode) currency.code else stringResource(R.string.wallet_sats), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - if (fiatMode && fiatSats != null && fiatSats > 0L) { - Spacer(Modifier.height(6.dp)) - Text( - "≈ %,d sats".format(fiatSats), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(start = 4.dp) - ) - } - - Spacer(Modifier.height(20.dp)) - - // NOTE field - Text( - stringResource(R.string.wallet_receive_note_label), - style = MaterialTheme.typography.labelMedium, - fontWeight = FontWeight.SemiBold, - letterSpacing = 0.5.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Spacer(Modifier.height(8.dp)) - - Box( - modifier = Modifier - .fillMaxWidth() - .background(fieldBg, fieldShape) - .padding(horizontal = 16.dp, vertical = 14.dp) - ) { - val noteStyle = MaterialTheme.typography.bodyMedium.copy( - color = MaterialTheme.colorScheme.onSurface - ) - BasicTextField( - value = description, - onValueChange = { description = it }, - textStyle = noteStyle, - singleLine = true, - cursorBrush = androidx.compose.ui.graphics.SolidColor(MaterialTheme.colorScheme.primary), - modifier = Modifier.fillMaxWidth(), - decorationBox = { inner -> - if (description.isEmpty()) { + onAmountChange(filtered) + }, + textStyle = amountTextStyle, + singleLine = true, + keyboardOptions = KeyboardOptions( + keyboardType = if (fiatMode) KeyboardType.Decimal else KeyboardType.NumberPassword + ), + cursorBrush = androidx.compose.ui.graphics.SolidColor(MaterialTheme.colorScheme.primary), + modifier = Modifier.weight(1f), + decorationBox = { inner -> + Box(contentAlignment = Alignment.CenterStart) { + if (amount.isEmpty()) { Text( - stringResource(R.string.wallet_receive_note_placeholder), - style = noteStyle.copy(color = MaterialTheme.colorScheme.onSurfaceVariant) + "0", + style = amountTextStyle.copy(color = MaterialTheme.colorScheme.onSurfaceVariant) ) } inner() } - ) - } - - Spacer(Modifier.height(20.dp)) - - // EXPIRES selector + } + ) + Spacer(Modifier.width(8.dp)) Text( - stringResource(R.string.wallet_receive_expires_label), - style = MaterialTheme.typography.labelMedium, - fontWeight = FontWeight.SemiBold, - letterSpacing = 0.5.sp, + if (fiatMode) currency.code else stringResource(R.string.wallet_sats), + style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) - Spacer(Modifier.height(8.dp)) - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - listOf( - InvoiceExpiry.ONE_HOUR to stringResource(R.string.wallet_receive_expires_1h), - InvoiceExpiry.ONE_DAY to stringResource(R.string.wallet_receive_expires_24h), - InvoiceExpiry.CUSTOM to stringResource(R.string.wallet_receive_expires_custom) - ).forEachIndexed { _, (expiry, label) -> - val isSelected = selectedExpiry == expiry - OutlinedButton( - onClick = { selectedExpiry = expiry }, - modifier = Modifier.weight(1f), - colors = ButtonDefaults.outlinedButtonColors( - containerColor = if (isSelected) MaterialTheme.colorScheme.primary.copy(alpha = 0.12f) else Color.Transparent, - contentColor = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant - ), - border = BorderStroke( - 1.dp, - if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline - ), - contentPadding = PaddingValues(horizontal = 8.dp, vertical = 6.dp) - ) { Text(label, style = MaterialTheme.typography.labelMedium) } - } - } - if (selectedExpiry == InvoiceExpiry.CUSTOM) { - Spacer(Modifier.height(8.dp)) - OutlinedTextField( - value = customHours, - onValueChange = { customHours = it.filter { c -> c.isDigit() } }, - label = { Text(stringResource(R.string.wallet_receive_expires_hours)) }, - singleLine = true, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.fillMaxWidth() - ) - } + } + if (fiatMode && fiatSats != null && fiatSats > 0L) { + Spacer(Modifier.height(6.dp)) + Text( + "≈ %,d sats".format(fiatSats), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 4.dp) + ) + } - Spacer(Modifier.height(24.dp)) + Spacer(Modifier.height(20.dp)) - if (isLoading) { - Box( - modifier = Modifier - .fillMaxWidth() - .background(fieldBg, fieldShape) - .padding(vertical = 14.dp), - contentAlignment = Alignment.Center - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - CircularProgressIndicator( - strokeWidth = 2.dp, - modifier = Modifier.size(18.dp) - ) - Spacer(Modifier.width(10.dp)) + // NOTE field + Text( + stringResource(R.string.wallet_receive_note_label), + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.SemiBold, + letterSpacing = 0.5.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(Modifier.height(8.dp)) + + Box( + modifier = Modifier + .fillMaxWidth() + .background(fieldBg, fieldShape) + .padding(horizontal = 16.dp, vertical = 14.dp) + ) { + val noteStyle = MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.onSurface + ) + BasicTextField( + value = description, + onValueChange = { description = it }, + textStyle = noteStyle, + singleLine = true, + cursorBrush = androidx.compose.ui.graphics.SolidColor(MaterialTheme.colorScheme.primary), + modifier = Modifier.fillMaxWidth(), + decorationBox = { inner -> + if (description.isEmpty()) { Text( - stringResource(R.string.wallet_creating_invoice), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + stringResource(R.string.wallet_receive_note_placeholder), + style = noteStyle.copy(color = MaterialTheme.colorScheme.onSurfaceVariant) ) } + inner() } - } else { - Button( - onClick = { - val sats = satAmount ?: return@Button - onGenerate(sats, description, expirySecs) - }, - enabled = canCreate, - modifier = Modifier - .fillMaxWidth() - .height(52.dp), - shape = fieldShape, - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = Color.White, - disabledContainerColor = fieldBg, - disabledContentColor = MaterialTheme.colorScheme.onSurfaceVariant - ) - ) { - Text( - stringResource(R.string.wallet_receive_create_invoice), - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.SemiBold - ) - } - } + ) + } - // Lightning address — shown below invoice form as "or receive via" row - if (!lightningAddress.isNullOrBlank()) { - Spacer(Modifier.height(24.dp)) - LightningAddressReceiveRow(address = lightningAddress, onShowQR = onShowAddressQR) + Spacer(Modifier.height(20.dp)) + + // EXPIRES selector + Text( + stringResource(R.string.wallet_receive_expires_label), + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.SemiBold, + letterSpacing = 0.5.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(Modifier.height(8.dp)) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + listOf( + InvoiceExpiry.ONE_HOUR to stringResource(R.string.wallet_receive_expires_1h), + InvoiceExpiry.ONE_DAY to stringResource(R.string.wallet_receive_expires_24h), + InvoiceExpiry.CUSTOM to stringResource(R.string.wallet_receive_expires_custom) + ).forEachIndexed { _, (expiry, label) -> + val isSelected = selectedExpiry == expiry + OutlinedButton( + onClick = { selectedExpiry = expiry }, + modifier = Modifier.weight(1f), + colors = ButtonDefaults.outlinedButtonColors( + containerColor = if (isSelected) MaterialTheme.colorScheme.primary.copy(alpha = 0.12f) else Color.Transparent, + contentColor = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant + ), + border = BorderStroke( + 1.dp, + if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline + ), + contentPadding = PaddingValues(horizontal = 8.dp, vertical = 6.dp) + ) { Text(label, style = MaterialTheme.typography.labelMedium) } } } + if (selectedExpiry == InvoiceExpiry.CUSTOM) { + Spacer(Modifier.height(8.dp)) + OutlinedTextField( + value = customHours, + onValueChange = { customHours = it.filter { c -> c.isDigit() } }, + label = { Text(stringResource(R.string.wallet_receive_expires_hours)) }, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.fillMaxWidth() + ) + } + + Spacer(Modifier.height(24.dp)) + + if (isLoading) { + Box( + modifier = Modifier + .fillMaxWidth() + .background(fieldBg, fieldShape) + .padding(vertical = 14.dp), + contentAlignment = Alignment.Center + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + CircularProgressIndicator( + strokeWidth = 2.dp, + modifier = Modifier.size(18.dp) + ) + Spacer(Modifier.width(10.dp)) + Text( + stringResource(R.string.wallet_creating_invoice), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } else { + Button( + onClick = { + val sats = satAmount ?: return@Button + onGenerate(sats, description, expirySecs) + }, + enabled = canCreate, + modifier = Modifier + .fillMaxWidth() + .height(52.dp), + shape = fieldShape, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = Color.White, + disabledContainerColor = fieldBg, + disabledContentColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) { + Text( + stringResource(R.string.wallet_receive_create_invoice), + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold + ) + } + } + + // Lightning address — shown below invoice form as "or receive via" row + if (!lightningAddress.isNullOrBlank()) { + Spacer(Modifier.height(24.dp)) + LightningAddressReceiveRow(address = lightningAddress, onShowQR = onShowAddressQR) + } Spacer(Modifier.height(24.dp)) } @@ -2672,485 +2562,6 @@ private fun LightningAddressReceiveRow( } } -// Bitcoin on-chain deposit address block (Spark only) -@Composable -private fun ReceiveBitcoinBlock( - address: String?, - isLoading: Boolean, - error: String?, - onRetry: () -> Unit -) { - val clipboardManager = LocalClipboardManager.current - val context = LocalContext.current - val cardShape = RoundedCornerShape(16.dp) - val cardBg = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f) - val actionShape = RoundedCornerShape(14.dp) - val actionBg = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f) - - when { - isLoading -> { - Box( - modifier = Modifier - .fillMaxWidth() - .background(cardBg, cardShape) - .padding(40.dp), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator(strokeWidth = 2.dp, modifier = Modifier.size(32.dp)) - } - } - error != null -> { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .fillMaxWidth() - .background(cardBg, cardShape) - .padding(24.dp) - ) { - Text(error, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.error, textAlign = TextAlign.Center) - Spacer(Modifier.height(16.dp)) - OutlinedButton(onClick = onRetry) { Text(stringResource(R.string.retry)) } - } - } - address != null -> { - val qrBitmap = remember(address) { - val writer = QRCodeWriter() - val matrix = writer.encode("bitcoin:$address", BarcodeFormat.QR_CODE, 512, 512) - val bitmap = Bitmap.createBitmap(matrix.width, matrix.height, Bitmap.Config.RGB_565) - for (x in 0 until matrix.width) { - for (y in 0 until matrix.height) { - bitmap.setPixel(x, y, if (matrix.get(x, y)) android.graphics.Color.BLACK else android.graphics.Color.WHITE) - } - } - bitmap - } - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Column( - modifier = Modifier - .fillMaxWidth() - .background(cardBg, cardShape) - .padding(20.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Image( - bitmap = qrBitmap.asImageBitmap(), - contentDescription = stringResource(R.string.cd_bitcoin_address_qr), - modifier = Modifier - .size(260.dp) - .background(Color.White, RoundedCornerShape(12.dp)) - .padding(8.dp) - ) - Spacer(Modifier.height(12.dp)) - Text( - address, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center, - fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace - ) - } - Spacer(Modifier.height(16.dp)) - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .background(actionBg, actionShape) - ) { - TextButton( - onClick = { clipboardManager.setText(AnnotatedString(address)) }, - modifier = Modifier.weight(1f), - colors = ButtonDefaults.textButtonColors(contentColor = WispThemeColors.zapColor) - ) { - Icon(Icons.Default.ContentCopy, contentDescription = null, modifier = Modifier.size(18.dp)) - Spacer(Modifier.width(6.dp)) - Text(stringResource(R.string.wallet_copy_address), fontWeight = FontWeight.Medium) - } - VerticalDivider(modifier = Modifier.height(24.dp)) - TextButton( - onClick = { - val intent = Intent(Intent.ACTION_SEND).apply { - type = "text/plain" - putExtra(Intent.EXTRA_TEXT, address) - } - context.startActivity(Intent.createChooser(intent, context.getString(R.string.wallet_share_address))) - }, - modifier = Modifier.weight(1f), - colors = ButtonDefaults.textButtonColors(contentColor = WispThemeColors.zapColor) - ) { - Icon(Icons.Default.Share, contentDescription = null, modifier = Modifier.size(18.dp)) - Spacer(Modifier.width(6.dp)) - Text(stringResource(R.string.wallet_share), fontWeight = FontWeight.Medium) - } - } - } - } - else -> { - // Initial state — trigger load - LaunchedEffect(Unit) { onRetry() } - } - } -} - -// On-chain send: amount entry (with inline fee quote) -@Composable -private fun OnchainSendAmountContent( - address: String, - amount: String, - balanceSats: Long, - isLoading: Boolean, - error: String?, - feeQuote: OnchainFeeQuote?, - onAmountChange: (String) -> Unit, - onUseAll: () -> Unit, - onGetFeeQuote: () -> Unit, - onContinue: () -> Unit, - onBack: () -> Unit, - modifier: Modifier = Modifier -) { - val amountSats = amount.toLongOrNull() ?: 0L - val fieldShape = RoundedCornerShape(14.dp) - val fieldBg = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f) - val accent = WispThemeColors.zapColor - - Column( - modifier = modifier - .fillMaxSize() - .padding(horizontal = 20.dp) - .verticalScroll(rememberScrollState()) - ) { - Spacer(Modifier.height(8.dp)) - Row(verticalAlignment = Alignment.CenterVertically) { - Icon(Icons.AutoMirrored.Filled.Send, contentDescription = null, tint = accent, modifier = Modifier.size(20.dp)) - Spacer(Modifier.width(8.dp)) - Text(stringResource(R.string.wallet_onchain_send_title), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold) - } - - Spacer(Modifier.height(20.dp)) - - // Recipient address (read-only) - Box( - modifier = Modifier - .fillMaxWidth() - .background(fieldBg, fieldShape) - .padding(16.dp) - ) { - Text( - address, - style = MaterialTheme.typography.bodyMedium, - fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace, - color = MaterialTheme.colorScheme.onSurface - ) - } - Spacer(Modifier.height(10.dp)) - Row(verticalAlignment = Alignment.CenterVertically) { - Icon(Icons.Default.CurrencyBitcoin, contentDescription = null, tint = accent, modifier = Modifier.size(16.dp)) - Spacer(Modifier.width(6.dp)) - Text( - stringResource(R.string.wallet_onchain_bitcoin_detected), - style = MaterialTheme.typography.bodySmall, - color = accent - ) - } - - Spacer(Modifier.height(20.dp)) - - // Amount label + Use All - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - stringResource(R.string.wallet_onchain_amount_sats), - style = MaterialTheme.typography.labelMedium, - fontWeight = FontWeight.SemiBold, - letterSpacing = 0.5.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Text( - stringResource(R.string.wallet_onchain_use_all, balanceSats), - style = MaterialTheme.typography.labelMedium, - fontWeight = FontWeight.SemiBold, - color = accent, - modifier = Modifier.clickable(onClick = onUseAll) - ) - } - Spacer(Modifier.height(8.dp)) - Box( - modifier = Modifier - .fillMaxWidth() - .background(fieldBg, fieldShape) - .padding(horizontal = 16.dp, vertical = 16.dp) - ) { - val amountStyle = MaterialTheme.typography.titleLarge.copy(color = MaterialTheme.colorScheme.onSurface) - BasicTextField( - value = amount, - onValueChange = { onAmountChange(it.filter { c -> c.isDigit() }) }, - textStyle = amountStyle, - singleLine = true, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - cursorBrush = androidx.compose.ui.graphics.SolidColor(MaterialTheme.colorScheme.primary), - modifier = Modifier.fillMaxWidth(), - decorationBox = { inner -> - if (amount.isEmpty()) Text("0", style = amountStyle.copy(color = MaterialTheme.colorScheme.onSurfaceVariant)) - inner() - } - ) - } - - if (error != null) { - Spacer(Modifier.height(12.dp)) - Text(error, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall) - } - - Spacer(Modifier.height(20.dp)) - - if (feeQuote != null) { - val feeSats = feeQuote.mediumFeeSats - val totalSats = amountSats + feeSats - Column( - modifier = Modifier - .fillMaxWidth() - .background(fieldBg, fieldShape) - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(10.dp) - ) { - OnchainAmountRow(stringResource(R.string.wallet_onchain_amount), "%,d sats".format(amountSats)) - OnchainAmountRow(stringResource(R.string.wallet_onchain_fee), "%,d sats".format(feeSats)) - HorizontalDivider() - OnchainAmountRow(stringResource(R.string.wallet_onchain_total), "%,d sats".format(totalSats), emphasize = true) - } - Spacer(Modifier.height(16.dp)) - Button( - onClick = onContinue, - modifier = Modifier.fillMaxWidth().height(52.dp), - shape = fieldShape, - colors = ButtonDefaults.buttonColors(containerColor = accent, contentColor = Color.White) - ) { - Icon(Icons.AutoMirrored.Filled.Send, contentDescription = null, modifier = Modifier.size(18.dp)) - Spacer(Modifier.width(8.dp)) - Text(stringResource(R.string.wallet_onchain_continue), style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold) - } - } else if (isLoading) { - Box( - modifier = Modifier.fillMaxWidth().background(fieldBg, fieldShape).padding(vertical = 14.dp), - contentAlignment = Alignment.Center - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - CircularProgressIndicator(strokeWidth = 2.dp, modifier = Modifier.size(18.dp)) - Spacer(Modifier.width(10.dp)) - Text(stringResource(R.string.wallet_onchain_fetching_fee), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) - } - } - } else { - Button( - onClick = onGetFeeQuote, - enabled = amountSats > 0L, - modifier = Modifier.fillMaxWidth().height(52.dp), - shape = fieldShape, - colors = ButtonDefaults.buttonColors( - containerColor = accent, - contentColor = Color.White, - disabledContainerColor = fieldBg, - disabledContentColor = MaterialTheme.colorScheme.onSurfaceVariant - ) - ) { - Text(stringResource(R.string.wallet_onchain_get_fee_quote), style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold) - } - } - - Spacer(Modifier.height(24.dp)) - } -} - -@Composable -private fun OnchainAmountRow(label: String, value: String, emphasize: Boolean = false) { - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - Text( - label, - style = if (emphasize) MaterialTheme.typography.titleSmall else MaterialTheme.typography.bodyMedium, - fontWeight = if (emphasize) FontWeight.SemiBold else FontWeight.Normal, - color = if (emphasize) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant - ) - Text( - value, - style = if (emphasize) MaterialTheme.typography.titleSmall else MaterialTheme.typography.bodyMedium, - fontWeight = if (emphasize) FontWeight.SemiBold else FontWeight.Normal, - color = if (emphasize) WispThemeColors.zapColor else MaterialTheme.colorScheme.onSurface - ) - } -} - -// On-chain send: confirm (chunked address, big amount hero, irreversibility warning) -@Composable -private fun OnchainSendConfirmContent( - address: String, - amountSats: Long, - feeQuote: OnchainFeeQuote, - isLoading: Boolean, - onConfirm: () -> Unit, - onBack: () -> Unit, - modifier: Modifier = Modifier -) { - val feeSats = feeQuote.mediumFeeSats - val accent = WispThemeColors.zapColor - val fieldShape = RoundedCornerShape(14.dp) - val fieldBg = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f) - - val chunked = remember(address) { - buildAnnotatedString { - address.chunked(4).forEachIndexed { i, group -> - withStyle(SpanStyle(color = if (i % 2 == 0) androidx.compose.ui.graphics.Color(0xFFE6E6E6) else accent)) { - append(group) - } - append(" ") - } - } - } - - Column( - modifier = modifier - .fillMaxSize() - .padding(horizontal = 20.dp) - .verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Spacer(Modifier.height(8.dp)) - Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - Icon(Icons.AutoMirrored.Filled.Send, contentDescription = null, tint = accent, modifier = Modifier.size(20.dp)) - Spacer(Modifier.width(8.dp)) - Text(stringResource(R.string.wallet_onchain_send_title), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold) - } - - Spacer(Modifier.height(32.dp)) - - Text( - "%,d sats".format(amountSats), - style = MaterialTheme.typography.headlineLarge, - fontWeight = FontWeight.Bold, - color = accent - ) - Spacer(Modifier.height(4.dp)) - Text( - stringResource(R.string.wallet_onchain_plus_fee, feeSats), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Spacer(Modifier.height(28.dp)) - - Column( - modifier = Modifier - .fillMaxWidth() - .background(fieldBg, fieldShape) - .padding(20.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - stringResource(R.string.wallet_onchain_sending_to), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Spacer(Modifier.height(10.dp)) - Text( - chunked, - style = MaterialTheme.typography.bodyLarge, - fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace, - textAlign = TextAlign.Center, - lineHeight = 28.sp - ) - } - - Spacer(Modifier.height(20.dp)) - - Text( - stringResource(R.string.wallet_onchain_irreversible_warning), - style = MaterialTheme.typography.bodySmall, - color = accent, - textAlign = TextAlign.Center - ) - - Spacer(Modifier.height(24.dp)) - - if (isLoading) { - CircularProgressIndicator() - } else { - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) { - OutlinedButton( - onClick = onBack, - modifier = Modifier.weight(1f).height(52.dp), - shape = fieldShape - ) { - Text(stringResource(R.string.btn_back)) - } - Button( - onClick = onConfirm, - modifier = Modifier.weight(1f).height(52.dp), - shape = fieldShape, - colors = ButtonDefaults.buttonColors(containerColor = accent, contentColor = Color.White) - ) { - Icon(Icons.AutoMirrored.Filled.Send, contentDescription = null, modifier = Modifier.size(18.dp)) - Spacer(Modifier.width(8.dp)) - Text(stringResource(R.string.wallet_onchain_send_confirm), fontWeight = FontWeight.SemiBold) - } - } - } - - Spacer(Modifier.height(24.dp)) - } -} - -// On-chain send: result -@Composable -private fun OnchainSendResultContent( - success: Boolean, - paymentId: String?, - message: String, - onDone: () -> Unit, - modifier: Modifier = Modifier -) { - val context = LocalContext.current - - Column( - modifier = modifier.fillMaxSize().padding(horizontal = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Icon( - if (success) Icons.Default.Check else Icons.Default.Close, - contentDescription = null, - modifier = Modifier.size(72.dp), - tint = if (success) WispThemeColors.zapColor else MaterialTheme.colorScheme.error - ) - Spacer(Modifier.height(16.dp)) - Text( - stringResource(if (success) R.string.wallet_onchain_payment_sent else R.string.wallet_onchain_payment_failed), - style = MaterialTheme.typography.headlineMedium, - color = MaterialTheme.colorScheme.onSurface - ) - Spacer(Modifier.height(8.dp)) - Text( - message, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center - ) - if (success && paymentId != null) { - Spacer(Modifier.height(16.dp)) - TextButton(onClick = { - val intent = Intent(Intent.ACTION_VIEW, android.net.Uri.parse("https://mempool.space/tx/$paymentId")) - context.startActivity(intent) - }) { - Text(stringResource(R.string.wallet_onchain_view_mempool)) - } - } - Spacer(Modifier.height(24.dp)) - FilledTonalButton(onClick = onDone, modifier = Modifier.fillMaxWidth()) { - Text(stringResource(R.string.btn_done)) - } - } -} - // --- Receive invoice (QR code) --- @Composable diff --git a/app/src/main/kotlin/com/darkwisp/app/viewmodel/WalletViewModel.kt b/app/src/main/kotlin/com/darkwisp/app/viewmodel/WalletViewModel.kt index 94cc002..45433c3 100644 --- a/app/src/main/kotlin/com/darkwisp/app/viewmodel/WalletViewModel.kt +++ b/app/src/main/kotlin/com/darkwisp/app/viewmodel/WalletViewModel.kt @@ -30,7 +30,6 @@ import com.darkwisp.app.repo.NwcRepository import com.darkwisp.app.repo.SparkRepository import com.darkwisp.app.repo.WalletMode import com.darkwisp.app.repo.WalletModeRepository -import com.darkwisp.app.repo.OnchainFeeQuote import com.darkwisp.app.repo.WalletProvider import android.util.Log import com.darkwisp.app.repo.WalletTransaction @@ -126,14 +125,6 @@ sealed class WalletPage { data class SendResult(val success: Boolean, val message: String) : WalletPage() object ReceiveAmount : WalletPage() data class ReceiveInvoice(val invoice: String, val amountSats: Long) : WalletPage() - data class OnchainSendAmount(val address: String) : WalletPage() - data class OnchainSendConfirm( - val address: String, - val amountSats: Long, - val feeQuote: OnchainFeeQuote, - val prepareData: Any - ) : WalletPage() - data class OnchainSendResult(val success: Boolean, val paymentId: String?, val message: String) : WalletPage() data class ReceiveSuccess(val amountSats: Long) : WalletPage() object Transactions : WalletPage() object Settings : WalletPage() @@ -205,25 +196,6 @@ class WalletViewModel( private val _receiveAmount = MutableStateFlow("") val receiveAmount: StateFlow = _receiveAmount - // On-chain deposit address (Spark only) - private val _depositAddress = MutableStateFlow(null) - val depositAddress: StateFlow = _depositAddress - - private val _depositAddressLoading = MutableStateFlow(false) - val depositAddressLoading: StateFlow = _depositAddressLoading - - private val _depositAddressError = MutableStateFlow(null) - val depositAddressError: StateFlow = _depositAddressError - - // On-chain send (Spark only) - private val _onchainFeeQuote = MutableStateFlow(null) - val onchainFeeQuote: StateFlow = _onchainFeeQuote - - private var _onchainPrepareData: Any? = null - - private val _onchainSendLoading = MutableStateFlow(false) - val onchainSendLoading: StateFlow = _onchainSendLoading - // Transactions private val _transactions = MutableStateFlow>(emptyList()) val transactions: StateFlow> = _transactions @@ -346,11 +318,6 @@ class WalletViewModel( // would otherwise stay at the prior wallet's acknowledged value // until the next refreshState() call. _seedBackupAcked.value = false - _depositAddress.value = null - _depositAddressError.value = null - _depositAddressLoading.value = false - _onchainFeeQuote.value = null - _onchainPrepareData = null } /** @@ -577,8 +544,6 @@ class WalletViewModel( _deleteConfirmText.value = "" _lightningAddressError.value = null _addressAvailable.value = null - _onchainFeeQuote.value = null - _onchainPrepareData = null } val isOnHome: Boolean get() = pageStack.size <= 1 @@ -1301,14 +1266,6 @@ class WalletViewModel( } } - private fun isBtcAddress(s: String): Boolean { - val lower = s.lowercase() - if (lower.startsWith("bc1") && s.length >= 14) return true - if ((s.startsWith("1") || s.startsWith("3")) && s.length in 26..35 && - s.all { it.isLetterOrDigit() && it != '0' && it != 'O' && it != 'I' && it != 'l' }) return true - return false - } - fun processInput(input: String = _sendInput.value) { val trimmed = input.trim() .removePrefix("lightning:").removePrefix("LIGHTNING:") @@ -1333,10 +1290,6 @@ class WalletViewModel( navigateTo(WalletPage.SendAmount(noffer.raw)) } } - _walletMode.value == WalletMode.SPARK && isBtcAddress(trimmed) -> { - clearOnchainQuote() - navigateTo(WalletPage.OnchainSendAmount(trimmed)) - } trimmed.lowercase().startsWith("lnbc") -> { val decoded = Bolt11.decode(trimmed) if (decoded == null) { @@ -1359,10 +1312,7 @@ class WalletViewModel( navigateTo(WalletPage.SendAmount(trimmed)) } else -> { - _sendError.value = if (_walletMode.value == WalletMode.SPARK) - "Enter a lightning address, BOLT11 invoice, CLINK offer, or Bitcoin address" - else - "Enter a lightning address (user@domain), BOLT11 invoice, or CLINK offer" + _sendError.value = "Enter a lightning address (user@domain), BOLT11 invoice, or CLINK offer" } } } @@ -1555,74 +1505,6 @@ class WalletViewModel( } } - // --- On-chain receive (Spark only) --- - - fun loadDepositAddress(force: Boolean = false) { - if (!force && _depositAddress.value != null) return - _depositAddressLoading.value = true - _depositAddressError.value = null - viewModelScope.launch { - sparkRepo.getDepositAddress().fold( - onSuccess = { address -> _depositAddress.value = address }, - onFailure = { e -> _depositAddressError.value = e.message ?: "Failed to load address" } - ) - _depositAddressLoading.value = false - } - } - - // --- On-chain send (Spark only) --- - - fun clearOnchainQuote() { - _onchainFeeQuote.value = null - _onchainPrepareData = null - } - - fun prepareOnchainSend(address: String, amountSats: Long) { - clearOnchainQuote() - _onchainSendLoading.value = true - _sendError.value = null - viewModelScope.launch { - sparkRepo.prepareOnchainSend(address, amountSats).fold( - onSuccess = { (quote, prepareData) -> - _onchainFeeQuote.value = quote - _onchainPrepareData = prepareData - }, - onFailure = { e -> _sendError.value = e.message ?: "Failed to estimate fee" } - ) - _onchainSendLoading.value = false - } - } - - fun continueToOnchainConfirm(address: String, amountSats: Long) { - val quote = _onchainFeeQuote.value ?: return - val prepareData = _onchainPrepareData ?: return - navigateTo(WalletPage.OnchainSendConfirm(address, amountSats, quote, prepareData)) - } - - fun sendOnchain(prepareData: Any) { - if (_isLoading.value) return - _isLoading.value = true - viewModelScope.launch { - sparkRepo.sendOnchain(prepareData).fold( - onSuccess = { paymentId -> - pageStack.removeAt(pageStack.lastIndex) - val resultPage = WalletPage.OnchainSendResult(true, paymentId, "Bitcoin sent!") - pageStack.add(resultPage) - _currentPage.value = resultPage - clearOnchainQuote() - refreshBalance() - }, - onFailure = { e -> - pageStack.removeAt(pageStack.lastIndex) - val resultPage = WalletPage.OnchainSendResult(false, null, e.message ?: "Send failed") - pageStack.add(resultPage) - _currentPage.value = resultPage - } - ) - _isLoading.value = false - } - } - // --- Transactions --- fun loadTransactions() { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 96d06cf..0071e75 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -776,9 +776,6 @@ NOTE (OPTIONAL) For coffee, etc. Create invoice - - Lightning - Bitcoin EXPIRES 1 hour @@ -790,29 +787,7 @@ Copy address Share address Share - - Bitcoin address QR code - - Send Payment - Bitcoin address detected – on-chain payment - Amount (sats) - Use All (%,d sats) - Estimating fee… - Get Fee Quote - Amount - Network Fee - Total - Continue - + %,d sats fee - Sending to: - Bitcoin transactions cannot be reversed. Please verify the address is correct. - Confirm Send - Sent - Send Failed - View on mempool.space - - Lightning invoice or Bitcoin address - Lightning or Bitcoin address, invoice, or CLINK offer + Lightning address, invoice, or CLINK offer Retry Cancel