Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
068fb91
fix: require payment pin for quickpay
ovitrif Aug 15, 2026
76cb9ef
chore: rename changelog fragment
ovitrif Aug 15, 2026
01cc208
fix: add quickpay daily spend limit
ovitrif Aug 15, 2026
99b47d8
fix: show multiplier steps as times
ovitrif Aug 15, 2026
fbe9321
fix: include steplider labels in layout bounds
ovitrif Aug 15, 2026
d90dfb1
fix: layout steplider labels from constraints
ovitrif Aug 15, 2026
ef76b51
refactor: measure slider as one unit
ovitrif Aug 15, 2026
af4163d
fix: spacing and copy
ovitrif Aug 15, 2026
17f544c
fix: snap slider tap from current value
ovitrif Aug 15, 2026
7385c73
fix: simplify multiplier string templates
ovitrif Aug 15, 2026
af959c9
fix: harden quickpay daily spend limits
ovitrif Aug 18, 2026
7cdae86
fix: localize error messages and texts
ovitrif Aug 18, 2026
3593678
refactor: use USD constant
ovitrif Aug 18, 2026
4761c75
feat: use quickpay routing fee on ui
ovitrif Aug 18, 2026
b64cc53
merge: sync master into quickpay daily limit
ovitrif Aug 18, 2026
07a88a2
fix: harden quickpay spend and slider
ovitrif Aug 18, 2026
a254ba2
test: fix quickpay day spend assertion
ovitrif Aug 18, 2026
c6136d7
merge: sync master into quickpay daily limit
ovitrif Aug 18, 2026
d98ad17
fix: wrap cache store test lines
ovitrif Aug 18, 2026
c874d2c
merge: sync master into quickpay daily limit
ovitrif Aug 19, 2026
c0806a6
fix: account quickpay daily spend in usd cents
ovitrif Aug 19, 2026
ddf9b71
test: fix quickpay viewmodel mock setup
ovitrif Aug 19, 2026
6d7bf18
refactor: move quickpay spend ledger to repo
ovitrif Aug 19, 2026
1bb0498
chore: cleanup imports
ovitrif Aug 19, 2026
37017df
fix: keep quickpay events and spend in sync
ovitrif Aug 19, 2026
ffe28d9
fix: release spend without pending gate
ovitrif Aug 19, 2026
68cba13
test: drop unshipped sat spend decode
ovitrif Aug 19, 2026
ef74df4
test: drop duplicate setmain in quickpay
ovitrif Aug 19, 2026
5c21813
refactor: inline quickpay day key
ovitrif Aug 19, 2026
b0f56d5
refactor: move quickpay types to repo
ovitrif Aug 19, 2026
d46ab05
refactor: drop experimentaltime opt-in
ovitrif Aug 19, 2026
0f59bc6
chore: fix quickpay import order
ovitrif Aug 19, 2026
7f18c0b
fix: count sub-cent quickpay as 1 cent
ovitrif Aug 20, 2026
ee5a67a
fix: clear stale quickpay pending state
ovitrif Aug 20, 2026
eed24c8
refactor: drop unused rtl and spend helper
ovitrif Aug 20, 2026
d0229ad
refactor: drop unused quickpay error param
ovitrif Aug 20, 2026
7375855
fix: settle quickpay spend through one ledger
ovitrif Aug 22, 2026
4261bc0
merge: origin/master
ovitrif Aug 22, 2026
ba9a206
refactor: split quickpay pay flow into stages
ovitrif Aug 22, 2026
cf4bb0a
refactor: extract quickpay coordinator
ovitrif Aug 22, 2026
dc7daba
refactor: inject quickpay invoice parser
ovitrif Aug 22, 2026
3d86167
refactor: rename quickpay terminal to completion
ovitrif Aug 22, 2026
0c0318a
test: inline quickpay ledger fixtures
ovitrif Aug 22, 2026
a8cef2a
refactor: dedupe quickpay reconcile paths
ovitrif Aug 22, 2026
7294f68
chore: self review
ovitrif Aug 22, 2026
d7009df
refactor: fold quickpay coordinator into repo
ovitrif Aug 23, 2026
673eab4
fix: keep live quickpay ops out of reconcile
ovitrif Aug 23, 2026
9eabf4c
fix: refund duplicate quickpay reserve
ovitrif Aug 23, 2026
816dd98
fix: settle quickpay refund without hash
ovitrif Aug 23, 2026
df06ff5
test: pin quickpay dispatch boundary
ovitrif Aug 23, 2026
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 @@ -34,12 +34,14 @@ class QuickPaySettingsScreenTest {
QuickPaySettingsScreenContent(
isQuickPayEnabled = true,
quickPayAmount = 5,
quickPayDailyLimitMultiplier = 5,
)
}
}

composeTestRule.onNodeWithTag("QuickpayToggle").assertIsDisplayed()
composeTestRule.onNodeWithTag("quickpay_amount_slider").assertIsDisplayed()
composeTestRule.onNodeWithTag("QuickpayAmountSlider").assertIsDisplayed()
composeTestRule.onNodeWithTag("QuickpayDailyLimitSlider").assertIsDisplayed()
}

@Test
Expand All @@ -52,6 +54,7 @@ class QuickPaySettingsScreenTest {
QuickPaySettingsScreenContent(
isQuickPayEnabled = false,
quickPayAmount = 5,
quickPayDailyLimitMultiplier = 5,
onToggleQuickPay = { enabled ->
toggleCalled = true
toggleValue = enabled
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/to/bitkit/data/CacheStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import to.bitkit.models.BalanceState
import to.bitkit.models.FxRate
import to.bitkit.models.NewTransactionSheetDetails
import to.bitkit.models.WalletScope
import to.bitkit.repositories.QuickPayLedger
import to.bitkit.utils.Logger
import javax.inject.Inject
import javax.inject.Singleton
Expand Down Expand Up @@ -164,6 +165,7 @@ data class AppCacheData(
val backgroundReceive: NewTransactionSheetDetails? = null,
val addressSearchLastUsedReceiveIndexes: Map<String, Int> = mapOf(),
val addressSearchLastUsedChangeIndexes: Map<String, Int> = mapOf(),
val quickPayLedger: QuickPayLedger? = null,
) {
fun isActivityDeleted(activityId: String, walletId: String): Boolean =
scopedActivityId(walletId, activityId) in deletedActivities ||
Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/to/bitkit/data/SettingsStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ data class SettingsData(
val bgPaymentsIntroSeen: Boolean = false,
val isQuickPayEnabled: Boolean = false,
val quickPayAmount: Int = 5,
val quickPayDailyLimitMultiplier: Int = 5,
val lightningSetupStep: Int = 0,
val isPinEnabled: Boolean = false,
val isBiometricEnabled: Boolean = false,
Expand Down
14 changes: 14 additions & 0 deletions app/src/main/java/to/bitkit/di/RepoModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@ import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.lightningdevkit.ldknode.Bolt11Invoice
import to.bitkit.repositories.AmountInputHandler
import to.bitkit.repositories.CurrencyRepo
import to.bitkit.repositories.LightningRepo
import to.bitkit.repositories.QuickPayInvoiceParser
import to.bitkit.repositories.QuickPayPaymentLookup
import to.bitkit.repositories.QuickPayReconcileRow
import javax.inject.Named

@Module
Expand All @@ -22,5 +27,14 @@ abstract class RepoModule {
@Provides
@Named("enablePolling")
fun provideEnablePolling(): Boolean = true

@Provides
fun provideQuickPayInvoiceParser(): QuickPayInvoiceParser = QuickPayInvoiceParser { bolt11 ->
runCatching { Bolt11Invoice.fromStr(bolt11).paymentHash() }.getOrNull()
}

@Provides
fun provideQuickPayPaymentLookup(lightningRepo: LightningRepo): QuickPayPaymentLookup =
QuickPayPaymentLookup { lightningRepo.listPaymentsOrNull()?.map { QuickPayReconcileRow(it) } }
}
}
2 changes: 2 additions & 0 deletions app/src/main/java/to/bitkit/models/Currency.kt
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ data class ConvertedAmount(
val sats: Long,
val locale: Locale = Locale.getDefault(),
) {
fun toUsdCents(): Long = value.movePointRight(2).setScale(0, RoundingMode.HALF_UP).toLong()

val isSymbolSuffix: Boolean get() = currency in SUFFIX_SYMBOL_CURRENCIES

data class BitcoinDisplayComponents(
Expand Down
19 changes: 17 additions & 2 deletions app/src/main/java/to/bitkit/repositories/LightningRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -278,8 +278,11 @@ class LightningRepo @Inject constructor(
}.onFailure {
// Cancellation is expected during pull-to-refresh, rethrow per Kotlin best practices
if (it is CancellationException) throw it

Logger.error("Error executing '$operationName'", it, context = TAG)
if (it is PaymentAbortedBeforeSend) {
Logger.debug("Aborted '$operationName' before dispatch", context = TAG)
} else {
Logger.error("Error executing '$operationName'", it, context = TAG)
}
}

private suspend fun setup(
Expand Down Expand Up @@ -1257,13 +1260,24 @@ class LightningRepo @Inject constructor(
suspend fun payInvoice(
bolt11: String,
sats: ULong? = null,
): Result<PaymentId> = payInvoice(bolt11, sats, onBeforeSend = { true })

suspend fun payInvoice(
bolt11: String,
sats: ULong? = null,
onBeforeSend: suspend () -> Boolean,
): Result<PaymentId> = executeWhenNodeRunning("payInvoice") {
waitForUsableChannels()
if (!onBeforeSend()) return@executeWhenNodeRunning Result.failure(PaymentAbortedBeforeSend())
runCatching { lightningService.send(bolt11, sats) }.also {
syncState()
}
}

suspend fun listPaymentsOrNull(): List<PaymentDetails>? = withContext(bgDispatcher) {
lightningService.listPayments()
}

suspend fun waitForUsableChannels() = withContext(bgDispatcher) {
var state = _lightningState.value
if (!state.nodeLifecycleState.canRun()) {
Expand Down Expand Up @@ -2095,6 +2109,7 @@ class NodeConfigNotAppliedError : AppError("Node already running, requested conf
class NodeRunTimeoutError(opName: String) : AppError("Timeout waiting for node to run and execute: '$opName'")
class GetPaymentsError : AppError("It wasn't possible get the payments")
class SyncUnhealthyError : AppError("Wallet sync failed before send")
class PaymentAbortedBeforeSend : AppError("Payment aborted before send")
class LnurlPayInvoiceMismatchError : AppError("The invoice did not match the requested payment. Payment cancelled.")
class PaymentRoutingRefreshTimeoutError : AppError("Timeout waiting for payment routing data refresh")

Expand Down
15 changes: 14 additions & 1 deletion app/src/main/java/to/bitkit/repositories/PendingPaymentRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,27 @@ class PendingPaymentRepo @Inject constructor() {

private val _resolution = MutableSharedFlow<PendingPaymentResolution>(extraBufferCapacity = 1)
val resolution = _resolution.asSharedFlow()
private val lastResolutions = MutableStateFlow<Map<String, PendingPaymentResolution>>(emptyMap())

fun track(paymentHash: String) {
lastResolutions.update { it - paymentHash }
_state.update { it.copy(pendingPayments = it.pendingPayments + paymentHash) }
}

fun isPending(hash: String): Boolean = _state.value.pendingPayments.contains(hash)

suspend fun resolve(resolution: PendingPaymentResolution) {
_state.update { it.copy(pendingPayments = it.pendingPayments - resolution.paymentHash) }
lastResolutions.update { it + (resolution.paymentHash to resolution) }
Comment thread
ovitrif marked this conversation as resolved.
_resolution.emit(resolution)
}

fun consumeResolution(paymentHash: String): PendingPaymentResolution? {
val taken = lastResolutions.value[paymentHash] ?: return null
lastResolutions.update { it - paymentHash }
return taken
}

fun setActiveHash(hash: String?) = _state.update { it.copy(activeHash = hash) }

fun isActive(hash: String): Boolean = _state.value.activeHash == hash
Expand All @@ -48,7 +57,11 @@ class PaymentPendingException(val paymentHash: String) : AppError("Payment pendi
sealed interface PendingPaymentResolution {
val paymentHash: String

data class Success(override val paymentHash: String) : PendingPaymentResolution
data class Success(
override val paymentHash: String,
val amountWithFeeSats: Long? = null,
) : PendingPaymentResolution

data class Failure(
override val paymentHash: String,
val reason: PaymentFailureReason? = null,
Expand Down
Loading
Loading