diff --git a/app/src/huawei/java/com/tangem/tap/HuaweiPushService.kt b/app/src/huawei/java/com/tangem/tap/HuaweiPushService.kt index cfd160dab2..83fd5c3d97 100644 --- a/app/src/huawei/java/com/tangem/tap/HuaweiPushService.kt +++ b/app/src/huawei/java/com/tangem/tap/HuaweiPushService.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index e998374c66..b2aa247f2c 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/common/pushes/PushMessageHandler.kt b/app/src/main/java/com/tangem/tap/common/pushes/PushMessageHandler.kt new file mode 100644 index 0000000000..d8589b644f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/pushes/PushMessageHandler.kt @@ -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) { + 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 { + val params = mutableMapOf() + queryParameterNames.forEach { name -> + val value = getQueryParameter(name) + if (name.uriValidate() && value?.uriValidate() == true) { + params[name] = value + } + } + return params + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt b/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt index aef6c2946b..86cc8de6f0 100644 --- a/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt +++ b/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/common/pushes/TokenDetailsPushHandler.kt b/app/src/main/java/com/tangem/tap/common/pushes/TokenDetailsPushHandler.kt new file mode 100644 index 0000000000..c370c505ce --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/pushes/TokenDetailsPushHandler.kt @@ -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) { + // 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) { + 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) + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/com/tangem/tap/common/pushes/TokenDetailsPushHandlerTest.kt b/app/src/test/kotlin/com/tangem/tap/common/pushes/TokenDetailsPushHandlerTest.kt new file mode 100644 index 0000000000..84779067f5 --- /dev/null +++ b/app/src/test/kotlin/com/tangem/tap/common/pushes/TokenDetailsPushHandlerTest.kt @@ -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(), + 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): AccountList = AccountList.empty( + userWalletId = userWalletId, + cryptoCurrencies = currencies, + ) + + private fun mockCryptoCurrency() = mockk { + 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"), + ) + } +} \ No newline at end of file diff --git a/common/routing/src/test/kotlin/com/tangem/common/routing/deeplink/PayloadToDeeplinkConverterTest.kt b/common/routing/src/test/kotlin/com/tangem/common/routing/deeplink/PayloadToDeeplinkConverterTest.kt index 8dd6c1ba5d..d60ee79c7c 100644 --- a/common/routing/src/test/kotlin/com/tangem/common/routing/deeplink/PayloadToDeeplinkConverterTest.kt +++ b/common/routing/src/test/kotlin/com/tangem/common/routing/deeplink/PayloadToDeeplinkConverterTest.kt @@ -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", ) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolBroadcastResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolBroadcastResponse.kt index 1970814c8c..fa07f77ec6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolBroadcastResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolBroadcastResponse.kt @@ -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, -} \ No newline at end of file +) \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/ethpool/P2PEthPoolBroadcastResultConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/ethpool/P2PEthPoolBroadcastResultConverter.kt index 13e58961c9..84fe2f7faf 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/ethpool/P2PEthPoolBroadcastResultConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/ethpool/P2PEthPoolBroadcastResultConverter.kt @@ -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 P2PEthPoolBroadcastStatus.SUCCESS - P2PEthPoolTxStatusDTO.FAILED -> P2PEthPoolBroadcastStatus.FAILED - } - } } \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/ethpool/P2PEthPoolBroadcastResult.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/ethpool/P2PEthPoolBroadcastResult.kt index 07f04f6b10..7d00699d88 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/ethpool/P2PEthPoolBroadcastResult.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/ethpool/P2PEthPoolBroadcastResult.kt @@ -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 -} \ No newline at end of file +) \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayPushNotificationType.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayPushNotificationType.kt index 1ff9b157c8..a45bf3886f 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayPushNotificationType.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayPushNotificationType.kt @@ -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 { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt index fb2d8cc290..3535144db7 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt @@ -57,6 +57,7 @@ internal class CreateMobileWalletModel @Inject constructor( onImportClick = ::onImportClick, onCreateClick = ::onCreateClick, createButtonLoading = false, + onTermsClick = { router.push(AppRoute.Disclaimer(isTosAccepted = true)) }, ), ) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/entity/CreateMobileWalletUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/entity/CreateMobileWalletUM.kt index 3746ece730..098f951a9c 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/entity/CreateMobileWalletUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/entity/CreateMobileWalletUM.kt @@ -5,4 +5,5 @@ internal data class CreateMobileWalletUM( val onBackClick: () -> Unit, val onImportClick: () -> Unit, val onCreateClick: () -> Unit, + val onTermsClick: () -> Unit, ) \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/ui/CreateMobileWalletContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/ui/CreateMobileWalletContent.kt index bf0f02e9b8..a4cfed989f 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/ui/CreateMobileWalletContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/ui/CreateMobileWalletContent.kt @@ -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 = {}, ), ) } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt index 365e636946..223cade12a 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt @@ -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, ), ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/ui/MultiWalletCreateWallet.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/ui/MultiWalletCreateWallet.kt index d687c5ba72..079af3f1a6 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/ui/MultiWalletCreateWallet.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/ui/MultiWalletCreateWallet.kt @@ -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, ), ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/ui/state/MultiWalletCreateWalletUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/ui/state/MultiWalletCreateWalletUM.kt index 2a6a97474f..7b2f267aaf 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/ui/state/MultiWalletCreateWalletUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/ui/state/MultiWalletCreateWalletUM.kt @@ -9,5 +9,6 @@ internal data class MultiWalletCreateWalletUM( val showOtherOptionsButton: Boolean, val onCreateWalletClick: () -> Unit, val onOtherOptionsClick: () -> Unit, + val onTermsOfUseClick: () -> Unit, val dialog: OnboardingDialogUM?, ) \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryDetailsConverter.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryDetailsConverter.kt index e4f3db8c2e..16115c40e6 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryDetailsConverter.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryDetailsConverter.kt @@ -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) diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/deeplink/DefaultTangemPayMainDeepLinkHandler.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/deeplink/DefaultTangemPayMainDeepLinkHandler.kt index d130d1683f..ef2a0b7745 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/deeplink/DefaultTangemPayMainDeepLinkHandler.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/deeplink/DefaultTangemPayMainDeepLinkHandler.kt @@ -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 } diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/deeplink/TangemPayPushAction.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/deeplink/TangemPayPushAction.kt index 4ae61bc268..2808e28da9 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/deeplink/TangemPayPushAction.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/deeplink/TangemPayPushAction.kt @@ -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() } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt index c6a3e8632a..719e0b1dc9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt @@ -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"), + ), } \ No newline at end of file diff --git a/features/wallet/impl/src/main/res/drawable/ill_nanovest_card2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_nanovest_card2_120_106.webp new file mode 100644 index 0000000000..1be5b7172e Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_nanovest_card2_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_nanovest_card3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_nanovest_card3_120_106.webp new file mode 100644 index 0000000000..6504ae71d6 Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_nanovest_card3_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_superteam_card2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_superteam_card2_120_106.webp new file mode 100644 index 0000000000..f31ec2e0c3 Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_superteam_card2_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_superteam_card3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_superteam_card3_120_106.webp new file mode 100644 index 0000000000..25559b202d Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_superteam_card3_120_106.webp differ