Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8296006
chore: create channel ready handler
jvsena42 Feb 17, 2026
37fc97d
fix: handle channel ready
jvsena42 Feb 17, 2026
345fca8
refactor: implement notifyChannelReadyHandler on AppViewModel.kt
jvsena42 Feb 17, 2026
21f9fd0
Merge branch 'master' into fix/channelready-cjit
jvsena42 Feb 18, 2026
bbb9a61
Merge branch 'master' into fix/channelready-cjit
jvsena42 Feb 23, 2026
4994d96
Merge branch 'master' into fix/channelready-cjit
jvsena42 Mar 5, 2026
91cb76c
Merge remote-tracking branch 'origin/master' into fix/channelready-cjit
jvsena42 Jun 11, 2026
cb7669d
fix: deduplicate channel ready notification
jvsena42 Jun 11, 2026
4e5649e
fix: deduplicate regular sent
jvsena42 Jun 12, 2026
73368bf
Merge branch 'master' into fix/channelready-cjit
jvsena42 Jun 12, 2026
b758da9
fix: check duplicated activity notification
jvsena42 Jun 15, 2026
b5d5385
Merge branch 'master' into fix/channelready-cjit
jvsena42 Jun 15, 2026
52fdc8b
Merge branch 'master' into fix/channelready-cjit
jvsena42 Jun 16, 2026
b668182
refactor: unify receive notification formating logic
jvsena42 Jun 16, 2026
af747ed
Merge branch 'master' into fix/channelready-cjit
jvsena42 Jun 16, 2026
e256a08
fix: apply formatting to WakeNodeWorker.onCjitChannelReady
jvsena42 Jun 16, 2026
5ec67b6
chore: update journeys with rich format
jvsena42 Jun 16, 2026
500285d
fix: cjit notification race condition
jvsena42 Jun 16, 2026
73e576f
refactor: inject node service state
jvsena42 Jun 16, 2026
3bcc9ff
fix: return early when the app is in foreground
jvsena42 Jun 16, 2026
10f3322
refactor: rename foreground state class
jvsena42 Jun 17, 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 @@ -20,6 +20,8 @@ import to.bitkit.App
import to.bitkit.R
import to.bitkit.data.CacheStore
import to.bitkit.di.UiDispatcher
import to.bitkit.domain.commands.NotifyChannelReady
import to.bitkit.domain.commands.NotifyChannelReadyHandler
import to.bitkit.domain.commands.NotifyPaymentReceived
import to.bitkit.domain.commands.NotifyPaymentReceivedHandler
import to.bitkit.models.NewTransactionSheetDetails
Expand Down Expand Up @@ -51,6 +53,9 @@ class LightningNodeService : Service() {
@Inject
lateinit var notifyPaymentReceivedHandler: NotifyPaymentReceivedHandler

@Inject
lateinit var notifyChannelReadyHandler: NotifyChannelReadyHandler

@Inject
lateinit var cacheStore: CacheStore

Expand All @@ -66,6 +71,7 @@ class LightningNodeService : Service() {
eventHandler = { event ->
Logger.debug("LDK-node event received in $TAG: ${jsonLogOf(event)}", context = TAG)
handlePaymentReceived(event)
if (event is Event.ChannelReady) handleChannelReady(event)
}
).onSuccess {
walletRepo.setWalletExistsState()
Expand All @@ -86,6 +92,15 @@ class LightningNodeService : Service() {
}
}

private suspend fun handleChannelReady(event: Event.ChannelReady) {
val command = NotifyChannelReady.Command(event = event, includeNotification = true)
notifyChannelReadyHandler(command).onSuccess {
Comment thread
jvsena42 marked this conversation as resolved.
Logger.debug("Channel ready notification result: $it", context = TAG)
if (it !is NotifyChannelReady.Result.ShowNotification) return
showPaymentNotification(it.sheet, it.notification)
}
}

private fun showPaymentNotification(
sheet: NewTransactionSheetDetails,
notification: NotificationDetails,
Expand Down
26 changes: 26 additions & 0 deletions app/src/main/java/to/bitkit/domain/commands/NotifyChannelReady.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package to.bitkit.domain.commands

import org.lightningdevkit.ldknode.Event
import to.bitkit.models.NewTransactionSheetDetails
import to.bitkit.models.NotificationDetails

sealed interface NotifyChannelReady {

data class Command(
val event: Event.ChannelReady,
val includeNotification: Boolean = false,
) : NotifyChannelReady

sealed interface Result : NotifyChannelReady {
data class ShowSheet(
val sheet: NewTransactionSheetDetails,
) : Result

data class ShowNotification(
val sheet: NewTransactionSheetDetails,
val notification: NotificationDetails,
) : Result

data object Skip : Result
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package to.bitkit.domain.commands

import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext
import to.bitkit.R
import to.bitkit.data.SettingsData
import to.bitkit.data.SettingsStore
import to.bitkit.di.IoDispatcher
import to.bitkit.ext.amountOnClose
import to.bitkit.models.BITCOIN_SYMBOL
import to.bitkit.models.NewTransactionSheetDetails
import to.bitkit.models.NewTransactionSheetDirection
import to.bitkit.models.NewTransactionSheetType
import to.bitkit.models.NotificationDetails
import to.bitkit.models.PrimaryDisplay
import to.bitkit.models.formatToModernDisplay
import to.bitkit.repositories.ActivityRepo
import to.bitkit.repositories.BlocktankRepo
import to.bitkit.repositories.CurrencyRepo
import to.bitkit.repositories.LightningRepo
import to.bitkit.utils.Logger
import javax.inject.Inject
import javax.inject.Singleton

@Suppress("LongParameterList")
@Singleton
class NotifyChannelReadyHandler @Inject constructor(
@ApplicationContext private val context: Context,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val lightningRepo: LightningRepo,
private val blocktankRepo: BlocktankRepo,
private val activityRepo: ActivityRepo,
private val currencyRepo: CurrencyRepo,
private val settingsStore: SettingsStore,
) {
companion object {
const val TAG = "NotifyChannelReadyHandler"
}

suspend operator fun invoke(
command: NotifyChannelReady.Command,
): Result<NotifyChannelReady.Result> = withContext(ioDispatcher) {
runCatching {
val channel = lightningRepo.getChannels()
?.find { it.channelId == command.event.channelId }
?: return@runCatching NotifyChannelReady.Result.Skip

val cjitEntry = blocktankRepo.getCjitEntry(channel)
?: return@runCatching NotifyChannelReady.Result.Skip

val sats = channel.amountOnClose.toLong()
activityRepo.insertActivityFromCjit(cjitEntry = cjitEntry, channel = channel)

val details = NewTransactionSheetDetails(
type = NewTransactionSheetType.LIGHTNING,
direction = NewTransactionSheetDirection.RECEIVED,
sats = sats,
)

if (command.includeNotification) {
val notification = buildNotificationContent(sats)
NotifyChannelReady.Result.ShowNotification(details, notification)
} else {
NotifyChannelReady.Result.ShowSheet(details)
}
}.onFailure {
Logger.error("Failed to process channel ready notification", it, context = TAG)
}
}

private suspend fun buildNotificationContent(sats: Long): NotificationDetails {
val settings = settingsStore.data.first()
val title = context.getString(R.string.notification__received__title)
val body = if (settings.showNotificationDetails) {
formatNotificationAmount(sats, settings)
} else {
context.getString(R.string.notification__received__body_hidden)
}
return NotificationDetails(title, body)
}

private fun formatNotificationAmount(sats: Long, settings: SettingsData): String {
val converted = currencyRepo.convertSatsToFiat(sats).getOrNull()

val amountText = converted?.let {
val btcDisplay = it.bitcoinDisplay(settings.displayUnit)
Comment thread
ovitrif marked this conversation as resolved.
Outdated
if (settings.primaryDisplay == PrimaryDisplay.BITCOIN) {
"${btcDisplay.symbol} ${btcDisplay.value} (${it.formattedWithSymbol()})"
} else {
"${it.formattedWithSymbol()} (${btcDisplay.symbol} ${btcDisplay.value})"
}
} ?: "$BITCOIN_SYMBOL ${sats.formatToModernDisplay()}"

return context.getString(R.string.notification__received__body_amount, amountText)
}
}
20 changes: 7 additions & 13 deletions app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,13 @@ import to.bitkit.data.SettingsStore
import to.bitkit.data.keychain.Keychain
import to.bitkit.data.resetPin
import to.bitkit.di.BgDispatcher
import to.bitkit.domain.commands.NotifyChannelReady
import to.bitkit.domain.commands.NotifyChannelReadyHandler
import to.bitkit.domain.commands.NotifyPaymentReceived
import to.bitkit.domain.commands.NotifyPaymentReceivedHandler
import to.bitkit.env.Defaults
import to.bitkit.env.Env
import to.bitkit.ext.WatchResult
import to.bitkit.ext.amountOnClose
import to.bitkit.ext.getClipboardText
import to.bitkit.ext.getSatsPerVByteFor
import to.bitkit.ext.maxSendableSat
Expand Down Expand Up @@ -147,6 +148,7 @@ class AppViewModel @Inject constructor(
private val blocktankRepo: BlocktankRepo,
private val appUpdaterService: AppUpdaterService,
private val notifyPaymentReceivedHandler: NotifyPaymentReceivedHandler,
private val notifyChannelReadyHandler: NotifyChannelReadyHandler,
private val cacheStore: CacheStore,
private val transferRepo: TransferRepo,
private val migrationService: MigrationService,
Expand Down Expand Up @@ -559,18 +561,10 @@ class AppViewModel @Inject constructor(
// region Notifications

private suspend fun notifyChannelReady(event: Event.ChannelReady) {
val channel = lightningRepo.getChannels()?.find { it.channelId == event.channelId }
val cjitEntry = channel?.let { blocktankRepo.getCjitEntry(it) }
if (cjitEntry != null) {
val amount = channel.amountOnClose.toLong()
showTransactionSheet(
NewTransactionSheetDetails(
type = NewTransactionSheetType.LIGHTNING,
direction = NewTransactionSheetDirection.RECEIVED,
sats = amount,
),
)
activityRepo.insertActivityFromCjit(cjitEntry = cjitEntry, channel = channel)
val command = NotifyChannelReady.Command(event = event)
val result = notifyChannelReadyHandler(command).getOrNull()
if (result is NotifyChannelReady.Result.ShowSheet) {
showTransactionSheet(result.sheet)
return
}
toast(
Expand Down
Loading
Loading