-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathNotifyPaymentReceivedHandler.kt
More file actions
143 lines (127 loc) · 5.87 KB
/
NotifyPaymentReceivedHandler.kt
File metadata and controls
143 lines (127 loc) · 5.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
package to.bitkit.domain.commands
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.delay
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.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.models.msatCeilOf
import to.bitkit.repositories.CurrencyRepo
import to.bitkit.utils.Logger
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class NotifyPaymentReceivedHandler @Inject constructor(
@ApplicationContext private val context: Context,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val activityRepo: ActivityRepo,
private val currencyRepo: CurrencyRepo,
private val settingsStore: SettingsStore,
) {
suspend operator fun invoke(
command: NotifyPaymentReceived.Command,
): Result<NotifyPaymentReceived.Result> = withContext(ioDispatcher) {
runCatching {
val shouldShow = when (command) {
is NotifyPaymentReceived.Command.Lightning -> shouldShowLightning(command)
is NotifyPaymentReceived.Command.Onchain -> shouldShowOnchain(command)
}
if (!shouldShow) return@runCatching NotifyPaymentReceived.Result.Skip
val details = buildSheetDetails(command)
if (command.includeNotification) {
val notification = buildNotificationContent(details.sats)
NotifyPaymentReceived.Result.ShowNotification(details, notification)
} else {
NotifyPaymentReceived.Result.ShowSheet(details)
}
}.onFailure { e ->
Logger.error("Failed to process payment notification", e, context = TAG)
}
}
private suspend fun shouldShowLightning(command: NotifyPaymentReceived.Command.Lightning): Boolean {
val paymentId = command.event.paymentId ?: return false
delay(DELAY_FOR_ACTIVITY_SYNC_MS)
if (activityRepo.isActivitySeen(paymentId)) return false
activityRepo.markActivityAsSeen(paymentId)
return true
}
private suspend fun shouldShowOnchain(command: NotifyPaymentReceived.Command.Onchain): Boolean {
activityRepo.handleOnchainTransactionReceived(command.event.txid, command.event.details)
if (command.event.details.amountSats <= 0) return false
delay(DELAY_FOR_ACTIVITY_SYNC_MS)
val shouldShowSheet = retryShouldShowReceivedSheet(
command.event.txid,
command.event.details.amountSats.toULong(),
)
if (shouldShowSheet) {
activityRepo.markOnchainActivityAsSeen(command.event.txid)
}
return shouldShowSheet
}
private suspend fun retryShouldShowReceivedSheet(txid: String, amountSats: ULong): Boolean {
repeat(MAX_RETRIES) {
if (activityRepo.shouldShowReceivedSheet(txid, amountSats)) return true
delay(RETRY_DELAY_MS)
}
return activityRepo.shouldShowReceivedSheet(txid, amountSats)
}
private fun buildSheetDetails(command: NotifyPaymentReceived.Command) = NewTransactionSheetDetails(
type = when (command) {
is NotifyPaymentReceived.Command.Lightning -> NewTransactionSheetType.LIGHTNING
is NotifyPaymentReceived.Command.Onchain -> NewTransactionSheetType.ONCHAIN
},
direction = NewTransactionSheetDirection.RECEIVED,
paymentHashOrTxId = when (command) {
is NotifyPaymentReceived.Command.Lightning -> command.event.paymentHash
is NotifyPaymentReceived.Command.Onchain -> command.event.txid
},
sats = when (command) {
is NotifyPaymentReceived.Command.Lightning -> msatCeilOf(command.event.amountMsat).toLong()
is NotifyPaymentReceived.Command.Onchain -> command.event.details.amountSats
},
)
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)
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)
}
companion object {
const val TAG = "NotifyPaymentReceivedHandler"
/**
* Delay after syncing onchain transaction to allow the database to fully process
* the transaction before checking for RBF replacement or channel closure.
*/
private const val DELAY_FOR_ACTIVITY_SYNC_MS = 500L
private const val RETRY_DELAY_MS = 300L
private const val MAX_RETRIES = 3
}
}