Updated on 2026-08-14
This commit is contained in:
commit
a31afce6c8
184 changed files with 2488 additions and 1558 deletions
41
Marathonfile
Normal file
41
Marathonfile
Normal 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
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
)
|
||||
|
|
@ -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
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">L’envoi d’actifs sur d’autres 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, j’ai 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 d’attente 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 d’abord 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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 Clore’s 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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -31,4 +31,5 @@ enum class PromoId {
|
|||
VisaPresale,
|
||||
BlackFriday,
|
||||
OnePlusOne,
|
||||
YieldPromo,
|
||||
}
|
||||
|
|
@ -33,6 +33,7 @@ class ShouldShowPromoWalletUseCase(
|
|||
PromoId.VisaPresale,
|
||||
PromoId.BlackFriday,
|
||||
PromoId.OnePlusOne,
|
||||
PromoId.YieldPromo,
|
||||
-> true
|
||||
PromoId.Sepa -> {
|
||||
val walletFirstUsageDate = settingsRepository.getWalletFirstUsageDate()
|
||||
|
|
|
|||
|
|
@ -60,5 +60,6 @@ sealed class PromoAnalyticsEvent(
|
|||
Sepa("Sepa"),
|
||||
BlackFriday("Black Friday"),
|
||||
OnePlusOne("One-Plus-One"),
|
||||
YieldPromo("Yield Promo"),
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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].
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -2,4 +2,5 @@ package com.tangem.features.feed.entry.featuretoggle
|
|||
|
||||
interface FeedFeatureToggle {
|
||||
val isFeedEnabled: Boolean
|
||||
val isEarnBlockEnabled: Boolean
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@ internal class TokenActionsModel @Inject constructor(
|
|||
private val tokenActionsHandler: TokenActionsHandler =
|
||||
tokenActionsIntentsFactory.create(
|
||||
currentAppCurrency = Provider { currentAppCurrency.value },
|
||||
updateTokenReceiveBSConfig = { },
|
||||
onHandleQuickAction = { handledAction -> handledQuickAction(handledAction) },
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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]")
|
||||
},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ internal class TokenActionsModel @Inject constructor(
|
|||
private val tokenActionsHandler: TokenActionsHandler =
|
||||
tokenActionsIntentsFactory.create(
|
||||
currentAppCurrency = Provider { currentAppCurrency.value },
|
||||
updateTokenReceiveBSConfig = { },
|
||||
onHandleQuickAction = { handledAction -> handledQuickAction(handledAction) },
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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]")
|
||||
},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,9 +50,6 @@ internal class TokensPortfolioUMConverter(
|
|||
buttonState = addButtonState,
|
||||
addToPortfolioBSConfig = bsConfig,
|
||||
onAddClick = onAddClick,
|
||||
tokenReceiveBSConfig = (currentState() as? MyPortfolioUM.Tokens)
|
||||
?.tokenReceiveBSConfig
|
||||
?: TangemBottomSheetConfig.Empty,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue