Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-02 16:35:29 +05:00
parent 40893754c5
commit b319350dd5
5 changed files with 299 additions and 0 deletions

View file

@ -0,0 +1,42 @@
package com.tangem.tap.common.pushes
import android.net.Uri
import androidx.core.net.toUri
import com.tangem.common.routing.DeepLinkRoute
import com.tangem.common.routing.deeplink.PayloadToDeeplinkConverter
import com.tangem.utils.extensions.uriValidate
import javax.inject.Inject
/**
* Routes pushes received while the app is running to the matching in-app handler.
*
* Converts the push payload to a deeplink (via [PayloadToDeeplinkConverter]) and routes by its
* [host][Uri.getHost] the same routing key [DeepLinkFactory][com.tangem.tap.routing.utils.DeepLinkFactory] uses
* for tapped deeplinks. Handlers receive the deeplink query params (not the raw payload), so both flat-key and
* `deeplink`-style payloads are handled uniformly. Each handler owns its own reaction; add a `when` branch per
* push type as new in-app reactions appear.
*/
internal class PushMessageHandler @Inject constructor(
private val tokenDetailsPushHandler: TokenDetailsPushHandler,
) {
fun onMessageReceived(data: Map<String, String>) {
val deeplink = PayloadToDeeplinkConverter.convert(data)?.toUri() ?: return
val queryParams = deeplink.getQueryParams()
when (deeplink.host) {
DeepLinkRoute.TokenDetails.host -> tokenDetailsPushHandler.handle(queryParams)
else -> Unit
}
}
private fun Uri.getQueryParams(): Map<String, String> {
val params = mutableMapOf<String, String>()
queryParameterNames.forEach { name ->
val value = getQueryParameter(name)
if (name.uriValidate() && value?.uriValidate() == true) {
params[name] = value
}
}
return params
}
}

View file

@ -4,11 +4,17 @@ import android.annotation.SuppressLint
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import com.tangem.utils.logging.TangemLogger
import dagger.hilt.android.AndroidEntryPoint
import io.customer.messagingpush.CustomerIOFirebaseMessagingService
import javax.inject.Inject
@AndroidEntryPoint
@SuppressLint("MissingFirebaseInstanceTokenRefresh")
internal class TangemPushNotificationService : FirebaseMessagingService() {
@Inject
lateinit var pushMessageHandler: PushMessageHandler
private val pushNotificationDelegate: PushNotificationDelegate by lazy {
PushNotificationDelegate(applicationContext)
}
@ -29,6 +35,8 @@ internal class TangemPushNotificationService : FirebaseMessagingService() {
handleNotificationTrigger = false,
)
pushMessageHandler.onMessageReceived(message.data)
val notification = message.notification ?: return
val channelId = notification.channelId ?: TANGEM_CHANNEL_ID

View file

@ -0,0 +1,81 @@
package com.tangem.tap.common.pushes
import com.tangem.common.routing.deeplink.DeeplinkConst.DERIVATION_PATH_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.NETWORK_ID_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.TOKEN_ID_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.WALLET_ID_KEY
import com.tangem.domain.account.fetcher.SingleAccountListFetcher
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.tap.ForegroundActivityObserver
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.launch
import javax.inject.Inject
/**
* Handles a received token-details push (same payload as
* [com.tangem.features.tokendetails.deeplink.TokenDetailsDeepLinkHandler]).
*
* When the app is open and the pushed token is not yet present in the wallet's portfolio (e.g. it was just added
* on the backend), refreshes the wallet accounts so it appears locally the open portfolio screen then updates
* reactively via [SingleAccountListSupplier]. Does nothing else.
*/
class TokenDetailsPushHandler @Inject constructor(
private val appCoroutineScope: AppCoroutineScope,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val singleAccountListSupplier: SingleAccountListSupplier,
private val singleAccountListFetcher: SingleAccountListFetcher,
) {
fun handle(queryParams: Map<String, String>) {
// Only when the app is open: a token just added on the backend should appear in the already-open portfolio.
// On cold start the fresh list is loaded by the regular auth flow instead.
if (ForegroundActivityObserver.foregroundActivity == null) return
appCoroutineScope.launch { refreshPortfolioIfTokenMissing(queryParams) }
}
internal suspend fun refreshPortfolioIfTokenMissing(queryParams: Map<String, String>) {
val networkId = queryParams[NETWORK_ID_KEY] ?: return
val tokenId = queryParams[TOKEN_ID_KEY] ?: return
val derivationPath = queryParams[DERIVATION_PATH_KEY]
val userWallet = resolveUserWallet(queryParams[WALLET_ID_KEY]) ?: return
// Token list refresh only makes sense for an unlocked multi-currency wallet.
if (userWallet.isLocked || !userWallet.isMultiCurrency) return
val isTokenPresent = singleAccountListSupplier.getSyncOrNull(userWallet.walletId)
?.flattenCurrencies()
?.any { it.matches(networkId = networkId, tokenId = tokenId, derivationPath = derivationPath) } == true
if (isTokenPresent) return
singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId = userWallet.walletId))
.onLeft { TangemLogger.e("Error on refreshing portfolio from push", it) }
}
private fun resolveUserWallet(walletId: String?): UserWallet? {
val userWalletId = walletId?.let(::UserWalletId)
return if (userWalletId != null) {
getUserWalletUseCase(userWalletId).getOrNull()
} else {
getSelectedWalletSyncUseCase().getOrNull()
}
}
private fun CryptoCurrency.matches(networkId: String, tokenId: String, derivationPath: String?): Boolean {
val isNetwork = network.rawId.equals(networkId, ignoreCase = true)
val isCurrency = id.rawCurrencyId?.value?.equals(tokenId, ignoreCase = true) == true
val isDefaultDerivation = network.derivationPath is Network.DerivationPath.Card
val isCustomDerivation = derivationPath?.equals(network.derivationPath.value) == true
return isNetwork && isCurrency && (isDefaultDerivation || isCustomDerivation)
}
}