Updated on 2026-08-14

This commit is contained in:
Tangem 2026-02-02 20:29:04 +07:00
commit a31afce6c8
184 changed files with 2488 additions and 1558 deletions

41
Marathonfile Normal file
View file

@ -0,0 +1,41 @@
name: "Tangem Android UI Tests"
outputDir: "build/reports/marathon"
testOutputTimeoutMillis: 180000 # 3 minutes
testBatchTimeoutMillis: 600000 # 10 minutes
vendorConfiguration:
type: "Android"
applicationApk: "app/build/outputs/apk/google/mocked/app-google-mocked.apk"
testApplicationApk: "app/build/outputs/apk/androidTest/google/mocked/app-google-mocked-androidTest.apk"
autoGrantPermission: true
applicationPmClear: true
testApplicationPmClear: true
# wiremockBaseUrl redirects API calls from wiremock.tests-d.com to local WireMock
# This is used on CI where each emulator has its own WireMock instance via adb reverse
instrumentationArgs:
wiremockBaseUrl: "http://localhost:8080"
allureConfiguration:
enabled: false # we collect allure via adb pull, not through marathon
poolingStrategy:
type: "omni"
batchingStrategy:
type: "isolate"
sortingStrategy:
type: "no-sorting"
retryStrategy:
type: "fixed-quota"
totalAllowedRetryQuota: 30
retryPerTestQuota: 1
flakinessStrategy:
type: "ignore"
analyticsConfiguration:
type: "disabled"
debug: false

View file

@ -6,6 +6,7 @@ import androidx.compose.ui.test.junit4.createEmptyComposeRule
import androidx.compose.ui.test.printToLog
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.intent.Intents
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import com.kaspersky.components.alluresupport.interceptors.step.ScreenshotStepInterceptor
import com.kaspersky.components.alluresupport.withForcedAllureSupport
@ -19,6 +20,7 @@ import com.tangem.common.rules.ApiEnvironmentRule
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.utils.WireMockRedirectInterceptor
import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.promo.models.PromoId
import com.tangem.tap.MainActivity
@ -86,6 +88,9 @@ abstract class BaseTestCase : TestCase(
additionalAfterSection: () -> Unit = {},
) = before {
Allure.label(ALLURE_LABEL_NAME, ALLURE_LABEL_VALUE)
// Setup WireMock redirect for CI with local WireMock instances
val wiremockUrl = InstrumentationRegistry.getArguments().getString(WIREMOCK_BASE_URL_ARG)
WireMockRedirectInterceptor.overriddenBaseUrl = wiremockUrl
hiltRule.inject()
runBlocking {
appPreferencesStore.editData { mutablePreferences ->
@ -138,7 +143,6 @@ abstract class BaseTestCase : TestCase(
private fun applicationInjectionRule(): ApplicationInjectionExecutionRule {
return ApplicationInjectionExecutionRule(
toggleStates = mapOf(
"NEW_TOKEN_RECEIVE_ENABLED" to true,
"SWAP_REDESIGN_ENABLED" to false,
"NEW_ONRAMP_MAIN_ENABLED" to true,
"HOT_WALLET_ENABLED" to true,
@ -147,4 +151,8 @@ abstract class BaseTestCase : TestCase(
)
)
}
private companion object {
const val WIREMOCK_BASE_URL_ARG = "wiremockBaseUrl"
}
}

View file

@ -1,22 +1,31 @@
package com.tangem.common.utils
import com.tangem.datasource.utils.WireMockRedirectInterceptor
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import timber.log.Timber
import java.io.IOException
private const val DEFAULT_WIREMOCK_URL = "[REDACTED_ENV_URL]"
/**
* Returns the WireMock base URL to use.
*/
private fun getWireMockBaseUrl(): String =
WireMockRedirectInterceptor.overriddenBaseUrl ?: DEFAULT_WIREMOCK_URL
/**
* Method uses to set WireMock scenario state
* @param scenarioName Name of the scenario to modify
* @param state The target state to set (must be one of the scenario's possibleStates)
* @param baseUrl WireMock base URL
* @param baseUrl WireMock base URL (defaults to local override if set, otherwise remote)
* @return true if state was set successfully, false otherwise
*/
fun setWireMockScenarioState(
scenarioName: String,
state: String,
baseUrl: String = "[REDACTED_ENV_URL]"
baseUrl: String = getWireMockBaseUrl()
): Boolean {
Timber.i("=== WireMock Scenario Set ===")
Timber.i("Setting scenario '$scenarioName' to state: $state")
@ -46,8 +55,9 @@ fun setWireMockScenarioState(
/**
* Method checks accessibility of WireMock
* @param baseUrl WireMock base URL (defaults to local override if set, otherwise remote)
*/
fun checkWireMockStatus(baseUrl: String = "[REDACTED_ENV_URL]"): Boolean {
fun checkWireMockStatus(baseUrl: String = getWireMockBaseUrl()): Boolean {
val client = OkHttpClient()
val request = Request.Builder()
.url("$baseUrl/__admin/scenarios")
@ -69,8 +79,9 @@ fun checkWireMockStatus(baseUrl: String = "[REDACTED_ENV_URL]"): Boolean {
/**
* Method to reset all WireMock scenarios
* @param baseUrl WireMock base URL (defaults to local override if set, otherwise remote)
*/
fun resetWireMockScenarios(baseUrl: String = "[REDACTED_ENV_URL]"): Boolean {
fun resetWireMockScenarios(baseUrl: String = getWireMockBaseUrl()): Boolean {
Timber.i("=== WireMock Scenarios Reset ===")
Timber.i("Base URL: $baseUrl")
@ -105,13 +116,13 @@ fun resetWireMockScenarios(baseUrl: String = "[REDACTED_ENV_URL]"): Boolean {
* Method to reset a specific WireMock scenario to its initial state
* @param scenarioName Name of the scenario to reset
* @param initialState The target state to reset the scenario to (must be one of the scenario's possibleStates)
* @param baseUrl WireMock base URL
* @param baseUrl WireMock base URL (defaults to local override if set, otherwise remote)
* @return true if reset was successful, false otherwise
*/
fun resetWireMockScenarioState(
scenarioName: String,
initialState: String = "Started",
baseUrl: String = "[REDACTED_ENV_URL]"
baseUrl: String = getWireMockBaseUrl()
): Boolean {
Timber.i("=== WireMock Scenario Reset ===")
Timber.i("Resetting scenario '$scenarioName' to initial state: $initialState")

View file

@ -42,6 +42,7 @@ import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.utils.NetworkLogsSaveInterceptor
import com.tangem.datasource.utils.WireMockRedirectInterceptor
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
@ -334,15 +335,23 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
ExceptionHandler.append(blockchainExceptionHandler)
if (LogConfig.network.isBlockchainSdkNetworkLogEnabled) {
BlockchainSdkRetrofitBuilder.interceptors = listOf(
createNetworkLoggingInterceptor(),
ChuckerInterceptor(this),
)
BlockchainSdkRetrofitBuilder.interceptors = buildList {
if (BuildConfig.MOCK_DATA_SOURCE) {
add(WireMockRedirectInterceptor())
}
add(createNetworkLoggingInterceptor())
add(ChuckerInterceptor(this@TangemApplication))
}
TangemApiServiceSettings.addInterceptors(
createNetworkLoggingInterceptor(),
ChuckerInterceptor(this),
NetworkLogsSaveInterceptor(appLogsStore),
*buildList {
if (BuildConfig.MOCK_DATA_SOURCE) {
add(WireMockRedirectInterceptor())
}
add(createNetworkLoggingInterceptor())
add(ChuckerInterceptor(this@TangemApplication))
add(NetworkLogsSaveInterceptor(appLogsStore))
}.toTypedArray(),
)
}

View file

@ -393,4 +393,18 @@ internal object TransactionDomainModule {
currencyChecksRepository = currencyChecksRepository,
)
}
@Provides
@Singleton
fun provideSignCloreMessageUseCase(
walletManagersFacade: WalletManagersFacade,
cardSdkConfigRepository: CardSdkConfigRepository,
tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory,
): SignCloreMessageUseCase {
return SignCloreMessageUseCase(
walletManagersFacade = walletManagersFacade,
cardSdkConfigRepository = cardSdkConfigRepository,
getHotWalletSigner = tangemHotWalletSignerFactory::create,
)
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.tap.di.domain
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.tokens.repository.CurrenciesRepository
@ -129,12 +130,14 @@ internal object WalletsDomainModule {
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
walletsRepository: WalletsRepository,
analyticsEventHandler: AnalyticsEventHandler,
): SaveWalletUseCase {
return SaveWalletUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
walletsRepository = walletsRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
analyticsEventHandler = analyticsEventHandler,
)
}

View file

@ -21,6 +21,7 @@ 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.wallets.R
import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
@ -390,6 +391,7 @@ internal class DefaultUserWalletsListRepository(
if (newUserWallet.walletId == oldUserWallet.walletId &&
oldUserWallet is UserWallet.Hot && newUserWallet is UserWallet.Cold
) {
trackWalletUpgradeEvent()
removeHotWalletsFromSDKAndRepos(walletIds = listOf(oldUserWallet.walletId))
// When upgrading from Hot to Cold, if biometric lock is available, set it
if (hasBiometry()) {
@ -536,4 +538,8 @@ internal class DefaultUserWalletsListRepository(
),
)
}
private fun trackWalletUpgradeEvent() {
analyticsEventHandler.send(event = WalletSettingsAnalyticEvents.WalletUpgraded())
}
}

View file

@ -1,79 +0,0 @@
package com.tangem.tap.proxy
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.lib.crypto.UserWalletManager
import com.tangem.lib.crypto.models.ProxyAmount
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.math.BigDecimal
class UserWalletManagerImpl(
private val walletManagersFacade: WalletManagersFacade,
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
private val dispatchers: CoroutineDispatcherProvider,
) : UserWalletManager {
override fun getWalletId(): String {
val selectedUserWallet = requireNotNull(
getSelectedWalletUseCase.sync().getOrNull(),
) { "selectedUserWallet shouldn't be null" }
return selectedUserWallet.walletId.stringValue
}
override suspend fun hideAllTokens() {
// FIXME: Used only in Tester Actions
Timber.w("Not implemented")
}
override suspend fun getWalletAddress(networkId: String, derivationPath: String?): String {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
val walletManager = getActualWalletManager(blockchain, derivationPath)
return walletManager.wallet.address
}
override suspend fun getLastTransactionHash(networkId: String, derivationPath: String?): String? {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
val walletManager = getActualWalletManager(blockchain, derivationPath)
return walletManager.wallet.recentTransactions
.lastOrNull { it.hash?.isNotEmpty() == true }
?.hash
}
override suspend fun getNativeTokenBalance(networkId: String, derivationPath: String?): ProxyAmount? {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
val walletManager = getActualWalletManager(blockchain, derivationPath)
return walletManager.wallet.amounts.firstNotNullOfOrNull { amountEntry ->
amountEntry.takeIf { amountEntry.key is AmountType.Coin }
}?.value?.let { amount ->
ProxyAmount(
amount.currencySymbol,
amount.value ?: BigDecimal.ZERO,
amount.decimals,
)
}
}
@Throws(IllegalArgumentException::class)
private suspend fun getActualWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager {
val selectedUserWallet = requireNotNull(
getSelectedWalletUseCase.sync().getOrNull(),
) { "userWallet or userWalletsListManager is null" }
val walletManager = withContext(dispatchers.io) {
walletManagersFacade.getOrCreateWalletManager(
selectedUserWallet.walletId,
blockchain,
derivationPath,
)
}
return requireNotNull(walletManager) {
"No wallet manager found"
}
}
}

View file

@ -1,11 +1,6 @@
package com.tangem.tap.proxy.di
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.lib.crypto.UserWalletManager
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.UserWalletManagerImpl
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -21,18 +16,4 @@ internal object ProxyModule {
fun provideAppStateHolder(): AppStateHolder {
return AppStateHolder()
}
@Provides
@Singleton
fun provideUserWalletManager(
walletManagersFacade: WalletManagersFacade,
getSelectedWalletUseCase: GetSelectedWalletUseCase,
dispatchers: CoroutineDispatcherProvider,
): UserWalletManager {
return UserWalletManagerImpl(
walletManagersFacade = walletManagersFacade,
getSelectedWalletUseCase = getSelectedWalletUseCase,
dispatchers = dispatchers,
)
}
}

View file

@ -6,4 +6,9 @@
<certificates src="user" />
</trust-anchors>
</base-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">10.0.2.2</domain>
<domain includeSubdomains="false">localhost</domain>
</domain-config>
</network-security-config>

View file

@ -6,6 +6,7 @@ import com.tangem.common.ui.R
import com.tangem.core.ui.components.account.AccountCharIcon
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.components.account.AccountResIcon
import com.tangem.core.ui.components.account.PaymentAccountIcon
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.domain.models.account.CryptoPortfolioIcon
@ -16,7 +17,7 @@ import com.tangem.domain.models.account.CryptoPortfolioIcon.Color
* - a single character (derived from the [name]) using [AccountCharIcon], or
* - a predefined vector resource (from [icon]) using [AccountResIcon].
*
* The background color is taken from [CryptoPortfolioIconUM.color],
* The background color is taken from [AccountIconUM.CryptoPortfolio.color],
* and the icon size, text style, and container shape are adapted based on the given [size].
*
* @param name A [TextReference] used to resolve and display the first letter
@ -31,29 +32,43 @@ import com.tangem.domain.models.account.CryptoPortfolioIcon.Color
@Composable
fun AccountIcon(
name: TextReference,
icon: CryptoPortfolioIconUM,
icon: AccountIconUM.CryptoPortfolio,
size: AccountIconSize,
modifier: Modifier = Modifier,
) {
val letter = name.resolveReference().firstOrNull()
val iconColor = icon.color.getUiColor()
when {
icon.value == CryptoPortfolioIcon.Icon.Letter && letter == null -> {
AccountResIcon(
resId = R.drawable.ic_tangem_24,
color = icon.color.getUiColor(),
color = iconColor,
size = size,
modifier = modifier,
)
}
icon.value == CryptoPortfolioIcon.Icon.Letter -> AccountCharIcon(
char = letter ?: 'N',
color = icon.color.getUiColor(),
color = iconColor,
size = size,
modifier = modifier,
)
else -> AccountResIcon(
resId = icon.value.getResId(),
color = icon.color.getUiColor(),
color = iconColor,
size = size,
modifier = modifier,
)
}
}
@Composable
fun AccountIcon(name: TextReference, icon: AccountIconUM, size: AccountIconSize, modifier: Modifier = Modifier) {
when (icon) {
is AccountIconUM.Payment -> PaymentAccountIcon(size = size, modifier = modifier)
is AccountIconUM.CryptoPortfolio -> AccountIcon(
name = name,
icon = icon,
size = size,
modifier = modifier,
)
@ -62,7 +77,7 @@ fun AccountIcon(
object AccountIconPreviewData {
fun randomAccountIcon(letter: Boolean = false) = CryptoPortfolioIconUM(
fun randomAccountIcon(letter: Boolean = false) = AccountIconUM.CryptoPortfolio(
value = if (letter) CryptoPortfolioIcon.Icon.Letter else CryptoPortfolioIcon.Icon.entries.random(),
color = Color.entries.random(),
)

View file

@ -0,0 +1,10 @@
package com.tangem.common.ui.account
import com.tangem.domain.models.account.CryptoPortfolioIcon.Color
import com.tangem.domain.models.account.CryptoPortfolioIcon.Icon
sealed class AccountIconUM {
data class CryptoPortfolio(val value: Icon, val color: Color) : AccountIconUM()
data object Payment : AccountIconUM()
}

View file

@ -27,7 +27,7 @@ import com.tangem.core.ui.res.TangemTheme
@Composable
fun AccountLabel(
name: TextReference,
icon: CryptoPortfolioIconUM,
icon: AccountIconUM,
iconSize: AccountIconSize,
modifier: Modifier = Modifier,
nameStyle: TextStyle = TangemTheme.typography.subtitle2,

View file

@ -48,13 +48,15 @@ class AccountPortfolioItemUMConverter(
)
UserWalletItemUM.Information.Loaded(text)
}
is Account.Payment -> TODO("[REDACTED_JIRA]")
}
private fun getImageState(account: Account.CryptoPortfolio) = when (account) {
is Account.CryptoPortfolio -> UserWalletItemUM.ImageState.Account(
name = account.accountName.toUM().value,
icon = account.icon.toUM(),
icon = CryptoPortfolioIconConverter.convert(account.icon),
)
is Account.Payment -> TODO("[REDACTED_JIRA]")
}
private fun getBalanceInfo(): UserWalletItemUM.Balance {

View file

@ -41,7 +41,7 @@ import com.tangem.core.ui.res.TangemThemePreview
fun AccountRow(
title: TextReference,
subtitle: TextReference,
icon: CryptoPortfolioIconUM,
icon: AccountIconUM.CryptoPortfolio,
modifier: Modifier = Modifier,
isReverse: Boolean = false,
) {

View file

@ -1,7 +1,9 @@
package com.tangem.common.ui.account
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@ -9,11 +11,16 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.SendScreenTestTags
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.utils.StringsSigns
/**
* A composable function that displays an account label (icon + name) with an optional prefix.
@ -61,4 +68,28 @@ fun AccountTitle(
)
}
}
}
@Preview
@Composable
private fun PreviewAccountTitle() {
TangemThemePreview {
Column {
AccountTitle(
accountTitleUM = AccountTitleUM.Account(
prefixText = stringReference(StringsSigns.DOT),
name = stringReference("Main Wallet"),
icon = AccountIconUM.CryptoPortfolio(
value = CryptoPortfolioIcon.Icon.Bookmark,
color = CryptoPortfolioIcon.Color.Pattypan,
),
),
modifier = Modifier.padding(4.dp),
)
AccountTitle(
accountTitleUM = AccountTitleUM.Account.payment(prefixText = stringReference(StringsSigns.DOT)),
modifier = Modifier.padding(4.dp),
)
}
}
}

View file

@ -1,7 +1,9 @@
package com.tangem.common.ui.account
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
/**
* A sealed interface representing the title of an account, which can be either a simple text
@ -19,6 +21,16 @@ sealed interface AccountTitleUM {
data class Account(
val prefixText: TextReference,
val name: TextReference,
val icon: CryptoPortfolioIconUM,
) : AccountTitleUM
val icon: AccountIconUM,
) : AccountTitleUM {
companion object {
fun payment(prefixText: TextReference = TextReference.EMPTY): Account {
return Account(
prefixText = prefixText,
name = resourceReference(R.string.tangempay_payment_account),
icon = AccountIconUM.Payment,
)
}
}
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.common.ui.account
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.utils.converter.TwoWayConverter
object CryptoPortfolioIconConverter : TwoWayConverter<CryptoPortfolioIcon, AccountIconUM.CryptoPortfolio> {
override fun convert(value: CryptoPortfolioIcon): AccountIconUM.CryptoPortfolio {
return AccountIconUM.CryptoPortfolio(value = value.value, color = value.color)
}
override fun convertBack(value: AccountIconUM.CryptoPortfolio): CryptoPortfolioIcon {
return CryptoPortfolioIcon.ofCustomAccount(value = value.value, color = value.color)
}
}

View file

@ -47,7 +47,4 @@ fun CryptoPortfolioIcon.Icon.getResId(): Int {
CryptoPortfolioIcon.Icon.Package -> R.drawable.ic_package_24
CryptoPortfolioIcon.Icon.Gift -> R.drawable.ic_gift_24
}
}
fun CryptoPortfolioIcon.toUM() = CryptoPortfolioIconUM(domainModel = this)
fun CryptoPortfolioIconUM.toDomain() = CryptoPortfolioIcon.ofCustomAccount(value = this.value, color = this.color)
}

View file

@ -1,15 +0,0 @@
package com.tangem.common.ui.account
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.domain.models.account.CryptoPortfolioIcon.Color
import com.tangem.domain.models.account.CryptoPortfolioIcon.Icon
data class CryptoPortfolioIconUM(
val value: Icon,
val color: Color,
) {
constructor(domainModel: CryptoPortfolioIcon) : this(
value = domainModel.value,
color = domainModel.color,
)
}

View file

@ -82,7 +82,7 @@ fun PortfolioSelectRow(
@Immutable
data class PortfolioSelectUM(
val icon: CryptoPortfolioIconUM?,
val icon: AccountIconUM.CryptoPortfolio?,
val name: TextReference,
val isAccountMode: Boolean,
val isMultiChoice: Boolean,

View file

@ -1,6 +1,7 @@
package com.tangem.common.ui.amountScreen.converters
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.common.ui.account.CryptoPortfolioIconConverter
import com.tangem.common.ui.account.toUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.models.account.Account
@ -15,7 +16,7 @@ class AmountAccountConverter(
return if (value != null && isAccountsMode) {
AccountTitleUM.Account(
name = value.accountName.toUM().value,
icon = value.icon.toUM(),
icon = CryptoPortfolioIconConverter.convert(value.icon),
prefixText = prefixText,
)
} else {

View file

@ -5,7 +5,7 @@ import androidx.compose.foundation.text.KeyboardOptions
import com.tangem.common.ui.R
import com.tangem.common.ui.account.AccountNameUM
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.common.ui.account.toUM
import com.tangem.common.ui.account.CryptoPortfolioIconConverter
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
@ -88,7 +88,7 @@ object AmountStatePreviewData {
val amountStateV2Accounts = amountState.copy(
accountTitleUM = AccountTitleUM.Account(
name = AccountNameUM.DefaultMain.value,
icon = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM(),
icon = CryptoPortfolioIconConverter.convert(CryptoPortfolioIcon.ofDefaultCustomAccount()),
prefixText = resourceReference(R.string.common_from),
),
availableBalanceCrypto = stringReference("2 130,81231238 USDT"),

View file

@ -1,15 +1,23 @@
package com.tangem.common.ui.bottomsheet.chooseaddress
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.common.ui.bottomsheet.receive.AddressModel
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SimpleSettingsRow
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.collections.immutable.persistentListOf
@Composable
fun ChooseAddressBottomSheet(config: TangemBottomSheetConfig) {
@ -31,4 +39,40 @@ private fun ChooseAddressBottomSheetContent(content: ChooseAddressBottomSheetCon
)
}
}
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun ChooseAddressBottomSheet_Preview(
@PreviewParameter(ChooseAddressBottomSheetPreviewProvider::class) params: ChooseAddressBottomSheetConfig,
) {
TangemThemePreview {
ChooseAddressBottomSheetContent(params)
}
}
private class ChooseAddressBottomSheetPreviewProvider : PreviewParameterProvider<ChooseAddressBottomSheetConfig> {
override val values: Sequence<ChooseAddressBottomSheetConfig>
get() = sequenceOf(
ChooseAddressBottomSheetConfig(
addressModels = persistentListOf(
AddressModel(
displayName = stringReference("Main address"),
fullName = stringReference("Bitcoin"),
value = "0x1234...abcd",
type = AddressModel.Type.Default,
),
AddressModel(
displayName = stringReference("Legacy address"),
fullName = stringReference("Bitcoin"),
value = "0x1234...abcd",
type = AddressModel.Type.Default,
),
),
onClick = {},
),
)
}
// endregion

View file

@ -1,10 +1,9 @@
package com.tangem.common.ui.bottomsheet.chooseaddress
import com.tangem.common.ui.bottomsheet.receive.AddressModel
import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig
import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.NetworkAddress
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@ -14,13 +13,12 @@ class ChooseAddressBottomSheetConfig(
val onClick: (AddressModel) -> Unit,
) : TangemBottomSheetConfigContent {
constructor(
asset: TokenReceiveBottomSheetConfig.Asset,
network: Network,
currency: CryptoCurrency,
networkAddress: NetworkAddress,
onClick: (AddressModel) -> Unit,
) : this(
addressModels = networkAddress.availableAddresses
.mapToAddressModels(asset, network)
.mapToAddressModels(cryptoCurrency = currency)
.toImmutableList(),
onClick = onClick,
)

View file

@ -1,9 +1,7 @@
package com.tangem.common.ui.bottomsheet.receive
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
@ -20,20 +18,6 @@ fun Set<NetworkAddress.Address>.mapToAddressModels(cryptoCurrency: CryptoCurrenc
cryptoCurrency.network,
)
fun Set<NetworkAddress.Address>.mapToAddressModels(
asset: TokenReceiveBottomSheetConfig.Asset,
network: Network,
): List<AddressModel> = when (asset) {
is TokenReceiveBottomSheetConfig.Asset.Currency -> mapToAddressModels(
stringReference("${asset.name} (${asset.symbol})"),
network,
)
is TokenReceiveBottomSheetConfig.Asset.NFT -> mapToAddressModels(
resourceReference(R.string.common_nft),
network,
)
}
private fun Set<NetworkAddress.Address>.mapToAddressModels(name: TextReference, network: Network): List<AddressModel> =
this
.sortedBy { it.type.ordinal }

View file

@ -1,330 +0,0 @@
package com.tangem.common.ui.bottomsheet.receive
import android.content.res.Configuration
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import com.tangem.core.res.getStringSafe
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SecondaryButtonIconStart
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.components.rememberQrPainters
import com.tangem.core.ui.components.snackbar.CopiedTextSnackbarHost
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.launch
@Composable
fun TokenReceiveBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet(config) { content: TokenReceiveBottomSheetConfig ->
TokenReceiveBottomSheetContent(content = content)
}
}
@Composable
private fun TokenReceiveBottomSheetContent(content: TokenReceiveBottomSheetConfig) {
var selectedAddress by remember { mutableStateOf(content.addresses.first()) }
val snackbarHostState = remember(::SnackbarHostState)
ContainerWithSnackbarHost(snackbarHostState = snackbarHostState) {
Column(
modifier = Modifier
.verticalScroll(state = rememberScrollState())
.padding(top = 24.dp, bottom = 16.dp)
.padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(space = 24.dp),
) {
QrCodeContent(content = content, onAddressChange = { selectedAddress = it })
Info(
showMemoDisclaimer = content.showMemoDisclaimer,
notifications = content.notifications,
)
Buttons(
snackbarHostState = snackbarHostState,
onShareClick = { content.onShareClick(selectedAddress.value) },
onCopyClick = { content.onCopyClick(selectedAddress.value) },
)
}
}
}
@Composable
private fun Info(
showMemoDisclaimer: Boolean,
notifications: ImmutableList<NotificationConfig>,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(space = 16.dp),
) {
if (showMemoDisclaimer) {
Text(
modifier = Modifier
.padding(horizontal = 18.dp)
.fillMaxWidth(),
text = stringResourceSafe(R.string.receive_bottom_sheet_no_memo_required_message),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
)
}
notifications.fastForEach { notificationConfig ->
key(notificationConfig.hashCode()) {
Notification(config = notificationConfig)
}
}
}
}
@Composable
private fun ContainerWithSnackbarHost(snackbarHostState: SnackbarHostState, content: @Composable () -> Unit) {
Box {
content()
CopiedTextSnackbarHost(
hostState = snackbarHostState,
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 80.dp),
)
}
}
@Composable
private fun QrCodeContent(content: TokenReceiveBottomSheetConfig, onAddressChange: (AddressModel) -> Unit) {
val qrCodes = rememberQrPainters(content.addresses.map(AddressModel::value))
val pagerState = rememberPagerState(
initialPage = 0,
initialPageOffsetFraction = 0f,
pageCount = content.addresses::count,
)
LaunchedEffect(key1 = pagerState.currentPage) {
onAddressChange.invoke(content.addresses[pagerState.currentPage])
}
HorizontalPager(state = pagerState) { currentPage ->
QrCodePage(
content = content,
qrCodePainter = qrCodes[currentPage],
currentIndex = currentPage,
)
}
if (pagerState.pageCount > 1) {
val indicatorState = rememberLazyListState()
val selectedColor = TangemTheme.colors.icon.primary1
val unselectedColor = TangemTheme.colors.icon.informative
LazyRow(
modifier = Modifier.height(20.dp),
state = indicatorState,
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
repeat(pagerState.pageCount) { iteration ->
item(key = iteration) {
val color by animateColorAsState(
targetValue = if (pagerState.currentPage == iteration) selectedColor else unselectedColor,
label = "",
)
Box(
modifier = Modifier
.padding(horizontal = 4.dp, vertical = 6.dp)
.background(color = color, shape = CircleShape)
.size(7.dp),
)
}
}
}
}
}
@Composable
private fun QrCodePage(content: TokenReceiveBottomSheetConfig, qrCodePainter: Painter, currentIndex: Int) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(24.dp),
) {
Text(
text = stringResourceSafe(
R.string.receive_bottom_sheet_warning_message_compact,
content.addresses[currentIndex].fullName.resolveReference(),
content.network,
),
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
style = TangemTheme.typography.h3,
)
Image(
painter = qrCodePainter,
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier.size(248.dp),
)
Text(
text = content.addresses[currentIndex].value,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
style = TangemTheme.typography.subtitle1,
)
}
}
@Composable
private fun Buttons(
snackbarHostState: SnackbarHostState,
onShareClick: () -> Unit,
onCopyClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val hapticFeedback = LocalHapticFeedback.current
val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current
val resources = context.resources
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
SecondaryButtonIconStart(
modifier = Modifier.weight(1f),
text = stringResourceSafe(id = R.string.common_copy),
iconResId = R.drawable.ic_copy_24,
onClick = {
onCopyClick()
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
coroutineScope.launch {
snackbarHostState.showSnackbar(
message = resources.getStringSafe(R.string.wallet_notification_address_copied),
)
}
},
)
SecondaryButtonIconStart(
modifier = Modifier.weight(1f),
text = stringResourceSafe(id = R.string.common_share),
iconResId = R.drawable.ic_share_24,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onShareClick()
},
)
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun Preview_TokenReceiveBottomSheet(
@PreviewParameter(TokenReceiveBottomSheetConfigPreviewProvider::class) params: TokenReceiveBottomSheetConfig,
) {
TangemThemePreview {
val config = TangemBottomSheetConfig(
isShown = true,
content = params,
onDismissRequest = {},
)
TokenReceiveBottomSheet(config)
}
}
private class TokenReceiveBottomSheetConfigPreviewProvider : PreviewParameterProvider<TokenReceiveBottomSheetConfig> {
val address = AddressModel(
displayName = stringReference("Address 1"),
fullName = combinedReference(
stringReference("Address 1"),
stringReference(" Stellar"),
),
value = "0xe5178c7d4d0e861ed2e9414e045b501226b0de8d",
type = AddressModel.Type.Default,
)
private val baseConfig = TokenReceiveBottomSheetConfig(
asset = TokenReceiveBottomSheetConfig.Asset.Currency(
name = "Stellar",
symbol = "XLM",
),
network = "Ethereum",
notifications = persistentListOf(
NotificationConfig(
title = stringReference("Send only XLM on the Ethereum network"),
subtitle = resourceReference(R.string.receive_bottom_sheet_warning_message_description),
iconResId = R.drawable.ic_alert_circle_24,
),
),
addresses = persistentListOf(address),
showMemoDisclaimer = false,
onCopyClick = {},
onShareClick = {},
)
override val values: Sequence<TokenReceiveBottomSheetConfig>
get() = sequenceOf(
baseConfig,
baseConfig.copy(
addresses = persistentListOf(
address,
address.copy(displayName = stringReference("Address 2")),
),
),
baseConfig.copy(
showMemoDisclaimer = true,
),
baseConfig.copy(
showMemoDisclaimer = true,
addresses = persistentListOf(
address,
address.copy(displayName = stringReference("Address 2")),
),
),
)
}
// endregion Preview

View file

@ -1,72 +0,0 @@
package com.tangem.common.ui.bottomsheet.receive
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.R
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.network.NetworkAddress
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
data class TokenReceiveBottomSheetConfig(
val asset: Asset,
val network: String,
val addresses: ImmutableList<AddressModel>,
val showMemoDisclaimer: Boolean,
val notifications: ImmutableList<NotificationConfig>,
val onCopyClick: (String) -> Unit,
val onShareClick: (String) -> Unit,
) : TangemBottomSheetConfigContent {
constructor(
asset: Asset,
network: Network,
networkAddress: NetworkAddress,
showMemoDisclaimer: Boolean,
customNotifications: ImmutableList<NotificationConfig> = persistentListOf(),
onCopyClick: (String) -> Unit,
onShareClick: (String) -> Unit,
) : this(
asset = asset,
network = network.name,
addresses = networkAddress.availableAddresses
.mapToAddressModels(asset, network)
.toImmutableList(),
showMemoDisclaimer = showMemoDisclaimer,
notifications = persistentListOf(defaultAssetNotificationConfig(asset, network)).addAll(0, customNotifications),
onCopyClick = onCopyClick,
onShareClick = onShareClick,
)
companion object {
private fun defaultAssetNotificationConfig(asset: Asset, network: Network): NotificationConfig =
NotificationConfig(
title = resourceReference(
R.string.receive_bottom_sheet_warning_title,
wrappedList(asset.displaySymbol, network.name),
),
subtitle = resourceReference(R.string.receive_bottom_sheet_warning_message_description),
iconResId = R.drawable.ic_alert_circle_24,
iconTint = NotificationConfig.IconTint.Accent,
)
}
@Immutable
sealed class Asset {
abstract val displaySymbol: TextReference
data class Currency(val name: String, val symbol: String) : Asset() {
override val displaySymbol = stringReference(symbol)
}
data object NFT : Asset() {
override val displaySymbol = resourceReference(R.string.common_nft)
}
}
}

View file

@ -24,6 +24,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.common.ui.navigationButtons.preview.NavigationButtonsPreview
import com.tangem.core.ui.components.HoldToConfirmButton
import com.tangem.core.ui.components.Keyboard
import com.tangem.core.ui.components.SecondaryButtonIconStart
import com.tangem.core.ui.components.buttons.common.TangemButton
@ -98,34 +99,48 @@ fun NavigationPrimaryButton(primaryButton: NavigationButton?, modifier: Modifier
modifier = modifier.fillMaxWidth(),
) { button ->
if (button != null && button.textReference != TextReference.EMPTY) {
val icon = if (button.iconRes != null && button.isIconVisible) {
TangemButtonIconPosition.End(iconResId = button.iconRes)
} else {
TangemButtonIconPosition.None
}
val color = if (button.isDimmed) {
TangemButtonsDefaults.secondaryButtonColors
.copy(contentColor = TangemTheme.colors.text.tertiary)
} else {
TangemButtonsDefaults.primaryButtonColors
}
TangemButton(
text = button.textReference.resolveReference(),
enabled = button.isEnabled,
onClick = {
GlobalMultipleClickPreventer.processEvent {
if (button.isHapticClick) {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
if (button.isHoldToConfirm) {
HoldToConfirmButton(
text = button.textReference.resolveReference(),
enabled = button.isEnabled,
isLoading = button.shouldShowProgress,
onConfirm = {
GlobalMultipleClickPreventer.processEvent {
button.onClick()
}
button.onClick()
}
},
showProgress = button.shouldShowProgress,
colors = color,
textStyle = TangemTheme.typography.subtitle1,
icon = icon,
modifier = Modifier.fillMaxWidth(),
)
},
modifier = Modifier.fillMaxWidth(),
)
} else {
val icon = if (button.iconRes != null && button.isIconVisible) {
TangemButtonIconPosition.End(iconResId = button.iconRes)
} else {
TangemButtonIconPosition.None
}
val color = if (button.isDimmed) {
TangemButtonsDefaults.secondaryButtonColors
.copy(contentColor = TangemTheme.colors.text.tertiary)
} else {
TangemButtonsDefaults.primaryButtonColors
}
TangemButton(
text = button.textReference.resolveReference(),
enabled = button.isEnabled,
onClick = {
GlobalMultipleClickPreventer.processEvent {
if (button.isHapticClick) {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
}
button.onClick()
}
},
showProgress = button.shouldShowProgress,
colors = color,
textStyle = TangemTheme.typography.subtitle1,
icon = icon,
modifier = Modifier.fillMaxWidth(),
)
}
} else {
Spacer(modifier = Modifier.fillMaxWidth())
}

View file

@ -34,5 +34,6 @@ data class NavigationButton(
val isEnabled: Boolean = true,
val isDimmed: Boolean = false,
val isHapticClick: Boolean = false,
val isHoldToConfirm: Boolean = false,
val onClick: () -> Unit,
)

View file

@ -1,6 +1,6 @@
package com.tangem.common.ui.userwallet.state
import com.tangem.common.ui.account.CryptoPortfolioIconUM
import com.tangem.common.ui.account.AccountIconUM
import com.tangem.core.ui.components.artwork.ArtworkUM
import com.tangem.core.ui.extensions.TextReference
import javax.annotation.concurrent.Immutable
@ -64,7 +64,7 @@ data class UserWalletItemUM(
data class Account(
val name: TextReference,
val icon: CryptoPortfolioIconUM,
val icon: AccountIconUM.CryptoPortfolio,
) : ImageState()
data class Image(

View file

@ -89,6 +89,7 @@ sealed class AnalyticsParam {
data object ImportWallet : ScreensSources("Import Wallet")
data object CreateWalletIntro : ScreensSources("Create Wallet Intro")
data object AddNewWallet : ScreensSources("Add New Wallet")
data object AddNew : ScreensSources("Add New")
data object CreateWallet : ScreensSources("Create Wallet")
data object NewsList : ScreensSources("News List")
data object NewsLink : ScreensSources("News Link")

View file

@ -47,10 +47,6 @@
"name": "TANGEM_PAY_ENABLED",
"version": "5.31.0"
},
{
"name": "NEW_TOKEN_RECEIVE_ENABLED",
"version": "5.28.0"
},
{
"name": "YIELD_SUPPLY_FEATURE_ENABLED",
"version": "5.30.0"
@ -82,5 +78,9 @@
{
"name": "SWAP_MARKET_LIST_ENABLED",
"version": "undefined"
},
{
"name": "EARN_BLOCK_ENABLED",
"version": "undefined"
}
]

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.promotion.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class PromoBannerV2Response(
@Json(name = "promotions")
val promotions: List<PromoBannerResponse>,
)

View file

@ -2,6 +2,7 @@ package com.tangem.datasource.api.tangemTech
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.promotion.models.PromoBannerResponse
import com.tangem.datasource.api.promotion.models.PromoBannerV2Response
import com.tangem.datasource.api.promotion.models.StoryContentResponse
import com.tangem.datasource.api.tangemTech.models.*
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
@ -188,6 +189,12 @@ interface TangemTechApi {
@Query("programName") name: String,
@Header("Cache-Control") cacheControl: String = "max-age=600",
): ApiResponse<PromoBannerResponse>
@GET("/v2/promotion")
suspend fun getPromoBannersV2(
@Query("walletId") walletId: String,
@Header("Cache-Control") cacheControl: String = "max-age=600",
): ApiResponse<PromoBannerV2Response>
// endregion
/**

View file

@ -18,6 +18,7 @@ import com.tangem.datasource.api.utils.WriteTimeout
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.utils.NetworkLogsSaveInterceptor
import com.tangem.datasource.utils.WireMockRedirectInterceptor
import com.tangem.datasource.utils.addHeaders
import dagger.hilt.android.qualifiers.ApplicationContext
import okhttp3.Interceptor
@ -77,6 +78,7 @@ internal class RetrofitApiBuilder @Inject constructor(
.client(
OkHttpClient.Builder()
.applyApiConfig(apiConfigId = apiConfigId, environmentConfig = environmentConfig)
.applyWireMockRedirect()
.let {
if (applyTimeoutAnnotations) it.applyTimeoutAnnotations() else it
}
@ -181,6 +183,14 @@ internal class RetrofitApiBuilder @Inject constructor(
)
}
private fun OkHttpClient.Builder.applyWireMockRedirect(): OkHttpClient.Builder {
return if (BuildConfig.MOCK_DATA_SOURCE) {
addInterceptor(interceptor = WireMockRedirectInterceptor())
} else {
this
}
}
private fun OkHttpClient.Builder.addLoggers(apiConfigId: ApiConfig.ID, context: Context): OkHttpClient.Builder {
if (apiConfigId in excludedApiForLogging) return this

View file

@ -0,0 +1,40 @@
package com.tangem.datasource.utils
import okhttp3.Interceptor
import okhttp3.Response
import timber.log.Timber
/**
* OkHttp interceptor that redirects requests from wiremock.tests-d.com to a local WireMock instance.
*/
class WireMockRedirectInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val override = overriddenBaseUrl ?: return chain.proceed(chain.request())
val request = chain.request()
val url = request.url.toString()
if (url.contains(WIREMOCK_REMOTE_URL)) {
val newUrl = url.replace(WIREMOCK_REMOTE_URL, override.trimEnd('/'))
Timber.d("WireMockRedirect: $url -> $newUrl")
val newRequest = request.newBuilder()
.url(newUrl)
.build()
return chain.proceed(newRequest)
}
return chain.proceed(request)
}
companion object {
private const val WIREMOCK_REMOTE_URL = "[REDACTED_ENV_URL]"
/**
* Override base URL for WireMock requests.
* When null (default), requests go to wiremock.tests-d.com.
* When set (e.g., "http://localhost:8080"), requests are redirected to local WireMock instance.
*/
var overriddenBaseUrl: String? = null
}
}

View file

@ -899,6 +899,9 @@
<string name="notification_sepa_title">Krypto mit SEPA kaufen</string>
<string name="notification_visa_waitlist_promo_text">Trage dich in die Warteliste ein und erhalte eine Zahlungskarte, die es so noch nie gab.</string>
<string name="notification_visa_waitlist_promo_title">Tangem Visa Card</string>
<string name="notification_yield_promo_button">Bedingungen</string>
<string name="notification_yield_promo_text">Zahlen Sie mindestens $100 ein, halten Sie den Betrag 30 Tage und erhalten Sie $10.</string>
<string name="notification_yield_promo_title">Yield-Mode-Kampagne</string>
<string name="onboarding_access_code_feature_1_description">Du musst einen einzigen Zugangscode einrichten, um alle deine Geräte zu schützen</string>
<string name="onboarding_access_code_feature_1_title">Schützen</string>
<string name="onboarding_access_code_feature_2_description">Du kannst später auf jeder Karte oder Ring einen individuellen Zugangscode einrichten</string>

View file

@ -299,6 +299,7 @@
<string name="common_go_to_token">Ir al token</string>
<string name="common_got_it">Entendido</string>
<string name="common_hide">Ocultar</string>
<string name="common_hold_to">Mantener para %s</string>
<string name="common_hour">hora</string>
<string name="common_import">Importe</string>
<string name="common_in_progress">En progreso</string>
@ -323,6 +324,7 @@
<string name="common_no">No</string>
<string name="common_no_address">Ninguna dirección</string>
<string name="common_not_added">No agregadas</string>
<string name="common_not_available">No disponible</string>
<string name="common_not_now">Ahora no</string>
<string name="common_now">Ahora</string>
<string name="common_ok">OK</string>
@ -373,6 +375,7 @@
<string name="common_swap">Intercambiar</string>
<string name="common_tangem">Tangem</string>
<string name="common_tangem_wallet">Tangem Wallet</string>
<string name="common_tap_and_hold_hint">Toque y mantenga presionado</string>
<string name="common_terms_and_conditions">términos y condiciones</string>
<string name="common_terms_of_use">Condiciones de uso</string>
<string name="common_to">A</string>
@ -462,6 +465,11 @@
<string name="domain_receive_assets_onboarding_description">Enviar activos a otras redes resultará en una pérdida permanente.</string>
<string name="domain_receive_assets_onboarding_network_name">Red de %s</string>
<string name="domain_receive_assets_onboarding_title">Envíe fondos utilizando solo</string>
<string name="earn_best_opportunities">Las mejores oportunidades</string>
<string name="earn_filter_all_networks">Todas las redes</string>
<string name="earn_filter_all_types">Todos los tipos</string>
<string name="earn_mostly_used">Mayormente usado</string>
<string name="earn_title">Ganar</string>
<string name="email_preface_wc_error">Hola equipo de soporte, he encontrado un error con el código: %s</string>
<string name="email_subject_wc_error">Error de WalletConnect</string>
<string name="error_wrong_wallet_tapped">Ha usado una tarjeta de otra billetera. Toque la tarjeta asociada con esta billetera</string>
@ -561,6 +569,8 @@
<string name="feedback_subject_support_tangem">Comentarios en Tangem</string>
<string name="feedback_subject_tx_failed">No se puede completar una transacción</string>
<string name="feedback_token_description_error">Error de descripción de moneda</string>
<string name="gasless_not_enough_funds_to_cover_token_fee">Fondos insuficientes</string>
<string name="gasless_transaction_fee">Tarifa de transacción</string>
<string name="generic_error">Ocurrió un error</string>
<string name="generic_error_code">Ocurrió un error. Código: %s</string>
<string name="generic_requires_memo_error">Requiere memo</string>
@ -610,6 +620,7 @@
<string name="hw_backup_need_title">Finalizar la copia de seguridad primero</string>
<string name="hw_backup_no_backup">Incompleto</string>
<string name="hw_backup_section_other_title">Otros métodos</string>
<string name="hw_backup_seed_code_description">Guarde su frase de recuperación en un lugar seguro y manténgala privada para proteger sus fondos y configure un código de acceso para mayor seguridad.</string>
<string name="hw_backup_seed_description">Guarde su frase de recuperación en un lugar seguro y manténgala en privado para proteger sus fondos.</string>
<string name="hw_backup_seed_title">Frase de recuperación</string>
<string name="hw_backup_to_secure_description">Para proteger su billetera con un código de acceso, complete el proceso de copia de seguridad.</string>
@ -896,6 +907,9 @@
<string name="notification_sepa_title">Comprar criptomonedas con SEPA</string>
<string name="notification_visa_waitlist_promo_text">Únete a la lista de espera y consigue una tarjeta de pago como ninguna otra.</string>
<string name="notification_visa_waitlist_promo_title">Tangem Visa Card</string>
<string name="notification_yield_promo_button">Términos y condiciones</string>
<string name="notification_yield_promo_text">Deposite $100 o más, manténgalo durante 30 días y reciba $10.</string>
<string name="notification_yield_promo_title">¡Únase a la campaña Yield Mode!</string>
<string name="onboarding_access_code_feature_1_description">Configure un código de acceso único para proteger todos sus dispositivos.</string>
<string name="onboarding_access_code_feature_1_title">Proteger</string>
<string name="onboarding_access_code_feature_2_description">Puede configurar un código de acceso individual en cada tarjeta más adelante</string>
@ -995,6 +1009,7 @@
<string name="onboarding_wallet_info_title_fourth">Restaurar código de acceso</string>
<string name="onboarding_wallet_info_title_second">Tarjetas idénticas</string>
<string name="onboarding_wallet_info_title_third">Código de acceso</string>
<string name="only_one_mobile_wallet_explanation">Solo puede tener una billetera móvil. Actualícela a la billetera de hardware Tangem o añada una nueva billetera de hardware.</string>
<string name="onramp_all_offers_button_title">Todas las ofertas</string>
<string name="onramp_avaiable_with_payment_methods">Disponible con %s</string>
<string name="onramp_choose_provider_title_hint">La compra de criptomonedas en Tangem se lleva a cabo utilizando proveedores externos.</string>
@ -1266,6 +1281,7 @@
<string name="staking_amount_tron_integer_error">La cantidad de staking se redondeará a %1$s TRX debido a las reglas de la red.</string>
<string name="staking_amount_tron_integer_error_unstaking">La cantidad de unstaking se redondeará a %1$s TRX debido a las reglas de la red.</string>
<string name="staking_apr_earn_badge">APR %1$s%%</string>
<string name="staking_cardano_details_rewards_info_text">Sus recompensas de staking comenzarán después de 5 épocas (~25 días) mientras su delegación esté registrada y contabilizada por la red.</string>
<string name="staking_claim_unstaked">Reclamar lo que no está en staking</string>
<string name="staking_details_account_fee">Tarifa de staking de la cuenta </string>
<string name="staking_details_account_fee_info">Una cuenta de staking es una cuenta especial donde se almacenan los tokens SOL de staking. Se crea cuando delegas sus tokens a un validador para participar en la validación de transacciones y ganar recompensas. Se cobra una pequeña tarifa por crear la cuenta de staking, que se devuelve una vez que se completa el staking.</string>
@ -1276,6 +1292,7 @@
<string name="staking_details_apr">APR</string>
<string name="staking_details_apy">APY</string>
<string name="staking_details_auto_claiming_rewards_daily_text">Las recompensas se acumulan automáticamente en su saldo de staking diariamente.</string>
<string name="staking_details_autocompound_rewards_earned">Las recompensas se acumulan en su saldo de staking. Fondos ganados: %s</string>
<string name="staking_details_available">Disponible</string>
<string name="staking_details_average_reward_rate">Tasa de recompensa promedio</string>
<string name="staking_details_banner_text">¿Cómo funciona el staking?</string>
@ -1420,6 +1437,7 @@
<string name="swap_story_third_subtitle">Sencillo e intuitivo, permite cambiar tokens con solo unos cuantos toques</string>
<string name="swap_story_third_title">Simplemente cómodo</string>
<string name="swap_via_provider">Intercambio a través del proveedor</string>
<string name="swap_your_assets_title">Sus activos</string>
<string name="swapping_alert_cex_description">La cantidad incluye:\n• Tarifas del proveedor de servicios\n• Tarifas de red por enviar %s desde el intercambio a la dirección del usuario.</string>
<string name="swapping_alert_cex_description_with_slippage">El importe incluye:\n- comisión del proveedor de servicios\n- comisión de red por el envío de %1$s desde el exchange de vuelta a la dirección del usuario. \n\nEl deslizamiento (slippage) del proveedor es de hasta %2$s</string>
<string name="swapping_alert_dex_description">El importe incluye los honorarios del proveedor de servicios.</string>
@ -1434,6 +1452,7 @@
<string name="swapping_insufficient_funds">Fondos insuficientes</string>
<string name="swapping_permission_header">Dar autorización</string>
<string name="swapping_swap_action">Intercambiar</string>
<string name="swapping_swap_action_in_progress">Intercambiando...</string>
<string name="swapping_to_title">Usted recibe</string>
<string name="swapping_token_list_title">Elige token</string>
<string name="swapping_token_not_available">no disponible</string>
@ -1452,6 +1471,9 @@
<string name="tangem_pay_freeze_card_freeze">Mantén tu dinero seguro. Puedes desbloquear en cualquier momento.</string>
<string name="tangem_pay_freeze_card_success">Tu tarjeta está congelada.</string>
<string name="tangem_pay_get_help">Obtener ayuda</string>
<string name="tangem_pay_history_item_spend_mc_declined_reason">Razón: %s</string>
<string name="tangem_pay_history_item_spend_mc_title_format" formatted="false">%s · %s</string>
<string name="tangem_pay_history_item_spend_mcc">MCC %s</string>
<string name="tangem_pay_other">Otro</string>
<string name="tangem_pay_rooted_device_subtitle">No se puede usar en dispositivos rooteados</string>
<string name="tangem_pay_status_completed">Completado</string>
@ -1528,10 +1550,15 @@
<string name="tangempay_kyc_confirm_cancellation_description">¿Seguro que desea detener el proceso KYC? Puede retomarlo en cualquier momento.</string>
<string name="tangempay_kyc_failed_description">No pudimos verificar tu perfil. Si tienes alguna pregunta, contacta con el soporte.</string>
<string name="tangempay_kyc_failed_title">Lamentablemente, no pudimos verificar tu identidad</string>
<string name="tangempay_kyc_has_failed">El proceso KYC ha fallado</string>
<string name="tangempay_kyc_in_progress">KYC en curso</string>
<string name="tangempay_kyc_in_progress_notification_button">Ver estado</string>
<string name="tangempay_kyc_in_progress_notification_title">KYC en progreso para Tangem Pay</string>
<string name="tangempay_kyc_in_progress_popup_description">Los documentos suelen verificarse automáticamente en menos de 5 minutos. En casos excepcionales que requieran revisión manual, el proceso puede tardar hasta 48 horas.</string>
<string name="tangempay_kyc_rejected">KYC rechazado</string>
<string name="tangempay_kyc_rejected_button_text">Ocultar el bloque KYC</string>
<string name="tangempay_kyc_rejected_description">Lo sentimos, no pudimos verificarle</string>
<string name="tangempay_kyc_rejected_description_span">su perfil.</string>
<string name="tangempay_onboarding_banner_description">Obtén tu tarjeta virtual Tangem Visa gratuita</string>
<string name="tangempay_onboarding_banner_title">Usa USDC para pagos cotidianos</string>
<string name="tangempay_onboarding_get_card_button_text">Obtener tarjeta</string>
@ -1818,7 +1845,17 @@
<string name="warning_button_ok">Entendido</string>
<string name="warning_button_really_cool">¡Realmente genial!</string>
<string name="warning_button_refresh">Actualizar</string>
<string name="warning_clore_migration_button">Iniciar migración</string>
<string name="warning_clore_migration_copy_button">Copiar</string>
<string name="warning_clore_migration_error_signing_not_supported">La firma de mensajes no es compatible con esta red</string>
<string name="warning_clore_migration_error_wallet_manager_not_found">No se puede firmar el mensaje. Por favor, inténtelo de nuevo.</string>
<string name="warning_clore_migration_message">Según la documentación oficial de Clore, todas las monedas recibidas antes del 21 de diciembre serán migradas a Clore (token ERC-20); las monedas recibidas después de esa fecha no lo serán. Se está desarrollando una solución de transferencia — mantente atento.</string>
<string name="warning_clore_migration_message_label">Mensaje</string>
<string name="warning_clore_migration_open_portal_button">Abrir el portal de reclamaciones</string>
<string name="warning_clore_migration_sheet_description">Para mantener el acceso a sus fondos, comience la migración de acuerdo con las pautas oficiales de Clore.</string>
<string name="warning_clore_migration_sheet_title">Migración de la red Clore</string>
<string name="warning_clore_migration_sign_button">Firmar</string>
<string name="warning_clore_migration_signature_label">Firma</string>
<string name="warning_clore_migration_title">Migración de la red Clore</string>
<string name="warning_demo_mode_message">Actualmente estás en el modo Demo</string>
<string name="warning_demo_mode_title">Modo demo activo</string>

View file

@ -43,6 +43,7 @@
<string name="account_form_create_button">Ajouter un compte</string>
<string name="account_form_edit_button">Enregistrer</string>
<string name="account_form_name">Nom du compte</string>
<string name="account_form_name_already_exist_error_description">Un compte portant ce nom existe déjà. Veuillez choisir un autre nom.</string>
<string name="account_form_name_already_exist_error_title">Nom de compte déjà utilisé</string>
<string name="account_form_placeholder_edit_account">Compte</string>
<string name="account_form_placeholder_new_account">Nouveau compte</string>
@ -298,6 +299,7 @@
<string name="common_go_to_token">Aller au jeton</string>
<string name="common_got_it">Compris</string>
<string name="common_hide">Cacher</string>
<string name="common_hold_to">Maintenir contre %s</string>
<string name="common_hour">heure</string>
<string name="common_import">Importez</string>
<string name="common_in_progress">En cours</string>
@ -322,6 +324,7 @@
<string name="common_no">Non</string>
<string name="common_no_address">Aucune adresse</string>
<string name="common_not_added">Non ajouté</string>
<string name="common_not_available">Indisponible</string>
<string name="common_not_now">Pas maintenant</string>
<string name="common_now">Maintenant</string>
<string name="common_ok">OK</string>
@ -372,6 +375,7 @@
<string name="common_swap">Échanger</string>
<string name="common_tangem">Tangem</string>
<string name="common_tangem_wallet">Tangem Wallet</string>
<string name="common_tap_and_hold_hint">Appuyez et maintenez enfoncé</string>
<string name="common_terms_and_conditions">termes et conditions</string>
<string name="common_terms_of_use">Conditions d\'utilisation</string>
<string name="common_to">À</string>
@ -399,6 +403,8 @@
<string name="common_yield_mode">Mode Rendement</string>
<string name="contract_address_copied_message">Adresse du contrat copiée !</string>
<string name="currency_subtitle_expanded">Réseaux disponibles</string>
<string name="custom_token_another_account_dialog_description">La dérivation de votre token correspond à la dérivation de %1$s. Votre token sera ajouté à ce compte.</string>
<string name="custom_token_another_account_dialog_title">La dérivation appartient à un autre compte.</string>
<string name="custom_token_another_account_snackbar_text">Jeton ajouté au compte %1$s</string>
<string name="custom_token_contract_address_input_title">Adresse du contrat</string>
<string name="custom_token_creation_error_invalid_contract_address">L\'adresse du contrat n\'est pas valide</string>
@ -459,6 +465,11 @@
<string name="domain_receive_assets_onboarding_description">Lenvoi dactifs sur dautres réseaux entraînera une perte définitive.</string>
<string name="domain_receive_assets_onboarding_network_name">%s réseau</string>
<string name="domain_receive_assets_onboarding_title">Envoyez des fonds en utilisant uniquement</string>
<string name="earn_best_opportunities">Meilleures opportunités</string>
<string name="earn_filter_all_networks">Tous les réseaux</string>
<string name="earn_filter_all_types">Tous les types</string>
<string name="earn_mostly_used">Principalement utilisé</string>
<string name="earn_title">Gagner</string>
<string name="email_preface_wc_error">Bonjour équipe de support, jai rencontré une erreur avec le code : %s</string>
<string name="email_subject_wc_error">Erreur WalletConnect</string>
<string name="error_wrong_wallet_tapped">Vous avez utilisé une carte d\'un autre portefeuille. Appuyez sur la carte associée à ce portefeuille</string>
@ -558,6 +569,8 @@
<string name="feedback_subject_support_tangem">Commentaires sur Tangem</string>
<string name="feedback_subject_tx_failed">Impossible d\'effectuer une transaction</string>
<string name="feedback_token_description_error">Erreur de description de la pièce</string>
<string name="gasless_not_enough_funds_to_cover_token_fee">Fonds insuffisants</string>
<string name="gasless_transaction_fee">Frais de transaction</string>
<string name="generic_error">Une erreur s\'est produite</string>
<string name="generic_error_code">Une erreur s\'est produite. Code:%s</string>
<string name="generic_requires_memo_error">Un mémo est requis</string>
@ -607,6 +620,7 @@
<string name="hw_backup_need_title">Finalisez d\'abord la sauvegarde.</string>
<string name="hw_backup_no_backup">Incomplet</string>
<string name="hw_backup_section_other_title">Autres méthodes</string>
<string name="hw_backup_seed_code_description">Conservez votre seed phrase dans un endroit sûr et gardez-la confidentielle afin de protéger vos fonds, puis configurez un code d\'accès pour plus de sécurité.</string>
<string name="hw_backup_seed_description">Conservez votre seed phrase dans un endroit sûr et gardez-la confidentielle afin de protéger vos fonds.</string>
<string name="hw_backup_seed_title">Seed phrase</string>
<string name="hw_backup_to_secure_description">Pour sécuriser votre wallet avec un code d\'accès, effectuez la procédure de sauvegarde.</string>
@ -644,7 +658,7 @@
<string name="hw_upgrade_error_wallet2_card_required">Une erreur s\'est produite pendant l\'opération.</string>
<string name="hw_upgrade_funds_access_description">Vos fonds restent en sécurité et entièrement accessibles pendant toute la durée du processus.</string>
<string name="hw_upgrade_funds_access_title">Accès aux fonds</string>
<string name="hw_upgrade_general_security_description">Après la mise à niveau, votre portefeuille mobile sera supprimé de l\'application et stocké sur votre hardware wallet; votre seedphrase restera en votre possession.</string>
<string name="hw_upgrade_general_security_description">Après la mise à niveau, votre portefeuille mobile sera supprimé de l\'application et stocké sur votre hardware wallet; votre seed phrase restera en votre possession.</string>
<string name="hw_upgrade_general_security_title">Sécurité générale</string>
<string name="hw_upgrade_key_migration_description">Les clés privées seront transférées de l\'application vers votre hardware wallet Tangem.</string>
<string name="hw_upgrade_key_migration_title">Migration des clés</string>
@ -893,6 +907,9 @@
<string name="notification_sepa_title">Acheter des cryptomonnaies avec SEPA</string>
<string name="notification_visa_waitlist_promo_text">Rejoignez la liste dattente et obtenez une carte de paiement pas comme les autres.</string>
<string name="notification_visa_waitlist_promo_title">Tangem Visa Card</string>
<string name="notification_yield_promo_button">Conditions générales</string>
<string name="notification_yield_promo_text">Déposez 100 $ ou plus, conservez le dépôt pendant 30 jours et recevez 10 $.</string>
<string name="notification_yield_promo_title">Rejoignez la campagne Yield Mode !</string>
<string name="onboarding_access_code_feature_1_description">Vous devez définir un seul code d\'accès pour protéger tous vos appareils.</string>
<string name="onboarding_access_code_feature_1_title">Protéger</string>
<string name="onboarding_access_code_feature_2_description">Vous pourrez définir un code d\'accès individuel sur chaque carte plus tard</string>
@ -992,6 +1009,7 @@
<string name="onboarding_wallet_info_title_fourth">Restauration du code d\'accès</string>
<string name="onboarding_wallet_info_title_second">Cartes identiques</string>
<string name="onboarding_wallet_info_title_third">Code d\'accès</string>
<string name="only_one_mobile_wallet_explanation">Vous ne pouvez avoir qu\'un seul portefeuille mobile. Passez au portefeuille matériel Tangem ou ajoutez un nouveau portefeuille matériel.</string>
<string name="onramp_all_offers_button_title">Toutes les offres</string>
<string name="onramp_avaiable_with_payment_methods">Disponible avec %s</string>
<string name="onramp_choose_provider_title_hint">L\'achat de crypto-monnaies sur Tangem est assuré par des fournisseurs tiers selon leurs conditions.</string>
@ -1036,6 +1054,7 @@
<string name="onramp_title_available_from">Disponible auprès de</string>
<string name="onramp_title_available_up_to">Disponible jusqu\'à</string>
<string name="onramp_title_you_get">Vous obtenez</string>
<string name="onramp_tos_external_providers">Le service est fourni par un prestataire externe. Tangem n\'est pas responsable.</string>
<string name="onramp_transaction_status_footer_text">Vous pouvez vérifier l\'état de la transaction à partir de la page du jeton</string>
<string name="onramp_up_to_rate">Jusqu\'à</string>
<string name="onramp_via">Via</string>
@ -1248,18 +1267,21 @@
<string name="settings_forget_wallet_footer">Cela supprimera le portefeuille de l\'application. Le portefeuille lui-même peut être ajouté à nouveau.</string>
<string name="settings_upgrade_banner_advantage_feature_title">Facile à utiliser</string>
<string name="settings_upgrade_banner_description">Gardez vos cryptomonnaies en sécurité et hors ligne. Aussi fin qu\'une carte de crédit, plus sûr qu\'un coffre-fort bancaire.</string>
<string name="settings_upgrade_banner_seed_phrase_feature_title">Pas de seedphrase</string>
<string name="settings_upgrade_banner_seed_phrase_feature_title">Pas de seed phrase</string>
<string name="settings_upgrade_banner_superiority_feature_title">Le meilleur hardware wallet</string>
<string name="settings_upgrade_banner_title">Tangem Hardware Wallet</string>
<string name="settings_wallet_name_title">Nom</string>
<string name="stake_token_description">Faites travailler votre jeton pour vous</string>
<string name="staking_account_initialization_footer">Les frais de réseau sont une petite somme à payer pour traiter et confirmer ta transaction sur la blockchain.</string>
<string name="staking_account_initialization_message">Pour commencer à staker, votre compte TON doit être activé avec une transaction de 1 TON. Les fonds restent sur votre compte, car cette étape sert uniquement à l\'activer pour le staking.</string>
<string name="staking_account_initialization_title">Activation du compte</string>
<string name="staking_alert_network_fee_updated_message">Les frais de réseau ont changé. Veuillez vérifier le nouveau montant avant de continuer.</string>
<string name="staking_alert_network_fee_updated_title">Frais de réseau mis à jour</string>
<string name="staking_amount_requirement_error">Le montant à staker doit être au moins %s</string>
<string name="staking_amount_tron_integer_error">Le montant du staking sera arrondi à %1$s TRX en raison des règles du réseau.</string>
<string name="staking_amount_tron_integer_error_unstaking">Le montant d\'annulation du staking sera arrondi à %1$s TRX en raison des règles du réseau.</string>
<string name="staking_apr_earn_badge">TAEG %1$s%%</string>
<string name="staking_cardano_details_rewards_info_text">Vos récompenses de staking commenceront après 5 époques (~25 jours), une fois que votre délégation aura été enregistrée et comptabilisée par le réseau.</string>
<string name="staking_claim_unstaked">Réclamation déstakée</string>
<string name="staking_details_account_fee">Frais de staking du compte</string>
<string name="staking_details_account_fee_info">Un compte de staking est un compte spécial où sont stockés les jetons SOL stakés. Il est créé lorsque vous déléguez vos jetons à un validateur pour participer à la validation des transactions et gagner des récompenses. Des frais minimes sont facturés pour la création du compte de staking, qui sont restitués une fois le staking terminé.</string>
@ -1270,6 +1292,7 @@
<string name="staking_details_apr">APR</string>
<string name="staking_details_apy">APY</string>
<string name="staking_details_auto_claiming_rewards_daily_text">Les récompenses s\'accumulent automatiquement sur votre solde de staking quotidiennement.</string>
<string name="staking_details_autocompound_rewards_earned">Les récompenses sont ajoutées à votre solde de staking. Fonds gagnés : %s</string>
<string name="staking_details_available">Disponible</string>
<string name="staking_details_average_reward_rate">Taux de récompense moyen</string>
<string name="staking_details_banner_text">Qu\'est-ce que le Staking ?</string>
@ -1325,6 +1348,7 @@
<string name="staking_notification_restake_text">L\'option de restaker vous permet de déplacer vos fonds d\'un validateur à un autre sans avoir besoin de les déstaker.</string>
<string name="staking_notification_stake_entire_balance_text">Vous êtes sur le point de staker l\'intégralité de votre solde. Nous vous recommandons de laisser un petit montant pour couvrir les frais de réseau pour unstaking ou la réclamation des récompenses.</string>
<string name="staking_notification_ton_account_initialization_message">Pour commencer le staking, vous devez dabord activer votre compte TON</string>
<string name="staking_notification_ton_account_initialization_title">Activation du compte</string>
<string name="staking_notification_ton_activate_account">Pour commencer à staker TON, effectuez d\'abord une transaction sortante de n\'importe quel montant — cela activera votre portefeuille.</string>
<string name="staking_notification_ton_extra_reserve_info">Jusqu\'à 0,2 TON peuvent être requis en plus des frais de réseau pour terminer la transaction. Tout montant non utilisé sera remboursé.</string>
<string name="staking_notification_ton_extra_reserve_is_required">0,2 TON requis pour effectuer cette opération, en plus des frais de réseau. Veuillez recharger votre solde.</string>
@ -1413,6 +1437,7 @@
<string name="swap_story_third_subtitle">Simple et intuitif, vous permettant d\'échanger des jetons en quelques clics</string>
<string name="swap_story_third_title">Plus Simple Que Jamais</string>
<string name="swap_via_provider">Échange via le fournisseur</string>
<string name="swap_your_assets_title">Vos actifs</string>
<string name="swapping_alert_cex_description">Le montant comprend :\n• les frais du fournisseur de services\n• les frais de réseau pour l\'envoi de %s depuis l\'échange vers l\'adresse de l\'utilisateur.</string>
<string name="swapping_alert_cex_description_with_slippage">Le montant comprend :\n• les frais du fournisseur de services\n• les frais de réseau pour l\'envoi de %1$s depuis l\'échange vers l\'adresse de l\'utilisateur. \n\nLe slippage du fournisseur peut atteindre %2$s</string>
<string name="swapping_alert_dex_description">Le montant comprend les frais du fournisseur de services.</string>
@ -1427,6 +1452,7 @@
<string name="swapping_insufficient_funds">Fonds insuffisants</string>
<string name="swapping_permission_header">Donner l\'autorisation</string>
<string name="swapping_swap_action">Échanger</string>
<string name="swapping_swap_action_in_progress">Échange...</string>
<string name="swapping_to_title">Vous recevez</string>
<string name="swapping_token_list_title">Choisir le jeton</string>
<string name="swapping_token_not_available">non disponible</string>
@ -1445,6 +1471,9 @@
<string name="tangem_pay_freeze_card_freeze">Geler</string>
<string name="tangem_pay_freeze_card_success">Votre carte est gelée.</string>
<string name="tangem_pay_get_help">Obtenir de l\'aide</string>
<string name="tangem_pay_history_item_spend_mc_declined_reason">Raison: %s</string>
<string name="tangem_pay_history_item_spend_mc_title_format" formatted="false">%s · %s</string>
<string name="tangem_pay_history_item_spend_mcc">MCC %s</string>
<string name="tangem_pay_other">Autre</string>
<string name="tangem_pay_rooted_device_subtitle">Impossible à utiliser sur les appareils rootés</string>
<string name="tangem_pay_status_completed">Terminé</string>
@ -1521,10 +1550,15 @@
<string name="tangempay_kyc_confirm_cancellation_description">Êtes-vous sûr de vouloir interrompre le processus KYC ? Vous pouvez y revenir à tout moment.</string>
<string name="tangempay_kyc_failed_description">Nous n\'avons pas pu vérifier votre profil. Pour toute question, veuillez contacter le support.</string>
<string name="tangempay_kyc_failed_title">Malheureusement, nous n\'avons pas pu vérifier votre identité</string>
<string name="tangempay_kyc_has_failed">KYC a échoué</string>
<string name="tangempay_kyc_in_progress">KYC en cours</string>
<string name="tangempay_kyc_in_progress_notification_button">Voir le statut</string>
<string name="tangempay_kyc_in_progress_notification_title">KYC en cours pour Tangem Pay</string>
<string name="tangempay_kyc_in_progress_popup_description">Les documents sont généralement vérifiés automatiquement en moins de 5 minutes. Dans de rares cas nécessitant une vérification manuelle, le processus peut prendre jusquà 48 heures.</string>
<string name="tangempay_kyc_rejected">KYC rejeté</string>
<string name="tangempay_kyc_rejected_button_text">Masquer le bloc KYC</string>
<string name="tangempay_kyc_rejected_description">Désolé, nous n\'avons pas pu vérifier.</string>
<string name="tangempay_kyc_rejected_description_span">votre profil.</string>
<string name="tangempay_onboarding_banner_description">Obtenez votre carte virtuelle Tangem Visa gratuite</string>
<string name="tangempay_onboarding_banner_title">Utilisez USDC pour les paiements quotidiens</string>
<string name="tangempay_onboarding_get_card_button_text">Obtenir la carte</string>
@ -1579,6 +1613,7 @@
<string name="token_details_token_type_subtitle_no_standard">Jeton dans le %%image%% %1$s réseau</string>
<string name="token_details_unable_hide_alert_message">Le jeton %1$s (%2$s) est la principale devise du réseau %3$s et ne peut pas être masqué tant que vous avez d\'autres jetons de ce réseau dans la liste</string>
<string name="token_details_unable_hide_alert_title">Impossible de masquer %s</string>
<string name="token_receive_show_qr_code_title">Afficher le code QR</string>
<string name="token_swap_changelly_promotion_message">Échangez ce jeton contre un autre moyennant des frais de service de %1$s du %2$s au %3$s février.</string>
<string name="token_swap_changelly_promotion_title">Échangez avec Changelly, %s frais</string>
<string name="token_swap_promotion_button">Échangez maintenant</string>
@ -1710,6 +1745,12 @@
<string name="visa_unlock_notification_subtitle">Scannez votre carte pour déverrouiller l\'accès</string>
<string name="visa_unlock_notification_title">Déverrouillage nécessaire</string>
<string name="wallet_add_common_title">Choisissez comment ajouter votre portefeuille</string>
<string name="wallet_add_hardware_description">Scannez votre carte ou votre bague Tangem pour la restaurer ou l\'importer depuis un autre portefeuille.</string>
<string name="wallet_add_hardware_info_create">Créer un Hardware Wallet</string>
<string name="wallet_add_hardware_purchase">Vous souhaitez acheter un portefeuille Tangem ?</string>
<string name="wallet_add_import_seed_phrase">Importer la seed phrase</string>
<string name="wallet_add_mobile_description">Restaurez votre portefeuille sur votre téléphone ou importez-le depuis une autre application — pratique, mais moins sécurisé qu\'une carte Tangem.</string>
<string name="wallet_add_support_title">Que choisir ?</string>
<string name="wallet_balance_blockchain_unreachable_try_later">La blockchain n\'est pas accessible. Réessayez plus tard</string>
<string name="wallet_balance_missing_derivation">Scanner la carte ou la bague</string>
<string name="wallet_been_activated_message">Ce portefeuille a déjà été activé auparavant.\nSi cela n\'a pas été fait par vous, veuillez contacter le support.\nTangem ne vend jamais de portefeuilles avec le code d\'accès pré-généré.</string>
@ -1766,9 +1807,9 @@
<string name="wallet_import_navtitle">Ajouter un portefeuille existant</string>
<string name="wallet_import_scan_description">Dispositifs physiques qui stockent votre clé privée hors ligne en toute sécurité.</string>
<string name="wallet_import_scan_title">Scanner un Tangem Wallet</string>
<string name="wallet_import_seed_description">Importez un portefeuille existant à l\'aide de votre seedphrase.</string>
<string name="wallet_import_seed_description">Importez un portefeuille existant à l\'aide de votre seed phrase.</string>
<string name="wallet_import_seed_navtitle">Importer un wallet</string>
<string name="wallet_import_seed_title">Entrez la seedphrase</string>
<string name="wallet_import_seed_title">Entrez la seed phrase</string>
<string name="wallet_import_success_description">Vous avez importé votre portefeuille avec succès.</string>
<string name="wallet_import_success_navtitle">Importer un wallet</string>
<string name="wallet_import_success_title">Importation terminée</string>
@ -1984,7 +2025,7 @@
<string name="welcome_create_wallet_feature_delivery">Livraison rapide</string>
<string name="welcome_create_wallet_feature_one_tap">Commencez en un seul clic</string>
<string name="welcome_create_wallet_feature_seamless">Sans faille et sécurisé</string>
<string name="welcome_create_wallet_feature_seedphrase">Pas de seedphrase</string>
<string name="welcome_create_wallet_feature_seedphrase">Pas de seed phrase</string>
<string name="welcome_create_wallet_feature_use">Facile à utiliser</string>
<string name="welcome_create_wallet_hardware_description">Créez un harware wallet avec Tangem. Aussi fin qu\'une carte bancaire, aussi sûr qu\'un coffre-fort.</string>
<string name="welcome_create_wallet_mobile_description">Créer ou importer un portefeuille logiciel</string>
@ -2105,4 +2146,5 @@
<string name="yield_module_unable_to_cover_fee_title">Impossible de couvrir les frais %s</string>
<string name="yield_module_unavailable_subtitle">Le mode rendement n\'est pas disponible pour le moment. Veuillez réessayer plus tard.</string>
<string name="yield_module_unavailable_title">Le mode Rendement est indisponible</string>
<string name="yield_supply_chart_loading_error">Impossible de charger le graphique...</string>
</resources>

View file

@ -294,6 +294,7 @@
<string name="common_go_to_token">トークンへ移動</string>
<string name="common_got_it">わかりました</string>
<string name="common_hide">非表示</string>
<string name="common_hold_to">%sまで長押し</string>
<string name="common_hour">時間</string>
<string name="common_import">インポート</string>
<string name="common_in_progress">進行中</string>
@ -368,6 +369,7 @@
<string name="common_swap">スワップ</string>
<string name="common_tangem">Tangem</string>
<string name="common_tangem_wallet">Tangem Wallet</string>
<string name="common_tap_and_hold_hint">タップして長押し</string>
<string name="common_terms_and_conditions">利用規約</string>
<string name="common_terms_of_use">利用規約</string>
<string name="common_to">宛先</string>
@ -456,6 +458,11 @@
<string name="domain_receive_assets_onboarding_description">他のネットワークで資産を送金すると、永久に失われます。</string>
<string name="domain_receive_assets_onboarding_network_name">%sネットワーク</string>
<string name="domain_receive_assets_onboarding_title">下記のみを使用して資金を送金する</string>
<string name="earn_best_opportunities">おすすめ</string>
<string name="earn_filter_all_networks">すべてのネットワーク</string>
<string name="earn_filter_all_types">すべての種類</string>
<string name="earn_mostly_used">よく使われています</string>
<string name="earn_title">運用</string>
<string name="email_preface_wc_error">こんにちは、サポートチームの皆さん、コード %s のエラーが発生しました。</string>
<string name="email_subject_wc_error">WalletConnectエラー</string>
<string name="error_wrong_wallet_tapped">別のウォレットのカードまたはリングを使用しました。このウォレットにリンクしているカードまたはリングをタップしてください。</string>
@ -887,6 +894,9 @@
<string name="notification_sepa_title">SEPAで暗号資産を購入</string>
<string name="notification_visa_waitlist_promo_text">事前申し込み受付中。他にない特別なカードを、いち早く体験しよう。</string>
<string name="notification_visa_waitlist_promo_title">Tangem Visaカード</string>
<string name="notification_yield_promo_button">利用規約</string>
<string name="notification_yield_promo_text">100ドル以上を入金し、30日間保持すると、10ドルを受け取れます。</string>
<string name="notification_yield_promo_title">Yield Mode キャンペーンに参加しよう!</string>
<string name="onboarding_access_code_feature_1_description">すべてのデバイスを保護するには、単一のアクセスコードを設定してください。</string>
<string name="onboarding_access_code_feature_1_title">保護する</string>
<string name="onboarding_access_code_feature_2_description">後で各カードおよびリングに個別のアクセスコードを設定できます。</string>
@ -935,11 +945,11 @@
<string name="onboarding_navbar_upgrade_wallet_biometrics">生体認証</string>
<string name="onboarding_seed_button_read_more">シードフレーズについてもっと読む</string>
<plurals name="onboarding_seed_generate_message_words_count">
<item quantity="other">これらの %d 単語を以下の順番通りに書き留め、安全かつ秘密の場所に保管してください。</item>
<item quantity="other">以下に表示される%d個の単語を順番どおりに書き留め、安全で他人に知られない場所に保管してください。</item>
</plurals>
<string name="onboarding_seed_generate_title">あなたのシードフレーズ</string>
<plurals name="onboarding_seed_generate_words_count">
<item quantity="other">%d 単語</item>
<item quantity="other">%d個の単語</item>
</plurals>
<string name="onboarding_seed_import_message">ウォレットをインポートするには、下のフィールドにシードフレーズを入力してください。</string>
<string name="onboarding_seed_intro_button_generate">シードフレーズを生成する</string>
@ -1252,6 +1262,7 @@
<string name="staking_amount_tron_integer_error">ネットワークルールにより、ステーキング金額は%1$s TRX に切り上げられます。</string>
<string name="staking_amount_tron_integer_error_unstaking">ネットワークルールにより、ステーキング解除の量は%1$s TRX に切り上げられます。</string>
<string name="staking_apr_earn_badge">APR %1$s%%</string>
<string name="staking_cardano_details_rewards_info_text">デリゲーションがネットワークに登録され、集計されるまでの間、ステーキング報酬は5エポック後約25日後から開始されます。</string>
<string name="staking_claim_unstaked">ステーキング解除分を請求する</string>
<string name="staking_details_account_fee">ステーキングアカウント手数料</string>
<string name="staking_details_account_fee_info">ステーキングアカウントとは、ステーキングされたSOLが保管される特別なアカウントです。トークンをバリデーターに委任し、取引の検証に参加して報酬を受け取る際に作成されます。ステーキングアカウントの作成には少額の手数料がかかりますが、ステーキング完了後に返金されます。</string>
@ -1441,6 +1452,9 @@
<string name="tangem_pay_freeze_card_freeze">一時停止</string>
<string name="tangem_pay_freeze_card_success">カードが凍結されています</string>
<string name="tangem_pay_get_help">サポートを受ける</string>
<string name="tangem_pay_history_item_spend_mc_declined_reason">理由:%s</string>
<string name="tangem_pay_history_item_spend_mc_title_format" formatted="false">%s・%s</string>
<string name="tangem_pay_history_item_spend_mcc">MCC %s</string>
<string name="tangem_pay_other">その他</string>
<string name="tangem_pay_rooted_device_subtitle">Root化された端末では使用できません</string>
<string name="tangem_pay_status_completed">完了</string>
@ -1643,25 +1657,69 @@
<string name="user_wallet_list_rename_popup_title">ウォレット名の変更</string>
<string name="user_wallet_list_unlock_all">すべてのロックを解除</string>
<string name="user_wallet_list_unlock_all_with">%sですべてをロック解除</string>
<string name="visa_balance_limits_details_aml_verified">AML認証済み</string>
<string name="visa_balance_limits_details_available">利用可能</string>
<string name="visa_balance_limits_details_blocked">ブロック中</string>
<string name="visa_balance_limits_details_debt">負債</string>
<string name="visa_balance_limits_details_limits">利用上限</string>
<string name="visa_balance_limits_details_no_otp_limit">その他OTPなし</string>
<string name="visa_balance_limits_details_single_transaction">単一の取引</string>
<string name="visa_balance_limits_details_total">合計</string>
<plurals name="visa_limits_available_for_days_title">
<item quantity="other">%d日間利用可能</item>
</plurals>
<string name="visa_main_available_balance_alert_message">利用可能残高は、保留中の取引、ブロック中の金額、デビット残高を考慮した、実際に利用できる資金を示します。</string>
<string name="visa_main_available_till_date">%1$sまで利用可能</string>
<string name="visa_main_balances_and_limits">残高と限度額</string>
<string name="visa_main_button_alert_missing_payment_account_info_message">支払いアカウントの情報が見つかりません。サポートにお問い合わせください。</string>
<string name="visa_main_button_alert_missing_token_info">トークンの情報を読み込めません。</string>
<string name="visa_main_button_alert_still_loading">情報を読み込み中です。しばらくお待ちください。</string>
<string name="visa_main_limits_alert_description_message">コストの管理、セキュリティの向上、リスク管理のために利用上限が設けられています。店舗でのカード決済では期間内に%1$s、サブスクリプションや支払いなどのその他の取引では%2$sまで利用できます。</string>
<string name="visa_onboarding_access_code_description">アクセスコードは、支払いアカウントの管理および不正アクセスからの保護に使用されます。</string>
<string name="visa_onboarding_access_code_navigation_title">アクセスコード</string>
<string name="visa_onboarding_account_activation_navigation_title">アカウント有効化</string>
<string name="visa_onboarding_approve_wallet_selector_notification_message">登録手続きを開始したウォレットを選択し、ブロックチェーン上でアカウントを作成するための取引に署名してください。</string>
<string name="visa_onboarding_cancel_activation">有効化をキャンセル</string>
<string name="visa_onboarding_close_alert_message">本当に終了してもよろしいですか?中断したところから後で続行できます。</string>
<string name="visa_onboarding_in_progress_description">長くはかかりません。アカウントを設定しています。</string>
<string name="visa_onboarding_in_progress_issuer_description">長くはかかりません。アクティベーションを完了しています。</string>
<string name="visa_onboarding_in_progress_title">準備完了です!</string>
<string name="visa_onboarding_other_wallet">その他のウォレット</string>
<string name="visa_onboarding_pin_code_description">4桁のコードを設定します。 \nお支払いの際に使用されます。</string>
<string name="visa_onboarding_pin_code_navigation_title">PINコード</string>
<string name="visa_onboarding_pin_code_title">PINコードを作成する</string>
<string name="visa_onboarding_pin_not_accepted">PINの認証に失敗しました。もう一度お試しいただくか、別のコードを使用してください。</string>
<string name="visa_onboarding_pin_validation_error_message">無効な暗証番号:連続や繰り返しを避けてください</string>
<string name="visa_onboarding_success_screen_description">準備完了です!</string>
<string name="visa_onboarding_tangem_approve_description">Tangemカードを用意し、タップして承認してください。</string>
<string name="visa_onboarding_tangem_approve_title">Tangemウォレットを準備してください。</string>
<string name="visa_onboarding_wallet_connect_description">\nサードパーティのWebサイト\nで接続手続きを完了し、その後Tangemアプリに戻ってください。</string>
<string name="visa_onboarding_wallet_connect_title">ウェブサイトに移動</string>
<string name="visa_onboarding_wallet_connection_navigation_title">ウォレット接続</string>
<string name="visa_onboarding_wallet_list_header">ウォレットを選択</string>
<string name="visa_onboarding_welcome_back_button_title">有効化を続行</string>
<string name="visa_onboarding_welcome_back_description">アカウントの設定を続けましょう。</string>
<string name="visa_onboarding_welcome_back_title">お帰りなさい!</string>
<string name="visa_onboarding_welcome_button_title">有効化を開始</string>
<string name="visa_onboarding_welcome_description">手順に従ってアカウントを設定してください。</string>
<string name="visa_onboarding_welcome_title">ようこそ!</string>
<string name="visa_transaction_details_blockchain_amount">ブロックチェーン上の金額</string>
<string name="visa_transaction_details_currency_code">通貨コード</string>
<string name="visa_transaction_details_date">日付</string>
<string name="visa_transaction_details_error_code">エラーコード</string>
<string name="visa_transaction_details_header">取引詳細</string>
<string name="visa_transaction_details_merchant_category_code">加盟店カテゴリーコード</string>
<string name="visa_transaction_details_merchant_city">加盟店の所在地</string>
<string name="visa_transaction_details_merchant_country_code">加盟店の国コード</string>
<string name="visa_transaction_details_merchant_name">加盟店名</string>
<string name="visa_transaction_details_request_id">リクエストID</string>
<string name="visa_transaction_details_status">ステータス</string>
<string name="visa_transaction_details_title">取引</string>
<string name="visa_transaction_details_transaction_amount">取引金額</string>
<string name="visa_transaction_details_transaction_hash">トランザクションハッシュ</string>
<string name="visa_transaction_details_transaction_request">取引リクエスト</string>
<string name="visa_transaction_details_transaction_status">取引ステータス</string>
<string name="visa_transaction_details_type">種類</string>
<string name="visa_tx_dispute_button">この取引に異議を唱える</string>
<string name="visa_unlock_notification_button">ロック解除</string>
<string name="visa_unlock_notification_subtitle">カードをスキャンしてアクセスロックを解除する</string>

View file

@ -925,6 +925,9 @@
<string name="notification_sepa_title">Покупайте крипту через SEPA</string>
<string name="notification_visa_waitlist_promo_text">Присоединяйтесь к списку ожидания и получите уникальную платёжную карту, не похожую ни на одну другую.</string>
<string name="notification_visa_waitlist_promo_title">Tangem Visa Card</string>
<string name="notification_yield_promo_button">Условия программы</string>
<string name="notification_yield_promo_text">Внесите депозит от $100, удерживайте его 30 дней и получите бонус $10</string>
<string name="notification_yield_promo_title">Подключите Режим доходности!</string>
<string name="onboarding_access_code_feature_1_description">Установите единый код доступа для защиты всех ваших карт или колец</string>
<string name="onboarding_access_code_feature_1_title">Защита</string>
<string name="onboarding_access_code_feature_2_description">Установите индивидуальный код доступа для каждой карты или кольца позже.</string>
@ -1800,7 +1803,17 @@
<string name="warning_button_ok">Понятно!</string>
<string name="warning_button_really_cool">Очень круто!</string>
<string name="warning_button_refresh">Обновить</string>
<string name="warning_clore_migration_button">Начать миграцию</string>
<string name="warning_clore_migration_copy_button">Копировать</string>
<string name="warning_clore_migration_error_signing_not_supported">Подписание сообщений не поддерживается в этой сети</string>
<string name="warning_clore_migration_error_wallet_manager_not_found">Невозможно подписать сообщение. Пожалуйста, попробуй позже.</string>
<string name="warning_clore_migration_message">Согласно официальной документации Clore, все монеты, полученные до 21 декабря, будут мигрированы в токен Clore (ERC-20); монеты, полученные после этой даты, — нет. Решение для перевода находится в разработке — следите за обновлениями.</string>
<string name="warning_clore_migration_message_label">Сообщение</string>
<string name="warning_clore_migration_open_portal_button">Открыть портал клейма</string>
<string name="warning_clore_migration_sheet_description">Чтобы сохранить доступ к своим средствам, начните миграцию в соответствии с официальной инструкцией Clore.</string>
<string name="warning_clore_migration_sheet_title">Миграция сети Clore</string>
<string name="warning_clore_migration_sign_button">Подписать</string>
<string name="warning_clore_migration_signature_label">Подпись</string>
<string name="warning_clore_migration_title">Миграция сети Clore</string>
<string name="warning_demo_mode_message">Вы находитесь в режиме демо</string>
<string name="warning_demo_mode_title">Демо режим включен</string>

View file

@ -44,6 +44,7 @@
<string name="account_form_edit_button">Зберегти</string>
<string name="account_form_name">Назва акаунту</string>
<string name="account_form_name_already_exist_error_description">Акаунт з такою назвою вже існує. Будь ласка, виберіть іншу назву.</string>
<string name="account_form_name_already_exist_error_title">Обліковий запис з таким ім’ям уже існує</string>
<string name="account_form_placeholder_edit_account">Акаунт</string>
<string name="account_form_placeholder_new_account">Новий акаунт</string>
<string name="account_form_title_create">Додати акаунт</string>
@ -908,6 +909,9 @@
<string name="notification_sepa_title">Купуйте криптовалюту через SEPA</string>
<string name="notification_visa_waitlist_promo_text">Приєднуйтесь до списку очікування та отримайте унікальну платіжну картку, якої ще не було.</string>
<string name="notification_visa_waitlist_promo_title">Tangem Visa Card</string>
<string name="notification_yield_promo_button">Умови та положення</string>
<string name="notification_yield_promo_text">Внесіть депозит від $100, утримуйте його 30 днів та отримайте $10.</string>
<string name="notification_yield_promo_title">Кампанія Yield Mode!</string>
<string name="onboarding_access_code_feature_1_description">Налаштуйте єдиний код доступу для захисту всіх ваших карток або кілець</string>
<string name="onboarding_access_code_feature_1_title">Захист</string>
<string name="onboarding_access_code_feature_2_description">Встановіть індивідуальний код доступу для кожної картки або кільця пізніше.</string>

View file

@ -86,6 +86,8 @@
<string name="address_qr_code_message_format">Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds.</string>
<string name="address_type_default">Default</string>
<string name="address_type_legacy">Legacy</string>
<string name="alert_authentication_error_message">Try to reset biometrics on your device or contact support</string>
<string name="alert_authentication_error_title">Authentication error</string>
<string name="alert_button_how_to_scan">How to scan</string>
<string name="alert_button_request_support">Request support</string>
<string name="alert_button_try_again">Try again</string>
@ -908,6 +910,9 @@
<string name="notification_sepa_title">Buy Crypto with SEPA</string>
<string name="notification_visa_waitlist_promo_text">Join the waitlist and get a payment card unlike any other</string>
<string name="notification_visa_waitlist_promo_title">Tangem Visa Card</string>
<string name="notification_yield_promo_button">Terms and conditions</string>
<string name="notification_yield_promo_text">Deposit $100+, hold for 30 days, get $10</string>
<string name="notification_yield_promo_title">Join the Yield Mode Campaign!</string>
<string name="onboarding_access_code_feature_1_description">Set up a single access code to protect all your devices.</string>
<string name="onboarding_access_code_feature_1_title">Protect</string>
<string name="onboarding_access_code_feature_2_description">Set an individual access code for each card or ring later.</string>
@ -1843,7 +1848,18 @@
<string name="warning_button_ok">Ok, Got it!</string>
<string name="warning_button_really_cool">Really cool!</string>
<string name="warning_button_refresh">Refresh</string>
<string name="warning_clore_migration_button">Start migration</string>
<string name="warning_clore_migration_copy_button">Copy</string>
<string name="warning_clore_migration_description">To keep access to your funds, start the migration according to the official Clore guidelines.</string>
<string name="warning_clore_migration_error_signing_not_supported">Message signing is not supported for this network</string>
<string name="warning_clore_migration_error_wallet_manager_not_found">Unable to sign message. Please try again.</string>
<string name="warning_clore_migration_message">According to Clores official documentation, all coins received before December 21 will be migrated to Clore (ERC-20 token); coins received after that date will not. A transfer solution is coming — stay tuned.</string>
<string name="warning_clore_migration_message_label">Message</string>
<string name="warning_clore_migration_open_portal_button">Open Claim portal</string>
<string name="warning_clore_migration_sheet_description">To continue using your Clore tokens, you must complete the token migration according to the information on the Claim Portal.</string>
<string name="warning_clore_migration_sheet_title">Clore Network Migration</string>
<string name="warning_clore_migration_sign_button">Sign</string>
<string name="warning_clore_migration_signature_label">Signature</string>
<string name="warning_clore_migration_title">Clore Network Migration</string>
<string name="warning_demo_mode_message">You are currently in the Demo mode</string>
<string name="warning_demo_mode_title">Demo mode active</string>

View file

@ -15,6 +15,7 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.components.buttons.common.*
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@ -320,9 +321,9 @@ fun SelectorButton(text: String, onClick: () -> Unit, modifier: Modifier = Modif
@Composable
fun HoldToConfirmButton(
text: String,
hintText: String,
onConfirm: () -> Unit,
modifier: Modifier = Modifier,
hintText: String = stringResourceSafe(R.string.common_tap_and_hold_hint),
enabled: Boolean = true,
isLoading: Boolean = false,
) {

View file

@ -6,22 +6,20 @@ import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
@ -76,6 +74,33 @@ fun AccountResIcon(@DrawableRes resId: Int, color: Color, size: AccountIconSize,
}
}
/**
* Displays a PaymentAccount icon using a predefined painter resource.
*
* The icon size is adapted based on the provided [size].
*
* @param size The size of the icon, defined by [AccountIconSize].
*/
@Composable
fun PaymentAccountIcon(size: AccountIconSize, modifier: Modifier = Modifier) {
val boxSize by animateDpAsState(
targetValue = size.boxSizeInDp(),
animationSpec = animation(),
)
val boxShapeCornerSize by animateDpAsState(
targetValue = size.boxShapeSizeInDp(),
animationSpec = animation(),
)
Box(
contentAlignment = Alignment.Center,
modifier = modifier
.size(boxSize)
.clip(RoundedCornerShape(boxShapeCornerSize)),
) {
Image(painter = painterResource(id = R.drawable.img_visa_36), contentDescription = null)
}
}
/**
* Displays a portfolio icon using a single character.
*
@ -186,6 +211,7 @@ private fun Sample() {
AccountResIcon(resId = R.drawable.ic_user_24, color = Color.Magenta, size = AccountIconSize.Medium)
AccountResIcon(resId = R.drawable.ic_family_24, color = Color.DarkGray, size = AccountIconSize.Small)
AccountResIcon(resId = R.drawable.ic_wallet_24, color = Color.Green, size = AccountIconSize.ExtraSmall)
PaymentAccountIcon(size = AccountIconSize.Default)
}
Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) {
AccountCharIcon(char = 'D', color = Color.Red, size = sizeState)

View file

@ -87,6 +87,15 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
store.updateData { response }
}
override suspend fun update(
userWalletId: UserWalletId,
transform: (GetWalletAccountsResponse?) -> GetWalletAccountsResponse?,
) {
val store = getAccountsResponseStore(userWalletId = userWalletId)
store.updateData { transform(it) }
}
override suspend fun push(
userWalletId: UserWalletId,
accounts: List<WalletAccountDTO>,

View file

@ -15,6 +15,11 @@ interface WalletAccountsSaver {
/** Store wallet accounts [response] by [userWalletId] */
suspend fun store(userWalletId: UserWalletId, response: GetWalletAccountsResponse)
suspend fun update(
userWalletId: UserWalletId,
transform: (GetWalletAccountsResponse?) -> GetWalletAccountsResponse?,
)
/** Push wallet accounts [body] by [userWalletId] */
@Throws
suspend fun push(userWalletId: UserWalletId, body: SaveWalletAccountsResponse): GetWalletAccountsResponse?

View file

@ -66,6 +66,11 @@ internal class DefaultPromoRepository(
PromoId.OnePlusOne -> {
val isActive = getOnePlusOnePromoBanner()?.isActive == true
isActive && shouldShow
}
PromoId.YieldPromo -> {
val isActive = getYieldPromoBanner(userWalletId)?.isActive == true
isActive && shouldShow
}
}
@ -79,6 +84,7 @@ internal class DefaultPromoRepository(
PromoId.VisaPresale -> flowOf(false)
PromoId.BlackFriday -> flowOf(false)
PromoId.OnePlusOne -> flowOf(false)
PromoId.YieldPromo -> flowOf(false)
}
}
@ -190,12 +196,22 @@ internal class DefaultPromoRepository(
}.getOrNull()
}
private suspend fun getYieldPromoBanner(userWalletId: UserWalletId): PromoBanner? {
return runCatching(dispatchers.io) {
val response = tangemApi.getPromoBannersV2(userWalletId.stringValue).getOrThrow()
val yieldPromotion = response.promotions.find { it.name == YIELD_PROMO_NAME }
?: return@runCatching null
promoBannerConverter.convert(yieldPromotion)
}.getOrNull()
}
private companion object {
const val SEPA_NAME = "sepa"
const val VISA_NAME = "visa-waitlist"
const val BLACK_FRIDAY_NAME = "black-friday"
const val MOONPAY_NAME = "moonpay"
const val ONE_PLUS_ONE_NAME = "one-plus-one"
const val YIELD_PROMO_NAME = "promo-yield"
const val STORIES_LOAD_DELAY = 1000L
}
}

View file

@ -22,6 +22,7 @@ import com.tangem.blockchain.transactionhistory.models.TransactionHistoryRequest
import com.tangem.blockchain.yieldsupply.YieldSupplyContractCallDataProviderFactory
import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.blockchainsdk.models.UpdateWalletManagerResult
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.crypto.hdWallet.DerivationPath
@ -629,6 +630,22 @@ internal class DefaultWalletManagersFacade @Inject constructor(
)
}
override suspend fun getNativeTokenBalance(
userWalletId: UserWalletId,
networkId: String,
derivationPath: String?,
): BigDecimal {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
val walletManager = getOrCreateWalletManager(userWalletId, blockchain, derivationPath)
return walletManager?.wallet?.amounts
?.firstNotNullOfOrNull { amountEntry ->
amountEntry.takeIf { amountEntry.key is AmountType.Coin }
}
?.value?.value
?: BigDecimal.ZERO
}
override suspend fun getAssetRequirements(
userWalletId: UserWalletId,
currency: CryptoCurrency,

View file

@ -108,4 +108,6 @@ val UserWallet.isLocked
get() = when (this) {
is UserWallet.Cold -> isLocked
is UserWallet.Hot -> isLocked
}
}
inline val UserWallet.isHotWallet get() = this is UserWallet.Hot

View file

@ -31,4 +31,5 @@ enum class PromoId {
VisaPresale,
BlackFriday,
OnePlusOne,
YieldPromo,
}

View file

@ -33,6 +33,7 @@ class ShouldShowPromoWalletUseCase(
PromoId.VisaPresale,
PromoId.BlackFriday,
PromoId.OnePlusOne,
PromoId.YieldPromo,
-> true
PromoId.Sepa -> {
val walletFirstUsageDate = settingsRepository.getWalletFirstUsageDate()

View file

@ -60,5 +60,6 @@ sealed class PromoAnalyticsEvent(
Sepa("Sepa"),
BlackFriday("Black Friday"),
OnePlusOne("One-Plus-One"),
YieldPromo("Yield Promo"),
}
}

View file

@ -6,4 +6,5 @@ import kotlinx.serialization.Serializable
sealed class NavigationAction {
data object Staking : NavigationAction()
data class YieldSupply(val isActive: Boolean) : NavigationAction()
data object CloreMigration : NavigationAction()
}

View file

@ -0,0 +1,10 @@
package com.tangem.domain.transaction.error
sealed class SignCloreMessageError {
data object WalletManagerNotFound : SignCloreMessageError()
data object MessageSigningNotSupported : SignCloreMessageError()
data class SigningFailed(val message: String) : SignCloreMessageError()
}

View file

@ -0,0 +1,58 @@
package com.tangem.domain.transaction.usecase
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.blockchain.common.MessageSigner
import com.tangem.blockchain.common.TransactionSigner
import com.tangem.common.CompletionResult
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.transaction.error.SignCloreMessageError
import com.tangem.domain.walletmanager.WalletManagersFacade
class SignCloreMessageUseCase(
private val walletManagersFacade: WalletManagersFacade,
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val getHotWalletSigner: (UserWallet.Hot) -> TransactionSigner,
) {
suspend operator fun invoke(
userWallet: UserWallet,
currency: CryptoCurrency,
message: String,
): Either<SignCloreMessageError, String> {
val walletManager = walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWallet.walletId,
network = currency.network,
)
if (walletManager == null) {
return SignCloreMessageError.WalletManagerNotFound.left()
}
if (walletManager !is MessageSigner) {
return SignCloreMessageError.MessageSigningNotSupported.left()
}
val signer = when (userWallet) {
is UserWallet.Cold -> {
val card = userWallet.scanResponse.card
val isCardNotBackedUp = card.backupStatus?.isActive != true
cardSdkConfigRepository.getCommonSigner(
cardId = card.cardId.takeIf { isCardNotBackedUp },
twinKey = null,
)
}
is UserWallet.Hot -> getHotWalletSigner(userWallet)
}
return when (val result = walletManager.signMessage(message, signer)) {
is CompletionResult.Success -> result.data.right()
is CompletionResult.Failure -> SignCloreMessageError.SigningFailed(
message = result.error.message ?: "Unknown error",
).left()
}
}
}

View file

@ -228,6 +228,13 @@ interface WalletManagersFacade {
id: String? = null,
): BigDecimal
@Throws(IllegalStateException::class)
suspend fun getNativeTokenBalance(
userWalletId: UserWalletId,
networkId: String,
derivationPath: String?,
): BigDecimal
/**
* Get requirements for asset(currency)
* @return null if there's no requirement, otherwise [AssetRequirementsCondition].

View file

@ -1,6 +1,9 @@
package com.tangem.domain.wallets.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.AppsFlyerIncludedEvent
import com.tangem.core.analytics.models.OneTimeAnalyticsEvent
sealed class Settings(
category: String = "Settings",
@ -9,4 +12,13 @@ sealed class Settings(
) : AnalyticsEvent(category, event, params) {
class ButtonManageTokens : Settings(event = "Button - Manage Tokens")
class ColdWalletAdded(
source: AnalyticsParam.ScreensSources?,
) : Settings(
event = "Cold Wallet Added",
params = mapOf(AnalyticsParam.SOURCE to (source?.value ?: "Unknown")),
), OneTimeAnalyticsEvent, AppsFlyerIncludedEvent {
override val oneTimeEventId: String = id
}
}

View file

@ -4,6 +4,7 @@ import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.AnalyticsParam.Key.ACTION
import com.tangem.core.analytics.models.AnalyticsParam.Key.STATUS
import com.tangem.core.analytics.models.AppsFlyerIncludedEvent
sealed class WalletSettingsAnalyticEvents(
category: String = "Settings / Wallet Settings",
@ -166,6 +167,10 @@ sealed class WalletSettingsAnalyticEvents(
event = "Button - Start Upgrade",
)
class WalletUpgraded : WalletSettingsAnalyticEvents(
event = "Wallet Upgraded",
), AppsFlyerIncludedEvent
enum class RecoveryPhraseScreenAction(val value: String) {
Upgrade("Upgrade"),
Backup("Backup"),

View file

@ -6,9 +6,12 @@ import arrow.core.raise.either
import arrow.core.right
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.error.SaveWalletError
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.analytics.Settings
import com.tangem.domain.wallets.legacy.UserWalletsListError
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletsRepository
@ -16,8 +19,6 @@ import com.tangem.domain.wallets.repository.WalletsRepository
/**
* Use case for saving user wallet
*
* @property userWalletsListManager user wallets list manager
*
[REDACTED_AUTHOR]
*/
class SaveWalletUseCase(
@ -25,14 +26,21 @@ class SaveWalletUseCase(
private val userWalletsListRepository: UserWalletsListRepository,
private val walletsRepository: WalletsRepository,
private val useNewRepository: Boolean,
private val analyticsEventHandler: AnalyticsEventHandler,
) {
suspend operator fun invoke(userWallet: UserWallet, canOverride: Boolean = false): Either<SaveWalletError, Unit> {
suspend operator fun invoke(
userWallet: UserWallet,
canOverride: Boolean = false,
analyticsSource: AnalyticsParam.ScreensSources? = null,
): Either<SaveWalletError, Unit> {
return if (useNewRepository) {
either {
val newUserWallet =
userWalletsListRepository.userWalletsSync().none { it.walletId == userWallet.walletId }
val userWallet = userWalletsListRepository.saveWithoutLock(userWallet, canOverride).bind()
val userWallet = userWalletsListRepository.saveWithoutLock(userWallet, canOverride)
.onRight { trackColdWalletAddedIfNeeded(analyticsSource, it) }
.bind()
if (newUserWallet) {
when (userWallet) {
@ -76,4 +84,11 @@ class SaveWalletUseCase(
}
}
}
private suspend fun trackColdWalletAddedIfNeeded(source: AnalyticsParam.ScreensSources?, userWallet: UserWallet) {
val hasHotWallet = userWalletsListRepository.userWalletsSync().any { it is UserWallet.Hot }
if (hasHotWallet && userWallet is UserWallet.Cold) {
analyticsEventHandler.send(event = Settings.ColdWalletAdded(source))
}
}
}

View file

@ -1,7 +1,7 @@
package com.tangem.features.account.archived.entity
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.account.CryptoPortfolioIconUM
import com.tangem.common.ui.account.AccountIconUM
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
@ -23,7 +23,7 @@ internal sealed interface AccountArchivedUM {
internal data class ArchivedAccountUM(
val accountId: String,
val accountName: TextReference,
val accountIconUM: CryptoPortfolioIconUM,
val accountIconUM: AccountIconUM.CryptoPortfolio,
val tokensInfo: TextReference,
val networksInfo: TextReference,
val isLoading: Boolean,

View file

@ -1,5 +1,6 @@
package com.tangem.features.account.archived.entity
import com.tangem.common.ui.account.CryptoPortfolioIconConverter
import com.tangem.common.ui.account.toUM
import com.tangem.core.res.R
import com.tangem.core.ui.extensions.pluralReference
@ -28,7 +29,7 @@ internal class AccountArchivedUMBuilder @Inject constructor() {
ArchivedAccountUM(
accountId = accountId.value,
accountName = name.toUM().value,
accountIconUM = icon.toUM(),
accountIconUM = CryptoPortfolioIconConverter.convert(icon),
isLoading = false,
tokensInfo = pluralReference(
R.plurals.common_tokens_count,

View file

@ -2,6 +2,7 @@ package com.tangem.features.account.createedit
import androidx.annotation.StringRes
import com.tangem.common.ui.account.AccountNameUM
import com.tangem.common.ui.account.CryptoPortfolioIconConverter
import com.tangem.common.ui.account.toDomain
import com.tangem.common.ui.account.toUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
@ -109,7 +110,7 @@ internal class AccountCreateEditModel @Inject constructor(
private suspend fun createNewCryptoPortfolio(params: AccountCreateEditComponent.Params.Create) {
val state = uiState.value
val name = state.account.name.toDomain().getOrNull() ?: return
val icon = state.account.portfolioIcon.toDomain()
val icon = CryptoPortfolioIconConverter.convertBack(state.account.portfolioIcon)
val index = state.account.derivationInfo.index ?: return
val derivationIndex = DerivationIndex(value = index).getOrNull() ?: return
val event = AccountSettingsAnalyticEvents.ButtonAddNewAccount(
@ -161,7 +162,7 @@ internal class AccountCreateEditModel @Inject constructor(
private suspend fun editCryptoPortfolio(params: AccountCreateEditComponent.Params.Edit) {
val state = uiState.value
val name = state.account.name.toDomain().getOrNull() ?: return
val icon = state.account.portfolioIcon.toDomain()
val icon = CryptoPortfolioIconConverter.convertBack(state.account.portfolioIcon)
val isNewName = name != params.account.accountName
val isNewIcon = icon != params.account.portfolioIcon
analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonSave(name, icon))
@ -246,9 +247,10 @@ internal class AccountCreateEditModel @Inject constructor(
is AccountCreateEditComponent.Params.Create -> isValidName
is AccountCreateEditComponent.Params.Edit -> {
val oldName = params.account.accountName.toUM()
val oldIcon = CryptoPortfolioIconConverter.convert(params.account.portfolioIcon)
val isNewName = this.account.name.trim() != oldName
val isNewIcon = this.account.portfolioIcon != params.account.portfolioIcon.toUM()
val isNewIcon = this.account.portfolioIcon != oldIcon
isValidName && (isNewName || isNewIcon)
}
}

View file

@ -1,8 +1,8 @@
package com.tangem.features.account.createedit.entity
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.account.AccountIconUM
import com.tangem.common.ui.account.AccountNameUM
import com.tangem.common.ui.account.CryptoPortfolioIconUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.models.account.CryptoPortfolioIcon
import kotlinx.collections.immutable.ImmutableList
@ -18,7 +18,7 @@ internal data class AccountCreateEditUM(
data class Account(
val name: AccountNameUM,
val portfolioIcon: CryptoPortfolioIconUM,
val portfolioIcon: AccountIconUM.CryptoPortfolio,
val derivationInfo: DerivationInfo,
val inputPlaceholder: TextReference,
val onNameChange: (AccountNameUM) -> Unit,

View file

@ -1,6 +1,7 @@
package com.tangem.features.account.createedit.entity
import com.tangem.common.ui.account.AccountNameUM
import com.tangem.common.ui.account.CryptoPortfolioIconConverter
import com.tangem.common.ui.account.toUM
import com.tangem.core.res.R
import com.tangem.core.ui.extensions.TextReference
@ -17,7 +18,7 @@ internal class AccountCreateEditUMBuilder(
private val accountColors = CryptoPortfolioIcon.Color.entries.toImmutableList()
private val accountIcons = CryptoPortfolioIcon.Icon.entries.toImmutableList()
private val createIcon = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM()
private val createIcon = CryptoPortfolioIconConverter.convert(CryptoPortfolioIcon.ofDefaultCustomAccount())
val toolbarTitle: TextReference
get() = when (params) {
@ -36,7 +37,7 @@ internal class AccountCreateEditUMBuilder(
)
is AccountCreateEditComponent.Params.Edit -> AccountCreateEditUM.Account(
name = params.account.accountName.toUM(),
portfolioIcon = params.account.portfolioIcon.toUM(),
portfolioIcon = CryptoPortfolioIconConverter.convert(params.account.portfolioIcon),
derivationInfo = createAccountDerivationInfo(
index = (params.account as Account.CryptoPortfolio).derivationIndex.value,
),

View file

@ -1,6 +1,7 @@
package com.tangem.features.account.details
import com.tangem.common.routing.AppRoute
import com.tangem.common.ui.account.CryptoPortfolioIconConverter
import com.tangem.common.ui.account.toUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
@ -159,7 +160,7 @@ internal class AccountDetailsModel @Inject constructor(
?.isMultiCurrency == true
return AccountDetailsUM(
accountName = account.accountName.toUM().value,
accountIcon = account.portfolioIcon.toUM(),
accountIcon = CryptoPortfolioIconConverter.convert(account.portfolioIcon),
onCloseClick = { router.pop() },
onAccountEditClick = { onEditAccountClick(account) },
onManageTokensClick = { onManageTokensClick(account) },

View file

@ -1,12 +1,12 @@
package com.tangem.features.account.details.entity
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.account.CryptoPortfolioIconUM
import com.tangem.common.ui.account.AccountIconUM
import com.tangem.core.ui.extensions.TextReference
internal data class AccountDetailsUM(
val accountName: TextReference,
val accountIcon: CryptoPortfolioIconUM,
val accountIcon: AccountIconUM.CryptoPortfolio,
val archiveMode: ArchiveMode,
val isManageTokensAvailable: Boolean,
val onCloseClick: () -> Unit,

View file

@ -75,7 +75,12 @@ internal class UserWalletSaver @Inject constructor(
private suspend fun Raise<Error>.saveWallet(userWallet: UserWallet) {
fold(
block = { saveWalletUseCase(userWallet).bind() },
block = {
saveWalletUseCase(
userWallet = userWallet,
analyticsSource = AnalyticsParam.ScreensSources.Settings,
).bind()
},
recover = { error ->
when (error) {
is SaveWalletError.WalletAlreadySaved -> {

View file

@ -0,0 +1,33 @@
package com.tangem.features.feed.components.market.details.portfolio.add
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
interface AddToPortfolioPreselectedDataComponent : ComposableBottomSheetComponent {
/**
* @param tokenToAdd token and preselected network (no network selector).
* @param callback callbacks for add-to-portfolio flow.
*/
data class Params(
val tokenToAdd: TokenToAdd,
val callback: Callback,
)
interface Callback {
fun onDismiss()
fun onSuccess(addedToken: CryptoCurrency, walletId: UserWalletId)
}
data class TokenToAdd(
val network: TokenMarketInfo.Network,
val id: CryptoCurrency.RawID,
val name: String,
val symbol: String,
)
interface Factory : ComponentFactory<Params, AddToPortfolioPreselectedDataComponent>
}

View file

@ -2,4 +2,5 @@ package com.tangem.features.feed.entry.featuretoggle
interface FeedFeatureToggle {
val isFeedEnabled: Boolean
val isEarnBlockEnabled: Boolean
}

View file

@ -0,0 +1,118 @@
package com.tangem.features.feed.components.market.details.portfolio.add.impl
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.router.stack.ChildStack
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.account.PortfolioSelectorComponent
import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.AddToPortfolioRoutes
import com.tangem.features.feed.impl.R
@Composable
internal fun AddToPortfolioBottomSheet(
childStack: State<ChildStack<AddToPortfolioRoutes, ComposableContentComponent>>,
onBack: () -> Unit,
onDismiss: () -> Unit,
) {
val stack by childStack
val contentStack = remember { mutableStateOf(stack) }
val currentRoute = stack.active.configuration
val isNotEmpty = currentRoute != AddToPortfolioRoutes.Empty
if (isNotEmpty) {
contentStack.value = stack
}
TangemModalBottomSheet<TangemBottomSheetConfigContent.Empty>(
scrollableContent = false,
onBack = onBack,
config = TangemBottomSheetConfig(
isShown = isNotEmpty,
onDismissRequest = onDismiss,
content = TangemBottomSheetConfigContent.Empty,
),
containerColor = TangemTheme.colors.background.tertiary,
title = {
AnimatedContent(targetState = contentStack.value, label = "Title Animation") { animatedStack ->
AddToPortfolioBottomSheetTitle(
stack = animatedStack,
onBackClick = onBack,
modifier = Modifier.fillMaxWidth(),
)
}
},
content = {
AnimatedContent(targetState = contentStack.value, label = "Content Animation") { animatedStack ->
val paddingModifier = Modifier.padding(
start = 16.dp,
end = 16.dp,
bottom = 16.dp,
)
val isScrollableContent = when (animatedStack.active.configuration) {
AddToPortfolioRoutes.PortfolioSelector -> false
AddToPortfolioRoutes.AddToken,
AddToPortfolioRoutes.Empty,
is AddToPortfolioRoutes.NetworkSelector,
AddToPortfolioRoutes.TokenActions,
-> true
}
if (isScrollableContent) {
Column(
modifier = paddingModifier.verticalScroll(rememberScrollState()),
) {
animatedStack.active.instance.Content(modifier = Modifier)
}
} else {
animatedStack.active.instance.Content(modifier = paddingModifier)
}
}
},
)
}
@Composable
private fun AddToPortfolioBottomSheetTitle(
stack: ChildStack<AddToPortfolioRoutes, ComposableContentComponent>,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val title: TextReference = when (stack.active.configuration) {
AddToPortfolioRoutes.AddToken -> resourceReference(R.string.common_add_token)
AddToPortfolioRoutes.Empty -> TextReference.EMPTY
is AddToPortfolioRoutes.NetworkSelector -> resourceReference(R.string.common_choose_network)
AddToPortfolioRoutes.TokenActions -> resourceReference(R.string.common_get_token)
AddToPortfolioRoutes.PortfolioSelector -> (stack.active.instance as PortfolioSelectorComponent)
.title.collectAsStateWithLifecycle().value
}
val startIconRes: Int?
val endIconRes: Int?
if (stack.backStack.isNotEmpty()) {
startIconRes = R.drawable.ic_back_24
endIconRes = null
} else {
startIconRes = null
endIconRes = R.drawable.ic_close_24
}
TangemModalBottomSheetTitle(
modifier = modifier,
title = title,
startIconRes = startIconRes,
endIconRes = endIconRes,
onStartClick = onBackClick,
onEndClick = onBackClick,
)
}

View file

@ -1,21 +1,8 @@
package com.tangem.features.feed.components.market.details.portfolio.add.impl
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.stack.ChildStack
import com.arkivanov.decompose.router.stack.backStack
import com.arkivanov.decompose.router.stack.childStack
import com.arkivanov.decompose.router.stack.pop
@ -23,19 +10,11 @@ import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.account.PortfolioSelectorComponent
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent
import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.AddToPortfolioModel
import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.AddToPortfolioRoutes
import com.tangem.features.feed.impl.R
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -97,91 +76,10 @@ internal class DefaultAddToPortfolioComponent @AssistedInject constructor(
@Composable
override fun BottomSheet() {
val stack by childStack.subscribeAsState()
val contentStack = remember { mutableStateOf(stack) }
val currentRoute = stack.active.configuration
val isNotEmpty = currentRoute != AddToPortfolioRoutes.Empty
if (isNotEmpty) {
contentStack.value = stack
}
TangemModalBottomSheet<TangemBottomSheetConfigContent.Empty>(
scrollableContent = false,
AddToPortfolioBottomSheet(
childStack = childStack.subscribeAsState(),
onBack = ::onBack,
config = TangemBottomSheetConfig(
isShown = isNotEmpty,
onDismissRequest = ::dismiss,
content = TangemBottomSheetConfigContent.Empty,
),
containerColor = TangemTheme.colors.background.tertiary,
title = { state ->
AnimatedContent(targetState = contentStack.value) { stack ->
BottomSheetTitle(
stack = stack,
onBackClick = ::onBack,
modifier = Modifier.fillMaxWidth(),
)
}
},
content = { state ->
AnimatedContent(targetState = contentStack.value) { stack ->
val paddingModifier = Modifier.padding(
start = 16.dp,
end = 16.dp,
bottom = 16.dp,
)
val isScrollableContent = when (stack.active.configuration) {
AddToPortfolioRoutes.PortfolioSelector -> false
AddToPortfolioRoutes.AddToken,
AddToPortfolioRoutes.Empty,
is AddToPortfolioRoutes.NetworkSelector,
AddToPortfolioRoutes.TokenActions,
-> true
}
if (isScrollableContent) {
Column(
modifier = paddingModifier.verticalScroll(rememberScrollState()),
) {
stack.active.instance.Content(modifier = Modifier)
}
} else {
stack.active.instance.Content(modifier = paddingModifier)
}
}
},
)
}
@Composable
private fun BottomSheetTitle(
stack: ChildStack<AddToPortfolioRoutes, ComposableContentComponent>,
onBackClick: (() -> Unit),
modifier: Modifier = Modifier,
) {
val title: TextReference = when (stack.active.configuration) {
AddToPortfolioRoutes.AddToken -> resourceReference(R.string.common_add_token)
AddToPortfolioRoutes.Empty -> TextReference.EMPTY
is AddToPortfolioRoutes.NetworkSelector -> resourceReference(R.string.common_choose_network)
AddToPortfolioRoutes.TokenActions -> resourceReference(R.string.common_get_token)
AddToPortfolioRoutes.PortfolioSelector -> (stack.active.instance as PortfolioSelectorComponent)
.title.collectAsStateWithLifecycle().value
}
val startIconRes: Int?
val endIconRes: Int?
if (stack.backStack.isNotEmpty()) {
startIconRes = R.drawable.ic_back_24
endIconRes = null
} else {
startIconRes = null
endIconRes = R.drawable.ic_close_24
}
TangemModalBottomSheetTitle(
modifier = modifier,
title = title,
startIconRes = startIconRes,
endIconRes = endIconRes,
onStartClick = onBackClick,
onEndClick = onBackClick,
onDismiss = ::dismiss,
)
}

View file

@ -0,0 +1,90 @@
package com.tangem.features.feed.components.market.details.portfolio.add.impl
import androidx.compose.runtime.Composable
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.stack.backStack
import com.arkivanov.decompose.router.stack.childStack
import com.arkivanov.decompose.router.stack.pop
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.account.PortfolioSelectorComponent
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent
import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.AddToPortfolioPreselectedDataModel
import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.AddToPortfolioRoutes
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultAddToPortfolioPreselectedDataComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted private val params: AddToPortfolioPreselectedDataComponent.Params,
portfolioSelectorComponentFactory: PortfolioSelectorComponent.Factory,
addTokenComponentFactory: AddTokenComponent.Factory,
) : AppComponentContext by context, AddToPortfolioPreselectedDataComponent {
private val model: AddToPortfolioPreselectedDataModel = getOrCreateModel(params)
private val portfolioSelectorComponent: PortfolioSelectorComponent = portfolioSelectorComponentFactory.create(
context = child("portfolioSelectorComponent"),
params = PortfolioSelectorComponent.Params(
portfolioFetcher = model.portfolioFetcher,
controller = model.portfolioSelectorController,
),
)
private val addTokenComponent: AddTokenComponent = addTokenComponentFactory.create(
context = child("addTokenComponent"),
params = AddTokenComponent.Params(
eventBuilder = model.eventBuilder,
callbacks = model,
selectedPortfolio = model.selectedPortfolio,
selectedNetwork = model.selectedNetwork,
),
)
private val childStack = childStack(
key = "addToPortfolioFromEarnStack",
handleBackButton = true,
source = model.navigation,
serializer = AddToPortfolioRoutes.serializer(),
initialStack = { model.currentStack },
childFactory = { configuration, _ ->
contentChild(configuration)
},
)
private fun onBack() {
if (childStack.backStack.isNotEmpty()) model.navigation.pop() else dismiss()
}
override fun dismiss() {
params.callback.onDismiss()
}
@Composable
override fun BottomSheet() {
AddToPortfolioBottomSheet(
childStack = childStack.subscribeAsState(),
onBack = ::onBack,
onDismiss = ::dismiss,
)
}
private fun contentChild(config: AddToPortfolioRoutes): ComposableContentComponent = when (config) {
AddToPortfolioRoutes.AddToken -> addTokenComponent
AddToPortfolioRoutes.PortfolioSelector -> portfolioSelectorComponent
AddToPortfolioRoutes.TokenActions -> ComposableContentComponent.EMPTY
AddToPortfolioRoutes.Empty -> ComposableContentComponent.EMPTY
is AddToPortfolioRoutes.NetworkSelector -> ComposableContentComponent.EMPTY
}
@AssistedFactory
interface Factory : AddToPortfolioPreselectedDataComponent.Factory {
override fun create(
context: AppComponentContext,
params: AddToPortfolioPreselectedDataComponent.Params,
): DefaultAddToPortfolioPreselectedDataComponent
}
}

View file

@ -2,10 +2,7 @@ package com.tangem.features.feed.components.market.details.portfolio.add.impl.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.AddToPortfolioModel
import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.AddTokenModel
import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.ChooseNetworkModel
import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.TokenActionsModel
import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.*
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@ -26,6 +23,11 @@ internal interface AddToPortfolioModelModule {
@ClassKey(AddToPortfolioModel::class)
fun addToPortfolioModel(model: AddToPortfolioModel): Model
@Binds
@IntoMap
@ClassKey(AddToPortfolioPreselectedDataModel::class)
fun addToPortfolioPreselectedDataModel(model: AddToPortfolioPreselectedDataModel): Model
@Binds
@IntoMap
@ClassKey(TokenActionsModel::class)

View file

@ -67,7 +67,7 @@ internal class AddToPortfolioModel @Inject constructor(
private val addToPortfolioManager = params.addToPortfolioManager
val portfolioFetcher = addToPortfolioManager.portfolioFetcher
val eventBuilder = PortfolioAnalyticsEvent.EventBuilder(
token = addToPortfolioManager.token,
tokenSymbol = addToPortfolioManager.token.symbol,
source = addToPortfolioManager.analyticsParams?.source,
)

View file

@ -0,0 +1,280 @@
package com.tangem.features.feed.components.market.details.portfolio.add.impl.model
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.replaceAll
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
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.ui.UiMessageSender
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.ToastMessage
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.markets.GetTokenMarketCryptoCurrency
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.account.PortfolioFetcher
import com.tangem.features.account.PortfolioSelectorController
import com.tangem.features.feed.components.market.details.portfolio.add.*
import com.tangem.features.feed.components.market.details.portfolio.add.impl.AddTokenComponent
import com.tangem.features.feed.components.market.details.portfolio.impl.analytics.PortfolioAnalyticsEvent
import com.tangem.features.feed.impl.R
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import timber.log.Timber
import java.math.BigDecimal
import javax.inject.Inject
@Suppress("LongParameterList")
internal class AddToPortfolioPreselectedDataModel @Inject constructor(
paramsContainer: ParamsContainer,
portfolioFetcherFactory: PortfolioFetcher.Factory,
override val dispatchers: CoroutineDispatcherProvider,
val portfolioSelectorController: PortfolioSelectorController,
private val callbackDelegate: AddToPortfolioFromEarnCallbackDelegate,
private val messageSender: UiMessageSender,
private val analyticsEventHandler: AnalyticsEventHandler,
private val getTokenMarketCryptoCurrency: GetTokenMarketCryptoCurrency,
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
) : Model(), AddTokenComponent.Callbacks by callbackDelegate {
private val params = paramsContainer.require<AddToPortfolioPreselectedDataComponent.Params>()
val portfolioFetcher: PortfolioFetcher = portfolioFetcherFactory.create(
mode = PortfolioFetcher.Mode.All(isOnlyMultiCurrency = true),
scope = modelScope,
)
val navigation = StackNavigation<AddToPortfolioRoutes>()
var currentStack = listOf<AddToPortfolioRoutes>(AddToPortfolioRoutes.Empty)
private val _selectedNetwork = MutableStateFlow<SelectedNetwork?>(null)
val selectedNetwork: Flow<SelectedNetwork> = _selectedNetwork.asStateFlow().filterNotNull()
private val _selectedPortfolio = MutableStateFlow<SelectedPortfolio?>(null)
val selectedPortfolio: Flow<SelectedPortfolio> = _selectedPortfolio.asStateFlow().filterNotNull()
val eventBuilder = PortfolioAnalyticsEvent.EventBuilder(
tokenSymbol = params.tokenToAdd.symbol,
source = AnalyticsParam.ScreensSources.Markets.value,
)
init {
navigation.subscribe { currentStack = it.transformer.invoke(currentStack) }
startAddToPortfolioFlow()
}
@Suppress("LongMethod")
private fun startAddToPortfolioFlow() {
channelFlow<Unit> {
fun finishSuccessFlow(currency: CryptoCurrency, userWalletId: UserWalletId) {
params.callback.onSuccess(addedToken = currency, walletId = userWalletId)
channel.close()
}
val data = createAvailableToAddDataForPreselectedNetwork(params.tokenToAdd.network)
?: return@channelFlow
val isAccountMode = portfolioSelectorController.isAccountModeSync()
val firstSelectedPortfolio = setupPortfolioFlow(data)
.onEach { _selectedPortfolio.value = it }
val firstSelectedNetwork = firstSelectedPortfolio
.map { portfolio -> createSelectedNetwork(network = params.tokenToAdd.network, portfolio = portfolio) }
.filterNotNull()
.onEach { _selectedNetwork.value = it }
val isSinglePortfolio = data.isSinglePortfolio
if (isSinglePortfolio) {
val accountId = data.availableToAddWallets.values.first()
.availableToAddAccounts.values.first()
.account.account.accountId
// force select a portfolio, triggers [selectedPortfolio]
portfolioSelectorController.selectAccount(accountId)
} else {
logAccountSelector(isAccountMode)
navigation.replaceAll(AddToPortfolioRoutes.PortfolioSelector)
}
// main flow that combine all require data
val allRequireForAdd = combine(
flow = firstSelectedNetwork,
flow2 = firstSelectedPortfolio,
transform = { a, b -> a to b },
)
// suspend until all required data is selected
val (selectedNetworkValue, selectedPortfolioValue) = allRequireForAdd.first()
val isTokenAlreadyAdded = selectedPortfolioValue.account.addedMarketNetworks
.any { it.networkId == selectedNetworkValue.selectedNetwork.networkId }
if (isTokenAlreadyAdded) {
finishSuccessFlow(
currency = selectedNetworkValue.cryptoCurrency,
userWalletId = selectedPortfolioValue.userWallet.walletId,
)
return@channelFlow
}
navigation.replaceAll(AddToPortfolioRoutes.AddToken)
val addedToken = callbackDelegate.onTokenAdded.receiveAsFlow().first()
messageSender.send(ToastMessage(message = resourceReference(R.string.markets_token_added)))
finishSuccessFlow(addedToken.currency, selectedPortfolioValue.userWallet.walletId)
}
.catch { throwable ->
Timber.e(throwable)
params.callback.onDismiss()
}
.launchIn(modelScope)
}
private fun logAccountSelector(isAccountMode: Boolean) {
if (isAccountMode) {
analyticsEventHandler.send(eventBuilder.popupToChooseAccount())
}
}
private fun minimalTokenMarketParams() = with(params.tokenToAdd) {
TokenMarketParams(
id = id,
name = name,
symbol = symbol,
tokenQuotes = TokenMarketParams.Quotes(
currentPrice = BigDecimal.ZERO,
h24Percent = null,
weekPercent = null,
monthPercent = null,
),
imageUrl = null,
)
}
/**
* Creates [AvailableToAddData] for preselected network when token is already added in all networks.
* This allows user to select wallet/account and do smth after it with selected info.
*/
private suspend fun createAvailableToAddDataForPreselectedNetwork(
preSelectedNetwork: TokenMarketInfo.Network,
): AvailableToAddData? {
val portfolioData = portfolioFetcher.data.firstOrNull() ?: return null
val availableToOpenWallets = portfolioData.balances.mapNotNull { (walletId, balance) ->
val wallet = balance.userWallet
val accounts = balance.accountsBalance.accountStatuses
val availableToAddAccounts = accounts.mapNotNull { accountStatus ->
val accountIndex = when (accountStatus) {
is AccountStatus.CryptoPortfolio -> accountStatus.account.derivationIndex
}
val cryptoCurrency = getTokenMarketCryptoCurrency(
userWalletId = walletId,
tokenMarketParams = minimalTokenMarketParams(),
network = preSelectedNetwork,
accountIndex = accountIndex,
) ?: return@mapNotNull null
val addedNetworks = getAccountCurrencyStatusUseCase.invokeSync(
userWalletId = walletId,
currency = cryptoCurrency,
).fold(
ifEmpty = { emptySet() },
ifSome = { setOf(it.status.currency.network) },
)
AvailableToAddAccount(
account = accountStatus,
availableNetworks = setOf(preSelectedNetwork),
addedNetworks = addedNetworks,
)
}.associateBy { it.account.account.accountId }
if (availableToAddAccounts.isEmpty()) return@mapNotNull null
walletId to AvailableToAddWallet(
userWallet = wallet,
accounts = accounts,
availableNetworks = setOf(preSelectedNetwork),
availableToAddAccounts = availableToAddAccounts,
)
}.toMap()
if (availableToOpenWallets.isEmpty()) return null
return AvailableToAddData(availableToAddWallets = availableToOpenWallets)
}
private suspend fun createCryptoCurrency(
userWallet: UserWallet,
network: TokenMarketInfo.Network,
account: AvailableToAddAccount,
): CryptoCurrency? {
val accountIndex = when (val accountStatus = account.account) {
is AccountStatus.CryptoPortfolio -> accountStatus.account.derivationIndex
}
return getTokenMarketCryptoCurrency(
userWalletId = userWallet.walletId,
tokenMarketParams = minimalTokenMarketParams(),
network = network,
accountIndex = accountIndex,
)
}
private suspend fun createSelectedNetwork(
network: TokenMarketInfo.Network,
portfolio: SelectedPortfolio,
): SelectedNetwork? {
val cryptoCurrency = createCryptoCurrency(
userWallet = portfolio.userWallet,
network = network,
account = portfolio.account,
) ?: return null
return SelectedNetwork(
cryptoCurrency = cryptoCurrency,
selectedNetwork = network,
isAvailableMoreNetwork = false,
)
}
private fun setupPortfolioFlow(data: AvailableToAddData): Flow<SelectedPortfolio> = combine(
flow = portfolioSelectorController.isAccountMode,
flow2 = portfolioSelectorController.selectedAccount,
transform = { isAccountMode, selectedAccountId ->
selectedAccountId ?: return@combine null
val availableToAddWallets =
data.availableToAddWallets[selectedAccountId.userWalletId] ?: return@combine null
val availableToAddAccount =
availableToAddWallets.availableToAddAccounts[selectedAccountId] ?: return@combine null
if (!isAccountMode) analyticsEventHandler.send(eventBuilder.addToPortfolioWalletChanged())
SelectedPortfolio(
isAccountMode = isAccountMode,
userWallet = availableToAddWallets.userWallet,
account = availableToAddAccount,
isAvailableMorePortfolio = false,
)
},
)
.filterNotNull()
}
@ModelScoped
internal class AddToPortfolioFromEarnCallbackDelegate @Inject constructor() :
AddTokenComponent.Callbacks {
val onTokenAdded = Channel<CryptoCurrencyStatus>()
override fun onChangeNetworkClick() = Unit
override fun onChangePortfolioClick() = Unit
override fun onTokenAdded(status: CryptoCurrencyStatus) {
onTokenAdded.trySend(status)
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.feed.components.market.details.portfolio.add.impl.model
import com.tangem.common.ui.account.CryptoPortfolioIconUM
import com.tangem.common.ui.account.AccountIconUM
import com.tangem.common.ui.account.CryptoPortfolioIconConverter
import com.tangem.common.ui.account.PortfolioSelectUM
import com.tangem.common.ui.account.toUM
import com.tangem.common.ui.addtoken.AddTokenUM
@ -35,7 +36,7 @@ internal class AddTokenUiBuilder @Inject constructor(
}
private fun createPortfolio(selectedPortfolio: SelectedPortfolio): PortfolioSelectUM {
val accountIcon: CryptoPortfolioIconUM?
val accountIcon: AccountIconUM.CryptoPortfolio?
val portfolioName: TextReference
when (selectedPortfolio.isAccountMode) {
false -> {
@ -46,7 +47,7 @@ internal class AddTokenUiBuilder @Inject constructor(
val accountStatus = selectedPortfolio.account.account
portfolioName = accountStatus.account.accountName.toUM().value
accountIcon = when (accountStatus) {
is AccountStatus.CryptoPortfolio -> accountStatus.account.icon.toUM()
is AccountStatus.CryptoPortfolio -> CryptoPortfolioIconConverter.convert(accountStatus.account.icon)
}
}
}

View file

@ -47,7 +47,6 @@ internal class TokenActionsModel @Inject constructor(
private val tokenActionsHandler: TokenActionsHandler =
tokenActionsIntentsFactory.create(
currentAppCurrency = Provider { currentAppCurrency.value },
updateTokenReceiveBSConfig = { },
onHandleQuickAction = { handledAction -> handledQuickAction(handledAction) },
)

View file

@ -1,7 +1,6 @@
package com.tangem.features.feed.components.market.details.portfolio.impl.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.TokenActionsBSContentUM
internal class PortfolioAnalyticsEvent(
@ -10,14 +9,14 @@ internal class PortfolioAnalyticsEvent(
) : AnalyticsEvent(category = "Markets / Chart", event = event, params = params) {
data class EventBuilder(
val token: TokenMarketParams,
val tokenSymbol: String,
val source: String?,
) {
fun addToPortfolioClicked() = PortfolioAnalyticsEvent(
event = "Button - Add To Portfolio",
params = mapOf(
"Token" to token.symbol,
"Token" to tokenSymbol,
),
)
@ -35,7 +34,7 @@ internal class PortfolioAnalyticsEvent(
event = "Token Network Selected",
params = mapOf(
"Count" to blockchainNames.size.toString(),
"Token" to token.symbol,
"Token" to tokenSymbol,
"blockchain" to blockchainNames.joinToString(separator = ", "),
),
)
@ -51,7 +50,7 @@ internal class PortfolioAnalyticsEvent(
else -> "error"
},
params = buildMap {
put("Token", token.symbol)
put("Token", tokenSymbol)
source?.let { put("Source", it) }
put("blockchain", blockchainName)
},

View file

@ -39,7 +39,6 @@ import com.tangem.features.feed.components.market.details.portfolio.impl.loader.
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.MyPortfolioUM
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.TokenActionsBSContentUM
import com.tangem.features.feed.impl.R
import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle
import com.tangem.features.wallet.utils.UserWalletImageFetcher
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.operations.attestation.ArtworkSize
@ -68,7 +67,6 @@ internal class MarketsPortfolioModel @Inject constructor(
private val saveMarketTokensUseCase: SaveMarketTokensUseCase,
private val addToPortfolioManager: AddToPortfolioManager,
private val analyticsEventHandler: AnalyticsEventHandler,
private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle,
private val userWalletImageFetcher: UserWalletImageFetcher,
private val receiveAddressesFactory: ReceiveAddressesFactory,
accountsFeatureToggles: AccountsFeatureToggles,
@ -81,7 +79,7 @@ internal class MarketsPortfolioModel @Inject constructor(
private val params = paramsContainer.require<MarketsPortfolioComponent.Params>()
private val analyticsEventBuilder = PortfolioAnalyticsEvent.EventBuilder(
token = params.token,
tokenSymbol = params.token.symbol,
source = params.analyticsParams?.source,
)
@ -111,13 +109,6 @@ internal class MarketsPortfolioModel @Inject constructor(
private val tokenActionsHandler = tokenActionsIntentsFactory.create(
currentAppCurrency = Provider { currentAppCurrency.value },
updateTokenReceiveBSConfig = { updateBlock ->
if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled.not()) {
updateTokensState {
it.copy(tokenReceiveBSConfig = updateBlock(it.tokenReceiveBSConfig))
}
}
},
onHandleQuickAction = { handledAction ->
analyticsEventHandler.send(
analyticsEventBuilder.quickActionClick(
@ -130,9 +121,7 @@ internal class MarketsPortfolioModel @Inject constructor(
.name,
),
)
if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) {
configureReceiveAddresses(handledAction)
}
configureReceiveAddresses(handledAction)
},
)
@ -426,8 +415,7 @@ internal class MarketsPortfolioModel @Inject constructor(
}
private fun configureReceiveAddresses(quickAction: TokenActionsHandler.HandledQuickAction) {
val isNewReceive = quickAction.action == TokenActionsBSContentUM.Action.Receive &&
tokenReceiveFeatureToggle.isNewTokenReceiveEnabled
val isNewReceive = quickAction.action == TokenActionsBSContentUM.Action.Receive
if (isNewReceive) {
modelScope.launch {
val tokenConfig = receiveAddressesFactory.create(

View file

@ -3,6 +3,7 @@ package com.tangem.features.feed.components.market.details.portfolio.impl.model
import arrow.core.getOrElse
import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.common.ui.account.CryptoPortfolioIconConverter
import com.tangem.common.ui.account.toUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.TextReference
@ -285,7 +286,7 @@ internal class NewMarketsPortfolioDelegate @AssistedInject constructor(
prefixText = TextReference.EMPTY,
name = this.accountName.toUM().value,
icon = when (this) {
is Account.CryptoPortfolio -> this.icon.toUM()
is Account.CryptoPortfolio -> CryptoPortfolioIconConverter.convert(this.icon)
is Account.Payment -> TODO("[REDACTED_JIRA]")
},
),

View file

@ -1,20 +1,16 @@
package com.tangem.features.feed.components.market.details.portfolio.impl.model
import com.tangem.common.routing.AppRoute
import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig
import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.onramp.model.OnrampSource
import com.tangem.domain.redux.ReduxStateHolder
@ -36,11 +32,9 @@ internal class TokenActionsHandler @AssistedInject constructor(
private val uiMessageSender: UiMessageSender,
private val reduxStateHolder: ReduxStateHolder,
@Assisted private val currentAppCurrency: Provider<AppCurrency>,
@Assisted private val updateTokenReceiveBSConfig: ((TangemBottomSheetConfig) -> TangemBottomSheetConfig) -> Unit,
@Assisted private val onHandleQuickAction: (HandledQuickAction) -> Unit,
private val isDemoCardUseCase: IsDemoCardUseCase,
private val messageSender: UiMessageSender,
private val shareManager: ShareManager,
) {
private val disabledActionsInDemoMode = buildSet {
@ -60,7 +54,7 @@ internal class TokenActionsHandler @AssistedInject constructor(
when (action) {
TokenActionsBSContentUM.Action.Buy -> onBuyClick(cryptoCurrencyData)
TokenActionsBSContentUM.Action.Exchange -> onExchangeClick(cryptoCurrencyData)
TokenActionsBSContentUM.Action.Receive -> onReceiveClick(cryptoCurrencyData)
TokenActionsBSContentUM.Action.Receive -> Unit
TokenActionsBSContentUM.Action.CopyAddress -> onCopyAddress(cryptoCurrencyData)
TokenActionsBSContentUM.Action.Sell -> onSellClick(cryptoCurrencyData)
TokenActionsBSContentUM.Action.Send -> onSendClick(cryptoCurrencyData)
@ -87,34 +81,6 @@ internal class TokenActionsHandler @AssistedInject constructor(
messageSender.send(message)
}
private fun onReceiveClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
val cryptoCurrencyStatus = cryptoCurrencyData.status
val currency = cryptoCurrencyStatus.currency
val networkAddress = cryptoCurrencyStatus.value.networkAddress ?: return
updateTokenReceiveBSConfig {
TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {
updateTokenReceiveBSConfig {
it.copy(isShown = false)
}
},
content = TokenReceiveBottomSheetConfig(
asset = TokenReceiveBottomSheetConfig.Asset.Currency(
name = currency.name,
symbol = currency.symbol,
),
network = currency.network,
networkAddress = networkAddress,
showMemoDisclaimer = currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE,
onCopyClick = { clipboardManager.setText(networkAddress.defaultAddress.value, isSensitive = true) },
onShareClick = { shareManager.shareText(networkAddress.defaultAddress.value) },
),
)
}
}
private fun onCopyAddress(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
val cryptoCurrencyStatus = cryptoCurrencyData.status
val networkAddress = cryptoCurrencyStatus.value.networkAddress ?: return
@ -196,7 +162,6 @@ internal class TokenActionsHandler @AssistedInject constructor(
interface Factory {
fun create(
currentAppCurrency: Provider<AppCurrency>,
updateTokenReceiveBSConfig: ((TangemBottomSheetConfig) -> TangemBottomSheetConfig) -> Unit,
onHandleQuickAction: (HandledQuickAction) -> Unit,
): TokenActionsHandler
}

View file

@ -12,7 +12,6 @@ import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.isZero
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlin.collections.map
/**
* Converter from [Map] of [UserWallet] and [CryptoCurrencyStatus] to [MyPortfolioUM.Tokens]
@ -50,9 +49,6 @@ internal class TokensPortfolioUMConverter(
buttonState = addButtonState,
addToPortfolioBSConfig = bsConfig,
onAddClick = onAddClick,
tokenReceiveBSConfig = (currentState() as? MyPortfolioUM.Tokens)
?.tokenReceiveBSConfig
?: TangemBottomSheetConfig.Empty,
)
}

View file

@ -16,7 +16,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import com.tangem.common.ui.account.AccountTitle
import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheet
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SmallButtonShimmer
@ -140,8 +139,6 @@ private fun TokenList(state: MyPortfolioUM.Tokens, modifier: Modifier = Modifier
}
}
}
TokenReceiveBottomSheet(config = state.tokenReceiveBSConfig)
}
@Composable

View file

@ -23,21 +23,18 @@ internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider<MyPortfol
tokens = persistentListOf(sampleToken, sampleToken),
buttonState = MyPortfolioUM.Tokens.AddButtonState.Available,
addToPortfolioBSConfig = TangemBottomSheetConfig.Empty,
tokenReceiveBSConfig = TangemBottomSheetConfig.Empty,
onAddClick = {},
),
MyPortfolioUM.Tokens(
tokens = persistentListOf(sampleToken, sampleToken.copy(isQuickActionsShown = true)),
buttonState = MyPortfolioUM.Tokens.AddButtonState.Unavailable,
addToPortfolioBSConfig = TangemBottomSheetConfig.Empty,
tokenReceiveBSConfig = TangemBottomSheetConfig.Empty,
onAddClick = {},
),
MyPortfolioUM.Tokens(
tokens = persistentListOf(sampleToken.copy(isQuickActionsShown = true), sampleToken),
buttonState = MyPortfolioUM.Tokens.AddButtonState.Loading,
addToPortfolioBSConfig = TangemBottomSheetConfig.Empty,
tokenReceiveBSConfig = TangemBottomSheetConfig.Empty,
onAddClick = {},
),
MyPortfolioUM.AddFirstToken(

View file

@ -13,7 +13,6 @@ internal sealed class MyPortfolioUM {
override val addToPortfolioBSConfig: TangemBottomSheetConfig,
val tokens: ImmutableList<PortfolioTokenUM>,
val buttonState: AddButtonState,
val tokenReceiveBSConfig: TangemBottomSheetConfig,
val onAddClick: () -> Unit,
) : MyPortfolioUM() {

View file

@ -9,4 +9,7 @@ internal class DefaultFeedFeatureToggle(
override val isFeedEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("FEED_ENABLED")
override val isEarnBlockEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("EARN_BLOCK_ENABLED")
}

View file

@ -47,10 +47,10 @@ internal class FeedComponentModel @Inject constructor(
private val fetchTrendingNewsUseCase: FetchTrendingNewsUseCase,
private val manageTrendingNewsUseCase: ManageTrendingNewsUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
private val stateController: FeedStateController,
getTopFiveMarketTokenUseCase: GetTopFiveMarketTokenUseCase,
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
paramsContainer: ParamsContainer,
private val stateController: FeedStateController,
) : Model() {
private val params = paramsContainer.require<DefaultFeedComponent.FeedParams>()
@ -80,11 +80,9 @@ internal class FeedComponentModel @Inject constructor(
init {
initializeState()
updateCallbacks()
modelScope.launch(dispatchers.default) {
fetchTrendingNewsUseCase()
}
fetchTrendingNews()
subscribeOnCurrencyUpdate()
loadCharts()
modelScope.launch(dispatchers.default) {
combine(
flow = marketsBatchFlowManager.itemsByOrder,
@ -144,13 +142,23 @@ internal class FeedComponentModel @Inject constructor(
}
}.collect()
}
}
private fun fetchTrendingNews() {
modelScope.launch(dispatchers.default) {
fetchTrendingNewsUseCase()
}
}
private fun subscribeOnCurrencyUpdate() {
modelScope.launch(dispatchers.default) {
currentAppCurrency.drop(1).collect {
marketsBatchFlowManager.reloadAll()
}
}
}
private fun loadCharts() {
modelScope.launch(dispatchers.default) {
TokenMarketListConfig.Order.entries.forEach { order ->
marketsBatchFlowManager.getOnLastBatchLoadedSuccessFlow(order)?.collect { batchKey ->

View file

@ -0,0 +1,44 @@
package com.tangem.features.feed.ui.feed.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.models.serialization.SerializedBigDecimal
import kotlinx.collections.immutable.ImmutableList
internal data class EarnListUM(
val items: ImmutableList<EarnListItemUM>,
val contentState: EarnListContentState,
)
@Immutable
internal data class EarnListItemUM(
val network: TextReference.Str,
val symbol: String,
val tokenName: String,
val currencyIconState: CurrencyIconState,
val earnValue: EarnValueUM,
val earnType: EarnType,
val onItemClick: () -> Unit,
)
@Immutable
internal data class EarnValueUM(
val percent: SerializedBigDecimal,
val earnValueType: EarnValueType,
)
internal enum class EarnType {
Staking, Yield
}
internal enum class EarnValueType {
APR, APY
}
@Immutable
internal sealed interface EarnListContentState {
data object Loading : EarnListContentState
data object Content : EarnListContentState
data class Error(val onRetryClicked: () -> Unit) : EarnListContentState
}

View file

@ -190,7 +190,10 @@ internal class HomeModel @Inject constructor(
return
}
saveWalletUseCase(userWallet).fold(
saveWalletUseCase(
userWallet = userWallet,
analyticsSource = AnalyticsParam.ScreensSources.Intro,
).fold(
ifLeft = {
delay(HIDE_PROGRESS_DELAY)
setLoading(false)

View file

@ -133,7 +133,10 @@ internal class CreateHardwareWalletModel @Inject constructor(
return
}
saveWalletUseCase(userWallet = userWallet).fold(
saveWalletUseCase(
userWallet = userWallet,
analyticsSource = AnalyticsParam.ScreensSources.AddNew,
).fold(
ifLeft = { saveWalletError ->
delay(HIDE_PROGRESS_DELAY)
setLoading(false)

View file

@ -1,8 +1,10 @@
package com.tangem.features.markets.portfolio.add.impl.model
import com.tangem.common.ui.account.CryptoPortfolioIconUM
import com.tangem.common.ui.account.AccountIconUM
import com.tangem.common.ui.account.CryptoPortfolioIconConverter
import com.tangem.common.ui.account.PortfolioSelectUM
import com.tangem.common.ui.account.toUM
import com.tangem.common.ui.addtoken.AddTokenUM
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
@ -16,7 +18,6 @@ import com.tangem.features.markets.impl.R
import com.tangem.features.markets.portfolio.add.api.SelectedNetwork
import com.tangem.features.markets.portfolio.add.api.SelectedPortfolio
import com.tangem.features.markets.portfolio.add.impl.AddTokenComponent
import com.tangem.common.ui.addtoken.AddTokenUM
import javax.inject.Inject
@ModelScoped
@ -35,7 +36,7 @@ internal class AddTokenUiBuilder @Inject constructor(
}
private fun createPortfolio(selectedPortfolio: SelectedPortfolio): PortfolioSelectUM {
val accountIcon: CryptoPortfolioIconUM?
val accountIcon: AccountIconUM.CryptoPortfolio?
val portfolioName: TextReference
when (selectedPortfolio.isAccountMode) {
false -> {
@ -46,7 +47,7 @@ internal class AddTokenUiBuilder @Inject constructor(
val accountStatus = selectedPortfolio.account.account
portfolioName = accountStatus.account.accountName.toUM().value
accountIcon = when (accountStatus) {
is AccountStatus.CryptoPortfolio -> accountStatus.account.icon.toUM()
is AccountStatus.CryptoPortfolio -> CryptoPortfolioIconConverter.convert(accountStatus.account.icon)
}
}
}

View file

@ -48,7 +48,6 @@ internal class TokenActionsModel @Inject constructor(
private val tokenActionsHandler: TokenActionsHandler =
tokenActionsIntentsFactory.create(
currentAppCurrency = Provider { currentAppCurrency.value },
updateTokenReceiveBSConfig = { },
onHandleQuickAction = { handledAction -> handledQuickAction(handledAction) },
)

View file

@ -40,7 +40,6 @@ import com.tangem.features.markets.portfolio.impl.loader.PortfolioDataLoader
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens.AddButtonState
import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM
import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle
import com.tangem.features.wallet.utils.UserWalletImageFetcher
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.operations.attestation.ArtworkSize
@ -69,12 +68,11 @@ internal class MarketsPortfolioModel @Inject constructor(
private val saveMarketTokensUseCase: SaveMarketTokensUseCase,
private val addToPortfolioManager: AddToPortfolioManager,
private val analyticsEventHandler: AnalyticsEventHandler,
private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle,
private val userWalletImageFetcher: UserWalletImageFetcher,
private val receiveAddressesFactory: ReceiveAddressesFactory,
private val accountsFeatureToggles: AccountsFeatureToggles,
accountsFeatureToggles: AccountsFeatureToggles,
newAddToPortfolioManagerFactory: NewAddToPortfolioManager.Factory,
private val newMarketsPortfolioDelegateFactory: NewMarketsPortfolioDelegate.Factory,
newMarketsPortfolioDelegateFactory: NewMarketsPortfolioDelegate.Factory,
) : Model() {
private val _state: MutableStateFlow<MyPortfolioUM> = MutableStateFlow(value = MyPortfolioUM.Loading)
@ -111,13 +109,6 @@ internal class MarketsPortfolioModel @Inject constructor(
private val tokenActionsHandler = tokenActionsIntentsFactory.create(
currentAppCurrency = Provider { currentAppCurrency.value },
updateTokenReceiveBSConfig = { updateBlock ->
if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled.not()) {
updateTokensState {
it.copy(tokenReceiveBSConfig = updateBlock(it.tokenReceiveBSConfig))
}
}
},
onHandleQuickAction = { handledAction ->
val currencyNetwork = handledAction.cryptoCurrencyData.status.currency.network
analyticsEventHandler.send(
@ -126,9 +117,7 @@ internal class MarketsPortfolioModel @Inject constructor(
blockchainName = currencyNetwork.name,
),
)
if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) {
configureReceiveAddresses(handledAction)
}
configureReceiveAddresses(handledAction)
},
)
@ -423,8 +412,7 @@ internal class MarketsPortfolioModel @Inject constructor(
}
private fun configureReceiveAddresses(quickAction: TokenActionsHandler.HandledQuickAction) {
val isNewReceive = quickAction.action == TokenActionsBSContentUM.Action.Receive &&
tokenReceiveFeatureToggle.isNewTokenReceiveEnabled
val isNewReceive = quickAction.action == TokenActionsBSContentUM.Action.Receive
if (isNewReceive) {
modelScope.launch {
val tokenConfig = receiveAddressesFactory.create(

View file

@ -3,6 +3,7 @@ package com.tangem.features.markets.portfolio.impl.model
import arrow.core.getOrElse
import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.common.ui.account.CryptoPortfolioIconConverter
import com.tangem.common.ui.account.toUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.TextReference
@ -286,7 +287,7 @@ internal class NewMarketsPortfolioDelegate @AssistedInject constructor(
prefixText = TextReference.EMPTY,
name = this.accountName.toUM().value,
icon = when (this) {
is Account.CryptoPortfolio -> this.icon.toUM()
is Account.CryptoPortfolio -> CryptoPortfolioIconConverter.convert(this.icon)
is Account.Payment -> TODO("[REDACTED_JIRA]")
},
),

View file

@ -1,20 +1,16 @@
package com.tangem.features.markets.portfolio.impl.model
import com.tangem.common.routing.AppRoute
import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig
import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.onramp.model.OnrampSource
import com.tangem.domain.redux.ReduxStateHolder
@ -36,11 +32,9 @@ internal class TokenActionsHandler @AssistedInject constructor(
private val uiMessageSender: UiMessageSender,
private val reduxStateHolder: ReduxStateHolder,
@Assisted private val currentAppCurrency: Provider<AppCurrency>,
@Assisted private val updateTokenReceiveBSConfig: ((TangemBottomSheetConfig) -> TangemBottomSheetConfig) -> Unit,
@Assisted private val onHandleQuickAction: (HandledQuickAction) -> Unit,
private val isDemoCardUseCase: IsDemoCardUseCase,
private val messageSender: UiMessageSender,
private val shareManager: ShareManager,
) {
private val disabledActionsInDemoMode = buildSet {
@ -60,7 +54,7 @@ internal class TokenActionsHandler @AssistedInject constructor(
when (action) {
TokenActionsBSContentUM.Action.Buy -> onBuyClick(cryptoCurrencyData)
TokenActionsBSContentUM.Action.Exchange -> onExchangeClick(cryptoCurrencyData)
TokenActionsBSContentUM.Action.Receive -> onReceiveClick(cryptoCurrencyData)
TokenActionsBSContentUM.Action.Receive -> Unit
TokenActionsBSContentUM.Action.CopyAddress -> onCopyAddress(cryptoCurrencyData)
TokenActionsBSContentUM.Action.Sell -> onSellClick(cryptoCurrencyData)
TokenActionsBSContentUM.Action.Send -> onSendClick(cryptoCurrencyData)
@ -87,34 +81,6 @@ internal class TokenActionsHandler @AssistedInject constructor(
messageSender.send(message)
}
private fun onReceiveClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
val cryptoCurrencyStatus = cryptoCurrencyData.status
val currency = cryptoCurrencyStatus.currency
val networkAddress = cryptoCurrencyStatus.value.networkAddress ?: return
updateTokenReceiveBSConfig {
TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {
updateTokenReceiveBSConfig {
it.copy(isShown = false)
}
},
content = TokenReceiveBottomSheetConfig(
asset = TokenReceiveBottomSheetConfig.Asset.Currency(
name = currency.name,
symbol = currency.symbol,
),
network = currency.network,
networkAddress = networkAddress,
showMemoDisclaimer = currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE,
onCopyClick = { clipboardManager.setText(networkAddress.defaultAddress.value, isSensitive = true) },
onShareClick = { shareManager.shareText(networkAddress.defaultAddress.value) },
),
)
}
}
private fun onCopyAddress(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
val cryptoCurrencyStatus = cryptoCurrencyData.status
val networkAddress = cryptoCurrencyStatus.value.networkAddress ?: return
@ -196,7 +162,6 @@ internal class TokenActionsHandler @AssistedInject constructor(
interface Factory {
fun create(
currentAppCurrency: Provider<AppCurrency>,
updateTokenReceiveBSConfig: ((TangemBottomSheetConfig) -> TangemBottomSheetConfig) -> Unit,
onHandleQuickAction: (HandledQuickAction) -> Unit,
): TokenActionsHandler
}

View file

@ -50,9 +50,6 @@ internal class TokensPortfolioUMConverter(
buttonState = addButtonState,
addToPortfolioBSConfig = bsConfig,
onAddClick = onAddClick,
tokenReceiveBSConfig = (currentState() as? MyPortfolioUM.Tokens)
?.tokenReceiveBSConfig
?: TangemBottomSheetConfig.Empty,
)
}

View file

@ -16,7 +16,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import com.tangem.common.ui.account.AccountTitle
import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheet
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SmallButtonShimmer
@ -141,8 +140,6 @@ private fun TokenList(state: MyPortfolioUM.Tokens, modifier: Modifier = Modifier
}
}
}
TokenReceiveBottomSheet(config = state.tokenReceiveBSConfig)
}
@Composable

View file

@ -93,21 +93,18 @@ internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider<MyPortfol
tokens = persistentListOf(sampleToken, sampleToken),
buttonState = MyPortfolioUM.Tokens.AddButtonState.Available,
addToPortfolioBSConfig = TangemBottomSheetConfig.Empty,
tokenReceiveBSConfig = TangemBottomSheetConfig.Empty,
onAddClick = {},
),
MyPortfolioUM.Tokens(
tokens = persistentListOf(sampleToken, sampleToken.copy(isQuickActionsShown = true)),
buttonState = MyPortfolioUM.Tokens.AddButtonState.Unavailable,
addToPortfolioBSConfig = TangemBottomSheetConfig.Empty,
tokenReceiveBSConfig = TangemBottomSheetConfig.Empty,
onAddClick = {},
),
MyPortfolioUM.Tokens(
tokens = persistentListOf(sampleToken.copy(isQuickActionsShown = true), sampleToken),
buttonState = MyPortfolioUM.Tokens.AddButtonState.Loading,
addToPortfolioBSConfig = TangemBottomSheetConfig.Empty,
tokenReceiveBSConfig = TangemBottomSheetConfig.Empty,
onAddClick = {},
),
MyPortfolioUM.AddFirstToken(

Some files were not shown because too many files have changed in this diff Show more