Updated on 2026-08-14
This commit is contained in:
commit
09c52231e2
25 changed files with 447 additions and 85 deletions
|
|
@ -4,11 +4,18 @@ import android.os.Bundle
|
|||
import com.huawei.hms.push.HmsMessageService
|
||||
import com.huawei.hms.push.RemoteMessage
|
||||
import com.tangem.google.GoogleServicesHelper
|
||||
import com.tangem.tap.common.pushes.PushMessageHandler
|
||||
import com.tangem.tap.common.pushes.PushNotificationDelegate
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class HuaweiPushService : HmsMessageService() {
|
||||
|
||||
@Inject
|
||||
internal lateinit var pushMessageHandler: PushMessageHandler
|
||||
|
||||
private val pushNotificationDelegate: PushNotificationDelegate by lazy {
|
||||
PushNotificationDelegate(applicationContext)
|
||||
}
|
||||
|
|
@ -27,6 +34,9 @@ class HuaweiPushService : HmsMessageService() {
|
|||
super.onMessageReceived(message)
|
||||
val isGoogleServicesAvailable = GoogleServicesHelper.checkGoogleServicesAvailability(this)
|
||||
if (isGoogleServicesAvailable) return
|
||||
|
||||
message?.dataOfMap?.let(pushMessageHandler::onMessageReceived)
|
||||
|
||||
val notification = message?.notification ?: return
|
||||
val channelId = notification.channelId ?: TANGEM_CHANNEL_ID
|
||||
|
||||
|
|
|
|||
|
|
@ -19,10 +19,10 @@ import androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode
|
|||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTagsAsResourceId
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTagsAsResourceId
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.flowWithLifecycle
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
package com.tangem.tap.common.pushes
|
||||
|
||||
import arrow.core.Either
|
||||
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.models.AccountList
|
||||
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.models.GetUserWalletError
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class TokenDetailsPushHandlerTest {
|
||||
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase = mockk()
|
||||
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase = mockk()
|
||||
private val singleAccountListSupplier: SingleAccountListSupplier = mockk()
|
||||
private val singleAccountListFetcher: SingleAccountListFetcher = mockk()
|
||||
|
||||
private val handler = TokenDetailsPushHandler(
|
||||
appCoroutineScope = mockk<AppCoroutineScope>(),
|
||||
getUserWalletUseCase = getUserWalletUseCase,
|
||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||
singleAccountListSupplier = singleAccountListSupplier,
|
||||
singleAccountListFetcher = singleAccountListFetcher,
|
||||
)
|
||||
|
||||
private val userWalletId = UserWalletId("011")
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
mockkObject(TangemLogger)
|
||||
coEvery { singleAccountListFetcher.invoke(any()) } returns Either.Right(Unit)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN token absent in portfolio WHEN handle push THEN refresh accounts`() = runTest {
|
||||
every { getUserWalletUseCase.invoke(userWalletId) } returns Either.Right(multiCurrencyWallet())
|
||||
coEvery { singleAccountListSupplier.getSyncOrNull(userWalletId) } returns accountList(currencies = emptyList())
|
||||
|
||||
handler.refreshPortfolioIfTokenMissing(defaultData())
|
||||
|
||||
coVerify { singleAccountListFetcher.invoke(SingleAccountListFetcher.Params(userWalletId)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN token present in portfolio WHEN handle push THEN do not refresh`() = runTest {
|
||||
every { getUserWalletUseCase.invoke(userWalletId) } returns Either.Right(multiCurrencyWallet())
|
||||
coEvery {
|
||||
singleAccountListSupplier.getSyncOrNull(userWalletId)
|
||||
} returns accountList(currencies = listOf(mockCryptoCurrency()))
|
||||
|
||||
handler.refreshPortfolioIfTokenMissing(defaultData())
|
||||
|
||||
coVerify(exactly = 0) { singleAccountListFetcher.invoke(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no wallet id in payload WHEN handle push THEN refresh selected wallet`() = runTest {
|
||||
every { getSelectedWalletSyncUseCase.invoke() } returns Either.Right(multiCurrencyWallet())
|
||||
coEvery { singleAccountListSupplier.getSyncOrNull(userWalletId) } returns accountList(currencies = emptyList())
|
||||
|
||||
handler.refreshPortfolioIfTokenMissing(defaultData() - WALLET_ID_KEY)
|
||||
|
||||
coVerify { singleAccountListFetcher.invoke(SingleAccountListFetcher.Params(userWalletId)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no wallet id and no selected wallet WHEN handle push THEN do not refresh`() = runTest {
|
||||
every { getSelectedWalletSyncUseCase.invoke() } returns Either.Left(GetUserWalletError.UserWalletNotFound)
|
||||
|
||||
handler.refreshPortfolioIfTokenMissing(defaultData() - WALLET_ID_KEY)
|
||||
|
||||
coVerify(exactly = 0) { singleAccountListFetcher.invoke(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN locked wallet WHEN handle push THEN do not refresh`() = runTest {
|
||||
every { getUserWalletUseCase.invoke(userWalletId) } returns Either.Right(
|
||||
value = mockk { every { isLocked } returns true },
|
||||
)
|
||||
|
||||
handler.refreshPortfolioIfTokenMissing(defaultData())
|
||||
|
||||
coVerify(exactly = 0) { singleAccountListFetcher.invoke(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN single currency wallet WHEN handle push THEN do not refresh`() = runTest {
|
||||
every { getUserWalletUseCase.invoke(userWalletId) } returns Either.Right(
|
||||
value = mockk {
|
||||
every { isLocked } returns false
|
||||
every { isMultiCurrency } returns false
|
||||
},
|
||||
)
|
||||
|
||||
handler.refreshPortfolioIfTokenMissing(defaultData())
|
||||
|
||||
coVerify(exactly = 0) { singleAccountListFetcher.invoke(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN wallet not found WHEN handle push THEN do not refresh`() = runTest {
|
||||
every { getUserWalletUseCase.invoke(userWalletId) } returns Either.Left(
|
||||
value = GetUserWalletError.UserWalletNotFound,
|
||||
)
|
||||
|
||||
handler.refreshPortfolioIfTokenMissing(defaultData())
|
||||
|
||||
coVerify(exactly = 0) { singleAccountListFetcher.invoke(any()) }
|
||||
}
|
||||
|
||||
private fun defaultData() = mapOf(
|
||||
WALLET_ID_KEY to "011",
|
||||
NETWORK_ID_KEY to "123",
|
||||
TOKEN_ID_KEY to "321",
|
||||
DERIVATION_PATH_KEY to "777",
|
||||
)
|
||||
|
||||
private fun multiCurrencyWallet(): UserWallet = mockk {
|
||||
every { isLocked } returns false
|
||||
every { isMultiCurrency } returns true
|
||||
every { walletId } returns userWalletId
|
||||
}
|
||||
|
||||
private fun accountList(currencies: List<CryptoCurrency>): AccountList = AccountList.empty(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrencies = currencies,
|
||||
)
|
||||
|
||||
private fun mockCryptoCurrency() = mockk<CryptoCurrency> {
|
||||
every { network } returns mockk {
|
||||
every { rawId } returns "123"
|
||||
every { derivationPath } returns Network.DerivationPath.Card(value = "777")
|
||||
}
|
||||
every { id } returns CryptoCurrency.ID(
|
||||
prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX,
|
||||
body = CryptoCurrency.ID.Body.NetworkIdWithDerivationPath(rawId = "321", derivationPath = "777"),
|
||||
suffix = CryptoCurrency.ID.Suffix.RawID("321"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.common.routing.deeplink
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.DEEPLINK_KEY
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.CUSTOMER_WALLET_ID_KEY
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.DEEPLINK_KEY
|
||||
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
|
||||
|
|
@ -188,7 +188,7 @@ internal class PayloadToDeeplinkConverterTest {
|
|||
fun `GIVEN tangem pay top_up push payload WHEN convert THEN should return pay-app-main deeplink`() {
|
||||
// GIVEN
|
||||
val payload = mapOf(
|
||||
TYPE_KEY to TangemPayPushNotificationType.TOP_UP.value,
|
||||
TYPE_KEY to TangemPayPushNotificationType.DECLINED_TOP_UP.value,
|
||||
CUSTOMER_WALLET_ID_KEY to "wallet123",
|
||||
TRANSACTION_ID_KEY to "test456",
|
||||
)
|
||||
|
|
@ -206,7 +206,7 @@ internal class PayloadToDeeplinkConverterTest {
|
|||
fun `GIVEN tangem pay collateral push payload WHEN convert THEN should return pay-app-main deeplink`() {
|
||||
// GIVEN
|
||||
val payload = mapOf(
|
||||
TYPE_KEY to TangemPayPushNotificationType.COLLATERAL.value,
|
||||
TYPE_KEY to TangemPayPushNotificationType.COLLATERAL_DEPOSIT.value,
|
||||
CUSTOMER_WALLET_ID_KEY to "wallet123",
|
||||
)
|
||||
|
||||
|
|
@ -215,7 +215,7 @@ internal class PayloadToDeeplinkConverterTest {
|
|||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(
|
||||
"tangem://pay-app-main?type=collateral&customer_wallet_id=wallet123",
|
||||
"tangem://pay-app-main?type=collateral_deposit&customer_wallet_id=wallet123",
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,31 +11,19 @@ data class P2PEthPoolBroadcastResponse(
|
|||
@Json(name = "hash")
|
||||
val hash: String,
|
||||
@Json(name = "status")
|
||||
val status: P2PEthPoolTxStatusDTO,
|
||||
val status: String,
|
||||
@Json(name = "blockNumber")
|
||||
val blockNumber: Int,
|
||||
val blockNumber: Int? = null,
|
||||
@Json(name = "transactionIndex")
|
||||
val transactionIndex: Int,
|
||||
val transactionIndex: Int? = null,
|
||||
@Json(name = "gasUsed")
|
||||
val gasUsed: String,
|
||||
val gasUsed: String? = null,
|
||||
@Json(name = "cumulativeGasUsed")
|
||||
val cumulativeGasUsed: String,
|
||||
val cumulativeGasUsed: String? = null,
|
||||
@Json(name = "effectiveGasPrice")
|
||||
val effectiveGasPrice: String?,
|
||||
val effectiveGasPrice: String? = null,
|
||||
@Json(name = "from")
|
||||
val from: String,
|
||||
@Json(name = "to")
|
||||
val to: String,
|
||||
)
|
||||
|
||||
/**
|
||||
* Transaction status from P2PEthPool API
|
||||
*/
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class P2PEthPoolTxStatusDTO {
|
||||
@Json(name = "success")
|
||||
SUCCESS,
|
||||
|
||||
@Json(name = "failed")
|
||||
FAILED,
|
||||
}
|
||||
)
|
||||
|
|
@ -1,11 +1,8 @@
|
|||
package com.tangem.data.staking.converters.ethpool
|
||||
|
||||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolBroadcastResponse
|
||||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolTxStatusDTO
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolBroadcastResult
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolBroadcastStatus
|
||||
import com.tangem.utils.converter.Converter
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Converter from P2PEthPool Broadcast Transaction Response to Domain model
|
||||
|
|
@ -15,21 +12,14 @@ internal object P2PEthPoolBroadcastResultConverter : Converter<P2PEthPoolBroadca
|
|||
override fun convert(value: P2PEthPoolBroadcastResponse): P2PEthPoolBroadcastResult {
|
||||
return P2PEthPoolBroadcastResult(
|
||||
hash = value.hash,
|
||||
status = convertStatus(value.status),
|
||||
status = value.status,
|
||||
blockNumber = value.blockNumber,
|
||||
transactionIndex = value.transactionIndex,
|
||||
gasUsed = value.gasUsed.toBigDecimalOrNull() ?: BigDecimal.ZERO,
|
||||
cumulativeGasUsed = value.cumulativeGasUsed.toBigDecimalOrNull() ?: BigDecimal.ZERO,
|
||||
gasUsed = value.gasUsed?.toBigDecimalOrNull(),
|
||||
cumulativeGasUsed = value.cumulativeGasUsed?.toBigDecimalOrNull(),
|
||||
effectiveGasPrice = value.effectiveGasPrice?.toBigDecimalOrNull(),
|
||||
from = value.from,
|
||||
to = value.to,
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertStatus(status: P2PEthPoolTxStatusDTO): P2PEthPoolBroadcastStatus {
|
||||
return when (status) {
|
||||
P2PEthPoolTxStatusDTO.SUCCESS -> P2PEthPoolBroadcastStatus.SUCCESS
|
||||
P2PEthPoolTxStatusDTO.FAILED -> P2PEthPoolBroadcastStatus.FAILED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.domain.staking.model.ethpool
|
||||
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* P2P.org transaction broadcast result
|
||||
|
|
@ -9,21 +8,12 @@ import kotlinx.serialization.Serializable
|
|||
*/
|
||||
data class P2PEthPoolBroadcastResult(
|
||||
val hash: String,
|
||||
val status: P2PEthPoolBroadcastStatus,
|
||||
val blockNumber: Int,
|
||||
val transactionIndex: Int,
|
||||
val gasUsed: SerializedBigDecimal,
|
||||
val cumulativeGasUsed: SerializedBigDecimal,
|
||||
val status: String,
|
||||
val blockNumber: Int?,
|
||||
val transactionIndex: Int?,
|
||||
val gasUsed: SerializedBigDecimal?,
|
||||
val cumulativeGasUsed: SerializedBigDecimal?,
|
||||
val effectiveGasPrice: SerializedBigDecimal?,
|
||||
val from: String,
|
||||
val to: String,
|
||||
)
|
||||
|
||||
/**
|
||||
* Transaction broadcast status
|
||||
*/
|
||||
@Serializable
|
||||
enum class P2PEthPoolBroadcastStatus {
|
||||
SUCCESS, // Transaction confirmed successfully
|
||||
FAILED, // Transaction failed
|
||||
}
|
||||
)
|
||||
|
|
@ -3,8 +3,10 @@ package com.tangem.domain.visa.model
|
|||
enum class TangemPayPushNotificationType(val value: String) {
|
||||
CARD_READY("card_ready"),
|
||||
TRANSACTION_SPEND("transaction_spend"),
|
||||
TOP_UP("declined_top_up"),
|
||||
COLLATERAL("collateral"),
|
||||
DECLINED_TOP_UP("declined_top_up"),
|
||||
COLLATERAL_WITHDRAW("collateral_withdraw"),
|
||||
COLLATERAL_DEPOSIT("collateral_deposit"),
|
||||
TRANSACTION_SPEND_REFUND("transaction_spend_refund"),
|
||||
;
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ internal class CreateMobileWalletModel @Inject constructor(
|
|||
onImportClick = ::onImportClick,
|
||||
onCreateClick = ::onCreateClick,
|
||||
createButtonLoading = false,
|
||||
onTermsClick = { router.push(AppRoute.Disclaimer(isTosAccepted = true)) },
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,4 +5,5 @@ internal data class CreateMobileWalletUM(
|
|||
val onBackClick: () -> Unit,
|
||||
val onImportClick: () -> Unit,
|
||||
val onCreateClick: () -> Unit,
|
||||
val onTermsClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -9,7 +9,13 @@ import androidx.compose.material3.*
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.withLink
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
|
|
@ -19,6 +25,8 @@ import com.tangem.core.ui.components.appbar.TangemTopAppBar
|
|||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.components.feature.FeatureBlock
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.appendColored
|
||||
import com.tangem.core.ui.extensions.appendWithStyledPlaceholder
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
|
@ -92,6 +100,33 @@ internal fun CreateMobileWalletContent(state: CreateMobileWalletUM, modifier: Mo
|
|||
iconRes = R.drawable.ic_tangem_card_24,
|
||||
)
|
||||
}
|
||||
val termsTemplate = stringResourceSafe(R.string.onboarding_create_wallet_term_of_conditions_text)
|
||||
val termsLinkText = stringResourceSafe(R.string.disclaimer_title)
|
||||
val termsLinkColor = TangemTheme.colors.text.accent
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
appendWithStyledPlaceholder(template = termsTemplate) {
|
||||
withLink(
|
||||
LinkAnnotation.Clickable(
|
||||
tag = "tos_link",
|
||||
styles = TextLinkStyles(SpanStyle(textDecoration = TextDecoration.None)),
|
||||
) { state.onTermsClick() },
|
||||
) {
|
||||
appendColored(text = termsLinkText, color = termsLinkColor)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
top = 16.dp,
|
||||
end = 16.dp,
|
||||
),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
SecondaryButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -130,6 +165,7 @@ private fun PreviewCreateWalletContent() {
|
|||
createButtonLoading = false,
|
||||
onImportClick = {},
|
||||
onCreateClick = {},
|
||||
onTermsClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.mo
|
|||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
|
|
@ -10,6 +11,7 @@ import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent
|
|||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.domain.card.repository.CardRepository
|
||||
|
|
@ -39,6 +41,7 @@ import javax.inject.Inject
|
|||
@ModelScoped
|
||||
internal class MultiWalletCreateWalletModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
private val router: Router,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val tangemSdkManager: TangemSdkManager,
|
||||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
|
|
@ -77,6 +80,7 @@ internal class MultiWalletCreateWalletModel @Inject constructor(
|
|||
onDone.emit(Step.SeedPhrase)
|
||||
}
|
||||
},
|
||||
onTermsOfUseClick = { router.push(AppRoute.Disclaimer(isTosAccepted = true)) },
|
||||
dialog = null,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,13 +8,22 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.withLink
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R as CoreUiR
|
||||
import com.tangem.core.ui.components.BasicDialog
|
||||
import com.tangem.core.ui.components.DialogButtonUM
|
||||
import com.tangem.core.ui.components.PrimaryButtonIconEnd
|
||||
import com.tangem.core.ui.components.SecondaryButton
|
||||
import com.tangem.core.ui.extensions.appendColored
|
||||
import com.tangem.core.ui.extensions.appendWithStyledPlaceholder
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
|
|
@ -24,24 +33,10 @@ import com.tangem.core.ui.test.StoriesScreenTestTags
|
|||
import com.tangem.features.onboarding.v2.impl.R
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.ui.state.MultiWalletCreateWalletUM
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
internal fun MultiWalletCreateWallet(state: MultiWalletCreateWalletUM, modifier: Modifier = Modifier) {
|
||||
if (state.dialog != null) {
|
||||
BasicDialog(
|
||||
title = state.dialog.title.resolveReference(),
|
||||
message = state.dialog.message.resolveReference(),
|
||||
confirmButton = DialogButtonUM(
|
||||
title = state.dialog.confirmButtonText.resolveReference(),
|
||||
onClick = state.dialog.onConfirmClick,
|
||||
),
|
||||
dismissButton = DialogButtonUM(
|
||||
title = state.dialog.dismissButtonText.resolveReference(),
|
||||
isWarning = state.dialog.dismissWarningColor,
|
||||
onClick = state.dialog.onDismissButtonClick,
|
||||
),
|
||||
onDismissDialog = state.dialog.onDismiss,
|
||||
)
|
||||
}
|
||||
MultiWalletCreateWalletDialog(state)
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
|
|
@ -78,9 +73,33 @@ internal fun MultiWalletCreateWallet(state: MultiWalletCreateWalletUM, modifier:
|
|||
)
|
||||
}
|
||||
|
||||
val termsTemplate = stringResourceSafe(CoreUiR.string.onboarding_create_wallet_term_of_conditions_text)
|
||||
val termsLinkText = stringResourceSafe(CoreUiR.string.disclaimer_title)
|
||||
val termsLinkColor = TangemTheme.colors.text.accent
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
appendWithStyledPlaceholder(template = termsTemplate) {
|
||||
withLink(
|
||||
LinkAnnotation.Clickable(
|
||||
tag = "tos_link",
|
||||
styles = TextLinkStyles(SpanStyle(textDecoration = TextDecoration.None)),
|
||||
) { state.onTermsOfUseClick() },
|
||||
) {
|
||||
appendColored(text = termsLinkText, color = termsLinkColor)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
PrimaryButtonIconEnd(
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 12.dp)
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 8.dp)
|
||||
.fillMaxWidth(),
|
||||
iconResId = R.drawable.ic_tangem_24,
|
||||
text = stringResourceSafe(R.string.onboarding_create_wallet_button_create_wallet),
|
||||
|
|
@ -90,7 +109,7 @@ internal fun MultiWalletCreateWallet(state: MultiWalletCreateWalletUM, modifier:
|
|||
if (state.showOtherOptionsButton) {
|
||||
SecondaryButton(
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 8.dp)
|
||||
.fillMaxWidth(),
|
||||
text = stringResourceSafe(R.string.onboarding_create_wallet_options_button_options),
|
||||
onClick = state.onOtherOptionsClick,
|
||||
|
|
@ -99,6 +118,26 @@ internal fun MultiWalletCreateWallet(state: MultiWalletCreateWalletUM, modifier:
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MultiWalletCreateWalletDialog(state: MultiWalletCreateWalletUM) {
|
||||
if (state.dialog != null) {
|
||||
BasicDialog(
|
||||
title = state.dialog.title.resolveReference(),
|
||||
message = state.dialog.message.resolveReference(),
|
||||
confirmButton = DialogButtonUM(
|
||||
title = state.dialog.confirmButtonText.resolveReference(),
|
||||
onClick = state.dialog.onConfirmClick,
|
||||
),
|
||||
dismissButton = DialogButtonUM(
|
||||
title = state.dialog.dismissButtonText.resolveReference(),
|
||||
isWarning = state.dialog.dismissWarningColor,
|
||||
onClick = state.dialog.onDismissButtonClick,
|
||||
),
|
||||
onDismissDialog = state.dialog.onDismiss,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
|
|
@ -110,6 +149,7 @@ private fun Preview() {
|
|||
onCreateWalletClick = {},
|
||||
showOtherOptionsButton = true,
|
||||
onOtherOptionsClick = {},
|
||||
onTermsOfUseClick = {},
|
||||
dialog = null,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,5 +9,6 @@ internal data class MultiWalletCreateWalletUM(
|
|||
val showOtherOptionsButton: Boolean,
|
||||
val onCreateWalletClick: () -> Unit,
|
||||
val onOtherOptionsClick: () -> Unit,
|
||||
val onTermsOfUseClick: () -> Unit,
|
||||
val dialog: OnboardingDialogUM?,
|
||||
)
|
||||
|
|
@ -62,7 +62,7 @@ internal object TangemPayTxHistoryDetailsConverter :
|
|||
is TangemPayTxHistoryItem.Payment -> ImageReference.Res(R.drawable.ic_arrow_up_24)
|
||||
is TangemPayTxHistoryItem.Spend -> {
|
||||
val merchantIcon = this.enrichedMerchantIconUrl
|
||||
if (merchantIcon != null) {
|
||||
if (!merchantIcon.isNullOrEmpty()) {
|
||||
ImageReference.Url(merchantIcon)
|
||||
} else {
|
||||
ImageReference.Res(R.drawable.ic_category_24)
|
||||
|
|
|
|||
|
|
@ -61,9 +61,7 @@ internal class DefaultTangemPayMainDeepLinkHandler @AssistedInject constructor(
|
|||
onComplete = {
|
||||
walletDeepLinkActionTrigger.selectWallet(userWalletId)
|
||||
when (pushAction) {
|
||||
is TangemPayPushAction.CardReady,
|
||||
is TangemPayPushAction.TopUp,
|
||||
-> navigateToTangemPayDetails(userWalletId)
|
||||
is TangemPayPushAction.CardReady -> navigateToTangemPayDetails(userWalletId)
|
||||
is TangemPayPushAction.TransactionSpend -> {
|
||||
walletDeepLinkActionTrigger.showTangemPayTransaction(
|
||||
transaction = pushAction.transaction,
|
||||
|
|
@ -89,12 +87,14 @@ internal class DefaultTangemPayMainDeepLinkHandler @AssistedInject constructor(
|
|||
|
||||
return when (type) {
|
||||
TangemPayPushNotificationType.CARD_READY -> TangemPayPushAction.CardReady
|
||||
TangemPayPushNotificationType.TRANSACTION_SPEND -> {
|
||||
TangemPayPushNotificationType.TRANSACTION_SPEND,
|
||||
TangemPayPushNotificationType.TRANSACTION_SPEND_REFUND,
|
||||
TangemPayPushNotificationType.DECLINED_TOP_UP,
|
||||
-> {
|
||||
val transaction = TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(payload)
|
||||
if (transaction != null) TangemPayPushAction.TransactionSpend(transaction, customerId) else null
|
||||
}
|
||||
TangemPayPushNotificationType.TOP_UP -> TangemPayPushAction.TopUp
|
||||
TangemPayPushNotificationType.COLLATERAL -> {
|
||||
TangemPayPushNotificationType.COLLATERAL_DEPOSIT, TangemPayPushNotificationType.COLLATERAL_WITHDRAW -> {
|
||||
val transaction = TangemPayPushPayloadToTxHistoryItemConverter.convertCollateral(payload)
|
||||
if (transaction != null) TangemPayPushAction.CollateralTransaction(transaction, customerId) else null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,14 +7,12 @@ internal sealed class TangemPayPushAction {
|
|||
data object CardReady : TangemPayPushAction()
|
||||
|
||||
data class TransactionSpend(
|
||||
val transaction: TangemPayTxHistoryItem,
|
||||
val transaction: TangemPayTxHistoryItem.Spend,
|
||||
val customerId: String,
|
||||
) : TangemPayPushAction()
|
||||
|
||||
data object TopUp : TangemPayPushAction()
|
||||
|
||||
data class CollateralTransaction(
|
||||
val transaction: TangemPayTxHistoryItem,
|
||||
val transaction: TangemPayTxHistoryItem.Collateral,
|
||||
val customerId: String,
|
||||
) : TangemPayPushAction()
|
||||
}
|
||||
|
|
@ -422,4 +422,16 @@ internal enum class Wallet2CobrandImage(
|
|||
cards3ResId = R.drawable.ill_stronghold_card3_120_106,
|
||||
batchIds = setOf("BB000054"),
|
||||
),
|
||||
|
||||
Superteam(
|
||||
cards2ResId = R.drawable.ill_superteam_card2_120_106,
|
||||
cards3ResId = R.drawable.ill_superteam_card3_120_106,
|
||||
batchIds = setOf("BB000051"),
|
||||
),
|
||||
|
||||
Nanovest(
|
||||
cards2ResId = R.drawable.ill_nanovest_card2_120_106,
|
||||
cards3ResId = R.drawable.ill_nanovest_card3_120_106,
|
||||
batchIds = setOf("BB000052"),
|
||||
),
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 7.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
Loading…
Add table
Add a link
Reference in a new issue