Skip to content

Commit fb3e685

Browse files
committed
refactor: use CQRS command + handler
1 parent 6fbf7e8 commit fb3e685

File tree

8 files changed

+369
-138
lines changed

8 files changed

+369
-138
lines changed

app/src/main/java/to/bitkit/androidServices/LightningNodeService.kt

Lines changed: 15 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -11,30 +11,21 @@ import kotlinx.coroutines.CoroutineScope
1111
import kotlinx.coroutines.Dispatchers
1212
import kotlinx.coroutines.SupervisorJob
1313
import kotlinx.coroutines.cancel
14-
import kotlinx.coroutines.delay
15-
import kotlinx.coroutines.flow.first
1614
import kotlinx.coroutines.launch
1715
import org.lightningdevkit.ldknode.Event
1816
import to.bitkit.App
1917
import to.bitkit.R
20-
import to.bitkit.data.SettingsData
21-
import to.bitkit.data.SettingsStore
22-
import to.bitkit.models.BITCOIN_SYMBOL
18+
import to.bitkit.domain.commands.NotifyPaymentReceived
19+
import to.bitkit.domain.commands.NotifyPaymentReceivedHandler
2320
import to.bitkit.models.NewTransactionSheetDetails
24-
import to.bitkit.models.NewTransactionSheetDirection
25-
import to.bitkit.models.NewTransactionSheetType
26-
import to.bitkit.models.PrimaryDisplay
27-
import to.bitkit.models.formatToModernDisplay
28-
import to.bitkit.repositories.ActivityRepo
29-
import to.bitkit.repositories.CurrencyRepo
21+
import to.bitkit.models.NotificationState
3022
import to.bitkit.repositories.LightningRepo
3123
import to.bitkit.repositories.WalletRepo
3224
import to.bitkit.services.LdkNodeEventBus
3325
import to.bitkit.ui.MainActivity
3426
import to.bitkit.ui.pushNotification
3527
import to.bitkit.utils.Logger
3628
import javax.inject.Inject
37-
import kotlin.time.Duration.Companion.seconds
3829

3930
@AndroidEntryPoint
4031
class LightningNodeService : Service() {
@@ -51,13 +42,7 @@ class LightningNodeService : Service() {
5142
lateinit var ldkNodeEventBus: LdkNodeEventBus
5243

5344
@Inject
54-
lateinit var settingsStore: SettingsStore
55-
56-
@Inject
57-
lateinit var activityRepo: ActivityRepo
58-
59-
@Inject
60-
lateinit var currencyRepo: CurrencyRepo
45+
lateinit var notifyPaymentReceivedHandler: NotifyPaymentReceivedHandler
6146

6247
override fun onCreate() {
6348
super.onCreate()
@@ -91,62 +76,25 @@ class LightningNodeService : Service() {
9176
}
9277

9378
private suspend fun handleBackgroundEvent(event: Event) {
94-
delay(0.5.seconds) // Small delay to allow lifecycle callbacks to settle after app backgrounding
9579
if (App.currentActivity?.value != null) return
9680

97-
when (event) {
98-
is Event.PaymentReceived -> {
99-
val sats = event.amountMsat / 1000u
100-
showPaymentNotification(sats.toLong(), event.paymentHash, isOnchain = false)
101-
}
102-
103-
is Event.OnchainTransactionReceived -> {
104-
val sats = event.details.amountSats
105-
val shouldShow = activityRepo.shouldShowPaymentReceived(event.txid, sats.toULong())
106-
if (!shouldShow) return
81+
val command = NotifyPaymentReceived.Command.from(event, includeNotification = true) ?: return
10782

108-
showPaymentNotification(sats, event.txid, isOnchain = true)
83+
notifyPaymentReceivedHandler(command).onSuccess { result ->
84+
if (result is NotifyPaymentReceived.Result.ShowNotification) {
85+
if (App.currentActivity?.value != null) return@onSuccess
86+
showPaymentNotification(result.details, result.notification)
10987
}
110-
111-
else -> Unit
11288
}
11389
}
11490

115-
private suspend fun showPaymentNotification(sats: Long, paymentHashOrTxId: String?, isOnchain: Boolean) {
91+
private fun showPaymentNotification(
92+
details: NewTransactionSheetDetails,
93+
notification: NotificationState,
94+
) {
11695
if (App.currentActivity?.value != null) return
117-
118-
val settings = settingsStore.data.first()
119-
val type = if (isOnchain) NewTransactionSheetType.ONCHAIN else NewTransactionSheetType.LIGHTNING
120-
val direction = NewTransactionSheetDirection.RECEIVED
121-
122-
NewTransactionSheetDetails.save(
123-
this,
124-
NewTransactionSheetDetails(type, direction, paymentHashOrTxId, sats)
125-
)
126-
127-
val title = getString(R.string.notification_received_title)
128-
val body = if (settings.showNotificationDetails) {
129-
formatNotificationAmount(sats, settings)
130-
} else {
131-
getString(R.string.notification_received_body_hidden)
132-
}
133-
134-
pushNotification(title, body, context = this)
135-
}
136-
137-
private fun formatNotificationAmount(sats: Long, settings: SettingsData): String {
138-
val converted = currencyRepo.convertSatsToFiat(sats).getOrNull()
139-
140-
val amountText = converted?.let {
141-
val btcDisplay = it.bitcoinDisplay(settings.displayUnit)
142-
if (settings.primaryDisplay == PrimaryDisplay.BITCOIN) {
143-
"${btcDisplay.symbol} ${btcDisplay.value} (${it.symbol}${it.formatted})"
144-
} else {
145-
"${it.symbol}${it.formatted} (${btcDisplay.symbol} ${btcDisplay.value})"
146-
}
147-
} ?: "$BITCOIN_SYMBOL ${sats.formatToModernDisplay()}"
148-
149-
return getString(R.string.notification_received_body_amount, amountText)
96+
NewTransactionSheetDetails.save(this, details)
97+
pushNotification(notification.title, notification.body, context = this)
15098
}
15199

152100
private fun createNotification(
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package to.bitkit.domain.commands
2+
3+
import org.lightningdevkit.ldknode.Event
4+
import to.bitkit.models.NewTransactionSheetDetails
5+
import to.bitkit.models.NotificationState
6+
7+
sealed interface NotifyPaymentReceived {
8+
9+
sealed interface Command : NotifyPaymentReceived {
10+
val sats: Long
11+
val paymentId: String
12+
val includeNotification: Boolean
13+
14+
data class Lightning(
15+
override val sats: Long,
16+
override val paymentId: String,
17+
override val includeNotification: Boolean = false,
18+
) : Command
19+
20+
data class Onchain(
21+
override val sats: Long,
22+
override val paymentId: String,
23+
override val includeNotification: Boolean = false,
24+
) : Command
25+
26+
companion object {
27+
fun from(event: Event, includeNotification: Boolean = false): Command? =
28+
when (event) {
29+
is Event.PaymentReceived -> Lightning(
30+
sats = (event.amountMsat / 1000u).toLong(),
31+
paymentId = event.paymentHash,
32+
includeNotification = includeNotification,
33+
)
34+
35+
is Event.OnchainTransactionReceived -> Onchain(
36+
sats = event.details.amountSats,
37+
paymentId = event.txid,
38+
includeNotification = includeNotification,
39+
)
40+
41+
else -> null
42+
}
43+
}
44+
}
45+
46+
sealed interface Result : NotifyPaymentReceived {
47+
data class ShowSheet(
48+
val details: NewTransactionSheetDetails,
49+
) : Result
50+
51+
data class ShowNotification(
52+
val details: NewTransactionSheetDetails,
53+
val notification: NotificationState,
54+
) : Result
55+
56+
data object Skip : Result
57+
}
58+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package to.bitkit.domain.commands
2+
3+
import android.content.Context
4+
import dagger.hilt.android.qualifiers.ApplicationContext
5+
import kotlinx.coroutines.CoroutineDispatcher
6+
import kotlinx.coroutines.delay
7+
import kotlinx.coroutines.flow.first
8+
import kotlinx.coroutines.withContext
9+
import to.bitkit.R
10+
import to.bitkit.data.SettingsData
11+
import to.bitkit.data.SettingsStore
12+
import to.bitkit.di.IoDispatcher
13+
import to.bitkit.models.BITCOIN_SYMBOL
14+
import to.bitkit.models.NewTransactionSheetDetails
15+
import to.bitkit.models.NewTransactionSheetDirection
16+
import to.bitkit.models.NewTransactionSheetType
17+
import to.bitkit.models.NotificationState
18+
import to.bitkit.models.PrimaryDisplay
19+
import to.bitkit.models.formatToModernDisplay
20+
import to.bitkit.repositories.ActivityRepo
21+
import to.bitkit.repositories.CurrencyRepo
22+
import javax.inject.Inject
23+
import javax.inject.Singleton
24+
25+
@Singleton
26+
class NotifyPaymentReceivedHandler @Inject constructor(
27+
@param:ApplicationContext private val context: Context,
28+
@param:IoDispatcher private val ioDispatcher: CoroutineDispatcher,
29+
private val activityRepo: ActivityRepo,
30+
private val currencyRepo: CurrencyRepo,
31+
private val settingsStore: SettingsStore,
32+
) {
33+
suspend operator fun invoke(
34+
command: NotifyPaymentReceived.Command,
35+
): Result<NotifyPaymentReceived.Result> = withContext(ioDispatcher) {
36+
runCatching {
37+
delay(DELAY_MS)
38+
39+
val shouldShow = when (command) {
40+
is NotifyPaymentReceived.Command.Lightning -> true
41+
is NotifyPaymentReceived.Command.Onchain -> {
42+
activityRepo.shouldShowPaymentReceived(command.paymentId, command.sats.toULong())
43+
}
44+
}
45+
46+
if (!shouldShow) return@runCatching NotifyPaymentReceived.Result.Skip
47+
48+
val details = NewTransactionSheetDetails(
49+
type = when (command) {
50+
is NotifyPaymentReceived.Command.Lightning -> NewTransactionSheetType.LIGHTNING
51+
is NotifyPaymentReceived.Command.Onchain -> NewTransactionSheetType.ONCHAIN
52+
},
53+
direction = NewTransactionSheetDirection.RECEIVED,
54+
paymentHashOrTxId = command.paymentId,
55+
sats = command.sats,
56+
)
57+
58+
if (command.includeNotification) {
59+
val notification = buildNotificationContent(command.sats)
60+
NotifyPaymentReceived.Result.ShowNotification(details, notification)
61+
} else {
62+
NotifyPaymentReceived.Result.ShowSheet(details)
63+
}
64+
}
65+
}
66+
67+
private suspend fun buildNotificationContent(sats: Long): NotificationState {
68+
val settings = settingsStore.data.first()
69+
val title = context.getString(R.string.notification_received_title)
70+
val body = if (settings.showNotificationDetails) {
71+
formatNotificationAmount(sats, settings)
72+
} else {
73+
context.getString(R.string.notification_received_body_hidden)
74+
}
75+
return NotificationState(title, body)
76+
}
77+
78+
private fun formatNotificationAmount(sats: Long, settings: SettingsData): String {
79+
val converted = currencyRepo.convertSatsToFiat(sats).getOrNull()
80+
81+
val amountText = converted?.let {
82+
val btcDisplay = it.bitcoinDisplay(settings.displayUnit)
83+
if (settings.primaryDisplay == PrimaryDisplay.BITCOIN) {
84+
"${btcDisplay.symbol} ${btcDisplay.value} (${it.symbol}${it.formatted})"
85+
} else {
86+
"${it.symbol}${it.formatted} (${btcDisplay.symbol} ${btcDisplay.value})"
87+
}
88+
} ?: "$BITCOIN_SYMBOL ${sats.formatToModernDisplay()}"
89+
90+
return context.getString(R.string.notification_received_body_amount, amountText)
91+
}
92+
93+
companion object {
94+
const val TAG = "NotifyPaymentReceivedHandler"
95+
private const val DELAY_MS = 500L
96+
}
97+
}

app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ class WakeNodeWorker @AssistedInject constructor(
5050
) : CoroutineWorker(appContext, workerParams) {
5151
private val self = this
5252

53+
// TODO extract as global model and turn into data class.
5354
class VisibleNotification(var title: String = "", var body: String = "")
5455

5556
private var bestAttemptContent: VisibleNotification? = VisibleNotification()
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package to.bitkit.models
2+
3+
// TODO should replace WakeNodeWorker.VisibleNotification
4+
data class NotificationState(
5+
val title: String,
6+
val body: String,
7+
)

app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt

Lines changed: 22 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ import to.bitkit.data.SettingsStore
5656
import to.bitkit.data.keychain.Keychain
5757
import to.bitkit.data.resetPin
5858
import to.bitkit.di.BgDispatcher
59+
import to.bitkit.domain.commands.NotifyPaymentReceived
60+
import to.bitkit.domain.commands.NotifyPaymentReceivedHandler
5961
import to.bitkit.env.Env
6062
import to.bitkit.ext.WatchResult
6163
import to.bitkit.ext.amountOnClose
@@ -105,8 +107,10 @@ import javax.inject.Inject
105107
@Suppress("LongParameterList")
106108
@HiltViewModel
107109
class AppViewModel @Inject constructor(
108-
@ApplicationContext private val context: Context,
109-
@BgDispatcher private val bgDispatcher: CoroutineDispatcher,
110+
connectivityRepo: ConnectivityRepo,
111+
healthRepo: HealthRepo,
112+
@param:ApplicationContext private val context: Context,
113+
@param:BgDispatcher private val bgDispatcher: CoroutineDispatcher,
110114
private val keychain: Keychain,
111115
private val lightningRepo: LightningRepo,
112116
private val walletRepo: WalletRepo,
@@ -117,9 +121,8 @@ class AppViewModel @Inject constructor(
117121
private val activityRepo: ActivityRepo,
118122
private val preActivityMetadataRepo: PreActivityMetadataRepo,
119123
private val blocktankRepo: BlocktankRepo,
120-
private val connectivityRepo: ConnectivityRepo,
121-
private val healthRepo: HealthRepo,
122124
private val appUpdaterService: AppUpdaterService,
125+
private val notifyPaymentReceivedHandler: NotifyPaymentReceivedHandler,
123126
) : ViewModel() {
124127
val healthState = healthRepo.healthState
125128

@@ -228,15 +231,7 @@ class AppViewModel @Inject constructor(
228231
runCatching {
229232
when (event) {
230233
is Event.PaymentReceived -> {
231-
showNewTransactionSheet(
232-
NewTransactionSheetDetails(
233-
type = NewTransactionSheetType.LIGHTNING,
234-
direction = NewTransactionSheetDirection.RECEIVED,
235-
paymentHashOrTxId = event.paymentHash,
236-
sats = (event.amountMsat / 1000u).toLong(),
237-
),
238-
event,
239-
)
234+
NotifyPaymentReceived.Command.from(event)?.let { handlePaymentReceived(it, event) }
240235
}
241236

242237
is Event.ChannelReady -> {
@@ -292,22 +287,7 @@ class AppViewModel @Inject constructor(
292287
is Event.PaymentForwarded -> Unit
293288

294289
is Event.OnchainTransactionReceived -> {
295-
val sats = event.details.amountSats
296-
launch(bgDispatcher) {
297-
delay(500)
298-
val shouldShow = activityRepo.shouldShowPaymentReceived(event.txid, sats.toULong())
299-
if (!shouldShow) return@launch
300-
301-
showNewTransactionSheet(
302-
NewTransactionSheetDetails(
303-
type = NewTransactionSheetType.ONCHAIN,
304-
direction = NewTransactionSheetDirection.RECEIVED,
305-
paymentHashOrTxId = event.txid,
306-
sats = sats,
307-
),
308-
event,
309-
)
310-
}
290+
NotifyPaymentReceived.Command.from(event)?.let { handlePaymentReceived(it, event) }
311291
}
312292

313293
is Event.OnchainTransactionConfirmed -> Unit
@@ -326,6 +306,19 @@ class AppViewModel @Inject constructor(
326306
}
327307
}
328308

309+
private fun handlePaymentReceived(
310+
receivedEvent: NotifyPaymentReceived.Command,
311+
originalEvent: Event,
312+
) {
313+
viewModelScope.launch(bgDispatcher) {
314+
notifyPaymentReceivedHandler(receivedEvent).onSuccess { result ->
315+
if (result is NotifyPaymentReceived.Result.ShowSheet) {
316+
showNewTransactionSheet(result.details, originalEvent)
317+
}
318+
}
319+
}
320+
}
321+
329322
// region send
330323

331324
private fun observeSendEvents() {

0 commit comments

Comments
 (0)