Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-03 20:46:58 +04:00
commit 09c52231e2
25 changed files with 447 additions and 85 deletions

View file

@ -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

View file

@ -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

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)
}
}

View file

@ -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"),
)
}
}

View file

@ -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",
)
}

View file

@ -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,
}
)

View file

@ -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
}
}
}

View file

@ -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
}
)

View file

@ -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 {

View file

@ -57,6 +57,7 @@ internal class CreateMobileWalletModel @Inject constructor(
onImportClick = ::onImportClick,
onCreateClick = ::onCreateClick,
createButtonLoading = false,
onTermsClick = { router.push(AppRoute.Disclaimer(isTosAccepted = true)) },
),
)

View file

@ -5,4 +5,5 @@ internal data class CreateMobileWalletUM(
val onBackClick: () -> Unit,
val onImportClick: () -> Unit,
val onCreateClick: () -> Unit,
val onTermsClick: () -> Unit,
)

View file

@ -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 = {},
),
)
}

View file

@ -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,
),
)

View file

@ -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,
),
)

View file

@ -9,5 +9,6 @@ internal data class MultiWalletCreateWalletUM(
val showOtherOptionsButton: Boolean,
val onCreateWalletClick: () -> Unit,
val onOtherOptionsClick: () -> Unit,
val onTermsOfUseClick: () -> Unit,
val dialog: OnboardingDialogUM?,
)

View file

@ -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)

View file

@ -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
}

View file

@ -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()
}

View file

@ -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