From 81a5c37a7d72a0db63728d6fd504e538e3e1465f Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 15 Dec 2025 10:46:12 -0300 Subject: [PATCH 01/23] refactor: remove self variable --- .../main/java/to/bitkit/fcm/WakeNodeWorker.kt | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt b/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt index 53a517296d..89450d074f 100644 --- a/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt +++ b/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt @@ -52,8 +52,6 @@ class WakeNodeWorker @AssistedInject constructor( private val settingsStore: SettingsStore, private val cacheStore: CacheStore, ) : CoroutineWorker(appContext, workerParams) { - private val self = this - private var bestAttemptContent: NotificationDetails? = null private var notificationType: BlocktankNotificationType? = null @@ -83,7 +81,7 @@ class WakeNodeWorker @AssistedInject constructor( lightningRepo.connectToTrustedPeers() // Once node is started, handle the manual channel opening if needed - if (self.notificationType == orderPaymentConfirmed) { + if (notificationType == orderPaymentConfirmed) { val orderId = (notificationPayload?.get("orderId") as? JsonPrimitive)?.contentOrNull if (orderId == null) { @@ -94,11 +92,11 @@ class WakeNodeWorker @AssistedInject constructor( coreService.blocktank.open(orderId = orderId) } catch (e: Exception) { Logger.error("failed to open channel", e) - self.bestAttemptContent = NotificationDetails( + bestAttemptContent = NotificationDetails( title = appContext.getString(R.string.notification_channel_open_failed_title), body = e.message ?: appContext.getString(R.string.notification_unknown_error), ) - self.deliver() + deliver() } } } @@ -108,12 +106,12 @@ class WakeNodeWorker @AssistedInject constructor( } catch (e: Exception) { val reason = e.message ?: appContext.getString(R.string.notification_unknown_error) - self.bestAttemptContent = NotificationDetails( + bestAttemptContent = NotificationDetails( title = appContext.getString(R.string.notification_lightning_error_title), body = reason, ) Logger.error("Lightning error", e) - self.deliver() + deliver() return Result.failure(workDataOf("Reason" to reason)) } @@ -130,7 +128,7 @@ class WakeNodeWorker @AssistedInject constructor( is Event.PaymentReceived -> onPaymentReceived(event, showDetails, hiddenBody) is Event.ChannelPending -> { - self.bestAttemptContent = NotificationDetails( + bestAttemptContent = NotificationDetails( title = appContext.getString(R.string.notification_channel_opened_title), body = appContext.getString(R.string.notification_channel_pending_body), ) @@ -141,13 +139,13 @@ class WakeNodeWorker @AssistedInject constructor( is Event.ChannelClosed -> onChannelClosed(event) is Event.PaymentFailed -> { - self.bestAttemptContent = NotificationDetails( + bestAttemptContent = NotificationDetails( title = appContext.getString(R.string.notification_payment_failed_title), body = "⚡ ${event.reason}", ) - if (self.notificationType == wakeToTimeout) { - self.deliver() + if (notificationType == wakeToTimeout) { + deliver() } } @@ -156,7 +154,7 @@ class WakeNodeWorker @AssistedInject constructor( } private suspend fun onChannelClosed(event: Event.ChannelClosed) { - self.bestAttemptContent = when (self.notificationType) { + bestAttemptContent = when (notificationType) { mutualClose -> NotificationDetails( title = appContext.getString(R.string.notification_channel_closed_title), body = appContext.getString(R.string.notification_channel_closed_mutual_body), @@ -173,7 +171,7 @@ class WakeNodeWorker @AssistedInject constructor( ) } - self.deliver() + deliver() } private suspend fun onPaymentReceived( @@ -196,8 +194,8 @@ class WakeNodeWorker @AssistedInject constructor( title = appContext.getString(R.string.notification_received_title), body = content, ) - if (self.notificationType == incomingHtlc) { - self.deliver() + if (notificationType == incomingHtlc) { + deliver() } } @@ -207,8 +205,8 @@ class WakeNodeWorker @AssistedInject constructor( hiddenBody: String, ) { val viaNewChannel = appContext.getString(R.string.notification_via_new_channel_body) - if (self.notificationType == cjitPaymentArrived) { - self.bestAttemptContent = NotificationDetails( + if (notificationType == cjitPaymentArrived) { + bestAttemptContent = NotificationDetails( title = appContext.getString(R.string.notification_received_title), body = viaNewChannel, ) @@ -216,7 +214,7 @@ class WakeNodeWorker @AssistedInject constructor( lightningRepo.getChannels()?.find { it.channelId == event.channelId }?.let { channel -> val sats = channel.amountOnClose val content = if (showDetails) "$BITCOIN_SYMBOL $sats" else hiddenBody - self.bestAttemptContent = NotificationDetails( + bestAttemptContent = NotificationDetails( title = content, body = viaNewChannel, ) @@ -233,13 +231,13 @@ class WakeNodeWorker @AssistedInject constructor( activityRepo.insertActivityFromCjit(cjitEntry = cjitEntry, channel = channel) } } - } else if (self.notificationType == orderPaymentConfirmed) { - self.bestAttemptContent = NotificationDetails( + } else if (notificationType == orderPaymentConfirmed) { + bestAttemptContent = NotificationDetails( title = appContext.getString(R.string.notification_channel_opened_title), body = appContext.getString(R.string.notification_channel_ready_body), ) } - self.deliver() + deliver() } private suspend fun deliver() { From 2a77012c6b46048b58d2eb06c3de8f2030ab5523 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 15 Dec 2025 10:52:52 -0300 Subject: [PATCH 02/23] refactor: remove repeated method --- app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt b/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt index 89450d074f..c31b6edfe0 100644 --- a/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt +++ b/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt @@ -78,7 +78,6 @@ class WakeNodeWorker @AssistedInject constructor( timeout = timeout, eventHandler = { event -> handleLdkEvent(event) } ) - lightningRepo.connectToTrustedPeers() // Once node is started, handle the manual channel opening if needed if (notificationType == orderPaymentConfirmed) { From 05b2a37f7b0578c6bd812e3432426a376592130a Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 15 Dec 2025 11:06:07 -0300 Subject: [PATCH 03/23] chore: add tag parameter to logs --- app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt b/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt index c31b6edfe0..7672f23c0f 100644 --- a/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt +++ b/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt @@ -15,6 +15,7 @@ import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.contentOrNull import kotlinx.serialization.json.jsonObject import org.lightningdevkit.ldknode.Event +import to.bitkit.App import to.bitkit.R import to.bitkit.data.CacheStore import to.bitkit.data.SettingsStore @@ -61,15 +62,15 @@ class WakeNodeWorker @AssistedInject constructor( private val deliverSignal = CompletableDeferred() override suspend fun doWork(): Result { - Logger.debug("Node wakeup from notification…") + Logger.debug("Node wakeup from notification…", context = TAG) notificationType = workerParams.inputData.getString("type")?.let { BlocktankNotificationType.valueOf(it) } notificationPayload = workerParams.inputData.getString("payload")?.let { runCatching { json.parseToJsonElement(it).jsonObject }.getOrNull() } - Logger.debug("${this::class.simpleName} notification type: $notificationType") - Logger.debug("${this::class.simpleName} notification payload: $notificationPayload") + Logger.debug("$TAG notification type: $notificationType", context = TAG) + Logger.debug("$TAG notification payload: $notificationPayload", context = TAG) try { withPerformanceLogging { @@ -249,4 +250,8 @@ class WakeNodeWorker @AssistedInject constructor( deliverSignal.complete(Unit) } + + companion object { + private const val TAG = "WakeNodeWorker" + } } From 7b1019c19dd9ca991ab7a0d85e45586ca8626c6c Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 15 Dec 2025 11:07:12 -0300 Subject: [PATCH 04/23] chore: add tag parameter to logs --- app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt b/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt index 7672f23c0f..a77225e8ce 100644 --- a/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt +++ b/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt @@ -85,13 +85,13 @@ class WakeNodeWorker @AssistedInject constructor( val orderId = (notificationPayload?.get("orderId") as? JsonPrimitive)?.contentOrNull if (orderId == null) { - Logger.error("Missing orderId") + Logger.error("Missing orderId", context = TAG) } else { try { - Logger.info("Open channel request for order $orderId") + Logger.info("Open channel request for order $orderId", context = TAG) coreService.blocktank.open(orderId = orderId) } catch (e: Exception) { - Logger.error("failed to open channel", e) + Logger.error("failed to open channel", e, context = TAG) bestAttemptContent = NotificationDetails( title = appContext.getString(R.string.notification_channel_open_failed_title), body = e.message ?: appContext.getString(R.string.notification_unknown_error), @@ -110,7 +110,7 @@ class WakeNodeWorker @AssistedInject constructor( title = appContext.getString(R.string.notification_lightning_error_title), body = reason, ) - Logger.error("Lightning error", e) + Logger.error("Lightning error", e, context = TAG) deliver() return Result.failure(workDataOf("Reason" to reason)) @@ -245,7 +245,7 @@ class WakeNodeWorker @AssistedInject constructor( bestAttemptContent?.run { appContext.pushNotification(title, body) - Logger.info("Delivered notification") + Logger.info("Delivered notification", context = TAG) } deliverSignal.complete(Unit) From 4982d509ff2be69feeb52aaaf799677be104ccc7 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 15 Dec 2025 11:08:06 -0300 Subject: [PATCH 05/23] refactor: replace coreService.blocktank.open() with blocktankRepo.openChannel() --- app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt b/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt index a77225e8ce..69a5fb79b9 100644 --- a/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt +++ b/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt @@ -87,11 +87,9 @@ class WakeNodeWorker @AssistedInject constructor( if (orderId == null) { Logger.error("Missing orderId", context = TAG) } else { - try { - Logger.info("Open channel request for order $orderId", context = TAG) - coreService.blocktank.open(orderId = orderId) - } catch (e: Exception) { - Logger.error("failed to open channel", e, context = TAG) + Logger.info("Open channel request for order $orderId", context = TAG) + blocktankRepo.openChannel(orderId).onFailure { e -> + Logger.error("Failed to open channel", e, context = TAG) bestAttemptContent = NotificationDetails( title = appContext.getString(R.string.notification_channel_open_failed_title), body = e.message ?: appContext.getString(R.string.notification_unknown_error), From c7425eef39af4694a93f052098b725ce65a23ccc Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 15 Dec 2025 11:09:14 -0300 Subject: [PATCH 06/23] fix: Add foreground check before stopping node in deliver() --- app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt b/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt index 69a5fb79b9..b63677ec32 100644 --- a/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt +++ b/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt @@ -239,7 +239,14 @@ class WakeNodeWorker @AssistedInject constructor( } private suspend fun deliver() { - lightningRepo.stop() + // Only stop node if app is not in foreground + // LightningNodeService will keep node running in background when notifications are enabled + if (App.currentActivity?.value == null) { + Logger.debug("App in background, stopping node after notification delivery", context = TAG) + lightningRepo.stop() + } else { + Logger.debug("App in foreground, keeping node running", context = TAG) + } bestAttemptContent?.run { appContext.pushNotification(title, body) From d026b47ae98935c6c1f1ddaba5bf5676e29632af Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 15 Dec 2025 11:14:19 -0300 Subject: [PATCH 07/23] chore: lint --- app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt b/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt index b63677ec32..d407fcbe17 100644 --- a/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt +++ b/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt @@ -35,7 +35,6 @@ import to.bitkit.models.NotificationDetails import to.bitkit.repositories.ActivityRepo import to.bitkit.repositories.BlocktankRepo import to.bitkit.repositories.LightningRepo -import to.bitkit.services.CoreService import to.bitkit.ui.pushNotification import to.bitkit.utils.Logger import to.bitkit.utils.withPerformanceLogging @@ -46,7 +45,6 @@ import kotlin.time.Duration.Companion.minutes class WakeNodeWorker @AssistedInject constructor( @Assisted private val appContext: Context, @Assisted private val workerParams: WorkerParameters, - private val coreService: CoreService, private val lightningRepo: LightningRepo, private val blocktankRepo: BlocktankRepo, private val activityRepo: ActivityRepo, From 4a082971b30d4357b628afdc94483423d8efa2f7 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 17 Dec 2025 08:27:26 -0300 Subject: [PATCH 08/23] chore: add context tag --- app/src/main/java/to/bitkit/fcm/FcmService.kt | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/to/bitkit/fcm/FcmService.kt b/app/src/main/java/to/bitkit/fcm/FcmService.kt index 4992e1758c..59e08ba5dc 100644 --- a/app/src/main/java/to/bitkit/fcm/FcmService.kt +++ b/app/src/main/java/to/bitkit/fcm/FcmService.kt @@ -46,7 +46,7 @@ class FcmService : FirebaseMessagingService() { * Act on received messages. [Debug](https://goo.gl/39bRNJ) */ override fun onMessageReceived(message: RemoteMessage) { - Logger.debug("New FCM at: ${Date(message.sentTime)}") + Logger.debug("New FCM at: ${Date(message.sentTime)}", context = TAG) message.notification?.run { Logger.debug("FCM title: $title") @@ -63,7 +63,7 @@ class FcmService : FirebaseMessagingService() { } isEncryptedNotification }.getOrElse { - Logger.error("Failed to read encrypted notification payload", it) + Logger.error("Failed to read encrypted notification payload", it, context = TAG) // Let the node to spin up and handle incoming events true } @@ -90,21 +90,21 @@ class FcmService : FirebaseMessagingService() { } private fun handleNow(data: Map) { - Logger.warn("FCM handler not implemented for: $data") + Logger.warn("FCM handler not implemented for: $data", context = TAG) } private fun decryptPayload(response: EncryptedNotification) { val ciphertext = runCatching { response.cipher.fromBase64() }.getOrElse { - Logger.error("Failed to decode cipher", it) + Logger.error("Failed to decode cipher", it, context = TAG) return } val privateKey = runCatching { keychain.load(Keychain.Key.PUSH_NOTIFICATION_PRIVATE_KEY.name)!! }.getOrElse { - Logger.error("Missing PUSH_NOTIFICATION_PRIVATE_KEY", it) + Logger.error("Missing PUSH_NOTIFICATION_PRIVATE_KEY", it, context = TAG) return } val password = runCatching { crypto.generateSharedSecret(privateKey, response.publicKey, DERIVATION_NAME) }.getOrElse { - Logger.error("Failed to generate shared secret", it) + Logger.error("Failed to generate shared secret", it, context = TAG) return } @@ -114,20 +114,20 @@ class FcmService : FirebaseMessagingService() { ) val decoded = decrypted.decodeToString() - Logger.debug("Decrypted payload: $decoded") + Logger.debug("Decrypted payload: $decoded", context = TAG) val (payload, type) = runCatching { json.decodeFromString(decoded) }.getOrElse { - Logger.error("Failed to decode decrypted data", it) + Logger.error("Failed to decode decrypted data", it, context = TAG) return } if (payload == null) { - Logger.error("Missing payload") + Logger.error("Missing payload", context = TAG) return } if (type == null) { - Logger.error("Missing type") + Logger.error("Missing type", context = TAG) return } From 51c932674c7be45f5b80660de53997bc8055f984 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 17 Dec 2025 09:07:51 -0300 Subject: [PATCH 09/23] chore: clean log --- app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt b/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt index d407fcbe17..5cfc8c228b 100644 --- a/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt +++ b/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt @@ -67,8 +67,8 @@ class WakeNodeWorker @AssistedInject constructor( runCatching { json.parseToJsonElement(it).jsonObject }.getOrNull() } - Logger.debug("$TAG notification type: $notificationType", context = TAG) - Logger.debug("$TAG notification payload: $notificationPayload", context = TAG) + Logger.debug("notification type: $notificationType", context = TAG) + Logger.debug("notification payload: $notificationPayload", context = TAG) try { withPerformanceLogging { From 17a57bdab8d03f60fc3b10d51f17cd41f6bef359 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 17 Dec 2025 09:58:13 -0300 Subject: [PATCH 10/23] fix: clean FCM token on wallet wipe --- app/src/main/java/to/bitkit/usecases/WipeWalletUseCase.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/usecases/WipeWalletUseCase.kt b/app/src/main/java/to/bitkit/usecases/WipeWalletUseCase.kt index a836be49b0..024cc76037 100644 --- a/app/src/main/java/to/bitkit/usecases/WipeWalletUseCase.kt +++ b/app/src/main/java/to/bitkit/usecases/WipeWalletUseCase.kt @@ -1,5 +1,6 @@ package to.bitkit.usecases +import com.google.firebase.messaging.FirebaseMessaging import to.bitkit.data.AppDb import to.bitkit.data.CacheStore import to.bitkit.data.SettingsStore @@ -27,7 +28,8 @@ class WipeWalletUseCase @Inject constructor( private val blocktankRepo: BlocktankRepo, private val activityRepo: ActivityRepo, private val lightningRepo: LightningRepo, -) { + private val firebaseMessaging: FirebaseMessaging, + ) { @Suppress("TooGenericExceptionCaught") suspend operator fun invoke( walletIndex: Int = 0, @@ -39,6 +41,7 @@ class WipeWalletUseCase @Inject constructor( backupRepo.reset() keychain.wipe() + firebaseMessaging.deleteToken() coreService.wipeData() db.clearAllTables() From 50c8b2f5c799248966a2c97083672695d6a86614 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 17 Dec 2025 10:28:54 -0300 Subject: [PATCH 11/23] fix: add empty token validation --- app/src/main/java/to/bitkit/repositories/LightningRepo.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt index 5163ca0e92..81a17019a4 100644 --- a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt @@ -872,6 +872,8 @@ class LightningRepo @Inject constructor( val token = token ?: firebaseMessaging.token.await() val cachedToken = keychain.loadString(Keychain.Key.PUSH_NOTIFICATION_TOKEN.name) + requireNotNull(token.takeIf { it.isNotEmpty() }) + if (cachedToken == token) { Logger.debug("Skipped registering for notifications, current device token already registered") return@executeWhenNodeRunning Result.success(Unit) From 34b10db67dd8fc83df16d116c4f2c8c90239ab1a Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 17 Dec 2025 14:27:25 -0300 Subject: [PATCH 12/23] fix: isProduction flag --- app/src/main/java/to/bitkit/services/LspNotificationsService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/services/LspNotificationsService.kt b/app/src/main/java/to/bitkit/services/LspNotificationsService.kt index 7100389b42..d29e8b95bf 100644 --- a/app/src/main/java/to/bitkit/services/LspNotificationsService.kt +++ b/app/src/main/java/to/bitkit/services/LspNotificationsService.kt @@ -52,7 +52,7 @@ class LspNotificationsService @Inject constructor( isoTimestamp = "$timestamp", signature = signature, customUrl = Env.blocktankNotificationApiUrl, - isProduction = null, + isProduction = !Env.isDebug, ) } From 9ffee5415b3297ee8cd2653a7257caae38ae362d Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 17 Dec 2025 14:28:32 -0300 Subject: [PATCH 13/23] fix: set work as important to the user --- app/src/main/java/to/bitkit/fcm/FcmService.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/to/bitkit/fcm/FcmService.kt b/app/src/main/java/to/bitkit/fcm/FcmService.kt index 59e08ba5dc..b9b70c466f 100644 --- a/app/src/main/java/to/bitkit/fcm/FcmService.kt +++ b/app/src/main/java/to/bitkit/fcm/FcmService.kt @@ -3,6 +3,7 @@ package to.bitkit.fcm import android.os.Bundle import androidx.core.os.toPersistableBundle import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.OutOfQuotaPolicy import androidx.work.WorkManager import androidx.work.workDataOf import com.google.firebase.messaging.FirebaseMessagingService @@ -83,6 +84,7 @@ class FcmService : FirebaseMessagingService() { "payload" to notificationPayload?.toString(), ) ) + .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) .build() WorkManager.getInstance(this) .beginWith(work) From da64e09980f3ff574a60dd8e53f219662f038b15 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 17 Dec 2025 14:29:56 -0300 Subject: [PATCH 14/23] fix: close activities gracefully without force-stopping the app --- .../java/to/bitkit/androidServices/LightningNodeService.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/androidServices/LightningNodeService.kt b/app/src/main/java/to/bitkit/androidServices/LightningNodeService.kt index 1e51625d59..990838e4fc 100644 --- a/app/src/main/java/to/bitkit/androidServices/LightningNodeService.kt +++ b/app/src/main/java/to/bitkit/androidServices/LightningNodeService.kt @@ -136,8 +136,8 @@ class LightningNodeService : Service() { when (intent?.action) { ACTION_STOP_SERVICE_AND_APP -> { Logger.debug("ACTION_STOP_SERVICE_AND_APP detected", context = TAG) - // Close all activities - App.currentActivity?.value?.finishAndRemoveTask() + // Close activities gracefully without force-stopping the app + App.currentActivity?.value?.finishAffinity() // Stop the service stopSelf() return START_NOT_STICKY From a38eb45cf0b14881d547ea20c168972d78cc6dac Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 18 Dec 2025 09:04:23 -0300 Subject: [PATCH 15/23] chore: log --- app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt b/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt index 5cfc8c228b..bf600a671a 100644 --- a/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt +++ b/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt @@ -70,6 +70,10 @@ class WakeNodeWorker @AssistedInject constructor( Logger.debug("notification type: $notificationType", context = TAG) Logger.debug("notification payload: $notificationPayload", context = TAG) + if (notificationType == null) { + Logger.warn("Notification type is null, proceeding with node wake", context = TAG) + } + try { withPerformanceLogging { lightningRepo.start( From 9dcf16fed68b744debaed027f4e0404b8f061bbb Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 19 Dec 2025 06:54:34 -0300 Subject: [PATCH 16/23] fix: wait for node run before open order --- app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt b/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt index 59fa2ebc0e..ee8ec95871 100644 --- a/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt @@ -33,6 +33,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout import org.lightningdevkit.ldknode.ChannelDetails import to.bitkit.async.ServiceQueue import to.bitkit.data.CacheStore @@ -52,6 +53,7 @@ import javax.inject.Named import javax.inject.Singleton import kotlin.math.ceil import kotlin.time.Duration +import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds @Singleton @@ -282,6 +284,11 @@ class BlocktankRepo @Inject constructor( suspend fun openChannel(orderId: String): Result = withContext(bgDispatcher) { try { Logger.debug("Opening channel for order: '$orderId'", context = TAG) + + withTimeout(1.minutes) { + lightningRepo.lightningState.first { it.nodeStatus?.isRunning ?: false } + } + val order = coreService.blocktank.open(orderId) // Update the order in state From fcb5564b02f368008ab653f94882655cc5767a32 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 19 Dec 2025 07:22:18 -0300 Subject: [PATCH 17/23] test: update test --- app/src/test/java/to/bitkit/usecases/WipeWalletUseCaseTest.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/test/java/to/bitkit/usecases/WipeWalletUseCaseTest.kt b/app/src/test/java/to/bitkit/usecases/WipeWalletUseCaseTest.kt index cb6c8dfab7..d7bc27af86 100644 --- a/app/src/test/java/to/bitkit/usecases/WipeWalletUseCaseTest.kt +++ b/app/src/test/java/to/bitkit/usecases/WipeWalletUseCaseTest.kt @@ -1,5 +1,6 @@ package to.bitkit.usecases +import com.google.firebase.messaging.FirebaseMessaging import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -33,6 +34,7 @@ class WipeWalletUseCaseTest : BaseUnitTest() { private val blocktankRepo = mock() private val activityRepo = mock() private val lightningRepo = mock() + private val firebaseMessaging = mock() private lateinit var sut: WipeWalletUseCase @@ -56,6 +58,7 @@ class WipeWalletUseCaseTest : BaseUnitTest() { blocktankRepo = blocktankRepo, activityRepo = activityRepo, lightningRepo = lightningRepo, + firebaseMessaging = firebaseMessaging, ) } From 81c6bbd507f13ae3f7047fa9729bbe29ad7329cc Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 19 Dec 2025 08:00:28 -0300 Subject: [PATCH 18/23] chore: remove wait for not running, because openChannel is already called in a pooling method --- app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt b/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt index ee8ec95871..59fa2ebc0e 100644 --- a/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt @@ -33,7 +33,6 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import kotlinx.coroutines.withTimeout import org.lightningdevkit.ldknode.ChannelDetails import to.bitkit.async.ServiceQueue import to.bitkit.data.CacheStore @@ -53,7 +52,6 @@ import javax.inject.Named import javax.inject.Singleton import kotlin.math.ceil import kotlin.time.Duration -import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds @Singleton @@ -284,11 +282,6 @@ class BlocktankRepo @Inject constructor( suspend fun openChannel(orderId: String): Result = withContext(bgDispatcher) { try { Logger.debug("Opening channel for order: '$orderId'", context = TAG) - - withTimeout(1.minutes) { - lightningRepo.lightningState.first { it.nodeStatus?.isRunning ?: false } - } - val order = coreService.blocktank.open(orderId) // Update the order in state From fd11394ad6061a6af591a0c06aaef934c7d1b314 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 19 Dec 2025 08:04:02 -0300 Subject: [PATCH 19/23] chore: lint --- app/src/main/java/to/bitkit/usecases/WipeWalletUseCase.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/usecases/WipeWalletUseCase.kt b/app/src/main/java/to/bitkit/usecases/WipeWalletUseCase.kt index 024cc76037..12025db7b2 100644 --- a/app/src/main/java/to/bitkit/usecases/WipeWalletUseCase.kt +++ b/app/src/main/java/to/bitkit/usecases/WipeWalletUseCase.kt @@ -29,7 +29,7 @@ class WipeWalletUseCase @Inject constructor( private val activityRepo: ActivityRepo, private val lightningRepo: LightningRepo, private val firebaseMessaging: FirebaseMessaging, - ) { +) { @Suppress("TooGenericExceptionCaught") suspend operator fun invoke( walletIndex: Int = 0, From 13dc30fe73fc8dccf8fc28dd7cb60b8270b2ced9 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 19 Dec 2025 14:17:16 -0300 Subject: [PATCH 20/23] fix:race condition when click in the push notification --- app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt b/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt index bf600a671a..42193d0a41 100644 --- a/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt +++ b/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt @@ -8,6 +8,7 @@ import androidx.work.workDataOf import dagger.assisted.Assisted import dagger.assisted.AssistedInject import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first import kotlinx.coroutines.withTimeout import kotlinx.serialization.json.JsonObject @@ -241,6 +242,15 @@ class WakeNodeWorker @AssistedInject constructor( } private suspend fun deliver() { + // Send notification first + bestAttemptContent?.run { + appContext.pushNotification(title, body) + Logger.info("Delivered notification", context = TAG) + } + + // Delay briefly to allow app to come to foreground if user clicked notification + delay(500) + // Only stop node if app is not in foreground // LightningNodeService will keep node running in background when notifications are enabled if (App.currentActivity?.value == null) { @@ -250,11 +260,6 @@ class WakeNodeWorker @AssistedInject constructor( Logger.debug("App in foreground, keeping node running", context = TAG) } - bestAttemptContent?.run { - appContext.pushNotification(title, body) - Logger.info("Delivered notification", context = TAG) - } - deliverSignal.complete(Unit) } From 04016944b7134839bfd0ec16ce101a15508eb6e0 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 19 Dec 2025 14:48:17 -0300 Subject: [PATCH 21/23] fix: increase delay --- app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt b/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt index 42193d0a41..19fc7bdc63 100644 --- a/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt +++ b/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt @@ -40,6 +40,7 @@ import to.bitkit.ui.pushNotification import to.bitkit.utils.Logger import to.bitkit.utils.withPerformanceLogging import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds @Suppress("LongParameterList") @HiltWorker @@ -249,7 +250,7 @@ class WakeNodeWorker @AssistedInject constructor( } // Delay briefly to allow app to come to foreground if user clicked notification - delay(500) + delay(1.seconds) // Only stop node if app is not in foreground // LightningNodeService will keep node running in background when notifications are enabled From 06ebba8901cc15c37226de07359f657a590af898 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 19 Dec 2025 14:49:05 -0300 Subject: [PATCH 22/23] chore: improve token error log --- app/src/main/java/to/bitkit/repositories/LightningRepo.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt index 6a14854f58..bbde9cefb1 100644 --- a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt @@ -869,7 +869,7 @@ class LightningRepo @Inject constructor( val token = token ?: firebaseMessaging.token.await() val cachedToken = keychain.loadString(Keychain.Key.PUSH_NOTIFICATION_TOKEN.name) - requireNotNull(token.takeIf { it.isNotEmpty() }) + require(token.isNotEmpty()) { "FCM token is empty or null" } if (cachedToken == token) { Logger.debug("Skipped registering for notifications, current device token already registered") From 9544954fc30e1d273c1774d785e2a48d134aa6ae Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 19 Dec 2025 14:50:20 -0300 Subject: [PATCH 23/23] chore: add context to logs --- app/src/main/java/to/bitkit/fcm/FcmService.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/to/bitkit/fcm/FcmService.kt b/app/src/main/java/to/bitkit/fcm/FcmService.kt index b9b70c466f..a9ab62d452 100644 --- a/app/src/main/java/to/bitkit/fcm/FcmService.kt +++ b/app/src/main/java/to/bitkit/fcm/FcmService.kt @@ -50,13 +50,13 @@ class FcmService : FirebaseMessagingService() { Logger.debug("New FCM at: ${Date(message.sentTime)}", context = TAG) message.notification?.run { - Logger.debug("FCM title: $title") - Logger.debug("FCM body: $body") + Logger.debug("FCM title: $title", context = TAG) + Logger.debug("FCM body: $body", context = TAG) sendNotification(title, body, Bundle(message.data.toPersistableBundle())) } if (message.data.isNotEmpty()) { - Logger.debug("FCM data: ${message.data}") + Logger.debug("FCM data: ${message.data}", context = TAG) val shouldSchedule = runCatching { val isEncryptedNotification = message.data.tryAs {