Updated on 2026-08-14

This commit is contained in:
Tangem 2024-11-29 11:07:53 +03:00
commit f4198d9a68
208 changed files with 3133 additions and 604 deletions

View file

@ -81,7 +81,7 @@ dependencies {
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
implementation(projects.core.navigation)
implementation(projects.core.featuretoggles)
implementation(projects.core.configToggles)
implementation(projects.core.res)
implementation(projects.core.ui)
implementation(projects.core.datasource)

View file

@ -5,7 +5,8 @@ import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.blockchainsdk.signer.TransactionSignerFactory
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.filter.OneTimeEventFilter
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.datasource.connection.NetworkConnectionManager
@ -34,6 +35,7 @@ import com.tangem.tap.common.log.TangemAppLoggerInitializer
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
import com.tangem.tap.features.home.featuretoggles.HomeFeatureToggles
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@ -52,6 +54,8 @@ interface ApplicationEntryPoint {
fun getFeatureTogglesManager(): FeatureTogglesManager
fun getExcludedBlockchainsManager(): ExcludedBlockchainsManager
fun getNetworkConnectionManager(): NetworkConnectionManager
fun getCardScanningFeatureToggles(): CardScanningFeatureToggles
@ -113,4 +117,6 @@ interface ApplicationEntryPoint {
fun getOnrampFeatureToggles(): OnrampFeatureToggles
fun getOnboardingV2FeatureToggles(): OnboardingV2FeatureToggles
fun getCoroutineDispatcherProvider(): CoroutineDispatcherProvider
}

View file

@ -74,6 +74,7 @@ import com.tangem.tap.common.redux.NotificationsHandler
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
import com.tangem.tap.features.intentHandler.IntentProcessor
import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler
import com.tangem.tap.features.intentHandler.handlers.OnPushClickedIntentHandler
import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler
import com.tangem.tap.features.main.MainViewModel
import com.tangem.tap.features.main.model.Toast
@ -392,6 +393,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
private fun initIntentHandlers() {
val hasSavedWalletsProvider = { userWalletsListManager.hasUserWallets }
intentProcessor.addHandler(OnPushClickedIntentHandler(analyticsEventsHandler))
intentProcessor.addHandler(BackgroundScanIntentHandler(hasSavedWalletsProvider, lifecycleScope))
intentProcessor.addHandler(WalletConnectLinkIntentHandler())
}

View file

@ -17,7 +17,8 @@ import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.core.analytics.models.Basic.TransactionSent.WalletForm
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.datasource.api.common.createNetworkLoggingInterceptor
import com.tangem.datasource.connection.NetworkConnectionManager
@ -57,9 +58,11 @@ import com.tangem.tap.domain.tasks.product.DerivationsFinder
import com.tangem.tap.features.home.featuretoggles.HomeFeatureToggles
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.wallet.BuildConfig
import dagger.hilt.EntryPoints
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import org.rekotlin.Store
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository as WalletConnect2Repository
@ -88,6 +91,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
private val featureTogglesManager: FeatureTogglesManager
get() = entryPoint.getFeatureTogglesManager()
private val excludedBlockchainsManager: ExcludedBlockchainsManager
get() = entryPoint.getExcludedBlockchainsManager()
private val networkConnectionManager: NetworkConnectionManager
get() = entryPoint.getNetworkConnectionManager()
@ -180,6 +186,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles
get() = entryPoint.getOnboardingV2FeatureToggles()
private val dispatchers: CoroutineDispatcherProvider
get() = entryPoint.getCoroutineDispatcherProvider()
// endregion
override fun onCreate() {
@ -200,9 +209,11 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
// TODO: Try to performance and user experience.
// [REDACTED_JIRA]
runBlocking {
featureTogglesManager.init()
initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize())
awaitAll(
async { featureTogglesManager.init() },
async { excludedBlockchainsManager.init() },
async { initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize()) },
)
}
loadNativeLibraries()
@ -216,7 +227,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
derivationsFinder = DerivationsFinder(
appPreferencesStore = appPreferencesStore,
dispatchers = AppCoroutineDispatcherProvider(),
dispatchers = dispatchers,
)
appStateHolder.mainStore = store

View file

@ -1,25 +0,0 @@
package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.models.AnalyticsEvent
/**
[REDACTED_AUTHOR]
*/
sealed class MainScreen(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent("Main Screen", event, params) {
class ScreenOpened : MainScreen("Screen opened")
class ButtonScanCard : MainScreen("Button - Scan Card")
class EnableBiometrics(state: AnalyticsParam.OnOffState) : MainScreen(
event = "Enable Biometric",
params = mapOf("State" to state.value),
)
class NoticeRateAppButton(result: AnalyticsParam.RateApp) : MainScreen(
event = "Notice - Rate The App Button Tapped",
params = mapOf("Result" to result.value),
)
}

View file

@ -0,0 +1,13 @@
package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.models.AnalyticsEvent
internal sealed class Push(event: String) : AnalyticsEvent(
category = "Push",
event = event,
params = emptyMap(),
error = null,
) {
data object PushNotificationOpened : Push(event = "Push Notification Opened")
}

View file

@ -11,7 +11,7 @@ sealed class SignIn(
error: Throwable? = null,
) : AnalyticsEvent("Sign In", event, params, error) {
class ScreenOpened : SignIn(event = "Sing In Screen Opened")
class ScreenOpened : SignIn(event = "Sign In Screen Opened")
class ButtonBiometricSignIn : SignIn(event = "Button - Biometric Sign In")
class ButtonCardSignIn : SignIn(event = "Button - Card Sign In")

View file

@ -12,11 +12,12 @@ internal sealed class WalletConnect(
) : AnalyticsEvent("Wallet Connect", event, params, error) {
class ScreenOpened : WalletConnect(event = "WC Screen Opened")
class NewSessionEstablished(dAppName: String, dAppUrl: String) : WalletConnect(
class NewSessionEstablished(dAppName: String, dAppUrl: String, blockchainNames: List<String>) : WalletConnect(
event = "New Session Established",
params = mapOf(
AnalyticsParam.DAPP_NAME to dAppName,
AnalyticsParam.DAPP_URL to dAppUrl,
AnalyticsParam.BLOCKCHAIN to blockchainNames.joinToString(","),
),
)

View file

@ -9,7 +9,6 @@ import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.IntroductionProcess
import com.tangem.tap.common.analytics.events.MainScreen
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.proxy.redux.DaggerGraphState
@ -31,7 +30,7 @@ class CardContextInterceptor(
override fun canBeAppliedTo(event: AnalyticsEvent): Boolean {
return when (event) {
is IntroductionProcess.ButtonScanCard, is MainScreen.ButtonScanCard -> false
is IntroductionProcess.ButtonScanCard -> false
else -> true
}
}

View file

@ -1,18 +0,0 @@
package com.tangem.tap.common.extensions
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.remove
fun Blockchain.getNetworkStandardName(): String {
return when (this) {
Blockchain.Ethereum, Blockchain.EthereumTestnet -> "ERC20"
Blockchain.BSC, Blockchain.BSCTestnet -> "BEP20"
Blockchain.Binance, Blockchain.BinanceTestnet -> "BEP2"
Blockchain.Tron, Blockchain.TronTestnet -> "TRC20"
Blockchain.TON -> "TON"
else -> ""
}
}
val Blockchain.networkNameWithoutTestnet
get() = this.getNetworkName().remove(" Testnet")

View file

@ -18,6 +18,7 @@ import com.google.firebase.messaging.RemoteMessage
import com.tangem.domain.common.LogConfig
import com.tangem.tap.MainActivity
import com.tangem.tap.common.images.createCoilImageLoader
import com.tangem.tap.features.intentHandler.handlers.OnPushClickedIntentHandler
import com.tangem.wallet.R
import timber.log.Timber
@ -37,6 +38,7 @@ internal class TangemPushNotificationService : FirebaseMessagingService() {
// TODO refactoring: [REDACTED_JIRA]
val intent = Intent(applicationContext, MainActivity::class.java)
intent.putExtra(OnPushClickedIntentHandler.IS_OPENED_FROM_PUSH, true)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
val pendingIntent = PendingIntent.getActivity(
/* context = */ this,

View file

@ -1,6 +1,7 @@
package com.tangem.tap.common.url
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
@ -34,6 +35,8 @@ internal class CustomTabsUrlOpener : UrlOpener {
)
.build()
customTabsIntent.intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
customTabsIntent.launchUrl(context, Uri.parse(url))
}
}

View file

@ -1,6 +1,6 @@
package com.tangem.tap.di.domain
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase

View file

@ -10,6 +10,7 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Suppress("TooManyFunctions")
@Module
@InstallIn(SingletonComponent::class)
internal object OnrampDomainModule {
@ -125,4 +126,35 @@ internal object OnrampDomainModule {
fun provideGetOnrampQuotesUseCase(onrampRepository: OnrampRepository): GetOnrampQuotesUseCase {
return GetOnrampQuotesUseCase(onrampRepository)
}
@Provides
@Singleton
fun provideGetOnrampSelectedPaymentMethodUseCase(
onrampRepository: OnrampRepository,
): GetOnrampSelectedPaymentMethodUseCase {
return GetOnrampSelectedPaymentMethodUseCase(onrampRepository)
}
@Provides
@Singleton
fun provideGetOnrampProviderWithQuoteUseCase(
onrampRepository: OnrampRepository,
): GetOnrampProviderWithQuoteUseCase {
return GetOnrampProviderWithQuoteUseCase(onrampRepository)
}
@Provides
@Singleton
fun provideOnrampSaveSelectedPaymentMethod(onrampRepository: OnrampRepository): OnrampSaveSelectedPaymentMethod {
return OnrampSaveSelectedPaymentMethod(onrampRepository)
}
@Provides
@Singleton
fun provideGetOnrampRedirectUrlUseCase(
onrampRepository: OnrampRepository,
transactionRepository: OnrampTransactionRepository,
): GetOnrampRedirectUrlUseCase {
return GetOnrampRedirectUrlUseCase(onrampRepository, transactionRepository)
}
}

View file

@ -1,6 +1,6 @@
package com.tangem.tap.domain.scanCard
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
/**
* Add custom token feature toggles

View file

@ -7,6 +7,7 @@ import com.tangem.common.authentication.storage.AuthenticatedStorage
import com.tangem.common.json.TangemSdkAdapter
import com.tangem.common.services.secure.SecureStorage
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.models.scan.serialization.*
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.sdk.storage.AndroidSecureStorage
import com.tangem.sdk.storage.createEncryptedSharedPreferences
@ -19,7 +20,6 @@ import com.tangem.tap.domain.userWalletList.repository.implementation.BiometricU
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultSelectedUserWalletRepository
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsPublicInformationRepository
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsSensitiveInformationRepository
import com.tangem.tap.domain.userWalletList.utils.json.*
import com.tangem.tap.tangemSdkManager
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider

View file

@ -1 +0,0 @@
package com.tangem.tap.domain.userWalletList.utils

View file

@ -267,7 +267,7 @@ internal class DefaultLegacyWalletConnectRepository(
)
}
override fun approve(userNamespaces: Map<NetworkNamespace, List<Account>>) {
override fun approve(userNamespaces: Map<NetworkNamespace, List<Account>>, blockchainNames: List<String>) {
val sessionProposal: Wallet.Model.SessionProposal = requireNotNull(this.sessionProposal)
val userChains = userNamespaces.flatMap { namespace ->
@ -311,6 +311,7 @@ internal class DefaultLegacyWalletConnectRepository(
WalletConnect.NewSessionEstablished(
dAppName = sessionProposal.name,
dAppUrl = sessionProposal.url,
blockchainNames = blockchainNames,
),
)
},

View file

@ -3,7 +3,7 @@ package com.tangem.tap.domain.walletconnect2.di
import android.app.Application
import com.squareup.moshi.Moshi
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.datasource.di.SdkMoshi
import com.tangem.datasource.files.FileReader
import com.tangem.domain.tokens.repository.CurrenciesRepository

View file

@ -21,7 +21,7 @@ interface LegacyWalletConnectRepository {
fun disconnect(topic: String)
fun approve(userNamespaces: Map<NetworkNamespace, List<Account>>)
fun approve(userNamespaces: Map<NetworkNamespace, List<Account>>, blockchainNames: List<String>)
fun reject()

View file

@ -1,5 +1,6 @@
package com.tangem.tap.domain.walletconnect2.domain
import arrow.core.flatten
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.domain.tokens.model.CryptoCurrency
@ -236,8 +237,12 @@ class WalletConnectInteractor(
blockchainHelper.getNamespaceFromFullChainIdOrNull(account.chainId)
?.let { NetworkNamespace(it) }
}.filterNotNull()
val blockchainNames = blockchainHelper.chainIdsToBlockchains(userNamespaces.values.flatten().map { it.chainId })
.map { it.fullName }
walletConnectRepository.approve(
userNamespaces = userNamespaces,
blockchainNames = blockchainNames,
)
}

View file

@ -1,6 +1,6 @@
package com.tangem.tap.domain.walletconnect2.toggles
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
internal class WalletConnectFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,

View file

@ -1,6 +1,6 @@
package com.tangem.tap.features.home.featuretoggles
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import javax.inject.Inject
class HomeFeatureToggles @Inject constructor(

View file

@ -0,0 +1,24 @@
package com.tangem.tap.features.intentHandler.handlers
import android.content.Intent
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.tap.common.analytics.events.Push
import com.tangem.tap.features.intentHandler.IntentHandler
internal class OnPushClickedIntentHandler(val analyticsEventHandler: AnalyticsEventHandler) : IntentHandler {
override fun handleIntent(intent: Intent?, isFromForeground: Boolean): Boolean {
val fromPush = intent?.extras?.getBoolean(IS_OPENED_FROM_PUSH) ?: false
return if (fromPush) {
analyticsEventHandler.send(Push.PushNotificationOpened)
true
} else {
false
}
}
companion object {
const val IS_OPENED_FROM_PUSH = "IS_OPENED_FROM_PUSH"
}
}

View file

@ -5,11 +5,11 @@ import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent
import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.tap.*
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.MainScreen
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.dispatchWithMain
@ -110,7 +110,11 @@ internal class SaveWalletMiddleware {
// TODO: Remove after onboarding refactoring
Analytics.send(Onboarding.EnableBiometrics(AnalyticsParam.OnOffState.On))
} else {
Analytics.send(MainScreen.EnableBiometrics(AnalyticsParam.OnOffState.On))
Analytics.send(
MainScreenAnalyticsEvent.EnableBiometrics(
state = com.tangem.core.analytics.models.AnalyticsParam.OnOffState.On,
),
)
}
/*
@ -148,7 +152,11 @@ internal class SaveWalletMiddleware {
// TODO: Remove after onboarding refactoring
Analytics.send(Onboarding.EnableBiometrics(AnalyticsParam.OnOffState.Off))
} else {
Analytics.send(MainScreen.EnableBiometrics(AnalyticsParam.OnOffState.Off))
Analytics.send(
MainScreenAnalyticsEvent.EnableBiometrics(
com.tangem.core.analytics.models.AnalyticsParam.OnOffState.Off,
),
)
}
}

View file

@ -56,14 +56,14 @@ object TradeCryptoMiddleware {
private fun proceedBuyAction(state: () -> AppState?, action: TradeCryptoAction.Buy) {
val isOnrampEnabled = store.inject(DaggerGraphState::onrampFeatureToggles).isFeatureEnabled
if (isOnrampEnabled) {
proceedWithOnramp(action.cryptoCurrencyStatus.currency)
proceedWithOnramp(action.userWallet.walletId, action.cryptoCurrencyStatus.currency)
} else {
proceedWithLegacyBuyAction(state, action)
}
}
private fun proceedWithOnramp(cryptoCurrency: CryptoCurrency) {
store.dispatchNavigationAction { push(AppRoute.Onramp(cryptoCurrency)) }
private fun proceedWithOnramp(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency) {
store.dispatchNavigationAction { push(AppRoute.Onramp(userWalletId = userWalletId, currency = cryptoCurrency)) }
}
private fun proceedWithLegacyBuyAction(state: () -> AppState?, action: TradeCryptoAction.Buy) {

View file

@ -197,7 +197,7 @@ internal class ChildFactory @Inject constructor(
is AppRoute.Onramp -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = OnrampComponent.Params(route.currency),
params = OnrampComponent.Params(userWalletId = route.userWalletId, cryptoCurrency = route.currency),
componentFactory = onrampComponentFactory,
)
}

View file

@ -295,7 +295,10 @@ sealed class AppRoute(val path: String) : Route {
}
@Serializable
data class Onramp(val currency: CryptoCurrency) : AppRoute(path = "/onramp/${currency.symbol}"), RouteBundleParams {
data class Onramp(
val userWalletId: UserWalletId,
val currency: CryptoCurrency,
) : AppRoute(path = "/onramp/${userWalletId.stringValue}/${currency.symbol}"), RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer())
}
@ -322,6 +325,7 @@ sealed class AppRoute(val path: String) : Route {
) : AppRoute(path = "/swap_crypto/${userWalletId.stringValue}")
// Onboarding V2
@Serializable
data class Onboarding(
val scanResponse: ScanResponse,
val startFromBackup: Boolean,

View file

@ -76,6 +76,9 @@ sealed class AnalyticsParam {
data object MyWallets : ScreensSources("My Wallets")
data object Token : ScreensSources("Token")
data object Stories : ScreensSources("Stories")
data object Buy : ScreensSources("Buy")
data object Swap : ScreensSources("Swap")
data object Sell : ScreensSources("Sell")
}
sealed class TxSentFrom(val value: String) {
@ -161,6 +164,12 @@ sealed class AnalyticsParam {
}
}
enum class Status(val value: String) {
Success(value = "Success"),
Error(value = "Error"),
Pending(value = "Pending"),
}
companion object Key {
const val BLOCKCHAIN = "blockchain"
const val TOKEN_PARAM = "Token"

View file

@ -0,0 +1,75 @@
package com.tangem.core.analytics.models.event
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
/**
* Main screen analytics event
*
* @param event event name
* @param params params
*/
sealed class MainScreenAnalyticsEvent(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(category = "Main Screen", event = event, params = params) {
class EnableBiometrics(state: AnalyticsParam.OnOffState) : MainScreenAnalyticsEvent(
event = "Enable Biometric",
params = mapOf("State" to state.value),
)
// region Action Buttons feature
data class ButtonBuy(val status: AnalyticsParam.Status) : MainScreenAnalyticsEvent(
event = "Button - Buy",
params = mapOf(AnalyticsParam.STATUS to status.value),
)
data class ButtonSwap(val status: AnalyticsParam.Status) : MainScreenAnalyticsEvent(
event = "Button - Swap",
params = mapOf(AnalyticsParam.STATUS to status.value),
)
data class ButtonSell(val status: AnalyticsParam.Status) : MainScreenAnalyticsEvent(
event = "Button - Sell",
params = mapOf(AnalyticsParam.STATUS to status.value),
)
data object BuyScreenOpened : MainScreenAnalyticsEvent(event = "Buy Screen Opened")
data object SwapScreenOpened : MainScreenAnalyticsEvent(event = "Swap Screen Opened")
data object SellScreenOpened : MainScreenAnalyticsEvent(event = "Sell Screen Opened")
data class BuyTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent(
event = "Buy Token Clicked",
params = mapOf(TOKEN_PARAM to currencySymbol),
)
data class SellTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent(
event = "Sell Token Clicked",
params = mapOf(TOKEN_PARAM to currencySymbol),
)
data class SwapTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent(
event = "Swap Token Clicked",
params = mapOf(TOKEN_PARAM to currencySymbol),
)
data class ReceiveTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent(
event = "Receive Token Clicked",
params = mapOf(TOKEN_PARAM to currencySymbol),
)
data class RemoveTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent(
event = "Remove Button Clicked",
params = mapOf(TOKEN_PARAM to currencySymbol),
)
data class ButtonClose(val source: AnalyticsParam.ScreensSources) : MainScreenAnalyticsEvent(
event = "Button - Close",
params = mapOf(AnalyticsParam.SOURCE to source.value),
)
// endregion
}

View file

@ -7,7 +7,7 @@ plugins {
}
android {
namespace = "com.tangem.core.featuretoggles"
namespace = "com.tangem.core.configtoggle"
}
dependencies {

View file

@ -0,0 +1,10 @@
[
{
"name": "",
"version": "undefined"
},
{
"name": "NEXA",
"version": "undefined"
}
]

View file

@ -0,0 +1,8 @@
package com.tangem.core.configtoggle.blockchain
interface ExcludedBlockchainsManager {
val excludedBlockchainsIds: Set<String>
suspend fun init()
}

View file

@ -0,0 +1,10 @@
package com.tangem.core.configtoggle.blockchain
interface MutableExcludedBlockchainsManager : ExcludedBlockchainsManager {
suspend fun excludeBlockchain(mainnetId: String, isExcluded: Boolean)
fun isMatchLocalConfig(): Boolean
suspend fun recoverLocalConfig()
}

View file

@ -0,0 +1,74 @@
package com.tangem.core.configtoggle.blockchain.impl
import com.tangem.core.configtoggle.blockchain.MutableExcludedBlockchainsManager
import com.tangem.core.configtoggle.storage.TogglesStorage
import com.tangem.core.configtoggle.utils.associateToggles
import com.tangem.core.configtoggle.version.VersionProvider
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.datasource.local.preferences.utils.store
internal class DefaultExcludedBlockchainsManager(
private val localTogglesStorage: TogglesStorage,
private val appPreferencesStore: AppPreferencesStore,
private val versionProvider: VersionProvider,
) : MutableExcludedBlockchainsManager {
private var isInitialized: Boolean = false
private lateinit var currentExcludedBlockchainsIds: Set<String>
private lateinit var localExcludedBlockchainsIds: Set<String>
override val excludedBlockchainsIds: Set<String>
get() {
if (!isInitialized) error("ExcludedBlockchainsManager is not initialized")
return currentExcludedBlockchainsIds
}
override suspend fun init() {
localTogglesStorage.populate(path = "configs/excluded_blockchains_config")
val storedExcludedBlockchainsIds = appPreferencesStore.getSyncOrDefault(
key = PreferencesKeys.EXCLUDED_BLOCKCHAINS_KEY,
default = emptySet(),
)
localExcludedBlockchainsIds = localTogglesStorage.toggles
.associateToggles(currentVersion = versionProvider.get().orEmpty())
.filterValues { isIncluded -> !isIncluded }
.keys
currentExcludedBlockchainsIds = localExcludedBlockchainsIds + storedExcludedBlockchainsIds
isInitialized = true
}
override suspend fun excludeBlockchain(mainnetId: String, isExcluded: Boolean) {
if (isExcluded) {
currentExcludedBlockchainsIds += mainnetId
} else {
currentExcludedBlockchainsIds -= mainnetId
}
storeCurrent()
}
override fun isMatchLocalConfig(): Boolean {
return currentExcludedBlockchainsIds == localExcludedBlockchainsIds
}
override suspend fun recoverLocalConfig() {
currentExcludedBlockchainsIds = localExcludedBlockchainsIds
storeCurrent()
}
private suspend fun storeCurrent() {
appPreferencesStore.store(
PreferencesKeys.EXCLUDED_BLOCKCHAINS_KEY,
currentExcludedBlockchainsIds,
)
}
}

View file

@ -0,0 +1,49 @@
package com.tangem.core.configtoggle.di
import android.content.Context
import com.tangem.core.configtoggle.BuildConfig
import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager
import com.tangem.core.configtoggle.blockchain.MutableExcludedBlockchainsManager
import com.tangem.core.configtoggle.blockchain.impl.DefaultExcludedBlockchainsManager
import com.tangem.core.configtoggle.storage.LocalTogglesStorage
import com.tangem.core.configtoggle.version.DefaultVersionProvider
import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.local.preferences.AppPreferencesStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object ExcludedBlockchainsManagerModule {
@Provides
@Singleton
fun provideExcludedBlockchainsManager(
@ApplicationContext context: Context,
assetLoader: AssetLoader,
appPreferencesStore: AppPreferencesStore,
): ExcludedBlockchainsManager {
val localTogglesStorage = LocalTogglesStorage(assetLoader)
val versionProvider = DefaultVersionProvider(context)
return DefaultExcludedBlockchainsManager(
localTogglesStorage,
appPreferencesStore,
versionProvider,
)
}
@Provides
@Singleton
fun provideMutableExcludedBlockchainsManager(
manager: ExcludedBlockchainsManager,
): MutableExcludedBlockchainsManager? {
if (!BuildConfig.TESTER_MENU_ENABLED) return null
return manager as MutableExcludedBlockchainsManager
}
}

View file

@ -1,12 +1,12 @@
package com.tangem.core.featuretoggle.di
package com.tangem.core.configtoggle.di
import android.content.Context
import com.tangem.core.featuretoggle.manager.DevFeatureTogglesManager
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.core.featuretoggle.manager.ProdFeatureTogglesManager
import com.tangem.core.featuretoggle.storage.LocalFeatureTogglesStorage
import com.tangem.core.featuretoggle.version.DefaultVersionProvider
import com.tangem.core.featuretoggles.BuildConfig
import com.tangem.core.configtoggle.BuildConfig
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.core.configtoggle.feature.impl.DevFeatureTogglesManager
import com.tangem.core.configtoggle.feature.impl.ProdFeatureTogglesManager
import com.tangem.core.configtoggle.storage.LocalTogglesStorage
import com.tangem.core.configtoggle.version.DefaultVersionProvider
import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.local.preferences.AppPreferencesStore
import dagger.Module
@ -27,18 +27,18 @@ internal object FeatureTogglesManagerModule {
assetLoader: AssetLoader,
appPreferencesStore: AppPreferencesStore,
): FeatureTogglesManager {
val localFeatureTogglesStorage = LocalFeatureTogglesStorage(assetLoader)
val localTogglesStorage = LocalTogglesStorage(assetLoader)
val versionProvider = DefaultVersionProvider(context)
return if (BuildConfig.TESTER_MENU_ENABLED) {
DevFeatureTogglesManager(
localFeatureTogglesStorage = localFeatureTogglesStorage,
localTogglesStorage = localTogglesStorage,
appPreferencesStore = appPreferencesStore,
versionProvider = versionProvider,
)
} else {
ProdFeatureTogglesManager(
localFeatureTogglesStorage = localFeatureTogglesStorage,
localTogglesStorage = localTogglesStorage,
versionProvider = versionProvider,
)
}

View file

@ -1,4 +1,4 @@
package com.tangem.core.featuretoggle.manager
package com.tangem.core.configtoggle.feature
/**
* Component for getting information about the availability of feature toggles

View file

@ -1,4 +1,4 @@
package com.tangem.core.featuretoggle.manager
package com.tangem.core.configtoggle.feature
/**
* Component for change information about the availability of feature toggles

View file

@ -1,9 +1,10 @@
package com.tangem.core.featuretoggle.manager
package com.tangem.core.configtoggle.feature.impl
import androidx.annotation.VisibleForTesting
import com.tangem.core.featuretoggle.storage.FeatureTogglesStorage
import com.tangem.core.featuretoggle.utils.associateToggles
import com.tangem.core.featuretoggle.version.VersionProvider
import com.tangem.core.configtoggle.feature.MutableFeatureTogglesManager
import com.tangem.core.configtoggle.storage.TogglesStorage
import com.tangem.core.configtoggle.utils.associateToggles
import com.tangem.core.configtoggle.version.VersionProvider
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
@ -13,12 +14,12 @@ import kotlin.properties.Delegates
/**
* Feature toggles manager implementation in DEV build
*
* @property localFeatureTogglesStorage local feature toggles storage
* @property localTogglesStorage local feature toggles storage
* @property appPreferencesStore application local store
* @property versionProvider application version provider
*/
internal class DevFeatureTogglesManager(
private val localFeatureTogglesStorage: FeatureTogglesStorage,
private val localTogglesStorage: TogglesStorage,
private val appPreferencesStore: AppPreferencesStore,
private val versionProvider: VersionProvider,
) : MutableFeatureTogglesManager {
@ -27,13 +28,13 @@ internal class DevFeatureTogglesManager(
private var localFeatureTogglesMap: Map<String, Boolean> by Delegates.notNull()
override suspend fun init() {
localFeatureTogglesStorage.init()
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
val savedFeatureToggles = appPreferencesStore.getObjectSyncOrNull<Map<String, Boolean>>(
key = PreferencesKeys.FEATURE_TOGGLES_KEY,
) ?: emptyMap()
val localFeatureToggles = localFeatureTogglesStorage.featureToggles
val localFeatureToggles = localTogglesStorage.toggles
.associateToggles(currentVersion = versionProvider.get().orEmpty())
localFeatureTogglesMap = localFeatureToggles

View file

@ -0,0 +1,6 @@
package com.tangem.core.configtoggle.feature.impl
internal object FeatureTogglesConstants {
const val LOCAL_CONFIG_PATH: String = "configs/feature_toggles_config"
}

View file

@ -1,27 +1,28 @@
package com.tangem.core.featuretoggle.manager
package com.tangem.core.configtoggle.feature.impl
import androidx.annotation.VisibleForTesting
import com.tangem.core.featuretoggle.storage.FeatureTogglesStorage
import com.tangem.core.featuretoggle.utils.associateToggles
import com.tangem.core.featuretoggle.version.VersionProvider
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.core.configtoggle.storage.TogglesStorage
import com.tangem.core.configtoggle.utils.associateToggles
import com.tangem.core.configtoggle.version.VersionProvider
import kotlin.properties.Delegates
/**
* Feature toggles manager implementation in PROD build
*
* @property localFeatureTogglesStorage local feature toggles storage
* @property localTogglesStorage local feature toggles storage
* @property versionProvider application version provider
*/
internal class ProdFeatureTogglesManager(
private val localFeatureTogglesStorage: FeatureTogglesStorage,
private val localTogglesStorage: TogglesStorage,
private val versionProvider: VersionProvider,
) : FeatureTogglesManager {
private var featureToggles: Map<String, Boolean> by Delegates.notNull()
override suspend fun init() {
localFeatureTogglesStorage.init()
featureToggles = localFeatureTogglesStorage.featureToggles
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
featureToggles = localTogglesStorage.toggles
.associateToggles(currentVersion = versionProvider.get() ?: "")
}

View file

@ -0,0 +1,20 @@
package com.tangem.core.configtoggle.storage
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Data model with information about config toggle
*
* @property name toggle name
* @property version version in which the toggle will be enabled
*
* IMPORTANT: if the version is "undefined", it means that toggle is disabled!
*
[REDACTED_AUTHOR]
*/
@JsonClass(generateAdapter = true)
internal data class ConfigToggle(
@Json(name = "name") val name: String,
@Json(name = "version") val version: String,
)

View file

@ -0,0 +1,24 @@
package com.tangem.core.configtoggle.storage
import com.tangem.datasource.asset.loader.AssetLoader
import kotlin.properties.Delegates
/**
* Storage implementation for storing local feature toggles.
* Feature toggles are declared in file [LOCAL_CONFIG_PATH].
*
* @property assetLoader asset loader
*
[REDACTED_AUTHOR]
*/
internal class LocalTogglesStorage(
private val assetLoader: AssetLoader,
) : TogglesStorage {
override var toggles: List<ConfigToggle> by Delegates.notNull()
private set
override suspend fun populate(path: String) {
toggles = assetLoader.loadList<ConfigToggle>(path)
}
}

View file

@ -0,0 +1,15 @@
package com.tangem.core.configtoggle.storage
/**
* Component that initializes and stores a list of feature toggles
*
[REDACTED_AUTHOR]
*/
internal interface TogglesStorage {
/** List of feature toggles */
val toggles: List<ConfigToggle>
/** Populate the storage with toggles */
suspend fun populate(path: String)
}

View file

@ -0,0 +1,13 @@
package com.tangem.core.configtoggle.utils
import com.tangem.core.configtoggle.storage.ConfigToggle
import com.tangem.core.configtoggle.version.VersionAvailabilityContract
internal fun List<ConfigToggle>.associateToggles(currentVersion: String): Map<String, Boolean> {
return associate { localToggle ->
Pair(
first = localToggle.name,
second = VersionAvailabilityContract(currentVersion, localToggle.version),
)
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.core.featuretoggle.version
package com.tangem.core.configtoggle.version
import android.content.Context
import android.content.pm.PackageManager

View file

@ -1,4 +1,4 @@
package com.tangem.core.featuretoggle.contract
package com.tangem.core.configtoggle.version
import androidx.annotation.VisibleForTesting
import timber.log.Timber

View file

@ -1,4 +1,4 @@
package com.tangem.core.featuretoggle.contract
package com.tangem.core.configtoggle.version
/**
* Version contract to evaluate availability of feature toggle

View file

@ -1,4 +1,4 @@
package com.tangem.core.featuretoggle.version
package com.tangem.core.configtoggle.version
/** Application version provider */
internal interface VersionProvider {

View file

@ -1,6 +1,7 @@
package com.tangem.core.featuretoggle.contract
package com.tangem.core.configtoggle.contract
import com.google.common.truth.Truth
import com.tangem.core.configtoggle.version.VersionAvailabilityContract
import org.junit.Test
/**

View file

@ -1,6 +1,7 @@
package com.tangem.core.featuretoggle.contract
package com.tangem.core.configtoggle.contract
import com.google.common.truth.Truth
import com.tangem.core.configtoggle.version.Version
import org.junit.Test
/**

View file

@ -1,11 +1,13 @@
package com.tangem.core.featuretoggle.manager
package com.tangem.core.configtoggle.manager
import android.annotation.SuppressLint
import com.google.common.truth.Truth
import com.tangem.core.featuretoggle.storage.FeatureToggle
import com.tangem.core.featuretoggle.storage.FeatureTogglesStorage
import com.tangem.core.featuretoggle.utils.associateToggles
import com.tangem.core.featuretoggle.version.VersionProvider
import com.tangem.core.configtoggle.feature.impl.DevFeatureTogglesManager
import com.tangem.core.configtoggle.feature.impl.FeatureTogglesConstants
import com.tangem.core.configtoggle.storage.ConfigToggle
import com.tangem.core.configtoggle.storage.TogglesStorage
import com.tangem.core.configtoggle.utils.associateToggles
import com.tangem.core.configtoggle.version.VersionProvider
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
@ -19,13 +21,13 @@ import kotlin.collections.set
[REDACTED_AUTHOR]
*/
@SuppressLint("CheckResult")
internal class DevFeatureTogglesManagerTest {
internal class DevTogglesManagerTest {
private val localFeatureTogglesStorage = mockk<FeatureTogglesStorage>()
private val localTogglesStorage = mockk<TogglesStorage>()
private val appPreferenceStore = mockk<AppPreferencesStore>(relaxed = true)
private val versionProvider = mockk<VersionProvider>()
private val manager = DevFeatureTogglesManager(
localFeatureTogglesStorage = localFeatureTogglesStorage,
localTogglesStorage = localTogglesStorage,
appPreferencesStore = appPreferenceStore,
versionProvider = versionProvider,
)
@ -34,17 +36,17 @@ internal class DevFeatureTogglesManagerTest {
fun `successfully initialize storage if shared prefs kept feature toggles`() = runTest {
val currentVersion = "0.1.0"
coEvery { localFeatureTogglesStorage.init() } just Runs
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
coEvery {
appPreferenceStore.getObjectSyncOrNull<Map<String, Boolean>>(PreferencesKeys.FEATURE_TOGGLES_KEY)
} returns savedFeatureTogglesMap
coEvery { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles
coEvery { localTogglesStorage.toggles } returns localFeatureToggles
coEvery { versionProvider.get() } returns currentVersion
manager.init()
coVerifyOrder {
localFeatureTogglesStorage.init()
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
versionProvider.get()
}
@ -61,17 +63,17 @@ internal class DevFeatureTogglesManagerTest {
fun `successfully initialize storage if shared prefs kept empty list`() = runTest {
val currentVersion = "0.1.0"
coEvery { localFeatureTogglesStorage.init() } just Runs
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
coEvery {
appPreferenceStore.getObjectSyncOrNull<Map<String, Boolean>>(PreferencesKeys.FEATURE_TOGGLES_KEY)
} returns emptyMap()
coEvery { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles
coEvery { localTogglesStorage.toggles } returns localFeatureToggles
coEvery { versionProvider.get() } returns currentVersion
manager.init()
coVerifyOrder {
localFeatureTogglesStorage.init()
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
versionProvider.get()
}
@ -88,17 +90,17 @@ internal class DevFeatureTogglesManagerTest {
fun `successfully initialize storage if shared prefs didn't keep feature toggles`() = runTest {
val currentVersion = "0.1.0"
coEvery { localFeatureTogglesStorage.init() } just Runs
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
coEvery {
appPreferenceStore.getObjectSyncOrNull<Map<String, Boolean>>(PreferencesKeys.FEATURE_TOGGLES_KEY)
} returns null
coEvery { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles
coEvery { localTogglesStorage.toggles } returns localFeatureToggles
coEvery { versionProvider.get() } returns currentVersion
manager.init()
coVerifyOrder {
localFeatureTogglesStorage.init()
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
versionProvider.get()
}
@ -111,15 +113,15 @@ internal class DevFeatureTogglesManagerTest {
@Test
fun `successfully initialize storage if versionProvider returns null`() = runTest {
coEvery { localFeatureTogglesStorage.init() } just Runs
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
coEvery { appPreferenceStore.getSyncOrNull(PreferencesKeys.FEATURE_TOGGLES_KEY) } returns savedFeatureToggles
coEvery { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles
coEvery { localTogglesStorage.toggles } returns localFeatureToggles
coEvery { versionProvider.get() } returns null
manager.init()
coVerifyOrder {
localFeatureTogglesStorage.init()
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
versionProvider.get()
}
@ -222,8 +224,8 @@ internal class DevFeatureTogglesManagerTest {
)
val localFeatureToggles = listOf(
FeatureToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"),
FeatureToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"),
ConfigToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"),
ConfigToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"),
)
}
}

View file

@ -1,11 +1,13 @@
package com.tangem.core.featuretoggle.manager
package com.tangem.core.configtoggle.manager
import android.content.pm.PackageManager
import com.google.common.truth.Truth
import com.tangem.core.featuretoggle.storage.FeatureToggle
import com.tangem.core.featuretoggle.storage.FeatureTogglesStorage
import com.tangem.core.featuretoggle.utils.associateToggles
import com.tangem.core.featuretoggle.version.VersionProvider
import com.tangem.core.configtoggle.feature.impl.FeatureTogglesConstants
import com.tangem.core.configtoggle.feature.impl.ProdFeatureTogglesManager
import com.tangem.core.configtoggle.storage.ConfigToggle
import com.tangem.core.configtoggle.storage.TogglesStorage
import com.tangem.core.configtoggle.utils.associateToggles
import com.tangem.core.configtoggle.version.VersionProvider
import io.mockk.*
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -13,24 +15,24 @@ import org.junit.Test
/**
[REDACTED_AUTHOR]
*/
internal class ProdFeatureTogglesManagerTest {
internal class ProdTogglesManagerTest {
private val localFeatureTogglesStorage = mockk<FeatureTogglesStorage>()
private val localTogglesStorage = mockk<TogglesStorage>()
private val versionProvider = mockk<VersionProvider>()
private val manager = ProdFeatureTogglesManager(localFeatureTogglesStorage, versionProvider)
private val manager = ProdFeatureTogglesManager(localTogglesStorage, versionProvider)
@Test
fun `successfully initialize storage`() = runTest {
val currentVersion = "1.0.0"
coEvery { localFeatureTogglesStorage.init() } just Runs
every { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
every { localTogglesStorage.toggles } returns localFeatureToggles
every { versionProvider.get() } returns currentVersion
manager.init()
coVerifyOrder {
localFeatureTogglesStorage.init()
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
versionProvider.get()
}
@ -40,14 +42,14 @@ internal class ProdFeatureTogglesManagerTest {
@Test
fun `successfully initialize storage if versionProvider returns null`() = runTest {
coEvery { localFeatureTogglesStorage.init() } just Runs
every { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
every { localTogglesStorage.toggles } returns localFeatureToggles
every { versionProvider.get() } returns null
manager.init()
coVerifyOrder {
localFeatureTogglesStorage.init()
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
versionProvider.get()
}
@ -56,8 +58,8 @@ internal class ProdFeatureTogglesManagerTest {
@Test
fun `failure initialize storage if localFeatureTogglesStorage throws exception`() = runTest {
coEvery { localFeatureTogglesStorage.init() } just Runs
every { localFeatureTogglesStorage.featureToggles } throws IllegalStateException(
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
every { localTogglesStorage.toggles } throws IllegalStateException(
"Property featureToggles should be initialized before get.",
)
@ -72,14 +74,14 @@ internal class ProdFeatureTogglesManagerTest {
Truth.assertThat(it).isInstanceOf(IllegalStateException::class.java)
}
coVerifyOrder { localFeatureTogglesStorage.init() }
coVerifyOrder { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) }
verifyAll(inverse = true) { versionProvider.get() }
}
@Test
fun `failure initialize storage if versionProvider throws exception`() = runTest {
coEvery { localFeatureTogglesStorage.init() } just Runs
every { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
every { localTogglesStorage.toggles } returns localFeatureToggles
every { versionProvider.get() } throws PackageManager.NameNotFoundException()
runCatching { manager.init() }
@ -89,7 +91,7 @@ internal class ProdFeatureTogglesManagerTest {
}
coVerifyOrder {
localFeatureTogglesStorage.init()
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
versionProvider.get()
}
}
@ -122,8 +124,8 @@ internal class ProdFeatureTogglesManagerTest {
private companion object {
val localFeatureToggles = listOf(
FeatureToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"),
FeatureToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"),
ConfigToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"),
ConfigToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"),
)
val disabledFeatureToggles = mapOf(

View file

@ -1,10 +1,11 @@
package com.tangem.core.featuretoggle.storage
package com.tangem.core.configtoggle.storage
import android.annotation.SuppressLint
import com.google.common.truth.Truth
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.tangem.core.configtoggle.feature.impl.FeatureTogglesConstants
import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.asset.reader.AssetReader
import io.mockk.*
@ -16,16 +17,16 @@ import java.io.IOException
[REDACTED_AUTHOR]
*/
@SuppressLint("CheckResult")
internal class LocalFeatureTogglesStorageTest {
internal class LocalTogglesStorageTest {
private val assetReader = mockk<AssetReader>()
private val moshi = mockk<Moshi>()
private val jsonAdapter = mockk<JsonAdapter<List<FeatureToggle>>>()
private val jsonAdapter = mockk<JsonAdapter<List<ConfigToggle>>>()
// Impossible to mockk AssetLoader because it implement inline functions
private val assetLoader = AssetLoader(assetReader = assetReader, moshi = moshi)
private val storage = LocalFeatureTogglesStorage(assetLoader)
private val storage = LocalTogglesStorage(assetLoader)
@Test
fun `successfully initialize storage`() = runTest {
@ -33,14 +34,14 @@ internal class LocalFeatureTogglesStorageTest {
everyCreatingMoshiAdapter() returns jsonAdapter
everyMappingJson() returns featureToggles
storage.init()
storage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
coVerifyOrder {
assetReader.read(CONFIG_FILE_NAME)
jsonAdapter.fromJson(json)
}
Truth.assertThat(storage.featureToggles).containsExactlyElementsIn(featureToggles)
Truth.assertThat(storage.toggles).containsExactlyElementsIn(featureToggles)
}
@Test
@ -49,33 +50,33 @@ internal class LocalFeatureTogglesStorageTest {
everyCreatingMoshiAdapter() returns jsonAdapter
everyMappingJson() throws IOException()
storage.init()
storage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
coVerifyOrder {
assetReader.read(CONFIG_FILE_NAME)
jsonAdapter.fromJson(json)
}
Truth.assertThat(storage.featureToggles).containsExactlyElementsIn(emptyList<FeatureToggle>())
Truth.assertThat(storage.toggles).containsExactlyElementsIn(emptyList<ConfigToggle>())
}
@Test
fun `failure initialize storage if jsonAdapter throws exception`() = runTest {
everyReadingJson() throws IOException()
storage.init()
storage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
coVerifyOrder { assetReader.read(CONFIG_FILE_NAME) }
verifyAll(inverse = true) { jsonAdapter.fromJson(any<String>()) }
Truth.assertThat(storage.featureToggles).containsExactlyElementsIn(emptyList<FeatureToggle>())
Truth.assertThat(storage.toggles).containsExactlyElementsIn(emptyList<ConfigToggle>())
}
private fun everyReadingJson() = coEvery { assetReader.read(CONFIG_FILE_NAME) }
private fun everyCreatingMoshiAdapter() = every {
val types = Types.newParameterizedType(List::class.java, FeatureToggle::class.java)
moshi.adapter<List<FeatureToggle>>(types)
val types = Types.newParameterizedType(List::class.java, ConfigToggle::class.java)
moshi.adapter<List<ConfigToggle>>(types)
}
private fun everyMappingJson() = every { jsonAdapter.fromJson(json) }
@ -97,8 +98,8 @@ internal class LocalFeatureTogglesStorageTest {
""".trimIndent()
val featureToggles = listOf(
FeatureToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"),
FeatureToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"),
ConfigToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"),
ConfigToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"),
)
}
}

View file

@ -20,6 +20,18 @@ data class OnrampDataResponse(
val signature: String,
)
@JsonClass(generateAdapter = true)
data class OnrampDataJson(
@Json(name = "widgetUrl")
val widgetUrl: String,
@Json(name = "externalTxId")
val externalTxId: String,
@Json(name = "externalTxUrl")
val externalTxUrl: String?,
)
@JsonClass(generateAdapter = true)
data class OnrampTxDetails(
@Json(name = "fromCurrencyCode")

View file

@ -39,8 +39,8 @@ data class OnrampQuoteResponse(
val maxFromAmount: String,
@Json(name = "minToAmount")
val minToAmount: String,
val minToAmount: String?,
@Json(name = "maxToAmount")
val maxToAmount: String,
val maxToAmount: String?,
)

View file

@ -1,11 +1,14 @@
package com.tangem.datasource.local.onramp.paymentmethods
import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO
import kotlinx.coroutines.flow.Flow
interface OnrampPaymentMethodsStore {
suspend fun getSyncOrNull(key: String): List<PaymentMethodDTO>?
fun get(key: String): Flow<List<PaymentMethodDTO>>
suspend fun store(key: String, value: List<PaymentMethodDTO>)
suspend fun contains(key: String): Boolean

View file

@ -55,6 +55,8 @@ object PreferencesKeys {
val FEATURE_TOGGLES_KEY by lazy { stringPreferencesKey(name = "featureToggles") }
val EXCLUDED_BLOCKCHAINS_KEY by lazy { stringSetPreferencesKey(name = "excludedBlockchains") }
val WAS_TWINS_ONBOARDING_SHOWN by lazy { booleanPreferencesKey(name = "twinsOnboardingShown") }
val IS_TANGEM_TOS_ACCEPTED_KEY by lazy { booleanPreferencesKey(name = "tangem_tos_accepted") }

View file

@ -2,11 +2,13 @@ package com.tangem.core.deeplink.global
import com.tangem.core.deeplink.DeepLink
class BuyCurrencyDeepLink(val onReceive: () -> Unit) : DeepLink {
class BuyCurrencyDeepLink(val onReceive: (txId: String) -> Unit) : DeepLink {
override val uri: String = "tangem://redirect?action=dismissBrowser"
override fun onReceive(params: Map<String, String>) {
onReceive()
onReceive(
params["txId"] ?: return,
)
}
}

View file

@ -1,20 +0,0 @@
package com.tangem.core.featuretoggle.storage
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Data model with information about feature toggle
*
* @property name feature toggle name
* @property version version number in which the feature will be included
*
* IMPORTANT: if the version is "undefined", it means that feature toggle is disabled!
*
[REDACTED_AUTHOR]
*/
@JsonClass(generateAdapter = true)
internal data class FeatureToggle(
@Json(name = "name") val name: String,
@Json(name = "version") val version: String,
)

View file

@ -1,15 +0,0 @@
package com.tangem.core.featuretoggle.storage
/**
* Component that initializes and stores a list of feature toggles
*
[REDACTED_AUTHOR]
*/
internal interface FeatureTogglesStorage {
/** List of feature toggles */
val featureToggles: List<FeatureToggle>
/** Initialize storage */
suspend fun init()
}

View file

@ -1,29 +0,0 @@
package com.tangem.core.featuretoggle.storage
import com.tangem.core.featuretoggle.storage.LocalFeatureTogglesStorage.Companion.LOCAL_CONFIG_PATH
import com.tangem.datasource.asset.loader.AssetLoader
import kotlin.properties.Delegates
/**
* Storage implementation for storing local feature toggles.
* Feature toggles are declared in file [LOCAL_CONFIG_PATH].
*
* @property assetLoader asset loader
*
[REDACTED_AUTHOR]
*/
internal class LocalFeatureTogglesStorage(
private val assetLoader: AssetLoader,
) : FeatureTogglesStorage {
override var featureToggles: List<FeatureToggle> by Delegates.notNull()
private set
override suspend fun init() {
featureToggles = assetLoader.loadList<FeatureToggle>(LOCAL_CONFIG_PATH)
}
private companion object {
const val LOCAL_CONFIG_PATH: String = "configs/feature_toggles_config"
}
}

View file

@ -1,13 +0,0 @@
package com.tangem.core.featuretoggle.utils
import com.tangem.core.featuretoggle.contract.VersionAvailabilityContract
import com.tangem.core.featuretoggle.storage.FeatureToggle
internal fun List<FeatureToggle>.associateToggles(currentVersion: String): Map<String, Boolean> {
return associate { localToggle ->
Pair(
first = localToggle.name,
second = VersionAvailabilityContract(currentVersion, localToggle.version),
)
}
}

View file

@ -1,13 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="action_buttons_buy_empty_search_message">Token nicht in Deinem Portfolio gefunden? Überprüfe die Märkte, um es zu finden und zum Kauf hinzuzufügen</string>
<string name="action_buttons_sell_empty_search_message">Token nicht in Deinem Portfolio gefunden? Überprüfe die Märkte, um es zu finden und zum Verkauf hinzuzufügen</string>
<string name="action_buttons_sell_empty_search_message">Token nicht in Ihrem Portfolio gefunden? Überprüfe die Märkte, um es zu finden und zum Verkauf hinzuzufügen</string>
<string name="action_buttons_swap_choose_token">Wähle den Token</string>
<string name="action_buttons_swap_empty_search_message">Token nicht in Deinem Portfolio gefunden? Überprüfe die Märkte, um es für Swaps zu finden und hinzuzufügen</string>
<string name="action_buttons_swap_no_available_pair_notification_message">Es gibt keine verfügbaren Token, die mit dem ausgewählten Token getauscht werden können. Bitte wähle einen anderen.</string>
<string name="action_buttons_swap_no_available_pair_notification_title">Kein verfügbares Paar</string>
<string name="action_buttons_you_want_to_receive">Du möchtest erhalten</string>
<string name="action_buttons_you_want_to_swap">Du möchtest tauschen</string>
<string name="action_buttons_you_want_to_receive">Wähle den Token aus, das Du erhalten möchtest</string>
<string name="action_buttons_you_want_to_swap">Wähle den Token, den Du tauschen möchtest</string>
<string name="add_custom_token_choose_network">Netzwerk wählen</string>
<string name="add_custom_token_title">Token anlegen</string>
<string name="add_tokens_title">Token verwalten</string>
@ -126,7 +126,7 @@
<string name="common_fee_selector_option_market">Markt</string>
<string name="common_fee_selector_option_slow">Langsam</string>
<string name="common_fee_selector_title">Gebühren</string>
<string name="common_generate_addresses">Adressen abrufen</string>
<string name="common_generate_addresses">Adressen synchronisieren</string>
<string name="common_go_to_provider">Zum Anbieter gehen</string>
<string name="common_go_to_token">Zum Token</string>
<string name="common_hour">stunde</string>
@ -251,6 +251,7 @@
<string name="express_choose_providers_subtitle">Tangem bietet Token-Swaps über Drittanbieter gemäß den jeweiligen Bedingungen des jeweiligen Anbieters an.</string>
<string name="express_choose_providers_title">Anbieter wählen</string>
<string name="express_error_code">Es ist ein Fehler aufgetreten. Code: %s</string>
<string name="express_error_provider_amount_roundup">Fehler %1$s. Der ausgewählte Anbieter kann die angegebene Transaktion nicht verarbeiten. Bitte runde den Wert auf %2$s auf oder änder diesen.</string>
<string name="express_error_swap_pair_unavailable">Der gewählte Anbieter ist im Moment nicht verfügbar. Bitte versuche es später noch einmal. (Code: %s)</string>
<string name="express_error_swap_unavailable">Swaps sind im Moment nicht verfügbar. Bitte versuche es später noch einmal. (Code: %s)</string>
<string name="express_estimated_amount">Geschätzter Betrag</string>
@ -296,7 +297,7 @@
<string name="express_provider_recommended">Empfohlen</string>
<string name="express_status_bought">Gekauft %s</string>
<string name="express_status_buying">Kauf %s</string>
<string name="express_status_buying_active">Kauf %s...</string>
<string name="express_status_buying_active">Kauf %s</string>
<string name="express_token_list_empty_search">Keine Token gefunden. Bitte versuche eine andere Anfrage</string>
<string name="express_transaction_id">ID: %s</string>
<string name="express_transaction_id_copied">Transaktions-ID kopiert</string>
@ -565,6 +566,8 @@
<string name="onboarding_wallet_info_title_fourth">Zugangscode wiederherstellen</string>
<string name="onboarding_wallet_info_title_second">Identische Karten oder Ring</string>
<string name="onboarding_wallet_info_title_third">Zugangscode</string>
<string name="onramp_avaiable_with_payment_methods">Verfügbar mit %s</string>
<string name="onramp_choose_provider_title_hint">Anbieter erleichtern Transaktionen und sorgen für einen reibungslosen und effizienten Token-Austausch</string>
<string name="onramp_country_search">Suche nach Land</string>
<string name="onramp_country_unavailable">Nicht verfügbar</string>
<string name="onramp_currency_search">Suche nach Währung</string>
@ -572,10 +575,11 @@
<string name="onramp_min_amount_restriction">Der zu kaufende Betrag muss mindestens %s betragen</string>
<string name="onramp_no_available_providers">Keine verfügbaren Anbieter für diese Währung</string>
<string name="onramp_pay_with">Bezahlen mit</string>
<string name="onramp_provider_max_amount">Verfügbar bis zu %s</string>
<string name="onramp_provider_min_amount">Erhältlich bei %s</string>
<string name="onramp_redirecting_to_provider_subtitle">Du kannst Deine Transaktion beim Drittanbieter %s abschließen.</string>
<string name="onramp_redirecting_to_provider_title">Umleitung auf %s…</string>
<string name="onramp_residency_bottomsheet_country_not_supported">Unsere Dienstleistungen sind in diesem Land nicht verfügbar</string>
<string name="onramp_residency_bottomsheet_country_subtitle">Änder oder bestätige bitte</string>
<string name="onramp_residency_bottomsheet_title">Dein Wohnsitz wurde identifiziert als</string>
<string name="onramp_settings_residence">Residenz</string>
<string name="onramp_settings_residence_description">Bitte wähle das richtige Land aus, um korrekte Zahlungsoptionen und Dienstleistungen zu gewährleisten.</string>
@ -590,8 +594,11 @@
<string name="qr_scanner_camera_denied_settings_button">Einstellungen</string>
<string name="qr_scanner_camera_denied_text">Du hast keinen Zugriff auf deine Kamera gewährt</string>
<string name="qr_scanner_camera_denied_title">Kamerazugriff verweigert</string>
<string name="receive_bottom_sheet_no_memo_required_message">Kein Memo erforderlich</string>
<string name="receive_bottom_sheet_warning_message">%1$s ( %2$s ) im %3$s Netzwerk</string>
<string name="receive_bottom_sheet_warning_message_description">Das Senden einer anderen Währung führt zu deren irreversiblem Verlust.</string>
<string name="receive_bottom_sheet_warning_message_full">Sende nur %s an diese Adresse. Der Versand einer anderen Währung führt zu ihrem unwiderruflichen Verlust.</string>
<string name="receive_bottom_sheet_warning_message_title">Sende nur %s an diese Adresse.</string>
<string name="receive_token_description">Überweise Geld von einem anderen Wallet oder einer anderen Börse</string>
<string name="referral_button_participate">Teilnehmen</string>
<string name="referral_error_failed_to_load_info">Die Informationen zum Empfehlungsprogramm konnten nicht geladen werden. Bitte versuche es später noch einmal.</string>
@ -1004,12 +1011,12 @@
<string name="warning_failed_to_verify_card_message">Bei dieser Karte oder Ring kann es sich um ein Produktionsmuster oder eine Fälschung handeln</string>
<string name="warning_failed_to_verify_card_title">Echtheitsprüfung fehlgeschlagen</string>
<string name="warning_hedera_missing_token_association_button_title">zuordnen</string>
<string name="warning_hedera_missing_token_association_message">Dieser Token muss mit Ihrem Hedera-Konto verbunden sein, bevor du diesen erhalten kannst. Zuordnungsgebühr ~%1$s %2$s</string>
<string name="warning_hedera_missing_token_association_message">Dieser Token muss mit Ihrem Hedera-Konto verbunden sein, bevor du diesen erhalten kannst. Zuordnungsgebühr ~%1$s %2$s</string>
<string name="warning_hedera_missing_token_association_message_brief">Dieses Token muss mit deinem Hedera-Konto verknüpft sein, bevor du ihn erhalten kannst.</string>
<string name="warning_hedera_missing_token_association_title">Verknüpfe deinen Token</string>
<string name="warning_hedera_token_association_not_enough_hbar_message">Nicht genug %s. Lade dein Hedera-Konto auf, um dieses Token zuzuordnen</string>
<string name="warning_kaspa_unfinished_token_transaction_discard_message">Bist Du sicher, dass Du die Transaktion stornieren willst? Du kannst die Transaktion nicht erneut starten.</string>
<string name="warning_kaspa_unfinished_token_transaction_message">Deine Transaktion mit einem Betrag von %1$s %2$s wurde nicht abgeschlossen. Du kannst versuchen, diese erneut abzuschließen.</string>
<string name="warning_kaspa_unfinished_token_transaction_message">Deine Transaktion mit einem Betrag von %1$s %2$s wurde nicht abgeschlossen. Du kannst versuchen, diese erneut abzuschließen.</string>
<string name="warning_kaspa_unfinished_token_transaction_title">Du hast eine nicht abgeschlossene Transaktion</string>
<string name="warning_low_signatures_message">Auf dieser Karte oder Ring sind nur noch %s Unterschriften übrig. Du musst dein gesamtes Guthaben abheben.</string>
<string name="warning_low_signatures_title">Geringe Anzahl von Unterschriften</string>

View file

@ -6,8 +6,6 @@
<string name="action_buttons_swap_empty_search_message">¿No encuentras el token en tu billetera? Consulte los mercados para encontrarlo y agregarlo al intercambio</string>
<string name="action_buttons_swap_no_available_pair_notification_message">No hay tokens disponibles para intercambiar con el token seleccionado. Por favor elige otro.</string>
<string name="action_buttons_swap_no_available_pair_notification_title">No hay par disponible</string>
<string name="action_buttons_you_want_to_receive">Desea recibir</string>
<string name="action_buttons_you_want_to_swap">Quiere intercambiar</string>
<string name="add_custom_token_choose_network">Elige red</string>
<string name="add_custom_token_title">Agregue un token personalizado</string>
<string name="add_tokens_title">Gestionar tokens</string>
@ -126,7 +124,7 @@
<string name="common_fee_selector_option_market">Mercado</string>
<string name="common_fee_selector_option_slow">Lento</string>
<string name="common_fee_selector_title">Velocidad y tarifa</string>
<string name="common_generate_addresses">Obtener direcciones</string>
<string name="common_generate_addresses">Sincronizar direcciones</string>
<string name="common_go_to_provider">Ir al proveedor</string>
<string name="common_go_to_token">Ir al token</string>
<string name="common_hour">hora</string>
@ -432,7 +430,6 @@
<string name="markets_token_details_fully_diluted_valuation_description">El valor teórico total de una criptomoneda si todas las monedas que podrían existir estuvieran en circulación, incluidas las que actualmente no circulan.</string>
<string name="markets_token_details_fully_diluted_valuation_full">Valoración totalmente diluida</string>
<string name="markets_token_details_genesis_date">Dato de Genesis</string>
<string name="markets_token_details_genesis_date_description">Vacío</string>
<string name="markets_token_details_high">Alto</string>
<string name="markets_token_details_holders">Titulares</string>
<string name="markets_token_details_holders_description">El cambio en el número de poseedores de tokens dentro de un período de tiempo específico</string>
@ -443,7 +440,6 @@
<string name="markets_token_details_liquidity_description">El cambio en la cantidad de liquidez disponible para el token durante el período de tiempo especificado</string>
<string name="markets_token_details_liquidity_full">Liquidez</string>
<string name="markets_token_details_liquidity_index">Índice de liquidez</string>
<string name="markets_token_details_liquidity_index_description">Vacío</string>
<string name="markets_token_details_listed_on">Listado en</string>
<string name="markets_token_details_low">Bajo</string>
<string name="markets_token_details_market_capitalization">Capital. de mercado</string>
@ -517,12 +513,12 @@
<string name="onboarding_navbar_title_creating_backup">Creando un backup</string>
<string name="onboarding_seed_button_read_more">Leer más sobre seed phrase</string>
<plurals name="onboarding_seed_generate_message_words_count">
<item quantity="one">Vacío</item>
<item quantity="one"></item>
<item quantity="other">Escriba estas %d palabras en el orden que se indica a continuación y guárdelas en un lugar seguro y secreto.</item>
</plurals>
<string name="onboarding_seed_generate_title">Su seed phrase</string>
<plurals name="onboarding_seed_generate_words_count">
<item quantity="one">Vacío</item>
<item quantity="one"></item>
<item quantity="other">%d palabras</item>
</plurals>
<string name="onboarding_seed_import_message">Para importar su billetera, ingrese su seed phrase en el campo de abajo</string>
@ -574,10 +570,11 @@
<string name="onramp_min_amount_restriction">La cantidad a comprar debe ser como mínimo %s</string>
<string name="onramp_no_available_providers">No hay proveedores disponibles para esta moneda</string>
<string name="onramp_pay_with">Pagar con</string>
<string name="onramp_provider_max_amount">Disponible hasta %s</string>
<string name="onramp_provider_min_amount">Disponible desde %s</string>
<string name="onramp_redirecting_to_provider_subtitle">Podrá completar su transacción en el proveedor externo, %s</string>
<string name="onramp_redirecting_to_provider_title">Redirigiendo a %s...</string>
<string name="onramp_residency_bottomsheet_country_not_supported">Nuestros servicios no están disponibles en este país</string>
<string name="onramp_residency_bottomsheet_country_subtitle">Cámbielo o confírmelo</string>
<string name="onramp_residency_bottomsheet_title">Su residencia ha sido identificada como</string>
<string name="onramp_settings_residence">Residencia</string>
<string name="onramp_settings_residence_description">Seleccione el país correcto para garantizar opciones de pago y servicios precisos.</string>
@ -1006,12 +1003,12 @@
<string name="warning_failed_to_verify_card_message">Esta tarjeta podría ser una muestra de producción o una falsificación</string>
<string name="warning_failed_to_verify_card_title">La verificación de autenticidad falló</string>
<string name="warning_hedera_missing_token_association_button_title">Asociar</string>
<string name="warning_hedera_missing_token_association_message">Este token debe estar asociado a tu cuenta de Hedera antes de que pueda recibirlo. Tarifa de asociación ~%1$s %2$s</string>
<string name="warning_hedera_missing_token_association_message">Este token debe estar asociado a tu cuenta de Hedera antes de que pueda recibirlo. Tarifa de asociación ~%1$s %2$s</string>
<string name="warning_hedera_missing_token_association_message_brief">Este token debe estar asociado con su cuenta de Hedera antes de poder recibirlo.</string>
<string name="warning_hedera_missing_token_association_title">Asocie su token</string>
<string name="warning_hedera_token_association_not_enough_hbar_message">No hay suficiente %s. Recargue su cuenta de Hedera para asociar este token</string>
<string name="warning_kaspa_unfinished_token_transaction_discard_message">¿Está seguro(a) de que desea cancelar la transacción? No podrá volver a intentar realizarla.</string>
<string name="warning_kaspa_unfinished_token_transaction_message">Su transacción con un importe de %1$s %2$s no se ha completado. Puede volver a intentarlo para completarla.</string>
<string name="warning_kaspa_unfinished_token_transaction_message">Su transacción con un importe de %1$s %2$s no se ha completado. Puede volver a intentarlo para completarla.</string>
<string name="warning_kaspa_unfinished_token_transaction_title">Tiene una transacción sin terminar</string>
<string name="warning_low_signatures_message">Solo quedan %s firmas en esta tarjeta. Deba retirar todos sus fondos.</string>
<string name="warning_low_signatures_title">Recuento de firmas bajo</string>

View file

@ -6,8 +6,6 @@
<string name="action_buttons_swap_empty_search_message">Jeton non trouvé dans votre portefeuille ? Consultez les marchés pour le trouver et l\'ajouter à l\'échange</string>
<string name="action_buttons_swap_no_available_pair_notification_message">Il n\'y a pas de token disponible pour échanger avec le token sélectionné. Veuillez en choisir un autre.</string>
<string name="action_buttons_swap_no_available_pair_notification_title">Aucune paire disponible</string>
<string name="action_buttons_you_want_to_receive">Vous souhaitez recevoir</string>
<string name="action_buttons_you_want_to_swap">Vous souhaitez échanger</string>
<string name="add_custom_token_choose_network">Choisissez le réseau</string>
<string name="add_custom_token_title">Ajouter un jeton personnalisé</string>
<string name="add_tokens_title">Gérer les jetons</string>
@ -126,7 +124,7 @@
<string name="common_fee_selector_option_market">Marché</string>
<string name="common_fee_selector_option_slow">Lent</string>
<string name="common_fee_selector_title">Vitesse et frais</string>
<string name="common_generate_addresses">Obtenir des adresses</string>
<string name="common_generate_addresses">Synchroniser les adresses</string>
<string name="common_go_to_provider">Aller au fournisseur</string>
<string name="common_go_to_token">Aller au jeton</string>
<string name="common_hour">heure</string>
@ -370,7 +368,7 @@
<string name="manage_tokens_wallet_selector_title">Choisissez un portefeuille</string>
<string name="manage_tokens_wallet_support_only_one_network_title">Le portefeuille ne prend pas en charge plus d\'un réseau</string>
<string name="markets_add_to_my_portfolio_description">Pour acheter, échanger ou recevoir cet actif, ajoutez-le à votre portefeuille</string>
<string name="markets_add_to_my_portfolio_unavailable_description">Cet actif n\'est plus pris en charge dans le portefeuille</string>
<string name="markets_add_to_my_portfolio_unavailable_description">Cet actif n\'est pas pris en charge dans le portefeuille</string>
<string name="markets_add_to_my_portfolio_unavailable_for_wallet_description">Cet actif n\'est pas disponible pour ce portefeuille</string>
<string name="markets_add_to_portfolio_button">Ajouter au portfolio</string>
<string name="markets_add_token">Ajoutez un jeton</string>
@ -432,7 +430,6 @@
<string name="markets_token_details_fully_diluted_valuation_description">La valeur théorique totale d\'une crypto-monnaie si toutes les pièces qui pourraient exister étaient en circulation, y compris celles qui ne circulent pas actuellement</string>
<string name="markets_token_details_fully_diluted_valuation_full">Valorisation entièrement diluée</string>
<string name="markets_token_details_genesis_date">Date de la Genesis</string>
<string name="markets_token_details_genesis_date_description">Vide</string>
<string name="markets_token_details_high">Haut</string>
<string name="markets_token_details_holders">Détenteurs</string>
<string name="markets_token_details_holders_description">L\'évolution du nombre de détenteurs de jetons au cours d\'une période donnée</string>
@ -443,7 +440,6 @@
<string name="markets_token_details_liquidity_description">Le changement dans la quantité de liquidité disponible pour le jeton pendant la période spécifiée</string>
<string name="markets_token_details_liquidity_full">Liquidité</string>
<string name="markets_token_details_liquidity_index">Indice de liquidité</string>
<string name="markets_token_details_liquidity_index_description">Vide</string>
<string name="markets_token_details_listed_on">Listé sur</string>
<string name="markets_token_details_low">Faible</string>
<string name="markets_token_details_market_capitalization">Cap. boursière</string>
@ -517,12 +513,12 @@
<string name="onboarding_navbar_title_creating_backup">Sauvegarde en cours</string>
<string name="onboarding_seed_button_read_more">En savoir plus sur les seed phrases</string>
<plurals name="onboarding_seed_generate_message_words_count">
<item quantity="one">Vide</item>
<item quantity="one"></item>
<item quantity="other">Écrivez ces %d mots dans l\'ordre indiqué ci-dessous et conservez-les dans un endroit sûr et secret.</item>
</plurals>
<string name="onboarding_seed_generate_title">Votre seed phrase</string>
<plurals name="onboarding_seed_generate_words_count">
<item quantity="one">Vide</item>
<item quantity="one"></item>
<item quantity="other">%d mots</item>
</plurals>
<string name="onboarding_seed_import_message">Pour importer votre portefeuille, entrez votre seed phrase dans le champ ci-dessous</string>
@ -574,10 +570,11 @@
<string name="onramp_min_amount_restriction">Le montant à acheter doit être au moins %s</string>
<string name="onramp_no_available_providers">Aucun fournisseur disponible pour cette devise</string>
<string name="onramp_pay_with">Payer avec</string>
<string name="onramp_provider_max_amount">Disponible jusqu\'à %s</string>
<string name="onramp_provider_min_amount">Disponible à partir de %s</string>
<string name="onramp_redirecting_to_provider_subtitle">Vous pourrez finaliser votre transaction sur le fournisseur tiers, %s</string>
<string name="onramp_redirecting_to_provider_title">Redirection vers %s...</string>
<string name="onramp_residency_bottomsheet_country_not_supported">Nos services ne sont pas disponibles dans ce pays</string>
<string name="onramp_residency_bottomsheet_country_subtitle">Modifiez-le ou confirmez-le</string>
<string name="onramp_residency_bottomsheet_title">Votre résidence a été identifiée comme</string>
<string name="onramp_settings_residence">Résidence</string>
<string name="onramp_settings_residence_description">Veuillez sélectionner le bon pays pour garantir des options de paiement et des services précis.</string>
@ -607,9 +604,9 @@
<item quantity="one">pour le portefeuille %d</item>
<item quantity="other">pour les portefeuilles %d </item>
</plurals>
<string name="referral_point_currencies_description">Obtiendras ^^%1$s^^ pour chaque portefeuille acheté par votre ami sur votre adresse de réseau %2$s %3$s ^^30 jours après^^</string>
<string name="referral_point_currencies_description">Obtiendrez ^^%1$s^^ pour chaque portefeuille acheté par votre ami sur votre adresse de réseau %2$s %3$s ^^30 jours après^^</string>
<string name="referral_point_currencies_title">Vous</string>
<string name="referral_point_discount_description_prefix">Obtiendrez</string>
<string name="referral_point_discount_description_prefix">Obtiendra</string>
<string name="referral_point_discount_description_suffix">lors de l\'achat d\'un portefeuille sur tangem.com</string>
<string name="referral_point_discount_description_value">%s réduction</string>
<string name="referral_point_discount_title">Votre ami</string>
@ -1006,12 +1003,12 @@
<string name="warning_failed_to_verify_card_message">Cette carte pourrait être un échantillon de production ou une contrefaçon</string>
<string name="warning_failed_to_verify_card_title">Échec de la vérification d\'authenticité</string>
<string name="warning_hedera_missing_token_association_button_title">Associer</string>
<string name="warning_hedera_missing_token_association_message">Ce jeton doit être associé à votre compte Hedera avant que vous puissiez le recevoir. Frais d\'association ~%1$s %2$s</string>
<string name="warning_hedera_missing_token_association_message">Ce jeton doit être associé à votre compte Hedera avant que vous puissiez le recevoir. Frais d\'association ~%1$s %2$s</string>
<string name="warning_hedera_missing_token_association_message_brief">Ce jeton doit être associé à votre compte Hedera avant que vous puissiez le recevoir</string>
<string name="warning_hedera_missing_token_association_title">Associez votre jeton</string>
<string name="warning_hedera_token_association_not_enough_hbar_message">%s insuffisant. Renflouez votre compte Hedera pour associer ce jeton</string>
<string name="warning_kaspa_unfinished_token_transaction_discard_message">Êtes-vous sûr(e) de vouloir annuler la transaction ? Vous ne pourrez plus réessayer.</string>
<string name="warning_kaspa_unfinished_token_transaction_message">Votre transaction d\'un montant de %1$s %2$s n\'a pas été finalisée. Vous pouvez réessayer plus tard pour la finaliser.</string>
<string name="warning_kaspa_unfinished_token_transaction_message">Votre transaction d\'un montant de %1$s %2$s n\'a pas été finalisée. Vous pouvez réessayer plus tard pour la finaliser.</string>
<string name="warning_kaspa_unfinished_token_transaction_title">Vous avez une transaction inachevée</string>
<string name="warning_low_signatures_message">Il ne reste que %s signatures sur cette carte. Vous devez retirer tous vos fonds.</string>
<string name="warning_low_signatures_title">Faible nombre de signatures</string>

View file

@ -2,12 +2,13 @@
<resources>
<string name="action_buttons_buy_empty_search_message">ポートフォリオにトークンが見つかりませんか?マーケットから見つけ、買付のために追加してください</string>
<string name="action_buttons_sell_empty_search_message">ポートフォリオにトークンが見つかりませんか?マーケットから見つけ、売却のために追加してください</string>
<string name="action_buttons_sell_navigation_bar_title">暗号資産を売却</string>
<string name="action_buttons_swap_choose_token">トークンを選択</string>
<string name="action_buttons_swap_empty_search_message">ポートフォリオにトークンが見つかりませんか?マーケットから見つけ、スワップのために追加してください</string>
<string name="action_buttons_swap_no_available_pair_notification_message">選択したトークンと交換できるトークンがありません。別のトークンを選択してください。</string>
<string name="action_buttons_swap_no_available_pair_notification_title">利用可能なペアがありません</string>
<string name="action_buttons_you_want_to_receive">受け取りたい</string>
<string name="action_buttons_you_want_to_swap">交換したい</string>
<string name="action_buttons_you_want_to_receive">受け取りたいトークンを選択します</string>
<string name="action_buttons_you_want_to_swap">スワップしたいトークンを選択します</string>
<string name="add_custom_token_choose_network">ネットワークを選択</string>
<string name="add_custom_token_title">カスタムトークンの追加</string>
<string name="add_tokens_title">トークンの管理</string>
@ -123,7 +124,7 @@
<string name="common_fee_selector_option_market">マーケット</string>
<string name="common_fee_selector_option_slow">遅い</string>
<string name="common_fee_selector_title">速度と料金</string>
<string name="common_generate_addresses">アドレスを取得する</string>
<string name="common_generate_addresses">アドレスを同期する</string>
<string name="common_go_to_provider">プロバイダーへ移動</string>
<string name="common_go_to_token">トークンへ移動</string>
<string name="common_hour">時間</string>
@ -248,6 +249,7 @@
<string name="express_choose_providers_subtitle">Tangemは、各プロバイダーの条件に従って、サードパーティプロバイダーを介してトークンスワップを提供します。</string>
<string name="express_choose_providers_title">プロバイダーを選択</string>
<string name="express_error_code">エラーが発生しました。コード: %s</string>
<string name="express_error_provider_amount_roundup">エラー%1$s 。選択したプロバイダーは指定された取引を処理できません。値を%2$sに切り上げるか、変更してください。</string>
<string name="express_error_swap_pair_unavailable">選択したプロバイダーは現在利用できません。しばらくしてからもう一度お試しください。(コード: %s )</string>
<string name="express_error_swap_unavailable">現在、交換はご利用いただけません。しばらくしてからもう一度お試しください。(コード: %s )</string>
<string name="express_estimated_amount">推定金額</string>
@ -557,6 +559,8 @@
<string name="onboarding_wallet_info_title_fourth">アクセスコードの復元</string>
<string name="onboarding_wallet_info_title_second">同一のカード</string>
<string name="onboarding_wallet_info_title_third">アクセスコード</string>
<string name="onramp_avaiable_with_payment_methods">%sで利用可能</string>
<string name="onramp_choose_provider_title_hint">プロバイダーは取引を促進し、スムーズで効率的なトークン交換を保証します</string>
<string name="onramp_country_search">国で検索</string>
<string name="onramp_country_unavailable">利用不可</string>
<string name="onramp_currency_search">通貨で検索</string>
@ -564,10 +568,11 @@
<string name="onramp_min_amount_restriction">買付金額は少なくとも%sである必要があります</string>
<string name="onramp_no_available_providers">この通貨で利用可能なプロバイダーはありません</string>
<string name="onramp_pay_with">支払う</string>
<string name="onramp_provider_max_amount">最大 %s まで使用可能</string>
<string name="onramp_provider_min_amount">%s 以上で利用可能</string>
<string name="onramp_redirecting_to_provider_subtitle">サードパーティプロバイダー%sで取引を完了できます。</string>
<string name="onramp_redirecting_to_provider_title">%sにリダイレクトしています...</string>
<string name="onramp_residency_bottomsheet_country_not_supported">この国では当社のサービスはご利用いただけません</string>
<string name="onramp_residency_bottomsheet_country_subtitle">変更または確認</string>
<string name="onramp_residency_bottomsheet_title">あなたの住居は次のように識別されています</string>
<string name="onramp_settings_residence">住居</string>
<string name="onramp_settings_residence_description">正確なお支払い方法とサービスを確保するため、正しい国を選択してください。</string>
@ -582,8 +587,11 @@
<string name="qr_scanner_camera_denied_settings_button">設定</string>
<string name="qr_scanner_camera_denied_text">カメラへのアクセスを許可していません。</string>
<string name="qr_scanner_camera_denied_title">カメラへのアクセスが拒否されました</string>
<string name="receive_bottom_sheet_no_memo_required_message">メモ不要</string>
<string name="receive_bottom_sheet_warning_message">%3$sネットワーク上の%1$s ( %2$s )</string>
<string name="receive_bottom_sheet_warning_message_description">他の暗号資産を送信すると、取り返しのつかない損失が発生します。</string>
<string name="receive_bottom_sheet_warning_message_full">このアドレスには%sのみを送金してください。他のトークンを送信すると、取り返しのつかない損失が発生します。</string>
<string name="receive_bottom_sheet_warning_message_title">このアドレスには%sのみを送信してください。</string>
<string name="receive_token_description">ウォレットや取引所から資金を送金する</string>
<string name="referral_button_participate">参加する</string>
<string name="referral_error_failed_to_load_info">紹介プログラムに関する情報を読み込めませんでした。しばらくしてからもう一度お試しください。</string>
@ -994,12 +1002,12 @@
<string name="warning_failed_to_verify_card_message">このカードは、サンプル品または偽造品である可能性があります</string>
<string name="warning_failed_to_verify_card_title">真正性チェックに失敗しました</string>
<string name="warning_hedera_missing_token_association_button_title">関連付ける</string>
<string name="warning_hedera_missing_token_association_message">このトークンを受け取るには、Hederaアカウントに関連付ける必要があります。関連付け手数料 ~ %1$s %2$s</string>
<string name="warning_hedera_missing_token_association_message">このトークンを受け取るには、Hederaアカウントに関連付ける必要があります。関連付け手数料 ~ %1$s %2$s</string>
<string name="warning_hedera_missing_token_association_message_brief">このトークンを受け取るには、Hederaアカウントに関連付ける必要があります。</string>
<string name="warning_hedera_missing_token_association_title">トークンを関連付ける</string>
<string name="warning_hedera_token_association_not_enough_hbar_message">%sが不足しています。このトークンを関連付けるには、Hederaアカウントに資金を追加してください。</string>
<string name="warning_kaspa_unfinished_token_transaction_discard_message">取引をキャンセルしてもよろしいですか? 取引を再試行することはできません</string>
<string name="warning_kaspa_unfinished_token_transaction_message">%1$s %2$sの取引が完了しませんでした。もう一度試して完了してください。</string>
<string name="warning_kaspa_unfinished_token_transaction_message">%1$s %2$sの取引が完了しませんでした。もう一度試して完了してください。</string>
<string name="warning_kaspa_unfinished_token_transaction_title">未完了の取引があります</string>
<string name="warning_low_signatures_message">このカードには%sの署名のみが残っています。資金をすべて引き出す必要があります。</string>
<string name="warning_low_signatures_title">署名数が少ないです</string>

View file

@ -1,10 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="action_buttons_buy_empty_search_message">Не нашли свой токен? Перейдите в раздел «Рынок» на главной странице и добавьте его в свой портфель для покупки.</string>
<string name="action_buttons_sell_empty_search_message">Не нашли свой токен? Перейдите в раздел «Рынок» на главной странице и добавьте его в свой портфель для продажи.</string>
<string name="action_buttons_sell_navigation_bar_title">Продать криптовалюту</string>
<string name="action_buttons_swap_choose_token">Выберите токен</string>
<string name="action_buttons_swap_no_available_pair_notification_message">Нет доступных токенов для обмена с выбранным токеном, пожалуйста, выберите другой.</string>
<string name="action_buttons_swap_empty_search_message">Не нашли свой токен? Перейдите в раздел «Рынок» на главной странице и добавьте его в свой портфель для обмена.</string>
<string name="action_buttons_swap_no_available_pair_notification_message">Нет доступных вариантов для обмена с выбранным токеном, пожалуйста, выберите другой.</string>
<string name="action_buttons_swap_no_available_pair_notification_title">Нет доступных пар</string>
<string name="action_buttons_you_want_to_receive">Вы хотите получить</string>
<string name="action_buttons_you_want_to_swap">Вы хотите обменять</string>
<string name="action_buttons_you_want_to_receive">Выберите токен для получения</string>
<string name="action_buttons_you_want_to_swap">Выберите токен для обмена</string>
<string name="add_custom_token_choose_network">Выберите сеть</string>
<string name="add_custom_token_title">Добавить токен</string>
<string name="add_tokens_title">Валюты</string>
@ -129,7 +133,7 @@
<string name="common_fee_selector_option_market">По рынку</string>
<string name="common_fee_selector_option_slow">Медленно</string>
<string name="common_fee_selector_title">Скорость и комиссия</string>
<string name="common_generate_addresses">Получить ваши адреса</string>
<string name="common_generate_addresses">Синхронизировать адреса</string>
<string name="common_go_to_provider">К провайдеру</string>
<string name="common_go_to_token">Перейти в токен</string>
<string name="common_hour">час</string>
@ -254,6 +258,7 @@
<string name="express_choose_providers_subtitle">Tangem предоставляет доступ к обмену через сторонних поставщиков в соответствии с их правилами</string>
<string name="express_choose_providers_title">Выберите провайдера</string>
<string name="express_error_code">Произошла ошибка. Код: %s</string>
<string name="express_error_provider_amount_roundup">Ошибка %1$s. Выбранный провайдер не может обработать указанную транзакцию. Округлите значение до %2$s или измените его.</string>
<string name="express_error_swap_pair_unavailable">Выбранный провайдер недоступен для обмена. Попробуйте позже. (Код: %s)</string>
<string name="express_error_swap_unavailable">В данный момент обмен невозможен. Попробуйте позже. (Код: %s)</string>
<string name="express_estimated_amount">Курс обмена</string>
@ -297,6 +302,9 @@
<string name="express_provider_not_available">Недоступно для этой пары</string>
<string name="express_provider_permission_needed">Требуется разрешение</string>
<string name="express_provider_recommended">Рекомендовано</string>
<string name="express_status_bought">Куплено %s</string>
<string name="express_status_buying">Покупка %s</string>
<string name="express_status_buying_active">Покупка %s...</string>
<string name="express_token_list_empty_search">Токены не найдены. Пожалуйста, попробуйте другой запрос</string>
<string name="express_transaction_id">ID: %s</string>
<string name="express_transaction_id_copied">ID транзакции скопирован</string>
@ -573,17 +581,25 @@
<string name="onboarding_wallet_info_title_fourth">Восстановление кода доступа</string>
<string name="onboarding_wallet_info_title_second">Идентичные карты</string>
<string name="onboarding_wallet_info_title_third">Код доступа</string>
<string name="onramp_avaiable_with_payment_methods">Доступно с %s</string>
<string name="onramp_choose_provider_title_hint">Tangem предоставляет доступ к покупке через сторонних провайдеров в соответствии с их правилами</string>
<string name="onramp_country_search">Поиск по стране</string>
<string name="onramp_country_unavailable">Недоступно</string>
<string name="onramp_currency_search">Поиск по валюте</string>
<string name="onramp_max_amount_restriction">Сумма покупки не может быть больше, чем %s</string>
<string name="onramp_min_amount_restriction">Сумма покупки должна составлять минимум %s</string>
<string name="onramp_no_available_providers">Для данной валюты нет доступных провайдеров</string>
<string name="onramp_no_available_providers">Нет доступных провайдеров для выбранной валюты</string>
<string name="onramp_pay_with">Оплата с</string>
<string name="onramp_redirecting_to_provider_subtitle">Вы сможете завершить транзакцию через сервис стороннего провайдера, %s</string>
<string name="onramp_provider_max_amount">Доступно до %s</string>
<string name="onramp_provider_min_amount">Доступно с %s</string>
<string name="onramp_redirecting_to_provider_subtitle">Вы сможете продолжить транзакцию через сервис стороннего провайдера, %s</string>
<string name="onramp_redirecting_to_provider_title">Переход к %s...</string>
<string name="onramp_residency_bottomsheet_country_not_supported">Наши сервисы недоступны в данной стране</string>
<string name="onramp_residency_bottomsheet_title">Ваше местоположение определено как</string>
<string name="onramp_settings_residence">Страна проживания</string>
<string name="onramp_settings_residence_description">Пожалуйста, укажите правильную страну, чтобы обеспечить точность доступных способов оплаты и услуг.</string>
<string name="onramp_settings_title">Настройки</string>
<string name="onramp_transaction_status_footer_text">Вы можете закрыть этот экран и проверить статус транзакции на экране информации о токене.</string>
<string name="onramp_via">Через</string>
<string name="organize_tokens_group">Группы</string>
<string name="organize_tokens_sort_by_balance">По балансу</string>
@ -593,8 +609,11 @@
<string name="qr_scanner_camera_denied_settings_button">Настройки</string>
<string name="qr_scanner_camera_denied_text">Вы не предоставили доступ к вашей камере</string>
<string name="qr_scanner_camera_denied_title">Доступ к камере запрещен</string>
<string name="receive_bottom_sheet_no_memo_required_message">Memo не требуется</string>
<string name="receive_bottom_sheet_warning_message">%1$s (%2$s) в сети %3$s</string>
<string name="receive_bottom_sheet_warning_message_description">Использование другой сети может привести к утрате средств.</string>
<string name="receive_bottom_sheet_warning_message_full">Отправляйте только %s на этот адрес. Использование другой сети может привести к утрате средств.</string>
<string name="receive_bottom_sheet_warning_message_title">Отправляйте только %s на этот адрес</string>
<string name="receive_token_description">Переводите средства с любого кошелька или биржи</string>
<string name="referral_button_participate">Участвовать</string>
<string name="referral_error_failed_to_load_info">Не удалось загрузить информацию по реферальной программе. Пожалуйста, попробуйте позже.</string>
@ -650,6 +669,10 @@
<string name="scan_card_settings_button">Сканировать</string>
<string name="scan_card_settings_message">Отсканируйте карту или кольцо, чтобы изменить ее настройки. Изменения затронут только ту карту или кольцо, которые вы отсканировали, и не повлияют на другие устройства, привязанные к вашему кошельку.</string>
<string name="scan_card_settings_title">Приготовьте свой Tangem!</string>
<string name="selling_insufficient_balance_alert_message">На вашем балансе недостаточно средств для продажи этой криптовалюты. Пожалуйста, пополните нужный актив, чтобы продолжить.</string>
<string name="selling_insufficient_balance_alert_title">Недостаточно средств</string>
<string name="selling_regional_restriction_alert_message">Продажа криптовалюты в вашем регионе временно недоступна. Мы активно работаем над тем, чтобы добавить эту возможность. Следите за нашими новостями!</string>
<string name="selling_regional_restriction_alert_title">Региональные ограничения</string>
<string name="send_additional_field_already_included">Уже содержится во введенном адресе</string>
<string name="send_alert_fee_too_high_text">Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна.</string>
<string name="send_alert_fee_too_low_text">Вы указали комиссию ниже рекомендуемой, это может привести к задержке исполнения вашей транзакции. Продолжить?</string>
@ -690,6 +713,7 @@
<string name="send_notification_fee_too_high_title">Установлена высокая комиссия</string>
<string name="send_notification_high_fee_text">Ввиду особенности сети %1$s комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить %2$s.</string>
<string name="send_notification_high_fee_title">Комиссия повышена</string>
<string name="send_notification_invalid_amount_rent_fee">Баланс вашего счета не может быть ниже арендной платы. Пожалуйста, оставьте на счете не менее %1$s или выведите все средства.</string>
<string name="send_notification_invalid_amount_text">Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению</string>
<string name="send_notification_invalid_amount_title">Недопустимая сумма</string>
<string name="send_notification_invalid_minimum_amount_text">Минимальная сумма отправки - %1$s. Пожалуйста, убедитесь, что остаток после отправки также не будет меньше %2$s.</string>
@ -889,6 +913,7 @@
<string name="token_swap_promotion_button">Обменять</string>
<string name="tokens_list_unavailable_to_purchase_header">Недоступно для покупки</string>
<string name="tokens_list_unavailable_to_sell_header">Недоступно для продажи</string>
<string name="tokens_list_unavailable_to_swap_header">Недоступно для обмена с %s</string>
<string name="transaction_history_contract_address">контракт: %s</string>
<string name="transaction_history_empty_transactions">У вас еще нет транзакций</string>
<string name="transaction_history_error_failed_to_load">Не удалось загрузить историю транзакций.\nНажмите на кнопку перезагрузки, чтобы обновить информацию.</string>
@ -966,7 +991,7 @@
<string name="wallet_notification_address_copied">Адрес скопирован в буфер обмена</string>
<string name="wallet_notification_no_internet">Нет соединения с интернетом</string>
<string name="wallet_promo_banner_button_title">Получить с 10%% скидкой</string>
<string name="wallet_promo_banner_description">Получите доступ к более чем 13 000 криптовалют. Покупайте, продавайте, обменяйте и стейкайте в один клик. Свяжите до трех карт для резервного копирования. </string>
<string name="wallet_promo_banner_description">Получите доступ к более чем 13 000 криптовалют. Покупайте, продавайте, обменивайте и стейкайте в один клик. Свяжите до трех карт для резервного копирования. </string>
<string name="wallet_promo_banner_title">Откройте Tangem Wallet</string>
<string name="wallet_settings_title">Настройки кошелька</string>
<string name="wallet_title">Tangem</string>
@ -1009,12 +1034,12 @@
<string name="warning_failed_to_verify_card_message">Возможно, данная карта является образцом или подделкой</string>
<string name="warning_failed_to_verify_card_title">Ошибка проверки подлинности</string>
<string name="warning_hedera_missing_token_association_button_title">Ассоциировать</string>
<string name="warning_hedera_missing_token_association_message">Этот токен должен быть ассоциирован с вашей учетной записью Hedera, прежде чем вы сможете его принять. Стоимость ассоциации ~%1$s %2$s </string>
<string name="warning_hedera_missing_token_association_message">Этот токен должен быть ассоциирован с вашей учетной записью Hedera, прежде чем вы сможете его принять. Стоимость ассоциации ~%1$s %2$s</string>
<string name="warning_hedera_missing_token_association_message_brief">Этот токен должен быть ассоциирован с вашей учетной записью Hedera, прежде чем вы сможете его принять</string>
<string name="warning_hedera_missing_token_association_title">Ассоциируете свой токен</string>
<string name="warning_hedera_token_association_not_enough_hbar_message">Недостаточно %s. Пополните ваш аккаунт Hedera для ассоциации этого токена</string>
<string name="warning_kaspa_unfinished_token_transaction_discard_message">Вы уверены что хотите отменить эту транзакцию? Вы не сможете отправить ее еще раз</string>
<string name="warning_kaspa_unfinished_token_transaction_message">Ваша транзакция %1$s %2$s не была завершена. Вы можете попробовать отправить ее еще раз.</string>
<string name="warning_kaspa_unfinished_token_transaction_message">Ваша транзакция %1$s %2$s не была завершена. Вы можете попробовать отправить ее еще раз.</string>
<string name="warning_kaspa_unfinished_token_transaction_title">У вас есть незавершенная транзакция</string>
<string name="warning_low_signatures_message">На этой карте осталось всего %s подписей. Вам следует вывести все ваши средства.</string>
<string name="warning_low_signatures_title">Малое количество подписей</string>

View file

@ -6,8 +6,6 @@
<string name="action_buttons_swap_empty_search_message">Токен не знайдено у вашому портфелі? Перевірте ринки, щоб знайти та додати його для обміну</string>
<string name="action_buttons_swap_no_available_pair_notification_message">Немає доступних токенів для обміну з обраним токеном. Будь ласка, оберіть інший.</string>
<string name="action_buttons_swap_no_available_pair_notification_title">Немає вільної пари</string>
<string name="action_buttons_you_want_to_receive">Ви хочете отримати</string>
<string name="action_buttons_you_want_to_swap">Ви хочете обміняти</string>
<string name="add_custom_token_choose_network">Оберіть мережу</string>
<string name="add_custom_token_title">Додати токен</string>
<string name="add_tokens_title">Токени</string>
@ -132,7 +130,7 @@
<string name="common_fee_selector_option_market">За ринком</string>
<string name="common_fee_selector_option_slow">Повільно</string>
<string name="common_fee_selector_title">Швидкість та комісія</string>
<string name="common_generate_addresses">Отримати ваші адреси</string>
<string name="common_generate_addresses">Синхронізувати адреси</string>
<string name="common_go_to_provider">Перейти до провайдера</string>
<string name="common_go_to_token">Перейти до токену</string>
<string name="common_hour">година</string>
@ -301,7 +299,7 @@
<string name="express_provider_permission_needed">Потрібен дозвіл</string>
<string name="express_provider_recommended">Рекомендовано</string>
<string name="express_status_bought">Купив %s</string>
<string name="express_status_buying">Купівля %s...</string>
<string name="express_status_buying">Купівля %s</string>
<string name="express_status_buying_active">Купівля %s...</string>
<string name="express_token_list_empty_search">Токенів не знайдено. Будь ласка, спробуйте інший запит</string>
<string name="express_transaction_id">ID: %s</string>
@ -588,10 +586,11 @@
<string name="onramp_min_amount_restriction">Сума покупки повинна бути не менше %s</string>
<string name="onramp_no_available_providers">Для данної валюти немає доступних провайдерів</string>
<string name="onramp_pay_with">Оплата з</string>
<string name="onramp_provider_max_amount">Доступно до %s</string>
<string name="onramp_provider_min_amount">Доступно від %s</string>
<string name="onramp_redirecting_to_provider_subtitle">Ви зможете завершити транзакцію у стороннього постачальника, %s</string>
<string name="onramp_redirecting_to_provider_title">Перенаправлення на %s...</string>
<string name="onramp_residency_bottomsheet_country_not_supported">Наші сервіси недоступні в цій країні</string>
<string name="onramp_residency_bottomsheet_country_subtitle">Змінити або підтвердити</string>
<string name="onramp_residency_bottomsheet_title">Ваше місце проживання визначено як</string>
<string name="onramp_settings_residence_description">Будь ласка, виберіть правильну країну, щоб забезпечити точні способи оплати та послуги.</string>
<string name="onramp_settings_title">Параметри</string>
@ -1023,12 +1022,12 @@
<string name="warning_failed_to_verify_card_message">Ця картка може бути виробничим зразком або підробкою</string>
<string name="warning_failed_to_verify_card_title">Перевірка автентичності не вдалася</string>
<string name="warning_hedera_missing_token_association_button_title">Асоціювати</string>
<string name="warning_hedera_missing_token_association_message">Цей токен повинен бути асоційований з вашим обліковим записом Hedera, перш ніж ви зможете його прийняти. Вартість асоціації ~ %1$s %2$s</string>
<string name="warning_hedera_missing_token_association_message">Цей токен повинен бути асоційований з вашим обліковим записом Hedera, перш ніж ви зможете його прийняти. Вартість асоціації ~ %1$s %2$s</string>
<string name="warning_hedera_missing_token_association_message_brief">Цей токен повинен бути асоційований з вашим обліковим записом Hedera, перш ніж ви зможете його прийняти</string>
<string name="warning_hedera_missing_token_association_title">Асоціюйте свій токен</string>
<string name="warning_hedera_token_association_not_enough_hbar_message">Недостатньо %s. Поповніть ваш обліковий запис Hedera для асоціації цього токена</string>
<string name="warning_kaspa_unfinished_token_transaction_discard_message">Ви впевнені, що хочете скасувати цю транзакцію? Ви не зможете відправити її ще раз</string>
<string name="warning_kaspa_unfinished_token_transaction_message">Ваша транзакція %1$s %2$s не була завершена. Ви можете спробувати відправити її ще раз.</string>
<string name="warning_kaspa_unfinished_token_transaction_message">Ваша транзакція %1$s %2$s не була завершена. Ви можете спробувати відправити її ще раз.</string>
<string name="warning_kaspa_unfinished_token_transaction_title">У вас є незавершена транзакція</string>
<string name="warning_low_signatures_message">На цій картці залишається лише %s підписів. Ви повинні вивести всі свої кошти.</string>
<string name="warning_low_signatures_title">Низька кількість підписів</string>

View file

@ -1,13 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="action_buttons_buy_empty_search_message">Token not found in your portfolio? Check the Markets to find and add it for purchase</string>
<string name="action_buttons_sell_empty_search_message">Token not found in your portfolio? Check the Markets to find and add it for sell</string>
<string name="action_buttons_buy_empty_search_message">Cant find your token? Go to the Market section on the main page and add it to your portfolio for purchase</string>
<string name="action_buttons_sell_empty_search_message">Cant find your token? Go to the Market section on the main page and add it to your portfolio for selling.</string>
<string name="action_buttons_sell_navigation_bar_title">Sell crypto</string>
<string name="action_buttons_swap_choose_token">Choose the Token</string>
<string name="action_buttons_swap_empty_search_message">Token not found in your portfolio? Check the Markets to find and add it for swap</string>
<string name="action_buttons_swap_empty_search_message">Cant find your token? Go to the Market section on the main page and add it to your portfolio for swapping.</string>
<string name="action_buttons_swap_no_available_pair_notification_message">There are no available tokens to swap with the selected token. Please choose another one.</string>
<string name="action_buttons_swap_no_available_pair_notification_title">No available pair</string>
<string name="action_buttons_you_want_to_receive">You want to Receive</string>
<string name="action_buttons_you_want_to_swap">You want to Swap</string>
<string name="action_buttons_you_want_to_receive">Choose the token you want to receive</string>
<string name="action_buttons_you_want_to_swap">Choose the token you want to swap</string>
<string name="add_custom_token_choose_network">Choose network</string>
<string name="add_custom_token_title">Add custom token</string>
<string name="add_tokens_title">Manage tokens</string>
@ -126,7 +127,7 @@
<string name="common_fee_selector_option_market">Market</string>
<string name="common_fee_selector_option_slow">Slow</string>
<string name="common_fee_selector_title">Speed and fee</string>
<string name="common_generate_addresses">Get your addresses</string>
<string name="common_generate_addresses">Synchronize addresses</string>
<string name="common_go_to_provider">Go to provider</string>
<string name="common_go_to_token">Go to token</string>
<string name="common_hour">hour</string>
@ -251,6 +252,7 @@
<string name="express_choose_providers_subtitle">Tangem offers token swaps via 3rd-party providers according to each provider\'s terms</string>
<string name="express_choose_providers_title">Choose provider</string>
<string name="express_error_code">An error occurred. Code: %s</string>
<string name="express_error_provider_amount_roundup">Error %1$s. The selected provider cannot process the specified transaction. Please round the value up to %2$s or change it</string>
<string name="express_error_swap_pair_unavailable">Selected provider is unavailable at the moment. Please try again later. (Code: %s)</string>
<string name="express_error_swap_unavailable">Swaps are unavailable at the moment. Please try again later. (Code: %s)</string>
<string name="express_estimated_amount">Estimated amount</string>
@ -565,6 +567,8 @@
<string name="onboarding_wallet_info_title_fourth">Access code restore</string>
<string name="onboarding_wallet_info_title_second">Identical cards</string>
<string name="onboarding_wallet_info_title_third">Access code</string>
<string name="onramp_avaiable_with_payment_methods">Available with %s</string>
<string name="onramp_choose_provider_title_hint">Buying crypto in Tangem is powered by third-party providers on their terms.</string>
<string name="onramp_country_search">Search by country</string>
<string name="onramp_country_unavailable">Unavailable</string>
<string name="onramp_currency_search">Search by currency</string>
@ -572,15 +576,17 @@
<string name="onramp_min_amount_restriction">The amount to buy must be at least %s</string>
<string name="onramp_no_available_providers">No available providers for this currency</string>
<string name="onramp_pay_with">Pay with</string>
<string name="onramp_provider_max_amount">Available up to %s</string>
<string name="onramp_provider_min_amount">Available from %s</string>
<string name="onramp_redirecting_to_provider_subtitle">You will be able to complete your transaction on the third-party provider, %s</string>
<string name="onramp_redirecting_to_provider_title">Redirecting to %s...</string>
<string name="onramp_residency_bottomsheet_country_not_supported">Our services are not available in this country</string>
<string name="onramp_residency_bottomsheet_country_subtitle">Change or confirm it</string>
<string name="onramp_residency_bottomsheet_title">Your residence has been identified as</string>
<string name="onramp_settings_residence">Residence</string>
<string name="onramp_settings_residence_description">Please select the correct country to ensure accurate payment options and services.</string>
<string name="onramp_settings_title">Settings</string>
<string name="onramp_transaction_status_footer_text">You can check the transaction status from the token page</string>
<string name="onramp_statuses_view_footer">You can check the transaction status from the token page.</string>
<string name="onramp_transaction_status_footer_text">You can close this screen and check the transaction status on the token details screen.</string>
<string name="onramp_via">Via</string>
<string name="organize_tokens_group">Group</string>
<string name="organize_tokens_sort_by_balance">By balance</string>
@ -590,8 +596,11 @@
<string name="qr_scanner_camera_denied_settings_button">Settings</string>
<string name="qr_scanner_camera_denied_text">You have not given access to your camera</string>
<string name="qr_scanner_camera_denied_title">Camera access denied</string>
<string name="receive_bottom_sheet_no_memo_required_message">No memo required</string>
<string name="receive_bottom_sheet_warning_message">%1$s (%2$s) on %3$s network</string>
<string name="receive_bottom_sheet_warning_message_description">Sending any other currency will result in its irreversible loss.</string>
<string name="receive_bottom_sheet_warning_message_full">Send only %s to this address. Sending any other currency will result in its irreversible loss.</string>
<string name="receive_bottom_sheet_warning_message_title">Send only %s to this address.</string>
<string name="receive_token_description">Transfer funds from any wallet or exchange</string>
<string name="referral_button_participate">Participate</string>
<string name="referral_error_failed_to_load_info">Failed to load the information about the referral program. Please try again later.</string>
@ -643,6 +652,10 @@
<string name="scan_card_settings_button">Scan card or ring</string>
<string name="scan_card_settings_message">Scan the card or ring to change its settings. The changes will impact only the card or ring you\'ve scanned and will not affect other devices tied to your wallet.</string>
<string name="scan_card_settings_title">Get your Tangem ready!</string>
<string name="selling_insufficient_balance_alert_message">You dont have enough funds in your balance to sell this cryptocurrency. Please deposit the desired asset to proceed.</string>
<string name="selling_insufficient_balance_alert_title">Insufficient Balance</string>
<string name="selling_regional_restriction_alert_message">Selling cryptocurrency is unavailable in your region at the moment. Were actively working to bring this option to you soon—stay tuned!</string>
<string name="selling_regional_restriction_alert_title">Regional Restriction</string>
<string name="send_additional_field_already_included">Already included in the entered address</string>
<string name="send_alert_fee_too_high_text">Your commission amount is %s times higher than the recommended amount. Please review and adjust your custom settings.</string>
<string name="send_alert_fee_too_low_text">You specified a commission below the recommended amount, which could cause a delay in your transaction. Continue?</string>
@ -683,6 +696,7 @@
<string name="send_notification_fee_too_high_title">Custom fee is high</string>
<string name="send_notification_high_fee_text">Due to the peculiarities of the %1$s network, the fee for transferring the entire balance is higher. To reduce the commission, you can leave %2$s.</string>
<string name="send_notification_high_fee_title">The fee is higher</string>
<string name="send_notification_invalid_amount_rent_fee">Your account balance cannot be lower than the rent fee. Please maintain at least %1$s on your account or withdraw all funds.</string>
<string name="send_notification_invalid_amount_text">The included commission exceeds the transfer amount, leading to a negative value</string>
<string name="send_notification_invalid_amount_title">Invalid amount</string>
<string name="send_notification_invalid_minimum_amount_text">The minimum sending amount is %1$s. Please ensure that the remaining balance after sending will not be less than %2$s.</string>
@ -1004,12 +1018,12 @@
<string name="warning_failed_to_verify_card_message">This card might be a production sample or counterfeit</string>
<string name="warning_failed_to_verify_card_title">Authenticity check failed</string>
<string name="warning_hedera_missing_token_association_button_title">Associate</string>
<string name="warning_hedera_missing_token_association_message">This token must be associated with your Hedera account before you can receive it. Association fee ~%1$s %2$s</string>
<string name="warning_hedera_missing_token_association_message">This token must be associated with your Hedera account before you can receive it. Association fee ~%1$s %2$s</string>
<string name="warning_hedera_missing_token_association_message_brief">This token must be associated with your Hedera account before you can receive it</string>
<string name="warning_hedera_missing_token_association_title">Associate your token</string>
<string name="warning_hedera_token_association_not_enough_hbar_message">Not enough %s. Top up your Hedera account to associate this token</string>
<string name="warning_kaspa_unfinished_token_transaction_discard_message">Are you sure you want to cancel the transaction? You will not be able to retry the transaction again</string>
<string name="warning_kaspa_unfinished_token_transaction_message">Your transaction with an amount of %1$s %2$s was not completed. You can try again to complete it.</string>
<string name="warning_kaspa_unfinished_token_transaction_message">Your transaction with an amount of %1$s %2$s was not completed. You can try again to complete it.</string>
<string name="warning_kaspa_unfinished_token_transaction_title">You have unfinished transaction</string>
<string name="warning_low_signatures_message">Only %s signatures are left on this card. You must withdraw all of your funds.</string>
<string name="warning_low_signatures_title">Low signature count</string>

View file

@ -23,6 +23,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerW8
import com.tangem.core.ui.extensions.TextReference
@ -78,6 +79,7 @@ fun ActionButton(
)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun Button(
config: ActionButtonConfig,
@ -85,6 +87,7 @@ private fun Button(
modifier: Modifier = Modifier,
color: Color = TangemTheme.colors.button.secondary,
) {
val context = LocalContext.current
val backgroundColor by animateColorAsState(
targetValue = if (config.enabled) color else TangemTheme.colors.button.disabled,
label = "Update background color",
@ -92,30 +95,9 @@ private fun Button(
Box(
modifier = modifier
.heightIn(min = TangemTheme.dimens.size36)
.heightIn(min = 36.dp)
.widthIn(min = 100.dp)
.clip(shape)
.background(color = backgroundColor),
) {
Content(config = config)
if (config.isInProgress) {
Loading(
backgroundColor = backgroundColor,
modifier = Modifier
.matchParentSize()
.align(alignment = Alignment.Center),
)
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun Content(config: ActionButtonConfig) {
val context = LocalContext.current
Row(
modifier = Modifier
.combinedClickable(
enabled = config.enabled,
onClick = config.onClick,
@ -128,38 +110,55 @@ private fun Content(config: ActionButtonConfig) {
}
},
)
.padding(start = TangemTheme.dimens.spacing16, end = TangemTheme.dimens.spacing24)
.background(color = backgroundColor),
) {
Content(
modifier = Modifier.align(Alignment.Center),
config = config,
)
if (config.isInProgress) {
Loading(
modifier = Modifier
.align(alignment = Alignment.Center)
.matchParentSize(),
backgroundColor = backgroundColor,
)
}
}
}
@Composable
private fun Content(config: ActionButtonConfig, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing24,
)
.padding(vertical = TangemTheme.dimens.spacing8),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
val iconTint by animateColorAsState(
targetValue = when {
!config.enabled -> TangemTheme.colors.icon.informative
config.dimContent -> TangemTheme.colors.icon.informative
else -> TangemTheme.colors.icon.primary1
},
label = "Update tint color",
)
val iconTint = when {
!config.enabled -> TangemTheme.colors.icon.informative
config.dimContent -> TangemTheme.colors.icon.informative
else -> TangemTheme.colors.icon.primary1
}
Icon(
modifier = Modifier.size(size = TangemTheme.dimens.size20),
painter = painterResource(id = config.iconResId),
contentDescription = null,
modifier = Modifier.size(size = TangemTheme.dimens.size20),
tint = iconTint,
)
SpacerW8()
val textColor by animateColorAsState(
targetValue = when {
!config.enabled -> TangemTheme.colors.text.disabled
config.dimContent -> TangemTheme.colors.text.tertiary
else -> TangemTheme.colors.text.primary1
},
label = "Update text color",
)
val textColor = when {
!config.enabled -> TangemTheme.colors.text.disabled
config.dimContent -> TangemTheme.colors.text.tertiary
else -> TangemTheme.colors.text.primary1
}
Text(
text = config.text.resolveReference(),
color = textColor,

View file

@ -46,7 +46,7 @@ inline fun RowContentContainer(
}
@Composable
internal fun RowText(
fun RowText(
mainText: String,
secondText: String,
accentMainText: Boolean,

View file

@ -20,6 +20,7 @@ dependencies {
/** Domain modules */
implementation(projects.domain.onramp)
implementation(projects.domain.legacy)
// region DI

View file

@ -1,5 +1,7 @@
package com.tangem.data.onramp
import com.squareup.moshi.Moshi
import com.tangem.blockchain.extensions.toBigDecimalOrDefault
import com.tangem.data.common.api.safeApiCall
import com.tangem.data.onramp.converters.CountryConverter
import com.tangem.data.onramp.converters.CurrencyConverter
@ -12,13 +14,16 @@ import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.express.models.TangemExpressValues
import com.tangem.datasource.api.express.models.response.ExchangeProvider
import com.tangem.datasource.api.express.models.response.ExchangeProviderType
import com.tangem.datasource.api.onramp.OnrampApi
import com.tangem.datasource.api.onramp.models.common.OnrampDestinationDTO
import com.tangem.datasource.api.onramp.models.request.OnrampPairsRequest
import com.tangem.datasource.api.onramp.models.response.OnrampDataJson
import com.tangem.datasource.api.onramp.models.response.model.OnrampCountryDTO
import com.tangem.datasource.api.onramp.models.response.model.OnrampCurrencyDTO
import com.tangem.datasource.api.onramp.models.response.model.OnrampPairDTO
import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO
import com.tangem.datasource.crypto.DataSignatureVerifier
import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore
import com.tangem.datasource.local.onramp.paymentmethods.OnrampPaymentMethodsStore
import com.tangem.datasource.local.onramp.quotes.OnrampQuotesStore
@ -28,20 +33,26 @@ import com.tangem.datasource.local.preferences.utils.getObject
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.preferences.utils.storeObject
import com.tangem.domain.onramp.model.*
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.tokens.model.Amount
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.withContext
import org.joda.time.DateTime
import timber.log.Timber
import java.math.BigDecimal
import java.util.UUID
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LargeClass", "TooManyFunctions")
internal class DefaultOnrampRepository(
private val onrampApi: OnrampApi,
private val expressApi: TangemExpressApi,
@ -51,12 +62,16 @@ internal class DefaultOnrampRepository(
private val pairsStore: OnrampPairsStore,
private val quotesStore: OnrampQuotesStore,
private val quotesErrorConverter: OnrampQuotesErrorConverter,
private val walletManagersFacade: WalletManagersFacade,
private val dataSignatureVerifier: DataSignatureVerifier,
moshi: Moshi,
) : OnrampRepository {
private val currencyConverter = CurrencyConverter()
private val countryConverter = CountryConverter(currencyConverter)
private val statusConverter = StatusConverter()
private val paymentMethodsConverter = PaymentMethodConverter()
private val onrampDataAdapter = moshi.adapter(OnrampDataJson::class.java)
override suspend fun getCurrencies(): List<OnrampCurrency> = withContext(dispatchers.io) {
onrampApi.getCurrencies()
@ -194,14 +209,19 @@ internal class DefaultOnrampRepository(
providerId = provider.id,
).bind()
OnrampQuote.Data(
fromAmount = convertToBigDecimal(response.fromAmount),
toAmount = convertToBigDecimal(response.toAmount),
minFromAmount = convertToBigDecimal(response.minFromAmount),
maxFromAmount = convertToBigDecimal(response.maxFromAmount),
fromAmount = OnrampAmount(
value = response.fromAmount
.toBigDecimalOrDefault()
.movePointLeft(currency.precision),
decimals = currency.precision,
symbol = amount.currencySymbol,
),
toAmount = convertToAmount(response.toAmount, cryptoCurrency),
minFromAmount = convertToAmount(response.minFromAmount, cryptoCurrency),
maxFromAmount = convertToAmount(response.maxFromAmount, cryptoCurrency),
paymentMethod = paymentMethod,
provider = provider,
)
null
},
onError = { error ->
if (error is ApiResponseError.HttpException) {
@ -230,6 +250,69 @@ internal class DefaultOnrampRepository(
return quotesStore.get(QUOTES_KEY)
}
override suspend fun getQuotesSync(): List<OnrampQuote>? {
return quotesStore.getSyncOrNull(QUOTES_KEY)
}
override suspend fun getOnrampData(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
quote: OnrampProviderWithQuote.Data,
): OnrampTransaction = withContext(dispatchers.io) {
try {
val address = requireNotNull(
value = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network),
lazyMessage = { "Address must not be null" },
)
val fromAmountString = quote.fromAmount.value.movePointRight(quote.fromAmount.decimals).toString()
val currency = requireNotNull(getDefaultCurrencySync()) { "Default currency must not be null" }
val country = requireNotNull(getDefaultCountrySync()) { "Default country must not be null" }
val data = safeApiCall(
call = {
onrampApi.getData(
fromCurrencyCode = currency.code,
toContractAddress = cryptoCurrency.getContractAddress(),
toNetwork = cryptoCurrency.network.backendId,
paymentMethod = quote.paymentMethod.id,
countryCode = country.code,
fromAmount = fromAmountString,
toDecimals = cryptoCurrency.decimals,
providerId = quote.provider.id,
toAddress = address,
redirectUrl = "tangem://redirect?action=dismissBrowser",
language = null,
theme = null,
requestId = UUID.randomUUID().toString(),
).bind()
},
onError = { e ->
Timber.e(e)
throw e
},
)
if (dataSignatureVerifier.verifySignature(data.signature, data.dataJson)) {
val dataJson = requireNotNull(onrampDataAdapter.fromJson(data.dataJson)) {
"Can not parse dataJson. ${data.dataJson}"
}
createOnrampTransaction(
txId = data.txId,
onrampDataJson = dataJson,
userWalletId = userWalletId,
currency = currency,
provider = quote.provider,
cryptoCurrency = cryptoCurrency,
fromAmount = quote.fromAmount.value,
toAmount = quote.toAmount.value,
)
} else {
error("Express signature invalid")
}
} catch (e: Exception) {
Timber.e(e)
throw e
}
}
override suspend fun getPaymentMethods(): List<OnrampPaymentMethod> {
val paymentMethods = requireNotNull(paymentMethodsStore.getSyncOrNull(PAYMENT_METHODS_KEY)) {
"Onramp payment methods is absent in storage"
@ -237,6 +320,17 @@ internal class DefaultOnrampRepository(
return paymentMethodsConverter.convertList(paymentMethods)
}
override suspend fun saveSelectedPaymentMethod(paymentMethod: OnrampPaymentMethod) {
val dto = paymentMethodsConverter.convertBack(paymentMethod)
paymentMethodsStore.store(SELECTED_PAYMENT_METHOD_KEY, listOf(dto))
}
override fun getSelectedPaymentMethod(): Flow<OnrampPaymentMethod> {
return paymentMethodsStore
.get(SELECTED_PAYMENT_METHOD_KEY)
.mapNotNull { paymentMethods -> paymentMethods.firstOrNull()?.let(paymentMethodsConverter::convert) }
}
override suspend fun clearCache() = withContext(NonCancellable) {
paymentMethodsStore.clear()
pairsStore.clear()
@ -264,18 +358,53 @@ internal class DefaultOnrampRepository(
pairsStore.store(PAIRS_KEY, onrampPairs)
}
private fun createOnrampTransaction(
txId: String,
onrampDataJson: OnrampDataJson,
userWalletId: UserWalletId,
currency: OnrampCurrency,
provider: OnrampProvider,
cryptoCurrency: CryptoCurrency,
fromAmount: BigDecimal,
toAmount: BigDecimal,
): OnrampTransaction {
return OnrampTransaction(
txId = txId,
userWalletId = userWalletId,
fromAmount = fromAmount,
fromCurrency = currency,
toAmount = toAmount,
toCurrencyId = cryptoCurrency.id.value,
status = OnrampStatus.Status.Expired,
externalTxUrl = onrampDataJson.externalTxUrl,
externalTxId = onrampDataJson.externalTxId,
timestamp = DateTime.now().millis,
providerName = provider.info.name,
providerImageUrl = provider.info.imageLarge,
providerType = ExchangeProviderType.ONRAMP.name,
redirectUrl = onrampDataJson.widgetUrl,
)
}
private fun CryptoCurrency.getContractAddress(): String = when (this) {
is CryptoCurrency.Coin -> TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE
is CryptoCurrency.Token -> this.contractAddress
}
private fun convertToBigDecimal(value: String): BigDecimal =
requireNotNull(value.toBigDecimalOrNull()) { "Can not parse $value to BigDecimal" }
private fun convertToAmount(value: String, cryptoCurrency: CryptoCurrency): OnrampAmount {
val cryptoValue = requireNotNull(value.toBigDecimalOrNull()) { "Can not parse $value to BigDecimal" }
return OnrampAmount(
value = cryptoValue.movePointLeft(cryptoCurrency.decimals),
symbol = cryptoCurrency.symbol,
decimals = cryptoCurrency.decimals,
)
}
private fun List<PaymentMethodDTO>.removeApplePay(): List<PaymentMethodDTO> = filterNot { it.id == "apple-pay" }
private companion object {
const val PAYMENT_METHODS_KEY = "onramp_payment_methods"
const val SELECTED_PAYMENT_METHOD_KEY = "onramp_selected_payment_method"
const val PAIRS_KEY = "onramp_pairs"
const val QUOTES_KEY = "onramp_quotes"
}

View file

@ -2,12 +2,18 @@ package com.tangem.data.onramp.converters
import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO
import com.tangem.domain.onramp.model.OnrampPaymentMethod
import com.tangem.utils.converter.Converter
import com.tangem.utils.converter.TwoWayConverter
internal class PaymentMethodConverter : Converter<PaymentMethodDTO, OnrampPaymentMethod> {
internal class PaymentMethodConverter : TwoWayConverter<PaymentMethodDTO, OnrampPaymentMethod> {
override fun convert(value: PaymentMethodDTO): OnrampPaymentMethod = OnrampPaymentMethod(
id = value.id,
name = value.name,
imageUrl = value.image,
)
override fun convertBack(value: OnrampPaymentMethod): PaymentMethodDTO = PaymentMethodDTO(
id = value.id,
name = value.name,
image = value.imageUrl,
)
}

View file

@ -25,7 +25,7 @@ internal class TransactionConverter : TwoWayConverter<OnrampTransactionDTO, Onra
providerName = value.providerName,
providerImageUrl = value.providerImageUrl,
providerType = value.providerType,
redirectUrl = "", // not necessary field
)
}

View file

@ -3,9 +3,8 @@ package com.tangem.data.onramp.converters.error
import com.squareup.moshi.JsonAdapter
import com.tangem.datasource.api.express.models.response.ExpressError
import com.tangem.datasource.api.express.models.response.ExpressErrorResponse
import com.tangem.domain.onramp.model.OnrampAmount
import com.tangem.domain.onramp.model.OnrampQuote
import com.tangem.domain.tokens.model.Amount
import com.tangem.domain.tokens.model.AmountType
import com.tangem.utils.converter.Converter
internal class OnrampQuotesErrorConverter(
@ -40,7 +39,6 @@ internal class OnrampQuotesErrorConverter(
amountWithOffset = minAmount,
decimals = decimals,
symbol = input.amount.currencySymbol,
type = input.amount.type,
),
)
}
@ -59,24 +57,17 @@ internal class OnrampQuotesErrorConverter(
amountWithOffset = maxAmount,
decimals = decimals,
symbol = input.amount.currencySymbol,
type = input.amount.type,
),
)
}
private fun createFromAmountWithOffset(
amountWithOffset: String,
decimals: Int,
symbol: String,
type: AmountType,
): Amount {
return Amount(
private fun createFromAmountWithOffset(amountWithOffset: String, decimals: Int, symbol: String): OnrampAmount {
return OnrampAmount(
value = requireNotNull(
amountWithOffset.toBigDecimalOrNull()?.movePointLeft(decimals),
) { "wrong amount format, use only digits" },
decimals = decimals,
currencySymbol = symbol,
type = type,
symbol = symbol,
)
}
}

View file

@ -8,6 +8,7 @@ import com.tangem.data.onramp.converters.error.OnrampQuotesErrorConverter
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.express.models.response.ExpressErrorResponse
import com.tangem.datasource.api.onramp.OnrampApi
import com.tangem.datasource.crypto.DataSignatureVerifier
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore
import com.tangem.datasource.local.onramp.paymentmethods.OnrampPaymentMethodsStore
@ -16,6 +17,7 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -38,6 +40,9 @@ internal object OnrampDataModule {
pairsStore: OnrampPairsStore,
quotesStore: OnrampQuotesStore,
quotesErrorConverter: OnrampQuotesErrorConverter,
walletManagersFacade: WalletManagersFacade,
dataSignatureVerifier: DataSignatureVerifier,
@NetworkMoshi moshi: Moshi,
): OnrampRepository {
return DefaultOnrampRepository(
onrampApi = onrampApi,
@ -48,6 +53,9 @@ internal object OnrampDataModule {
pairsStore = pairsStore,
quotesStore = quotesStore,
quotesErrorConverter = quotesErrorConverter,
walletManagersFacade = walletManagersFacade,
dataSignatureVerifier = dataSignatureVerifier,
moshi = moshi,
)
}

View file

@ -3,10 +3,10 @@ package com.tangem.domain.managetokens.model
sealed class AddCustomTokenForm {
data class Raw(
val contractAddress: String,
val name: String,
val symbol: String,
val decimals: String,
val contractAddress: String = "",
val name: String = "",
val symbol: String = "",
val decimals: String = "",
) : AddCustomTokenForm()
sealed class Validated : AddCustomTokenForm() {

View file

@ -1,21 +1,27 @@
package com.tangem.domain.models.scan
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.common.card.WalletData
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.domain.models.scan.serialization.ScanResponseAsStringSerializer
import com.tangem.operations.backup.PrimaryCard
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import kotlinx.serialization.Serializable
// TODO: Move to :domain:card:models
/**
[REDACTED_AUTHOR]
*/
@Serializable(with = ScanResponseAsStringSerializer::class)
@JsonClass(generateAdapter = true)
data class ScanResponse(
val card: CardDTO,
val productType: ProductType,
val walletData: WalletData?,
val secondTwinPublicKey: String? = null,
val derivedKeys: Map<KeyWalletPublicKey, ExtendedPublicKeysMap> = mapOf(),
val primaryCard: PrimaryCard? = null,
@Json(name = "card") val card: CardDTO,
@Json(name = "productType") val productType: ProductType,
@Json(name = "walletData") val walletData: WalletData?,
@Json(name = "secondTwinPublicKey") val secondTwinPublicKey: String? = null,
@Json(name = "derivedKeys") val derivedKeys: Map<KeyWalletPublicKey, ExtendedPublicKeysMap> = mapOf(),
@Json(name = "primaryCard") val primaryCard: PrimaryCard? = null,
)
typealias KeyWalletPublicKey = ByteArrayKey

View file

@ -1,4 +1,4 @@
package com.tangem.tap.domain.userWalletList.utils.json
package com.tangem.domain.models.scan.serialization
import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonAdapter
@ -7,7 +7,7 @@ import com.squareup.moshi.JsonWriter
import com.squareup.moshi.ToJson
import com.tangem.common.extensions.ByteArrayKey
internal class ByteArrayKeyAdapter {
class ByteArrayKeyAdapter {
@ToJson
fun toJson(writer: JsonWriter, src: ByteArrayKey, byteArrayAdapter: JsonAdapter<ByteArray>) {
byteArrayAdapter.toJson(writer, src.bytes)

View file

@ -1,4 +1,4 @@
package com.tangem.tap.domain.userWalletList.utils.json
package com.tangem.domain.models.scan.serialization
import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonAdapter
@ -7,7 +7,7 @@ import com.squareup.moshi.JsonWriter
import com.squareup.moshi.ToJson
import com.tangem.domain.models.scan.CardDTO
internal class CardBackupStatusAdapter {
class CardBackupStatusAdapter {
@ToJson
fun toJson(writer: JsonWriter, src: CardDTO.BackupStatus?, mapAdapter: JsonAdapter<Map<String, String>>) {
val jsonMap = mutableMapOf<String, String>()

View file

@ -1,4 +1,4 @@
package com.tangem.tap.domain.userWalletList.utils.json
package com.tangem.domain.models.scan.serialization
import com.squareup.moshi.FromJson
import com.squareup.moshi.ToJson
@ -6,7 +6,6 @@ import com.tangem.common.extensions.guard
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.crypto.hdWallet.DerivationNode
import com.tangem.crypto.hdWallet.DerivationPath
import timber.log.Timber
class DerivationPathAdapterWithMigration {
@ToJson
@ -26,7 +25,6 @@ class DerivationPathAdapterWithMigration {
private fun fromLegacyScheme(jsonMap: Map<String, Any>): DerivationPath {
val rawPath = jsonMap["rawPath"] as String
val nodeIndexes = (jsonMap["nodes"] as? List<Number>).guard {
Timber.e("Unable to convert derivation path nodes from JSON")
return DerivationPath(rawPath)
}
val nodes = nodeIndexes.map { DerivationNode.fromIndex(it.toLong()) }

View file

@ -1,4 +1,4 @@
package com.tangem.tap.domain.userWalletList.utils.json
package com.tangem.domain.models.scan.serialization
import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonAdapter
@ -11,7 +11,7 @@ import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.operations.derivation.ExtendedPublicKeysMap
internal class ExtendedPublicKeysMapAdapter {
class ExtendedPublicKeysMapAdapter {
@ToJson
fun toJson(
writer: JsonWriter,

View file

@ -0,0 +1,37 @@
package com.tangem.domain.models.scan.serialization
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.tangem.common.json.TangemSdkAdapter
import com.tangem.domain.models.scan.ScanResponse
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
internal object ScanResponseAsStringSerializer : KSerializer<ScanResponse> {
private val moshi = Moshi.Builder()
.add(WalletDerivedKeysMapAdapter())
.add(ScanResponseDerivedKeysMapAdapter())
.add(ByteArrayKeyAdapter())
.add(ExtendedPublicKeysMapAdapter())
.add(CardBackupStatusAdapter())
.add(DerivationPathAdapterWithMigration())
.add(TangemSdkAdapter.DateAdapter())
.add(TangemSdkAdapter.DerivationNodeAdapter())
.add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model
.add(KotlinJsonAdapterFactory())
.build()
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ScanResponse", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): ScanResponse {
return moshi.adapter(ScanResponse::class.java).fromJson(decoder.decodeString())!!
}
override fun serialize(encoder: Encoder, value: ScanResponse) {
encoder.encodeString(moshi.adapter(ScanResponse::class.java).toJson(value)!!)
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.tap.domain.userWalletList.utils.json
package com.tangem.domain.models.scan.serialization
import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonAdapter
@ -8,7 +8,7 @@ import com.squareup.moshi.ToJson
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.operations.derivation.ExtendedPublicKeysMap
internal class ScanResponseDerivedKeysMapAdapter {
class ScanResponseDerivedKeysMapAdapter {
@ToJson
fun toJson(
writer: JsonWriter,

View file

@ -1,4 +1,4 @@
package com.tangem.tap.domain.userWalletList.utils.json
package com.tangem.domain.models.scan.serialization
import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonAdapter
@ -10,7 +10,7 @@ import com.tangem.common.extensions.toHexString
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
internal class WalletDerivedKeysMapAdapter {
class WalletDerivedKeysMapAdapter {
@ToJson
fun toJson(
writer: JsonWriter,

View file

@ -0,0 +1,11 @@
package com.tangem.domain.onramp.model
import com.tangem.domain.core.serialization.SerializedBigDecimal
import kotlinx.serialization.Serializable
@Serializable
data class OnrampAmount(
val symbol: String,
val value: SerializedBigDecimal,
val decimals: Int,
)

View file

@ -1,9 +1,13 @@
package com.tangem.domain.onramp.model
import kotlinx.serialization.Serializable
@Serializable
data class OnrampProvider(
val id: String,
val info: OnrampProviderInfo,
val paymentMethods: List<OnrampPaymentMethod>,
)
@Serializable
data class OnrampProviderInfo(val name: String, val imageLarge: String)

View file

@ -0,0 +1,27 @@
package com.tangem.domain.onramp.model
import kotlinx.serialization.Serializable
sealed interface OnrampProviderWithQuote {
val provider: OnrampProvider
@Serializable
data class Data(
override val provider: OnrampProvider,
val paymentMethod: OnrampPaymentMethod,
val toAmount: OnrampAmount,
val fromAmount: OnrampAmount,
) : OnrampProviderWithQuote
sealed interface Unavailable : OnrampProviderWithQuote {
data class NotSupportedPaymentMethod(
override val provider: OnrampProvider,
val availablePaymentMethods: List<OnrampPaymentMethod>,
) : Unavailable
data class AvailableFrom(override val provider: OnrampProvider, val amount: OnrampAmount) : Unavailable
data class AvailableUpTo(override val provider: OnrampProvider, val amount: OnrampAmount) : Unavailable
}
}

View file

@ -1,8 +1,5 @@
package com.tangem.domain.onramp.model
import com.tangem.domain.tokens.model.Amount
import java.math.BigDecimal
sealed class OnrampQuote {
abstract val paymentMethod: OnrampPaymentMethod
@ -11,10 +8,10 @@ sealed class OnrampQuote {
data class Data(
override val paymentMethod: OnrampPaymentMethod,
override val provider: OnrampProvider,
val fromAmount: BigDecimal,
val toAmount: BigDecimal,
val minFromAmount: BigDecimal,
val maxFromAmount: BigDecimal,
val fromAmount: OnrampAmount,
val toAmount: OnrampAmount,
val minFromAmount: OnrampAmount,
val maxFromAmount: OnrampAmount,
) : OnrampQuote()
sealed class Error : OnrampQuote() {
@ -22,13 +19,13 @@ sealed class OnrampQuote {
data class AmountTooSmallError(
override val paymentMethod: OnrampPaymentMethod,
override val provider: OnrampProvider,
val amount: Amount,
val amount: OnrampAmount,
) : Error()
data class AmountTooBigError(
override val paymentMethod: OnrampPaymentMethod,
override val provider: OnrampProvider,
val amount: Amount,
val amount: OnrampAmount,
) : Error()
}
}

View file

@ -36,4 +36,5 @@ data class OnrampTransaction(
val providerName: String, // todo onramp fix after SwapProvider moved to own module
val providerImageUrl: String, // todo onramp fix after SwapProvider moved to own module
val providerType: String, // todo onramp fix after SwapProvider moved to own module
val redirectUrl: String,
)

View file

@ -0,0 +1,68 @@
package com.tangem.domain.onramp
import arrow.core.Either
import com.tangem.domain.onramp.model.OnrampPaymentMethod
import com.tangem.domain.onramp.model.OnrampProvider
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
import com.tangem.domain.onramp.model.OnrampProviderWithQuote.Unavailable
import com.tangem.domain.onramp.model.OnrampQuote
import com.tangem.domain.onramp.repositories.OnrampRepository
class GetOnrampProviderWithQuoteUseCase(private val repository: OnrampRepository) {
suspend operator fun invoke(paymentMethod: OnrampPaymentMethod): Either<Throwable, List<OnrampProviderWithQuote>> {
return Either.catch {
val quotes: List<OnrampQuote> = requireNotNull(repository.getQuotesSync()) { "Quotes must not be null" }
quotes
.groupBy { it.provider }
.mapNotNull { (provider, quotes) ->
quotes.quoteWithProvider(provider = provider, selectedPaymentMethod = paymentMethod)
}
}
}
private fun List<OnrampQuote>.quoteWithProvider(
provider: OnrampProvider,
selectedPaymentMethod: OnrampPaymentMethod,
): OnrampProviderWithQuote? {
val quoteData = this.filterIsInstance<OnrampQuote.Data>()
val matchedPaymentMethodQuote = quoteData.firstOrNull { it.paymentMethod == selectedPaymentMethod }
if (matchedPaymentMethodQuote != null) {
return OnrampProviderWithQuote.Data(
provider = matchedPaymentMethodQuote.provider,
paymentMethod = matchedPaymentMethodQuote.paymentMethod,
toAmount = matchedPaymentMethodQuote.toAmount,
fromAmount = matchedPaymentMethodQuote.fromAmount,
)
}
val paymentMethodNotSupported = quoteData.filterNot { it.paymentMethod == selectedPaymentMethod }
if (paymentMethodNotSupported.isNotEmpty()) {
return Unavailable.NotSupportedPaymentMethod(
provider = provider,
availablePaymentMethods = paymentMethodNotSupported.map(OnrampQuote.Data::paymentMethod),
)
}
val amountError = this
.filterIsInstance<OnrampQuote.Error>()
.firstOrNull { it.paymentMethod == selectedPaymentMethod }
return when {
amountError != null -> {
when (amountError) {
is OnrampQuote.Error.AmountTooBigError -> Unavailable.AvailableUpTo(
provider = amountError.provider,
amount = amountError.amount,
)
is OnrampQuote.Error.AmountTooSmallError -> Unavailable.AvailableFrom(
provider = amountError.provider,
amount = amountError.amount,
)
}
}
else -> null
}
}
}

View file

@ -0,0 +1,30 @@
package com.tangem.domain.onramp
import arrow.core.Either
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
class GetOnrampRedirectUrlUseCase(
private val repository: OnrampRepository,
private val transactionRepository: OnrampTransactionRepository,
) {
suspend operator fun invoke(
userWalletId: UserWalletId,
quote: OnrampProviderWithQuote.Data,
cryptoCurrency: CryptoCurrency,
): Either<Throwable, String> {
return Either.catch {
val transaction = repository.getOnrampData(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrency,
quote = quote,
)
transactionRepository.storeTransaction(transaction)
transaction.redirectUrl
}
}
}

View file

@ -0,0 +1,19 @@
package com.tangem.domain.onramp
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.domain.core.utils.EitherFlow
import com.tangem.domain.onramp.model.OnrampPaymentMethod
import com.tangem.domain.onramp.repositories.OnrampRepository
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
class GetOnrampSelectedPaymentMethodUseCase(private val repository: OnrampRepository) {
operator fun invoke(): EitherFlow<Throwable, OnrampPaymentMethod> {
return repository.getSelectedPaymentMethod()
.map<OnrampPaymentMethod, Either<Throwable, OnrampPaymentMethod>> { it.right() }
.catch { emit(it.left()) }
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.domain.onramp
import arrow.core.Either
import com.tangem.domain.onramp.model.OnrampPaymentMethod
import com.tangem.domain.onramp.repositories.OnrampRepository
class OnrampSaveSelectedPaymentMethod(private val repository: OnrampRepository) {
suspend operator fun invoke(paymentMethod: OnrampPaymentMethod): Either<Throwable, Unit> {
return Either.catch { repository.saveSelectedPaymentMethod(paymentMethod) }
}
}

View file

@ -1,10 +1,13 @@
package com.tangem.domain.onramp.repositories
import com.tangem.domain.onramp.model.*
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.tokens.model.Amount
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
@Suppress("TooManyFunctions")
interface OnrampRepository {
// api
suspend fun getCurrencies(): List<OnrampCurrency>
@ -14,6 +17,11 @@ interface OnrampRepository {
suspend fun fetchPaymentMethodsIfAbsent()
suspend fun fetchPairs(currency: OnrampCurrency, country: OnrampCountry, cryptoCurrency: CryptoCurrency)
suspend fun fetchQuotes(cryptoCurrency: CryptoCurrency, amount: Amount)
suspend fun getOnrampData(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
quote: OnrampProviderWithQuote.Data,
): OnrampTransaction
// cache
suspend fun saveDefaultCurrency(currency: OnrampCurrency)
@ -23,6 +31,9 @@ interface OnrampRepository {
suspend fun getDefaultCountrySync(): OnrampCountry?
fun getDefaultCountry(): Flow<OnrampCountry?>
suspend fun getPaymentMethods(): List<OnrampPaymentMethod>
suspend fun saveSelectedPaymentMethod(paymentMethod: OnrampPaymentMethod)
fun getSelectedPaymentMethod(): Flow<OnrampPaymentMethod>
fun getQuotes(): Flow<List<OnrampQuote>>
suspend fun getQuotesSync(): List<OnrampQuote>?
suspend fun clearCache()
}

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