Updated on 2026-08-14

This commit is contained in:
Tangem 2024-11-14 11:03:01 +03:00
commit 00805c1380
268 changed files with 6515 additions and 1380 deletions

View file

@ -74,6 +74,7 @@ dependencies {
implementation(projects.domain.walletConnect)
implementation(projects.domain.markets)
implementation(projects.domain.manageTokens)
implementation(projects.domain.onramp)
implementation(projects.common)
implementation(projects.common.routing)
@ -112,6 +113,7 @@ dependencies {
implementation(projects.data.walletConnect)
implementation(projects.data.markets)
implementation(projects.data.manageTokens)
implementation(projects.data.onramp)
/** Features */
implementation(projects.features.onboarding)

View file

@ -749,6 +749,16 @@
}
]
},
{
"id": "chiliz",
"name": "Chiliz",
"symbol": "CHZ",
"networks": [
{
"networkId": "chiliz/test"
}
]
},
{
"id": "casper-network",
"name": "Casper",

View file

@ -52,6 +52,7 @@ import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import com.tangem.features.send.api.navigation.SendRouter
@ -59,6 +60,7 @@ import com.tangem.features.staking.api.navigation.StakingRouter
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
import com.tangem.features.wallet.navigation.WalletRouter
import com.tangem.operations.backup.BackupService
import com.tangem.sdk.api.BackupServiceHolder
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.sdk.extensions.init
import com.tangem.tap.common.ActivityResultCallbackHolder
@ -185,6 +187,12 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
lateinit var shouldInitiallyAskPermissionUseCase: ShouldInitiallyAskPermissionUseCase
@Inject
lateinit var backupServiceHolder: BackupServiceHolder
@Inject
lateinit var onboardingV2FeatureToggles: OnboardingV2FeatureToggles
internal val viewModel: MainViewModel by viewModels()
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode>
@ -294,7 +302,14 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
cardSdkOwner.register(activity = this)
tangemSdkManager = injectedTangemSdkManager
appStateHolder.tangemSdkManager = tangemSdkManager
backupService = BackupService.init(cardSdkConfigRepository.sdk, this)
if (onboardingV2FeatureToggles.isOnboardingV2Enabled) {
backupServiceHolder.createAndSetService(cardSdkConfigRepository.sdk, this)
backupService = backupServiceHolder.backupService.get()!! // will be deleted eventually
} else {
backupService = BackupService.init(cardSdkConfigRepository.sdk, this)
}
lockUserWalletsTimer = LockUserWalletsTimer(
owner = this,
settingsRepository = settingsRepository,
@ -550,6 +565,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
}
// TODO add onboarding v2 handling
store.dispatch(BackupAction.CheckForUnfinishedBackup)
}

View file

@ -64,12 +64,16 @@ private fun handleAction(action: Action, appState: () -> AppState?) {
}
val cardProvider: () -> CardDTO? = { scanResponseProvider.invoke()?.card }
val buyService = makeBuyExchangeService(config)
val sellService = makeSellExchangeService(config)
val exchangeManager = CurrencyExchangeManager(
buyService = makeBuyExchangeService(config),
sellService = makeSellExchangeService(config),
buyService = buyService,
sellService = sellService,
primaryRules = CardExchangeRules(cardProvider),
)
// TODO: for refactoring (after remove old design refactor CurrencyExchangeManager and use 1 instance)
store.inject(DaggerGraphState::appStateHolder).buyService = buyService
store.inject(DaggerGraphState::appStateHolder).sellService = sellService
store.inject(DaggerGraphState::appStateHolder).exchangeService = exchangeManager
store.dispatchOnMain(GlobalAction.ExchangeManager.Init.Success(exchangeManager))
store.dispatchOnMain(GlobalAction.ExchangeManager.Update)

View file

@ -1,5 +1,6 @@
package com.tangem.tap.di
import com.tangem.datasource.exchangeservice.swap.SwapServiceLoader
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.exchange.RampStateManager
@ -10,6 +11,8 @@ import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.domain.scanCard.repository.DefaultScanCardRepository
import com.tangem.tap.network.exchangeServices.DefaultRampManager
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -39,8 +42,18 @@ internal object ActivityModule {
@Provides
@Singleton
fun provideDefaultRampManager(appStateHolder: AppStateHolder): RampStateManager {
return DefaultRampManager(appStateHolder.exchangeService)
fun provideDefaultRampManager(
appStateHolder: AppStateHolder,
swapServiceLoader: SwapServiceLoader,
dispatchers: CoroutineDispatcherProvider,
): RampStateManager {
return DefaultRampManager(
exchangeService = appStateHolder.exchangeService,
buyService = Provider { requireNotNull(appStateHolder.buyService) },
sellService = Provider { requireNotNull(appStateHolder.sellService) },
swapServiceLoader = swapServiceLoader,
dispatchers = dispatchers,
)
}
@Provides

View file

@ -0,0 +1,17 @@
package com.tangem.tap.di
import com.tangem.sdk.api.BackupServiceHolder
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object TangemSdkModule {
@Provides
@Singleton
fun provideBackupServiceHolder(): BackupServiceHolder = BackupServiceHolder()
}

View file

@ -0,0 +1,50 @@
package com.tangem.tap.di.domain
import com.tangem.domain.onramp.*
import com.tangem.domain.onramp.repositories.OnrampRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object OnrampDomainModule {
@Provides
@Singleton
fun provideGetOnrampCurrenciesUseCase(onrampRepository: OnrampRepository): GetOnrampCurrenciesUseCase {
return GetOnrampCurrenciesUseCase(onrampRepository)
}
@Provides
@Singleton
fun provideOnrampSaveDefaultCurrencyUseCase(onrampRepository: OnrampRepository): OnrampSaveDefaultCurrencyUseCase {
return OnrampSaveDefaultCurrencyUseCase(onrampRepository)
}
@Provides
@Singleton
fun provideGetOnrampCountriesUseCase(onrampRepository: OnrampRepository): GetOnrampCountriesUseCase {
return GetOnrampCountriesUseCase(onrampRepository)
}
@Provides
@Singleton
fun provideGetOnrampCountryUseCase(onrampRepository: OnrampRepository): GetOnrampCountryUseCase {
return GetOnrampCountryUseCase(onrampRepository)
}
@Provides
@Singleton
fun provideOnrampSaveDefaultCountryUseCase(onrampRepository: OnrampRepository): OnrampSaveDefaultCountryUseCase {
return OnrampSaveDefaultCountryUseCase(onrampRepository)
}
@Provides
@Singleton
fun provideCheckOnrampAvailabilityUseCase(onrampRepository: OnrampRepository): CheckOnrampAvailabilityUseCase {
return CheckOnrampAvailabilityUseCase(onrampRepository)
}
}

View file

@ -0,0 +1,23 @@
package com.tangem.tap.di.domain
import com.tangem.feature.swap.domain.GetAvailablePairsUseCase
import com.tangem.feature.swap.domain.api.SwapRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
/**
[REDACTED_AUTHOR]
*/
@Module
@InstallIn(SingletonComponent::class)
internal object SwapDomainModule {
@Provides
@Singleton
fun provideGetAvailablePairsUseCase(swapRepository: SwapRepository): GetAvailablePairsUseCase {
return GetAvailablePairsUseCase(swapRepository = swapRepository)
}
}

View file

@ -7,7 +7,6 @@ import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.repository.*
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -119,26 +118,26 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository,
swapRepository: SwapRepository,
currencyChecksRepository: CurrencyChecksRepository,
showSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase,
promoRepository: PromoRepository,
stakingRepository: StakingRepository,
dispatchers: CoroutineDispatcherProvider,
rampStateManager: RampStateManager,
): GetCurrencyWarningsUseCase {
return GetCurrencyWarningsUseCase(
walletManagersFacade = walletManagersFacade,
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
marketCryptoCurrencyRepository = marketCryptoCurrencyRepository,
currencyChecksRepository = currencyChecksRepository,
swapRepository = swapRepository,
showSwapPromoTokenUseCase = showSwapPromoTokenUseCase,
promoRepository = promoRepository,
stakingRepository = stakingRepository,
dispatchers = dispatchers,
rampStateManager = rampStateManager,
)
}
@ -216,23 +215,19 @@ internal object TokensDomainModule {
fun provideGetCryptoCurrencyActionsUseCase(
rampStateManager: RampStateManager,
walletManagersFacade: WalletManagersFacade,
marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository,
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
stakingFeatureToggles: StakingFeatureToggles,
dispatchers: CoroutineDispatcherProvider,
): GetCryptoCurrencyActionsUseCase {
return GetCryptoCurrencyActionsUseCase(
rampManager = rampStateManager,
walletManagersFacade = walletManagersFacade,
marketCryptoCurrencyRepository = marketCryptoCurrencyRepository,
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
stakingFeatureToggles = stakingFeatureToggles,
dispatchers = dispatchers,
)
}

View file

@ -17,7 +17,6 @@ import com.tangem.domain.settings.IncrementAppLaunchCounterUseCase
import com.tangem.domain.settings.usercountry.FetchUserCountryUseCase
import com.tangem.domain.staking.FetchStakingTokensUseCase
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.features.home.featuretoggles.HomeFeatureToggles
import com.tangem.tap.features.main.model.MainScreenState
@ -40,7 +39,6 @@ internal class MainViewModel @Inject constructor(
private val blockchainSDKFactory: BlockchainSDKFactory,
private val userWalletsListManager: UserWalletsListManager,
private val dispatchers: CoroutineDispatcherProvider,
stakingFeatureToggles: StakingFeatureToggles,
private val fetchStakingTokensUseCase: FetchStakingTokensUseCase,
private val apiConfigsManager: ApiConfigsManager,
homeFeatureToggles: HomeFeatureToggles,
@ -78,9 +76,7 @@ internal class MainViewModel @Inject constructor(
displayBalancesHidingStatusToast()
displayHiddenBalancesModalNotification()
if (stakingFeatureToggles.isStakingEnabled) {
fetchStakingTokens()
}
fetchStakingTokens()
deleteDeprecatedLogsUseCase()
}

View file

@ -16,6 +16,7 @@ import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.settings.usercountry.models.UserCountry
import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Onboarding
@ -67,7 +68,10 @@ object OnboardingHelper {
fun whereToNavigate(scanResponse: ScanResponse): AppRoute {
if (store.inject(DaggerGraphState::onboardingV2FeatureToggles).isOnboardingV2Enabled) {
return AppRoute.Onboarding(scanResponse)
return AppRoute.Onboarding(
scanResponse = scanResponse,
startFromBackup = false,
)
}
return when (val type = scanResponse.productType) {
@ -88,6 +92,7 @@ object OnboardingHelper {
}
fun saveWallet(
alreadyCreatedWallet: UserWallet?,
scanResponse: ScanResponse,
accessCode: String? = null,
backupCardsIds: List<String>? = null,
@ -117,7 +122,7 @@ object OnboardingHelper {
// When should not save user wallets but device has biometry and save wallet screen has not been shown,
// then open save wallet screen
tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen() -> {
proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError)
proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError, alreadyCreatedWallet)
delay(timeMillis = 1_200)
@ -131,7 +136,7 @@ object OnboardingHelper {
}
// If device has no biometry and save wallet screen has been shown, then go through old scenario
else -> {
proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError)
proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError, alreadyCreatedWallet)
}
}
}
@ -242,9 +247,10 @@ object OnboardingHelper {
scanResponse: ScanResponse,
backupCardsIds: List<String>?,
hasBackupError: Boolean,
alreadyCreatedWallet: UserWallet? = null,
) {
val walletNameGenerateUseCase = store.inject(DaggerGraphState::generateWalletNameUseCase)
val userWallet = UserWalletBuilder(scanResponse, walletNameGenerateUseCase)
val userWallet = alreadyCreatedWallet ?: UserWalletBuilder(scanResponse, walletNameGenerateUseCase)
.hasBackupError(hasBackupError)
.backupCardsIds(backupCardsIds?.toSet())
.build()

View file

@ -196,11 +196,11 @@ private fun handleWalletAction(action: Action) {
}
}
private fun handleFinishBackup(scanResponse: ScanResponse) {
private fun handleFinishBackup(scanResponse: ScanResponse, userWallet: UserWallet? = null) {
val backupState = store.state.onboardingWalletState.backupState
val updatedScanResponse = updateScanResponseAfterBackup(scanResponse, backupState)
val userWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build()
val userWalletId = userWallet?.walletId ?: UserWalletIdBuilder.scanResponse(scanResponse).build()
if (backupState.hasRing && userWalletId != null) {
scope.launch {
store.inject(DaggerGraphState::walletsRepository).setHasWalletsWithRing(userWalletId = userWalletId)
@ -208,6 +208,7 @@ private fun handleFinishBackup(scanResponse: ScanResponse) {
}
OnboardingHelper.saveWallet(
alreadyCreatedWallet = userWallet,
scanResponse = updatedScanResponse,
accessCode = backupState.accessCode,
backupCardsIds = backupState.backupCardIds,
@ -667,7 +668,7 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
Analytics.send(Onboarding.Finished())
store.state.globalState.onboardingState.onboardingManager?.finishActivation(notActivatedCardIds)
handleFinishBackup(requireNotNull(scanResponse))
handleFinishBackup(requireNotNull(scanResponse), userWallet)
delay(1000)
store.dispatchWithMain(BackupAction.BackupFinished(userWalletId = userWallet?.walletId))
}
@ -728,6 +729,7 @@ private suspend fun createUserWallet(scanResponse: ScanResponse, backupState: Ba
return requireNotNull(
value = UserWalletBuilder(scanResponse, walletNameGenerateUseCase)
.backupCardsIds(backupState.backupCardIds.toSet())
.hasBackupError(backupState.hasBackupError)
.build(),
lazyMessage = { "User wallet not created" },
)

View file

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

View file

@ -8,6 +8,8 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchain.extensions.Result
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
@ -19,6 +21,8 @@ import com.tangem.tap.domain.model.Currency
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.math.BigDecimal
/**
@ -30,9 +34,19 @@ class CurrencyExchangeManager(
private val primaryRules: ExchangeRules,
) : ExchangeService {
override val initializationStatus: StateFlow<ExchangeServiceInitializationStatus>
get() = _initializationStatus
private val _initializationStatus: MutableStateFlow<ExchangeServiceInitializationStatus> =
MutableStateFlow(value = lceLoading())
override suspend fun update() {
_initializationStatus.value = lceLoading()
buyService.update()
sellService.update()
_initializationStatus.value = lceContent()
}
override fun isBuyAllowed(): Boolean = primaryRules.isBuyAllowed() && buyService.isBuyAllowed()

View file

@ -1,15 +1,29 @@
package com.tangem.tap.network.exchangeServices
import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE
import com.tangem.datasource.exchangeservice.swap.SwapServiceLoader
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
class DefaultRampManager(private val exchangeService: ExchangeService?) : RampStateManager {
internal class DefaultRampManager(
private val exchangeService: ExchangeService?,
private val buyService: Provider<ExchangeService>,
private val sellService: Provider<ExchangeService>,
private val swapServiceLoader: SwapServiceLoader,
private val dispatchers: CoroutineDispatcherProvider,
) : RampStateManager {
private val cryptoCurrencyConverter = CryptoCurrencyConverter()
override fun availableForBuy(scanResponse: ScanResponse, cryptoCurrency: CryptoCurrency): Boolean {
return exchangeService?.availableForBuy(
scanResponse,
scanResponse = scanResponse,
currency = cryptoCurrencyConverter.convertBack(cryptoCurrency),
) ?: false
}
@ -19,4 +33,46 @@ class DefaultRampManager(private val exchangeService: ExchangeService?) : RampSt
currency = cryptoCurrencyConverter.convertBack(cryptoCurrency),
) ?: false
}
override suspend fun availableForSwap(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean {
return getExchangeableFlag(userWalletId, cryptoCurrency) && !cryptoCurrency.isCustom
}
override fun getBuyInitializationStatus(): Flow<ExchangeServiceInitializationStatus> {
return buyService.invoke().initializationStatus
}
override suspend fun fetchBuyServiceData() {
withContext(dispatchers.io) {
buyService.invoke().update()
}
}
override fun getSellInitializationStatus(): Flow<ExchangeServiceInitializationStatus> {
return sellService.invoke().initializationStatus
}
override suspend fun fetchSellServiceData() {
withContext(dispatchers.io) {
sellService.invoke().update()
}
}
override fun getSwapInitializationStatus(userWalletId: UserWalletId): Flow<ExchangeServiceInitializationStatus> {
return swapServiceLoader.getInitializationStatus(userWalletId)
}
private suspend fun getExchangeableFlag(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean {
return withContext(dispatchers.io) {
val contractAddress = (cryptoCurrency as? CryptoCurrency.Token)?.contractAddress
?: EMPTY_CONTRACT_ADDRESS_VALUE
val asset = swapServiceLoader.getInitializationStatus(userWalletId).value.getOrNull()?.find {
it.network == cryptoCurrency.network.backendId &&
it.contractAddress.equals(contractAddress, ignoreCase = true)
}
asset?.exchangeAvailable ?: false
}
}
}

View file

@ -1,8 +1,14 @@
package com.tangem.tap.network.exchangeServices
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.domain.model.Currency
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
typealias ExchangeServiceInitializationStatus = Lce<Throwable, Any>
interface Exchanger {
fun isBuyAllowed(): Boolean
@ -12,10 +18,17 @@ interface Exchanger {
}
interface ExchangeService : Exchanger, ExchangeUrlBuilder {
val initializationStatus: StateFlow<ExchangeServiceInitializationStatus>
suspend fun update()
companion object {
fun dummy(): ExchangeService = object : ExchangeService {
override val initializationStatus: StateFlow<ExchangeServiceInitializationStatus> =
MutableStateFlow(value = lceLoading())
override suspend fun update() {}
override fun isBuyAllowed(): Boolean = false
override fun isSellAllowed(): Boolean = false

View file

@ -138,6 +138,8 @@ internal val Blockchain.mercuryoNetwork: String?
Blockchain.CasperTestnet -> null
Blockchain.Core -> null
Blockchain.CoreTestnet -> null
Blockchain.Chiliz -> null
Blockchain.ChilizTestnet -> null
Blockchain.Unknown -> null
Blockchain.Xodex -> null
Blockchain.Canxium -> null

View file

@ -6,12 +6,19 @@ import com.tangem.common.extensions.calculateSha512
import com.tangem.common.extensions.toHexString
import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.ExchangeService
import com.tangem.tap.network.exchangeServices.ExchangeServiceInitializationStatus
import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import timber.log.Timber
import java.util.concurrent.CopyOnWriteArrayList
/**
@ -19,10 +26,34 @@ import java.util.concurrent.CopyOnWriteArrayList
*/
internal class MercuryoService(private val environment: MercuryoEnvironment) : ExchangeService {
override val initializationStatus: StateFlow<ExchangeServiceInitializationStatus>
get() = _initializationStatus
private val _initializationStatus: MutableStateFlow<ExchangeServiceInitializationStatus> =
MutableStateFlow(value = lceLoading())
private val api: MercuryoApi = environment.mercuryoApi
private val availableMercuryoCurrencies = CopyOnWriteArrayList<MercuryoCurrenciesResponse.MercuryoCryptoCurrency>()
override suspend fun update() {
Timber.i("Start updating")
_initializationStatus.value = lceLoading()
val result = performRequest { api.currencies(environment.apiVersion) }
when {
result is Result.Success && result.data.status == RESPONSE_SUCCESS_STATUS_CODE -> {
handleSuccessfullyUpdatedData(data = result.data.data)
}
result is Result.Failure -> {
availableMercuryoCurrencies.clear()
Timber.e("Failed to load currencies", result.error)
_initializationStatus.value = result.error.lceError()
}
}
}
override fun isBuyAllowed(): Boolean = true
override fun isSellAllowed(): Boolean = false
@ -42,18 +73,6 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E
override fun availableForSell(currency: Currency): Boolean = false
override suspend fun update() {
val result = performRequest { api.currencies(environment.apiVersion) }
when {
result is Result.Success && result.data.status == RESPONSE_SUCCESS_STATUS_CODE -> {
handleSuccessfullyUpdatedData(data = result.data.data)
}
result is Result.Failure -> {
availableMercuryoCurrencies.clear()
}
}
}
override fun getUrl(
action: CurrencyExchangeManager.Action,
cryptoCurrency: CryptoCurrency,
@ -89,6 +108,9 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E
private fun handleSuccessfullyUpdatedData(data: MercuryoCurrenciesResponse.Data) {
availableMercuryoCurrencies.clear()
availableMercuryoCurrencies.addAll(data.config.cryptoCurrencies)
Timber.i("Successfully updated")
_initializationStatus.value = lceContent()
}
private fun signature(address: String) = (address + environment.secret).calculateSha512().toHexString().lowercase()

View file

@ -7,13 +7,20 @@ import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
import com.tangem.datasource.api.common.createRetrofitInstance
import com.tangem.domain.common.extensions.withIOContext
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.ExchangeService
import com.tangem.tap.network.exchangeServices.ExchangeServiceInitializationStatus
import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder.Companion.SCHEME
import com.tangem.tap.network.exchangeServices.moonpay.models.MoonPayAvailableCurrency
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import timber.log.Timber
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
@ -23,6 +30,12 @@ class MoonPayService(
private val logEnabled: Boolean,
) : ExchangeService {
override val initializationStatus: StateFlow<ExchangeServiceInitializationStatus>
get() = _initializationStatus
private val _initializationStatus: MutableStateFlow<ExchangeServiceInitializationStatus> =
MutableStateFlow(value = lceLoading())
private val api: MoonPayApi by lazy {
createRetrofitInstance(
baseUrl = MoonPayApi.MOOONPAY_BASE_URL,
@ -34,14 +47,25 @@ class MoonPayService(
override suspend fun update() {
withIOContext {
Timber.i("Start updating")
_initializationStatus.value = lceLoading()
performRequest {
val userStatus = when (val result = performRequest { api.getUserStatus(apiKey) }) {
is Result.Failure -> return@performRequest
is Result.Failure -> {
Timber.e("Failed to load user status", result.error)
_initializationStatus.value = result.error.lceError()
return@performRequest
}
is Result.Success -> result.data
}
val currencies = when (val result = performRequest { api.getCurrencies(apiKey) }) {
is Result.Failure -> return@performRequest
is Result.Failure -> {
Timber.e("Failed to load currencies", result.error)
_initializationStatus.value = result.error.lceError()
return@performRequest
}
is Result.Success -> result.data
}
@ -58,6 +82,8 @@ class MoonPayService(
)
}
Timber.i("Successfully updated")
_initializationStatus.value = lceContent()
status = MoonPayStatus(currenciesToSell, userStatus, currencies)
}
}

View file

@ -43,6 +43,7 @@ internal val Blockchain.moonPaySupportedCurrency: MoonPaySupportedCurrency?
Filecoin -> MoonPaySupportedCurrency(networkCode = "filecoin", currencyCode = "fil")
Sui -> MoonPaySupportedCurrency(networkCode = "sui", currencyCode = "sui")
Core -> MoonPaySupportedCurrency(networkCode = "core", currencyCode = "core")
Chiliz -> MoonPaySupportedCurrency(networkCode = "ethereum", currencyCode = "chz")
ArbitrumTestnet -> null
AvalancheTestnet -> null
BinanceTestnet -> null
@ -139,6 +140,7 @@ internal val Blockchain.moonPaySupportedCurrency: MoonPaySupportedCurrency?
Casper -> null
CasperTestnet -> null
CoreTestnet -> null
ChilizTestnet -> null
Unknown -> null
Xodex -> null
Canxium -> null

View file

@ -1,6 +1,5 @@
package com.tangem.tap.proxy
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.redux.StateDialog
@ -26,10 +25,8 @@ class AppStateHolder @Inject constructor() : ReduxStateHolder {
var mainStore: Store<AppState>? = null
var tangemSdkManager: TangemSdkManager? = null
var exchangeService: ExchangeService? = null
fun getActualCard(): CardDTO? {
return scanResponse?.card
}
var buyService: ExchangeService? = null
var sellService: ExchangeService? = null
override fun dispatch(action: Action) {
mainStore?.dispatch(action)

View file

@ -27,7 +27,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
override val stack: Value<ChildStack<AppRoute, Child>> = childStack(
source = navigationProvider.getOrCreateTyped(),
serializer = AppRoute.serializer(),
serializer = AppRoute.serializer(), // TODO Maybe set this to null for AppRoute.Onboarding case. Need to check
initialConfiguration = getInitialRoute(),
handleBackButton = false,
childFactory = ::child,

View file

@ -11,10 +11,11 @@ import com.tangem.features.disclaimer.api.components.DisclaimerComponent
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.managetokens.component.ManageTokensSource
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.features.onramp.component.BuyCryptoComponent
import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent
import com.tangem.features.onramp.component.BuyCryptoComponent
import com.tangem.features.onramp.component.OnrampComponent
import com.tangem.features.onramp.component.SellCryptoComponent
import com.tangem.features.onramp.component.SwapSelectTokensComponent
import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.staking.api.navigation.StakingRouter
@ -53,6 +54,7 @@ internal class ChildFactory @Inject constructor(
private val onrampComponentFactory: OnrampComponent.Factory,
private val buyCryptoComponentFactory: BuyCryptoComponent.Factory,
private val sellCryptoComponentFactory: SellCryptoComponent.Factory,
private val swapSelectTokensComponentFactory: SwapSelectTokensComponent.Factory,
private val onboardingEntryComponentFactory: OnboardingEntryComponent.Factory,
private val sendRouter: SendRouter,
private val tokenDetailsRouter: TokenDetailsRouter,
@ -197,7 +199,7 @@ internal class ChildFactory @Inject constructor(
is AppRoute.Onramp -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = OnrampComponent.Params(),
params = OnrampComponent.Params(route.currency.name),
componentFactory = onrampComponentFactory,
)
}
@ -215,10 +217,20 @@ internal class ChildFactory @Inject constructor(
componentFactory = sellCryptoComponentFactory,
)
}
is AppRoute.SwapCrypto -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = SwapSelectTokensComponent.Params(userWalletId = route.userWalletId),
componentFactory = swapSelectTokensComponentFactory,
)
}
is AppRoute.Onboarding -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = OnboardingEntryComponent.Params(route.scanResponse),
params = OnboardingEntryComponent.Params(
scanResponse = route.scanResponse,
startBackupFlow = route.startFromBackup,
),
componentFactory = onboardingEntryComponentFactory,
)
}

View file

@ -219,16 +219,24 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
data class Swap(
val currency: CryptoCurrency,
val currencyFrom: CryptoCurrency,
val currencyTo: CryptoCurrency? = null,
val userWalletId: UserWalletId,
val isInitialReverseOrder: Boolean = false,
) : AppRoute(path = "/swap/${currency.id.value}/${userWalletId.stringValue}/$isInitialReverseOrder"),
) : AppRoute(
path = "/swap" +
"/${currencyFrom.id.value}" +
"/${currencyTo?.id?.value}" +
"/${userWalletId.stringValue}" +
"/$isInitialReverseOrder",
),
RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer())
companion object {
const val CURRENCY_BUNDLE_KEY = "currency"
const val CURRENCY_FROM_KEY = "currencyFrom"
const val CURRENCY_TO_KEY = "currencyTo"
const val USER_WALLET_ID_KEY = "userWalletId"
const val IS_INITIAL_REVERSE_ORDER = "isInitialReverseOrder"
}
@ -287,7 +295,9 @@ sealed class AppRoute(val path: String) : Route {
}
@Serializable
data object Onramp : AppRoute(path = "/onramp")
data class Onramp(val currency: CryptoCurrency) : AppRoute(path = "/onramp/${currency.symbol}"), RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer())
}
@Serializable
data class BuyCrypto(
@ -299,6 +309,14 @@ sealed class AppRoute(val path: String) : Route {
val userWalletId: UserWalletId,
) : AppRoute(path = "/sell_crypto/${userWalletId.stringValue}")
@Serializable
data class SwapCrypto(
val userWalletId: UserWalletId,
) : AppRoute(path = "/swap_crypto/${userWalletId.stringValue}")
// Onboarding V2
data class Onboarding(val scanResponse: ScanResponse) : AppRoute(path = "/onboarding_v2")
data class Onboarding(
val scanResponse: ScanResponse,
val startFromBackup: Boolean,
) : AppRoute(path = "/onboarding_v2${if (startFromBackup) "/backup" else ""}")
}

View file

@ -5,6 +5,7 @@ import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIco
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
@ -39,8 +40,8 @@ class TokenItemStateConverter(
private val fiatAmountStateProvider: (CryptoCurrencyStatus) -> TokenItemState.FiatAmountState? = {
createFiatAmountState(it, appCurrency)
},
private val onItemClick: (CryptoCurrencyStatus) -> Unit,
private val onItemLongClick: ((CryptoCurrencyStatus) -> Unit)? = null,
private val onItemClick: ((TokenItemState, CryptoCurrencyStatus) -> Unit)? = null,
private val onItemLongClick: ((TokenItemState, CryptoCurrencyStatus) -> Unit)? = null,
) : Converter<CryptoCurrencyStatus, TokenItemState> {
override fun convert(value: CryptoCurrencyStatus): TokenItemState {
@ -75,9 +76,11 @@ class TokenItemStateConverter(
subtitleState = requireNotNull(subtitleStateProvider(this)),
fiatAmountState = requireNotNull(fiatAmountStateProvider(this)),
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = getFormattedAmount()),
onItemClick = { onItemClick(this) },
onItemLongClick = onItemLongClick?.let {
{ it(this) }
onItemClick = onItemClick?.let { onItemClick ->
{ onItemClick(it, this) }
},
onItemLongClick = onItemLongClick?.let { onItemLongClick ->
{ onItemLongClick(it, this) }
},
)
}
@ -94,9 +97,11 @@ class TokenItemStateConverter(
iconState = iconStateProvider(this),
titleState = titleStateProvider(this),
subtitleState = subtitleStateProvider(this),
onItemClick = { onItemClick(this) },
onItemLongClick = onItemLongClick?.let {
{ it(this) }
onItemClick = onItemClick?.let { onItemClick ->
{ onItemClick(it, this) }
},
onItemLongClick = onItemLongClick?.let { onItemLongClick ->
{ onItemLongClick(it, this) }
},
)
}
@ -107,8 +112,8 @@ class TokenItemStateConverter(
iconState = iconStateProvider(this),
titleState = titleStateProvider(this),
subtitleState = subtitleStateProvider(this),
onItemLongClick = onItemLongClick?.let {
{ it(this) }
onItemLongClick = onItemLongClick?.let { onItemLongClick ->
{ onItemLongClick(it, this) }
},
)
}
@ -122,7 +127,7 @@ class TokenItemStateConverter(
is CryptoCurrencyStatus.Unreachable,
is CryptoCurrencyStatus.NoAmount,
-> {
TokenItemState.TitleState.Content(text = currencyStatus.currency.name)
TokenItemState.TitleState.Content(text = stringReference(currencyStatus.currency.name))
}
is CryptoCurrencyStatus.Loaded,
is CryptoCurrencyStatus.Custom,
@ -130,7 +135,7 @@ class TokenItemStateConverter(
is CryptoCurrencyStatus.NoAccount,
-> {
TokenItemState.TitleState.Content(
text = currencyStatus.currency.name,
text = stringReference(currencyStatus.currency.name),
hasPending = value.hasCurrentNetworkTransactions,
)
}

View file

@ -21,6 +21,7 @@ dependencies {
implementation(projects.core.utils)
implementation(projects.libs.auth)
implementation(projects.domain.appTheme.models)
implementation(projects.domain.core)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.balanceHiding.models)

View file

@ -1,178 +0,0 @@
package com.tangem.datasource.api.onramp
import com.tangem.datasource.api.common.response.ApiResponse
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.OnrampDataResponse
import com.tangem.datasource.api.onramp.models.response.OnrampQuoteResponse
import com.tangem.datasource.api.onramp.models.response.OnrampStatusResponse
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
internal class MockedOnrampApi : OnrampApi {
override suspend fun getCurrencies(): ApiResponse<List<OnrampCurrencyDTO>> = ApiResponse.Success(
COUNTRIES.map(OnrampCountryDTO::defaultCurrency),
)
override suspend fun getCountries(): ApiResponse<List<OnrampCountryDTO>> = ApiResponse.Success(COUNTRIES + RUSSIA)
override suspend fun getCountryByIp(): ApiResponse<OnrampCountryDTO> = ApiResponse.Success(RUSSIA)
override suspend fun getPaymentMethods(): ApiResponse<List<PaymentMethodDTO>> = ApiResponse.Success(
listOf(
PaymentMethodDTO(id = "google", name = "Google Play", image = ""),
PaymentMethodDTO(id = "apple", name = "Apple Pay", image = ""),
PaymentMethodDTO(id = "card", name = "Card", image = ""),
),
)
override suspend fun getPairs(body: OnrampPairsRequest): ApiResponse<List<OnrampPairDTO>> = ApiResponse.Success(
listOf(
OnrampPairDTO(
fromCurrencyCode = "USD",
to = OnrampDestinationDTO(contractAddress = "0xcontract_address", network = "ethereum"),
providers = listOf(),
),
),
)
override suspend fun getQuote(
fromCurrencyCode: String,
toContractAddress: String,
toNetwork: String,
paymentMethod: String,
countryCode: String,
fromAmount: String,
toDecimals: Int,
providerId: String,
): ApiResponse<OnrampQuoteResponse> {
TODO("Not yet implemented")
}
override suspend fun getData(
fromCurrencyCode: String,
toContractAddress: String,
toNetwork: String,
paymentMethod: String,
countryCode: String,
fromAmount: String,
toDecimals: Int,
providerId: String,
toAddress: String,
redirectUrl: String,
language: String?,
theme: String?,
requestId: String,
): ApiResponse<OnrampDataResponse> {
TODO("Not yet implemented")
}
override suspend fun getStatus(txId: String): ApiResponse<OnrampStatusResponse> {
TODO("Not yet implemented")
}
private companion object {
private val RUSSIA = OnrampCountryDTO(
name = "Russia",
code = "RU",
image = "https://hatscripts.github.io/circle-flags/flags/ru.svg",
alpha3 = "RUS",
continent = "",
defaultCurrency = OnrampCurrencyDTO(
name = "Russian ruble",
code = "RUB",
image = "https://hatscripts.github.io/circle-flags/flags/ru.svg",
precision = 2,
),
onrampAvailable = false,
)
private val COUNTRIES = listOf(
OnrampCountryDTO(
name = "United States of America",
code = "USA",
image = "https://hatscripts.github.io/circle-flags/flags/us.svg",
alpha3 = "USA",
continent = "",
defaultCurrency = OnrampCurrencyDTO(
name = "US Dollar",
code = "USD",
image = "https://hatscripts.github.io/circle-flags/flags/us.svg",
precision = 2,
),
onrampAvailable = true,
),
OnrampCountryDTO(
name = "Europe Union",
code = "EU",
image = "https://hatscripts.github.io/circle-flags/flags/eu.svg",
alpha3 = "EUR",
continent = "",
defaultCurrency = OnrampCurrencyDTO(
name = "Euro",
code = "EUR",
image = "https://hatscripts.github.io/circle-flags/flags/eu.svg",
precision = 2,
),
onrampAvailable = true,
),
OnrampCountryDTO(
name = "Great Britain",
code = "GB",
image = "https://hatscripts.github.io/circle-flags/flags/gb.svg",
alpha3 = "GB",
continent = "",
defaultCurrency = OnrampCurrencyDTO(
name = "British Pound Sterling",
code = "GBP",
image = "https://hatscripts.github.io/circle-flags/flags/gb.svg",
precision = 2,
),
onrampAvailable = true,
),
OnrampCountryDTO(
name = "CANADA",
code = "CA",
image = "https://hatscripts.github.io/circle-flags/flags/ca.svg",
alpha3 = "CA",
continent = "",
defaultCurrency = OnrampCurrencyDTO(
name = "Canadian Dollar",
code = "CAD",
image = "https://hatscripts.github.io/circle-flags/flags/ca.svg",
precision = 2,
),
onrampAvailable = true,
),
OnrampCountryDTO(
name = "Hon Kong",
code = "HK",
image = "https://hatscripts.github.io/circle-flags/flags/hk.svg",
alpha3 = "HK",
continent = "",
defaultCurrency = OnrampCurrencyDTO(
name = "Hon Kong Dollar",
code = "HKD",
image = "https://hatscripts.github.io/circle-flags/flags/hk.svg",
precision = 2,
),
onrampAvailable = true,
),
OnrampCountryDTO(
name = "Australia",
code = "AU",
image = "https://hatscripts.github.io/circle-flags/flags/au.svg",
alpha3 = "AU",
continent = "",
defaultCurrency = OnrampCurrencyDTO(
name = "Australian Dollar",
code = "AUD",
image = "https://hatscripts.github.io/circle-flags/flags/au.svg",
precision = 2,
),
onrampAvailable = true,
),
)
}
}

View file

@ -11,7 +11,6 @@ import com.tangem.datasource.api.common.config.managers.ProdApiConfigsManager
import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.markets.TangemTechMarketsApi
import com.tangem.datasource.api.onramp.MockedOnrampApi
import com.tangem.datasource.api.onramp.OnrampApi
import com.tangem.datasource.api.stakekit.StakeKitApi
import com.tangem.datasource.api.tangemTech.TangemTechApi
@ -99,24 +98,22 @@ internal object NetworkModule {
@Provides
@Singleton
fun provideOnrampApi(
// @NetworkMoshi moshi: Moshi,
// @ApplicationContext context: Context,
// apiConfigsManager: ApiConfigsManager,
// appLogsStore: AppLogsStore,
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
apiConfigsManager: ApiConfigsManager,
appLogsStore: AppLogsStore,
): OnrampApi {
// TODO: Remove when backend will be ready - [REDACTED_TASK_KEY]
return MockedOnrampApi()
// return createApi(
// id = ApiConfig.ID.Express,
// moshi = moshi,
// context = context,
// apiConfigsManager = apiConfigsManager,
// clientBuilder = {
// addInterceptor(
// NetworkLogsSaveInterceptor(appLogsStore),
// )
// },
// )
return createApi(
id = ApiConfig.ID.Express,
moshi = moshi,
context = context,
apiConfigsManager = apiConfigsManager,
clientBuilder = {
addInterceptor(
NetworkLogsSaveInterceptor(appLogsStore),
)
},
)
}
@Provides

View file

@ -0,0 +1,18 @@
package com.tangem.datasource.di.exchangeservice
import com.tangem.datasource.exchangeservice.swap.DefaultSwapServiceLoader
import com.tangem.datasource.exchangeservice.swap.SwapServiceLoader
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface ExchangeServiceLoaderModule {
@Binds
@Singleton
fun bindSwapServiceLoader(defaultSwapServiceLoader: DefaultSwapServiceLoader): SwapServiceLoader
}

View file

@ -0,0 +1,91 @@
package com.tangem.datasource.exchangeservice.swap
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE
import com.tangem.datasource.api.express.models.request.AssetsRequestBody
import com.tangem.datasource.api.express.models.request.LeastTokenInfo
import com.tangem.datasource.api.express.models.response.Asset
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.token.ExpressAssetsStore
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
typealias InitializationStatusFlow = MutableStateFlow<Lce<Throwable, List<Asset>>>
/**
* Default implementation of [SwapServiceLoader]
*
* @property tangemExpressApi express api
* @property expressAssetsStore local storage
*
[REDACTED_AUTHOR]
*/
internal class DefaultSwapServiceLoader @Inject constructor(
private val tangemExpressApi: TangemExpressApi,
private val expressAssetsStore: ExpressAssetsStore,
private val dispatchers: CoroutineDispatcherProvider,
) : SwapServiceLoader {
private val initializationStatuses =
MutableStateFlow<Map<UserWalletId, InitializationStatusFlow>>(value = emptyMap())
override suspend fun update(userWalletId: UserWalletId, userTokens: UserTokensResponse) {
withContext(dispatchers.io) {
val initializationStatus = getInitializationStatusInternal(userWalletId)
initializationStatus.update { lceLoading() }
try {
val tokensList = userTokens.tokens.map {
LeastTokenInfo(
contractAddress = it.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE,
network = it.networkId,
)
}
if (tokensList.isNotEmpty()) {
val response = tangemExpressApi.getAssets(
body = AssetsRequestBody(tokensList = tokensList),
).getOrThrow()
expressAssetsStore.store(userWalletId, response)
initializationStatus.update { response.lceContent() }
}
} catch (e: Throwable) {
initializationStatus.update { e.lceError() }
Timber.e(e, "Unable to fetch assets for: ${userWalletId.stringValue}")
}
}
}
override fun getInitializationStatus(userWalletId: UserWalletId): InitializationStatusFlow {
return getInitializationStatusInternal(userWalletId)
}
private fun getInitializationStatusInternal(userWalletId: UserWalletId): InitializationStatusFlow {
val initializationStatus = initializationStatuses.value.get(key = userWalletId)
if (initializationStatus != null) return initializationStatus
val default: InitializationStatusFlow = MutableStateFlow(value = lceLoading())
initializationStatuses.update {
it.toMutableMap().apply {
put(key = userWalletId, value = default)
}
}
return default
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.datasource.exchangeservice.swap
import com.tangem.datasource.api.express.models.response.Asset
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.StateFlow
/**
* Swap service loader
*
[REDACTED_AUTHOR]
*/
interface SwapServiceLoader {
/** Update service using [userWalletId] and [userTokens] */
suspend fun update(userWalletId: UserWalletId, userTokens: UserTokensResponse)
/** Get initialization status by [userWalletId] */
fun getInitializationStatus(userWalletId: UserWalletId): StateFlow<Lce<Throwable, List<Asset>>>
}

View file

@ -103,6 +103,10 @@ object PreferencesKeys {
val SHOULD_SHOW_RING_PROMO_KEY by lazy { booleanPreferencesKey(name = "shouldShowRingPromo") }
val ONRAMP_DEFAULT_CURRENCY by lazy { stringPreferencesKey(name = "onrampDefaultCurrency") }
val ONRAMP_DEFAULT_COUNTRY by lazy { stringPreferencesKey(name = "onrampDefaultCountry") }
// region Permission
fun getShouldShowPermission(permission: String) = booleanPreferencesKey("shouldShowPushPermission_$permission")

View file

@ -7,10 +7,6 @@
"name": "WC_SOLANA_TX_SIGN_ENABLED",
"version": "5.18.0"
},
{
"name": "STAKING_ENABLED",
"version": "5.15.0"
},
{
"name": "IS_ETHEREUM_EIP_1559_ENABLED",
"version": "5.17.0"
@ -25,7 +21,7 @@
},
{
"name": "MAIN_ACTION_BUTTONS_ENABLED",
"version": "undefined"
"version": "5.19.0"
},
{
"name": "ONBOARDING_CODE_REFACTORING_ENABLED",

View file

@ -6,6 +6,8 @@
<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="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>
@ -260,7 +262,7 @@
<string name="express_exchange_status_canceled">Abgebrochen</string>
<string name="express_exchange_status_confirmed">Bestätigt</string>
<string name="express_exchange_status_confirming">Bestätigen</string>
<string name="express_exchange_status_confirming_active">wird bestätigt...</string>
<string name="express_exchange_status_confirming_active">Wird bestätigt...</string>
<string name="express_exchange_status_exchanged">Getauscht</string>
<string name="express_exchange_status_exchanging">Tauschen läuft</string>
<string name="express_exchange_status_exchanging_active">Austauschen...</string>
@ -451,6 +453,7 @@
<string name="markets_token_details_price_performance">Preisleistung</string>
<string name="markets_token_details_repository">Aufbewahrungsort</string>
<string name="markets_token_details_security_score">Sicherheitsbewertung</string>
<string name="markets_token_details_security_score_description">Der Security Score eines Tokens ist eine Metrik, die das Sicherheitsniveau einer Blockchain oder eines Tokens anhand verschiedener Faktoren bewertet und aus den unten aufgeführten Quellen zusammengestellt wird.</string>
<string name="markets_token_details_social">Soziales</string>
<string name="markets_token_details_total_supply">Gesamtangebot</string>
<string name="markets_token_details_total_supply_description">Die maximale Anzahl von Coins oder Tokens, die jemals für eine bestimmte Kryptowährung existieren können</string>
@ -675,7 +678,7 @@
<string name="send_notification_high_fee_text">Aufgrund der Besonderheiten des Netzes %1$s ist die Gebühr für die Überweisung des gesamten Guthabens höher. Um die Kommission zu reduzieren, Kannst du %2$s verlassen.</string>
<string name="send_notification_high_fee_title">Die Gebühr ist höher</string>
<string name="send_notification_invalid_amount_text">Die enthaltene Kommission übersteigt den Überweisungsbetrag, was zu einem negativen Wert führt</string>
<string name="send_notification_invalid_amount_title">ungültige Menge</string>
<string name="send_notification_invalid_amount_title">Ungültige Menge</string>
<string name="send_notification_invalid_minimum_amount_text">Der Mindestbetrag für den Versand beträgt %1$s. Bitte stell sicher, dass der Restbetrag nach dem Versand nicht unter %2$s liegt.</string>
<string name="send_notification_invalid_reserve_amount_text">Das Zielkonto wurde nicht erstellt. Bitte änder den zu sendenden Betrag.</string>
<string name="send_notification_invalid_reserve_amount_title">Der zu sendende Betrag muss mindestens %s betragen</string>
@ -804,7 +807,7 @@
<string name="staking_unbonding">Lösen der Bindungen</string>
<string name="staking_unlocked_locked">Gelocktes unlocken</string>
<string name="staking_unlocking">Entsperren</string>
<string name="staking_unstake_amount_requirement_error">Der unstaking-Betrag muss mindestens %s betragen.</string>
<string name="staking_unstake_amount_requirement_error">Der unstaking-Betrag muss mindestens %s betragen</string>
<string name="staking_unstake_amount_validation_error">Der Betrag übersteigt das eingesetzte Guthaben</string>
<string name="staking_unstaked">Unstaken</string>
<string name="staking_unstaking">Staking beenden</string>
@ -884,6 +887,7 @@
<string name="transaction_history_transaction_from_address">von: %s</string>
<string name="transaction_history_transaction_to_address">zu: %s</string>
<string name="transaction_history_transaction_validator">Validierer: %s</string>
<string name="transfer_min_amount_error">Minimum %s</string>
<string name="transfer_notification_invalid_minimum_transaction_amount_text">Der Mindesttransaktionsbetrag beträgt %1$s.</string>
<string name="try_to_load_data_again_button_title">Versuche es erneut</string>
<string name="twin_error_same_card">Du hast dieselbe Karte oder Ring gescannt. Um ein Zwillings-Wallet zu erstellen, musst du die Karte oder Ring mit der Nummer %d scannen.</string>

View file

@ -6,6 +6,8 @@
<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>
@ -740,7 +742,7 @@
<string name="staking_details_warmup_period">Periodo de calentamiento</string>
<string name="staking_details_warmup_period_info">El tiempo permitido para activar la participación en la apuesta.</string>
<string name="staking_give_permission_fee_footer">La red cobrará una tarifa de aprobación de token para verificar que usted está autorizando el uso de su token para el staking.</string>
<string name="staking_legal">Al utilizar la función de staking, usted acepta %1$s y %2$s del proveedor</string>
<string name="staking_legal">Al utilizar la función de staking, usted acepta %1$s y %2$s del proveedor</string>
<string name="staking_locked">Bloqueado</string>
<string name="staking_migrate">Migrar</string>
<string name="staking_native">Native staking</string>
@ -802,7 +804,7 @@
<string name="staking_unbonding">Desunión</string>
<string name="staking_unlocked_locked">Desbloquear</string>
<string name="staking_unlocking">Desbloqueando</string>
<string name="staking_unstake_amount_requirement_error">El monto del staking debe ser al menos %s</string>
<string name="staking_unstake_amount_requirement_error">La cantidad para el staking debe ser al menos %s</string>
<string name="staking_unstake_amount_validation_error">El monto excede el saldo apostado</string>
<string name="staking_unstaked">Sin staking</string>
<string name="staking_unstaking">Unstaking</string>

View file

@ -6,6 +6,8 @@
<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>
@ -740,7 +742,7 @@
<string name="staking_details_warmup_period">Période d\'échauffement</string>
<string name="staking_details_warmup_period_info">Le temps imparti pour activer la participation au staking.</string>
<string name="staking_give_permission_fee_footer">Le réseau facturera des frais dapprobation de jeton pour vérifier que vous autorisez lutilisation de votre jeton pour le jalonnement.</string>
<string name="staking_legal">En utilisant la fonctionnalité de staking, vous acceptez les %1$s et %2$s du fournisseur</string>
<string name="staking_legal">En utilisant la fonctionnalité de staking, vous acceptez les %1$s et %2$s du fournisseur</string>
<string name="staking_locked">Bloqué</string>
<string name="staking_migrate">Migrer</string>
<string name="staking_native">Native staking</string>
@ -802,7 +804,7 @@
<string name="staking_unbonding">Dissociation</string>
<string name="staking_unlocked_locked">Débloquer</string>
<string name="staking_unlocking">Déverrouillage</string>
<string name="staking_unstake_amount_requirement_error">Le montant à staker doit être au moins %s</string>
<string name="staking_unstake_amount_requirement_error">Le montant à destaker doit être au moins %s</string>
<string name="staking_unstake_amount_validation_error">Le montant dépasse le solde misé</string>
<string name="staking_unstaked">Non-staké</string>
<string name="staking_unstaking">Unstaking</string>

View file

@ -6,6 +6,8 @@
<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>
@ -445,6 +447,7 @@
<string name="markets_token_details_price_performance">値動き</string>
<string name="markets_token_details_repository">リポジトリ</string>
<string name="markets_token_details_security_score">セキュリティ・スコア</string>
<string name="markets_token_details_security_score_description">トークンのセキュリティ・スコアとは、ブロックチェーンやトークンのセキュリティ・レベルを様々な要因に基づいて評価する指標で、以下の情報源から集計されます。</string>
<string name="markets_token_details_social">ソーシャル</string>
<string name="markets_token_details_total_supply">総供給量</string>
<string name="markets_token_details_total_supply_description">特定の暗号資産に存在しうるコインまたはトークンの最大数</string>
@ -794,7 +797,7 @@
<string name="staking_unbonding">ステーキング解約中</string>
<string name="staking_unlocked_locked">ロック解除</string>
<string name="staking_unlocking">ロック解除中</string>
<string name="staking_unstake_amount_requirement_error">ステーキング金額は %s 以上である必要があります</string>
<string name="staking_unstake_amount_requirement_error">ステーキング解除する金額は少なくとも%sである必要があります</string>
<string name="staking_unstake_amount_validation_error">金額がステーキング残高を超えています</string>
<string name="staking_unstaked">ステーキングされていない</string>
<string name="staking_unstaking">ステーキング解除</string>

View file

@ -3,6 +3,8 @@
<string name="action_buttons_swap_choose_token">Выберите токен</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>
@ -311,7 +313,7 @@
<string name="give_permission_current_transaction">Транзакция</string>
<string name="give_permission_policy_type_footer">Укажите лимит доступа к выбранному токену</string>
<string name="give_permission_rows_amount">Количество %s</string>
<string name="give_permission_staking_footer">Функция подтверждения необходима для предоставления другому адресу разрешения на использование определенного количества ваших токенов.По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту StakeKit разрешение использовать ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете осуществить стейкинг токена.</string>
<string name="give_permission_staking_footer">Функция подтверждения необходима для предоставления другому адресу разрешения на использование определенного количества ваших токенов. По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту StakeKit разрешение использовать ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете осуществить стейкинг токена.</string>
<string name="give_permission_staking_subtitle">Чтобы продолжить, вам необходимо разрешить смарт контракту Polygon использовать ваш %s</string>
<string name="give_permission_swap_subtitle" formatted="false">Чтобы продолжить, вам нужно разрешить смарт-контракту %1s использовать ваш %2s</string>
<string name="give_permission_title">Дать разрешение</string>
@ -768,6 +770,7 @@
<string name="staking_notification_earn_rewards_text_weekly">Стейкайте безопасно и начинайте получать еженедельные награды.</string>
<string name="staking_notification_earn_rewards_title">Получите награду за стейкинг</string>
<string name="staking_notification_low_staked_balance_text">Оставшийся застейканный баланс будет слишком мал для вывода. Вам потребуется застейкать больше средств, чтобы достичь минимальной суммы для вывода.</string>
<string name="staking_notification_low_staked_balance_title">Низкий баланс стейкинга</string>
<string name="staking_notification_network_error_text">Стейкинг временно недоступен из-за проблем в сети. Пожалуйста, попробуйте позже.</string>
<string name="staking_notification_new_validator_funds_transfer">Стейкинг в сети %1$s с новым валидатором автоматически переведет ваши текущие застейканные средства на него.</string>
<string name="staking_notification_restake_rewards_text">Реинвестируйте свои заработанные награды в вашу застейканную сумму, увеличивая потенциальный доход</string>
@ -800,6 +803,7 @@
<string name="staking_rewards">Вознаграждения</string>
<string name="staking_stake_locked">Стейкинг закрыт</string>
<string name="staking_stake_more">Застейкать еще</string>
<string name="staking_staked_amount">Застейканная сумма</string>
<string name="staking_summary_description_text">Вы стейкаете %1$s и будете получать награду %2$s</string>
<string name="staking_tap_to_unlock">Нажмите для разблокировки</string>
<string name="staking_tap_to_unlock_or_vote">Нажмите для разблокировки</string>
@ -933,7 +937,7 @@
<string name="wallet_connect_error_with_framework_message">Произошла непредвиденная ошибка. Сообщение ошибки: %s Попробуйте, пожалуйста, позже. Если проблема будет продолжать возникать - обратитесь в службу поддержки.</string>
<string name="wallet_connect_error_wrong_card_selected">Неверная карта или кольцо выбрана в приложении Tangem</string>
<string name="wallet_connect_failed_to_build_tx">Не удалось создать транзакцию из данных Dapp. Код: %s</string>
<string name="wallet_connect_generic_error_with_code">Произошла непредвиденная ошибка. Код ошибки: %d. Попробуйте, пожалуйста, позже. Если проблема будет продолжать возникать - обратитесь в службу поддержки.</string>
<string name="wallet_connect_generic_error_with_code">Произошла непредвиденная ошибка. Код ошибки: %d. Попробуйте, пожалуйста, позже. Если проблема будет продолжать возникать обратитесь в службу поддержки.</string>
<string name="wallet_connect_no_sessions_message">Нет открытых сессий WalletConnect</string>
<string name="wallet_connect_no_sessions_title">Упс. Нет сессий.</string>
<string name="wallet_connect_pairing_error">Не удалось создать пару WalletConnect: %1$s</string>
@ -988,7 +992,7 @@
<string name="warning_express_notification_invalid_reserve_amount_title">Сумма получения не может быть менее %s</string>
<string name="warning_express_pair_unavailable_message">Это может произойти из-за того, что провайдер временно не предоставляет обмен выбранной вами пары. Пожалуйста, подождите некоторое время и попробуйте снова. (Код %s)</string>
<string name="warning_express_pair_unavailable_title">Выбранная пара временно недоступна</string>
<string name="warning_express_refresh_required_title">Cервис временно недоступен</string>
<string name="warning_express_refresh_required_title">Сервис временно недоступен</string>
<string name="warning_express_too_maximum_amount_title">Сумма для обмена должна быть не более %s</string>
<string name="warning_express_too_minimal_amount_title">Сумма для обмена должна быть не менее %s</string>
<string name="warning_express_wrong_amount_description">Пожалуйста, измените сумму для обмена</string>

View file

@ -6,6 +6,8 @@
<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>
@ -732,7 +734,7 @@
<string name="staking_amount_tron_integer_error_unstaking">Сума зняття зі стейкінгу буде округлена до %1$s TRX через мережеві правила.</string>
<string name="staking_claim_unstaked">Зняти кошти</string>
<string name="staking_details_account_fee">Комісія за стейкінг-акаунт</string>
<string name="staking_details_account_fee_info">Стейкінг-акаунт - це спеціальний рахунок, на якому зберігаються застейкані SOL токени. Він створюється, коли ви делегуєте свої токени валідатору для участі у перевірці транзакцій та отримання винагород. За створення стейкінг-акаунту стягується невелика комісія, яка повертається після завершення стейкінгу.</string>
<string name="staking_details_account_fee_info">Стейкінг-акаунт це спеціальний рахунок, на якому зберігаються застейкані SOL токени. Він створюється, коли ви делегуєте свої токени валідатору для участі у перевірці транзакцій та отримання винагород. За створення стейкінг-акаунту стягується невелика комісія, яка повертається після завершення стейкінгу.</string>
<string name="staking_details_annual_percentage_rate">Процентна ставка</string>
<string name="staking_details_annual_percentage_rate_info">Річний відсоток, який ви можете отримати, беручи участь у стейкінгу.</string>
<string name="staking_details_apr">APR</string>
@ -818,7 +820,6 @@
<string name="staking_unbonding">Розблокування</string>
<string name="staking_unlocked_locked">Розблокувати</string>
<string name="staking_unlocking">Розблокування</string>
<string name="staking_unstake_amount_requirement_error">Сума для стейкінгу має бути не менше %s</string>
<string name="staking_unstake_amount_validation_error">Сума перевищує баланс стейкінгу</string>
<string name="staking_unstaked">Вивід зі стейкінгу</string>
<string name="staking_unstaking">Зняти зі стейкінгу</string>

View file

@ -6,6 +6,8 @@
<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_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="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>
@ -451,6 +453,7 @@
<string name="markets_token_details_price_performance">Price performance</string>
<string name="markets_token_details_repository">Repository</string>
<string name="markets_token_details_security_score">Security score</string>
<string name="markets_token_details_security_score_description">Security score of a token is a metric that assesses the security level of a blockchain or token based on various factors and is compiled from the sources listed below.</string>
<string name="markets_token_details_social">Social</string>
<string name="markets_token_details_total_supply">Total supply</string>
<string name="markets_token_details_total_supply_description">The maximum number of coins or tokens that can ever exist for a particular cryptocurrency</string>

View file

@ -4,6 +4,7 @@ import android.content.res.Configuration
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
@ -26,6 +27,7 @@ fun AppBarWithBackButton(
modifier: Modifier = Modifier,
text: String? = null,
@DrawableRes iconRes: Int? = null,
containerColor: Color = Color.Transparent,
) {
TangemTopAppBar(
modifier = modifier,
@ -34,6 +36,7 @@ fun AppBarWithBackButton(
iconRes = iconRes ?: R.drawable.ic_back_24,
onIconClicked = onBackClick,
),
containerColor = containerColor,
)
}

View file

@ -23,4 +23,5 @@ data class ActionButtonConfig(
val onLongClick: (() -> TextReference?)? = null,
val enabled: Boolean = true,
val dimContent: Boolean = false,
val isInProgress: Boolean = false,
)

View file

@ -8,8 +8,9 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
@ -77,7 +78,6 @@ fun ActionButton(
)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun Button(
config: ActionButtonConfig,
@ -89,12 +89,33 @@ private fun Button(
targetValue = if (config.enabled) color else TangemTheme.colors.button.disabled,
label = "Update background color",
)
val context = LocalContext.current
Row(
Box(
modifier = modifier
.heightIn(min = TangemTheme.dimens.size36)
.clip(shape)
.background(color = backgroundColor)
.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,
@ -149,6 +170,19 @@ private fun Button(
}
}
@Composable
private fun Loading(backgroundColor: Color, modifier: Modifier = Modifier) {
Box(
modifier = modifier.background(color = backgroundColor),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator(
modifier = Modifier.size(TangemTheme.dimens.size24),
color = TangemTheme.colors.icon.accent,
)
}
}
@Preview(group = "RoundedActionButton", showBackground = true)
@Preview(group = "RoundedActionButton", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
@ -188,5 +222,12 @@ private class ActionStateProvider : CollectionPreviewParameterProvider<ActionBut
enabled = false,
onClick = {},
),
ActionButtonConfig(
text = TextReference.Str(value = "Loading"),
iconResId = R.drawable.ic_arrow_down_24,
enabled = false,
onClick = {},
isInProgress = true,
),
),
)

View file

@ -25,6 +25,7 @@ import com.tangem.core.ui.components.token.internal.*
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.rememberHapticFeedback
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@ -145,14 +146,16 @@ private fun Modifier.tokenClickable(state: TokenItemState): Modifier = composed
is TokenItemState.Unreachable,
-> {
val onClick = state.onItemClick
val onLongClick = state.onItemLongClick?.let { rememberHapticFeedback(state = state, onAction = it) }
val onLongClick = state.onItemLongClick?.let {
rememberHapticFeedback(state = state, onAction = { it(state) })
}
when {
onClick == null && onLongClick == null -> this
onClick == null && onLongClick != null -> combinedClickable(onClick = {}, onLongClick = onLongClick)
onClick != null && onLongClick == null -> combinedClickable(onClick = onClick)
onClick != null && onLongClick == null -> combinedClickable(onClick = { onClick(state) })
onClick != null && onLongClick != null -> {
combinedClickable(onClick = onClick, onLongClick = onLongClick)
combinedClickable(onClick = { onClick(state) }, onLongClick = onLongClick)
}
else -> this
}
@ -456,7 +459,7 @@ private class TokenItemStateProvider : CollectionPreviewParameterProvider<TokenI
tokenItemVisibleState.copy(
iconState = coinIconState.copy(showCustomBadge = true),
titleState = TokenItemState.TitleState.Content(
text = "PolygonPolygonPolygonPolygonPolygonPolygon",
text = stringReference(value = "PolygonPolygonPolygonPolygonPolygonPolygon"),
hasPending = true,
),
fiatAmountState = TokenItemState.FiatAmountState.Content(
@ -473,41 +476,41 @@ private class TokenItemStateProvider : CollectionPreviewParameterProvider<TokenI
TokenItemState.Unreachable(
id = UUID.randomUUID().toString(),
iconState = tokenIconState,
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")),
onItemClick = {},
onItemLongClick = {},
),
TokenItemState.Unreachable(
id = UUID.randomUUID().toString(),
iconState = tokenIconState,
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
subtitleState = TokenItemState.SubtitleState.TextContent("Token"),
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")),
subtitleState = TokenItemState.SubtitleState.TextContent(stringReference(value = "Token")),
onItemClick = {},
onItemLongClick = {},
),
TokenItemState.NoAddress(
id = UUID.randomUUID().toString(),
iconState = tokenIconState,
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")),
onItemLongClick = {},
),
TokenItemState.NoAddress(
id = UUID.randomUUID().toString(),
iconState = tokenIconState,
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
subtitleState = TokenItemState.SubtitleState.TextContent("Token"),
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")),
subtitleState = TokenItemState.SubtitleState.TextContent(value = stringReference(value = "Token")),
onItemLongClick = {},
),
TokenItemState.Draggable(
id = UUID.randomUUID().toString(),
iconState = tokenIconState,
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")),
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "3 172,14 $"),
),
TokenItemState.Content(
id = UUID.randomUUID().toString(),
iconState = tokenIconState,
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")),
fiatAmountState = TokenItemState.FiatAmountState.Content(text = "321 $", hasStaked = false),
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "5,412 MATIC"),
subtitleState = TokenItemState.SubtitleState.CryptoPriceContent(
@ -521,7 +524,7 @@ private class TokenItemStateProvider : CollectionPreviewParameterProvider<TokenI
TokenItemState.Content(
id = UUID.randomUUID().toString(),
iconState = tokenIconState,
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")),
fiatAmountState = TokenItemState.FiatAmountState.Content(text = "321 $", hasStaked = false),
subtitle2State = TokenItemState.Subtitle2State.LabelContent(
auditLabelUM = AuditLabelUM(
@ -540,21 +543,21 @@ private class TokenItemStateProvider : CollectionPreviewParameterProvider<TokenI
TokenItemState.Loading(
id = "Loading#1",
iconState = customTokenIconState.copy(isGrayscale = true),
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")),
),
tokenItemVisibleState.copy(
titleState = TokenItemState.TitleState.Content(text = "Polygon testnet"),
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon testnet")),
iconState = tokenIconState.copy(isGrayscale = true),
),
tokenItemVisibleState.copy(
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")),
iconState = customTokenIconState.copy(
tint = TangemColorPalette.White,
background = TangemColorPalette.Black,
),
),
tokenItemVisibleState.copy(
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")),
iconState = customTokenIconState.copy(isGrayscale = true),
),
),
@ -592,7 +595,10 @@ private class TokenItemStateProvider : CollectionPreviewParameterProvider<TokenI
TokenItemState.Content(
id = UUID.randomUUID().toString(),
iconState = coinIconState,
titleState = TokenItemState.TitleState.Content(text = "Polygon", hasPending = true),
titleState = TokenItemState.TitleState.Content(
text = stringReference(value = "Polygon"),
hasPending = true,
),
fiatAmountState = TokenItemState.FiatAmountState.Content(text = "321 $", hasStaked = true),
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "5,412 MATIC"),
subtitleState = TokenItemState.SubtitleState.Unknown,

View file

@ -21,6 +21,8 @@ import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerW4
import com.tangem.core.ui.components.SpacerW6
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.utils.StringsSigns.DASH_SIGN
@ -38,7 +40,7 @@ internal fun TokenPrice(state: TokenPriceState?, modifier: Modifier = Modifier)
)
}
is TokenPriceState.TextContent -> {
PriceText(text = state.value, modifier = modifier, isAvailable = state.isAvailable)
PriceText(text = state.value.resolveReference(), modifier = modifier, isAvailable = state.isAvailable)
}
is TokenPriceState.Unknown -> {
PriceText(text = DASH_SIGN, modifier = modifier)
@ -158,7 +160,7 @@ private class TokenPriceChangeStateProvider : CollectionPreviewParameterProvider
priceChangePercent = "2.5%",
type = PriceChangeType.NEUTRAL,
),
TokenPriceState.TextContent(value = "Subtitle", isAvailable = true),
TokenPriceState.TextContent(value = stringReference(value = "Subtitle"), isAvailable = true),
TokenPriceState.Unknown,
TokenPriceState.Loading,
TokenPriceState.Locked,

View file

@ -15,6 +15,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.R
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.components.token.state.TokenItemState.TitleState as TokenTitleState
@ -46,7 +47,7 @@ private fun ContentTitle(state: TokenTitleState.Content, modifier: Modifier = Mo
* So we need to use [weight] to avoid displacement.
*/
CurrencyNameText(
name = state.text,
name = state.text.resolveReference(),
isAvailable = state.isAvailable,
modifier = Modifier.weight(weight = 1f, fill = false),
)

View file

@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.audits.AuditLabelUM
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.extensions.TextReference
/** TokenItem component state */
@Immutable
@ -31,10 +32,10 @@ sealed class TokenItemState {
abstract val subtitle2State: Subtitle2State?
/** Callback which will be called when an item is clicked */
abstract val onItemClick: (() -> Unit)?
abstract val onItemClick: ((TokenItemState) -> Unit)?
/** Callback which will be called when an item is long clicked */
abstract val onItemLongClick: (() -> Unit)?
abstract val onItemLongClick: ((TokenItemState) -> Unit)?
/**
* Loading token state
@ -52,8 +53,8 @@ sealed class TokenItemState {
) : TokenItemState() {
override val fiatAmountState: FiatAmountState = FiatAmountState.Loading
override val subtitle2State: Subtitle2State = Subtitle2State.Loading
override val onItemClick: (() -> Unit)? = null
override val onItemLongClick: (() -> Unit)? = null
override val onItemClick: ((TokenItemState) -> Unit)? = null
override val onItemLongClick: ((TokenItemState) -> Unit)? = null
}
/**
@ -67,8 +68,8 @@ sealed class TokenItemState {
override val subtitleState: SubtitleState = SubtitleState.Locked
override val fiatAmountState: FiatAmountState = FiatAmountState.Locked
override val subtitle2State: Subtitle2State = Subtitle2State.Locked
override val onItemClick: (() -> Unit)? = null
override val onItemLongClick: (() -> Unit)? = null
override val onItemClick: ((TokenItemState) -> Unit)? = null
override val onItemLongClick: ((TokenItemState) -> Unit)? = null
}
/**
@ -90,8 +91,8 @@ sealed class TokenItemState {
override val subtitleState: SubtitleState,
override val fiatAmountState: FiatAmountState,
override val subtitle2State: Subtitle2State,
override val onItemClick: (() -> Unit)?,
override val onItemLongClick: (() -> Unit)?,
override val onItemClick: ((TokenItemState) -> Unit)?,
override val onItemLongClick: ((TokenItemState) -> Unit)?,
) : TokenItemState()
/**
@ -110,8 +111,8 @@ sealed class TokenItemState {
) : TokenItemState() {
override val subtitleState: SubtitleState? = null
override val fiatAmountState: FiatAmountState? = null
override val onItemClick: (() -> Unit)? = null
override val onItemLongClick: (() -> Unit)? = null
override val onItemClick: ((TokenItemState) -> Unit)? = null
override val onItemLongClick: ((TokenItemState) -> Unit)? = null
}
/**
@ -129,8 +130,8 @@ sealed class TokenItemState {
override val iconState: CurrencyIconState,
override val titleState: TitleState,
override val subtitleState: SubtitleState? = null,
override val onItemClick: (() -> Unit)?,
override val onItemLongClick: (() -> Unit)?,
override val onItemClick: ((TokenItemState) -> Unit)?,
override val onItemLongClick: ((TokenItemState) -> Unit)?,
) : TokenItemState() {
override val fiatAmountState: FiatAmountState? = null
override val subtitle2State: Subtitle2State? = null
@ -150,18 +151,18 @@ sealed class TokenItemState {
override val iconState: CurrencyIconState,
override val titleState: TitleState,
override val subtitleState: SubtitleState? = null,
override val onItemLongClick: (() -> Unit)?,
override val onItemLongClick: ((TokenItemState) -> Unit)?,
) : TokenItemState() {
override val fiatAmountState: FiatAmountState? = null
override val subtitle2State: Subtitle2State? = null
override val onItemClick: (() -> Unit)? = null
override val onItemClick: ((TokenItemState) -> Unit)? = null
}
@Immutable
sealed class TitleState {
data class Content(
val text: String,
val text: TextReference,
val hasPending: Boolean = false,
val isAvailable: Boolean = true,
) : TitleState()
@ -180,7 +181,7 @@ sealed class TokenItemState {
val type: PriceChangeType,
) : SubtitleState()
data class TextContent(val value: String, val isAvailable: Boolean = true) : SubtitleState()
data class TextContent(val value: TextReference, val isAvailable: Boolean = true) : SubtitleState()
data object Unknown : SubtitleState()

View file

@ -6,6 +6,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.fields.SearchBar
import com.tangem.core.ui.components.token.TokenItem
import com.tangem.core.ui.components.tokenlist.internal.GroupTitleItem
import com.tangem.core.ui.components.tokenlist.internal.NetworkTitleItem
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.extensions.resolveReference
@ -25,6 +26,9 @@ fun TokenListItem(state: TokensListItemUM, isBalanceHidden: Boolean, modifier: M
is TokensListItemUM.NetworkGroupTitle -> {
NetworkTitleItem(networkName = state.name.resolveReference(), modifier = modifier)
}
is TokensListItemUM.GroupTitle -> {
GroupTitleItem(state, modifier)
}
is TokensListItemUM.Token -> {
TokenItem(
state = state.state,

View file

@ -0,0 +1,31 @@
package com.tangem.core.ui.components.tokenlist.internal
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.rows.NetworkTitle
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
/**
* Group title item
*
* @param state state
* @param modifier modifier
*
[REDACTED_AUTHOR]
*/
@Composable
fun GroupTitleItem(state: TokensListItemUM.GroupTitle, modifier: Modifier = Modifier) {
NetworkTitle(
title = {
Text(
text = state.text.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
},
modifier = modifier,
)
}

View file

@ -4,7 +4,6 @@ import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -78,9 +77,7 @@ private fun BaseNetworkTitleItem(
@Composable
private fun DraggableIcon(reorderableTokenListState: ReorderableLazyListState) {
Box(
modifier = Modifier
.size(TangemTheme.dimens.size32)
.detectReorder(reorderableTokenListState),
modifier = Modifier.detectReorder(reorderableTokenListState),
contentAlignment = Alignment.Center,
) {
Icon(

View file

@ -29,8 +29,17 @@ sealed interface TokensListItemUM {
* @property id id
* @property name network group name
*/
@Deprecated("Use GroupTitle instead") // TODO: [REDACTED_JIRA]
data class NetworkGroupTitle(override val id: Int, val name: TextReference) : TokensListItemUM
/**
* Group title
*
* @property id id
* @property text title value
*/
data class GroupTitle(override val id: Any, val text: TextReference) : TokensListItemUM
/**
* Token item
*

View file

@ -82,6 +82,7 @@ fun getActiveIconRes(blockchainId: String): Int {
"casper", "casper/test" -> R.drawable.img_casper_22
"xodex" -> R.drawable.img_xodex_22
"canxium" -> R.drawable.img_canxium_22
"chiliz", "chiliz/test" -> R.drawable.img_chiliz_22
else -> R.drawable.ic_alert_24
}
}
@ -162,6 +163,7 @@ fun getActiveIconResByCoinId(coinId: String): Int {
"casper-network" -> R.drawable.img_casper_22
"xodex" -> R.drawable.img_xodex_22
"canxium" -> R.drawable.img_canxium_22
"chiliz", "chiliz/test" -> R.drawable.img_chiliz_22
else -> R.drawable.ic_alert_24
}
}
@ -245,6 +247,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int {
"casper", "casper/test" -> R.drawable.ic_casper_22
"xodex" -> R.drawable.ic_xodex_22
"canxium" -> R.drawable.ic_canxium_22
"chiliz", "chiliz/test" -> R.drawable.ic_chiliz_22
else -> R.drawable.ic_alert_24
}
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<path
android:fillColor="#000000"
android:pathData="M9.807,4.515L7.378,6.014C7.33,6.043 7.266,6.029 7.237,5.981L7.094,5.749C6.911,5.454 7.003,5.066 7.298,4.884L8.245,4.304C8.406,4.204 8.432,3.98 8.297,3.846L7.505,3.114C7.257,2.869 7.256,2.47 7.502,2.223L7.694,2.029C7.733,1.99 7.798,1.99 7.838,2.029L9.866,3.991C10.02,4.144 10.02,4.394 9.807,4.515ZM9.09,11.632C9.686,12.288 9.921,13.194 9.722,14.056L9.724,14.055C9.724,14.055 9.607,15.192 8.941,17.83C8.755,18.564 9.472,19.211 10.18,18.942C15,17.111 14.995,12.218 14.995,12.218C15.138,9.067 11.943,6.016 11.025,5.169C10.884,5.04 10.672,5.021 10.51,5.124L8,6.728C6.913,7.423 6.702,8.924 7.555,9.893L9.09,11.632ZM8.589,7.638C9.223,7.236 9.967,6.764 10.351,6.522V6.523C10.468,6.449 10.621,6.463 10.721,6.558C15.663,11.206 13.294,14.712 11.604,16.171C11.206,16.514 10.602,16.156 10.718,15.644L10.988,14.465C11.296,13.133 10.931,11.737 10.012,10.726L8.408,8.872C8.079,8.491 8.165,7.908 8.589,7.638Z" />
</vector>

View file

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<group>
<clip-path
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"/>
<path
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"
android:fillColor="#ffffff"/>
<path
android:pathData="M0,0h22v22h-22z"
android:fillColor="#FF1256"/>
<path
android:pathData="M9.807,4.515L7.378,6.014C7.33,6.043 7.266,6.029 7.237,5.981L7.094,5.749C6.911,5.454 7.003,5.066 7.298,4.884L8.245,4.304C8.406,4.204 8.432,3.98 8.297,3.846L7.505,3.114C7.257,2.869 7.256,2.47 7.502,2.223L7.694,2.029C7.733,1.99 7.798,1.99 7.838,2.029L9.866,3.991C10.02,4.144 10.02,4.394 9.807,4.515ZM9.09,11.632C9.686,12.288 9.921,13.194 9.722,14.056L9.724,14.055C9.724,14.055 9.607,15.192 8.941,17.83C8.755,18.564 9.472,19.211 10.18,18.942C15,17.111 14.995,12.218 14.995,12.218C15.138,9.067 11.943,6.016 11.025,5.169C10.884,5.04 10.672,5.021 10.51,5.124L8,6.728C6.913,7.423 6.702,8.924 7.555,9.893L9.09,11.632ZM8.589,7.638C9.223,7.236 9.967,6.764 10.351,6.522V6.523C10.468,6.449 10.621,6.463 10.721,6.558C15.663,11.206 13.294,14.712 11.604,16.171C11.206,16.514 10.602,16.156 10.718,15.644L10.988,14.465C11.296,13.133 10.931,11.737 10.012,10.726L8.408,8.872C8.079,8.491 8.165,7.908 8.589,7.638Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
</group>
</vector>

View file

@ -33,6 +33,7 @@ dependencies {
// region Others dependencies
implementation(deps.kotlin.coroutines)
implementation(deps.jodatime)
implementation(deps.moshi)
implementation(deps.moshi.kotlin)
implementation(deps.timber)

View file

@ -5,6 +5,7 @@ import com.tangem.blockchainsdk.compatibility.applyL2Compatibility
import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.data.common.currency.getNetwork
import com.tangem.data.common.utils.retryOnError
@ -13,8 +14,10 @@ import com.tangem.data.markets.converters.*
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.markets.TangemTechMarketsApi
import com.tangem.datasource.api.markets.models.response.TokenMarketExchangesResponse
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.TangemTechApi.Companion.marketsQuoteFields
import com.tangem.datasource.local.datastore.RuntimeStateStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.markets.*
import com.tangem.domain.markets.repositories.MarketsTokenRepository
@ -27,12 +30,15 @@ import kotlinx.coroutines.withContext
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicLong
@Suppress("LongParameterList")
internal class DefaultMarketsTokenRepository(
private val marketsApi: TangemTechMarketsApi,
private val tangemTechApi: TangemTechApi,
private val userWalletsStore: UserWalletsStore,
private val dispatcherProvider: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
private val cacheRegistry: CacheRegistry,
private val tokenExchangesStore: RuntimeStateStore<List<TokenMarketExchangesResponse.Exchange>>,
) : MarketsTokenRepository {
private fun createTokenMarketsFetcher(firstBatchSize: Int, nextBatchSize: Int) = LimitOffsetBatchFetcher(
@ -266,9 +272,13 @@ internal class DefaultMarketsTokenRepository(
override suspend fun getTokenExchanges(tokenId: String): List<TokenMarketExchange> {
return withContext(dispatcherProvider.io) {
val response = marketsApi.getCoinExchanges(coinId = tokenId).getOrThrow()
cacheRegistry.invokeOnExpire(key = "coins/$tokenId/exchanges", skipCache = false) {
val response = marketsApi.getCoinExchanges(coinId = tokenId).getOrThrow()
TokenMarketExchangeConverter.convertList(input = response.exchanges)
tokenExchangesStore.store(value = response.exchanges)
}
TokenMarketExchangeConverter.convertList(input = tokenExchangesStore.get().value)
}
}

View file

@ -1,9 +1,11 @@
package com.tangem.data.markets.di
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.markets.DefaultMarketsTokenRepository
import com.tangem.datasource.api.markets.TangemTechMarketsApi
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.datastore.RuntimeStateStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.markets.repositories.MarketsTokenRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -25,6 +27,7 @@ internal object MarketsDataModule {
userWalletsStore: UserWalletsStore,
dispatchers: CoroutineDispatcherProvider,
analyticsEventHandler: AnalyticsEventHandler,
cacheRegistry: CacheRegistry,
): MarketsTokenRepository {
return DefaultMarketsTokenRepository(
marketsApi = marketsApi,
@ -32,6 +35,8 @@ internal object MarketsDataModule {
dispatcherProvider = dispatchers,
userWalletsStore = userWalletsStore,
analyticsEventHandler = analyticsEventHandler,
cacheRegistry = cacheRegistry,
tokenExchangesStore = RuntimeStateStore(defaultValue = emptyList()),
)
}
}

View file

@ -4,19 +4,29 @@ import com.tangem.data.onramp.converters.CountryConverter
import com.tangem.data.onramp.converters.CurrencyConverter
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.onramp.OnrampApi
import com.tangem.datasource.api.onramp.models.response.model.OnrampCountryDTO
import com.tangem.datasource.api.onramp.models.response.model.OnrampCurrencyDTO
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
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.OnrampCountry
import com.tangem.domain.onramp.model.OnrampCurrency
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
internal class DefaultOnrampRepository(
private val onrampApi: OnrampApi,
private val dispatchers: CoroutineDispatcherProvider,
private val appPreferencesStore: AppPreferencesStore,
) : OnrampRepository {
private val currencyConverter = CurrencyConverter()
private val countryConverter = CountryConverter()
private val countryConverter = CountryConverter(currencyConverter)
override suspend fun getCurrencies(): List<OnrampCurrency> = withContext(dispatchers.io) {
onrampApi.getCurrencies()
@ -29,4 +39,48 @@ internal class DefaultOnrampRepository(
.getOrThrow()
.map(countryConverter::convert)
}
override suspend fun getCountryByIp(): OnrampCountry = withContext(dispatchers.io) {
onrampApi.getCountryByIp()
.getOrThrow()
.let(countryConverter::convert)
}
override suspend fun saveDefaultCurrency(currency: OnrampCurrency) = withContext(dispatchers.io) {
appPreferencesStore.storeObject<OnrampCurrencyDTO>(
key = PreferencesKeys.ONRAMP_DEFAULT_CURRENCY,
value = currencyConverter.convertBack(currency),
)
}
override suspend fun getDefaultCurrencySync(): OnrampCurrency? = withContext(dispatchers.io) {
appPreferencesStore
.getObjectSyncOrNull<OnrampCurrencyDTO>(PreferencesKeys.ONRAMP_DEFAULT_CURRENCY)
?.let(currencyConverter::convert)
}
override fun getDefaultCurrency(): Flow<OnrampCurrency?> {
return appPreferencesStore
.getObject<OnrampCurrencyDTO>(PreferencesKeys.ONRAMP_DEFAULT_CURRENCY)
.map { it?.let(currencyConverter::convert) }
}
override suspend fun saveDefaultCountry(country: OnrampCountry) = withContext(dispatchers.io) {
appPreferencesStore.storeObject<OnrampCountryDTO>(
key = PreferencesKeys.ONRAMP_DEFAULT_COUNTRY,
value = countryConverter.convertBack(country),
)
}
override suspend fun getDefaultCountrySync(): OnrampCountry? = withContext(dispatchers.io) {
appPreferencesStore
.getObjectSyncOrNull<OnrampCountryDTO>(PreferencesKeys.ONRAMP_DEFAULT_COUNTRY)
?.let(countryConverter::convert)
}
override fun getDefaultCountry(): Flow<OnrampCountry?> {
return appPreferencesStore
.getObject<OnrampCountryDTO>(PreferencesKeys.ONRAMP_DEFAULT_COUNTRY)
.map { it?.let(countryConverter::convert) }
}
}

View file

@ -2,13 +2,14 @@ package com.tangem.data.onramp.converters
import com.tangem.datasource.api.onramp.models.response.model.OnrampCountryDTO
import com.tangem.domain.onramp.model.OnrampCountry
import com.tangem.utils.converter.Converter
import com.tangem.utils.converter.TwoWayConverter
internal class CountryConverter : Converter<OnrampCountryDTO, OnrampCountry> {
private val currencyConverter = CurrencyConverter()
internal class CountryConverter(
private val currencyConverter: CurrencyConverter,
) : TwoWayConverter<OnrampCountryDTO, OnrampCountry> {
override fun convert(value: OnrampCountryDTO) = OnrampCountry(
id = "${value.alpha3}-${value.name}",
name = value.name,
code = value.code,
image = value.image,
@ -17,4 +18,14 @@ internal class CountryConverter : Converter<OnrampCountryDTO, OnrampCountry> {
defaultCurrency = currencyConverter.convert(value.defaultCurrency),
onrampAvailable = value.onrampAvailable,
)
override fun convertBack(value: OnrampCountry): OnrampCountryDTO = OnrampCountryDTO(
name = value.name,
code = value.code,
image = value.image,
alpha3 = value.alpha3,
continent = value.continent,
defaultCurrency = currencyConverter.convertBack(value.defaultCurrency),
onrampAvailable = value.onrampAvailable,
)
}

View file

@ -2,9 +2,9 @@ package com.tangem.data.onramp.converters
import com.tangem.datasource.api.onramp.models.response.model.OnrampCurrencyDTO
import com.tangem.domain.onramp.model.OnrampCurrency
import com.tangem.utils.converter.Converter
import com.tangem.utils.converter.TwoWayConverter
internal class CurrencyConverter : Converter<OnrampCurrencyDTO, OnrampCurrency> {
internal class CurrencyConverter : TwoWayConverter<OnrampCurrencyDTO, OnrampCurrency> {
override fun convert(value: OnrampCurrencyDTO) = OnrampCurrency(
name = value.name,
@ -12,4 +12,11 @@ internal class CurrencyConverter : Converter<OnrampCurrencyDTO, OnrampCurrency>
image = value.image,
precision = value.precision,
)
override fun convertBack(value: OnrampCurrency): OnrampCurrencyDTO = OnrampCurrencyDTO(
name = value.name,
code = value.code,
image = value.image,
precision = value.precision,
)
}

View file

@ -2,6 +2,7 @@ package com.tangem.data.onramp.di
import com.tangem.data.onramp.DefaultOnrampRepository
import com.tangem.datasource.api.onramp.OnrampApi
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
@ -16,10 +17,15 @@ internal object OnrampDataModule {
@Provides
@Singleton
fun provideOnrampRepository(onrampApi: OnrampApi, dispatchers: CoroutineDispatcherProvider): OnrampRepository {
fun provideOnrampRepository(
onrampApi: OnrampApi,
dispatchers: CoroutineDispatcherProvider,
appPreferencesStore: AppPreferencesStore,
): OnrampRepository {
return DefaultOnrampRepository(
onrampApi = onrampApi,
dispatchers = dispatchers,
appPreferencesStore = appPreferencesStore,
)
}
}

View file

@ -48,7 +48,6 @@ import com.tangem.domain.tokens.model.Network
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import com.tangem.lib.crypto.BlockchainUtils.isSolana
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.orZero
@ -67,7 +66,6 @@ internal class DefaultStakingRepository(
private val stakingBalanceStore: StakingBalanceStore,
private val cacheRegistry: CacheRegistry,
private val dispatchers: CoroutineDispatcherProvider,
private val stakingFeatureToggle: StakingFeatureToggles,
private val walletManagersFacade: WalletManagersFacade,
private val getUserWalletUseCase: GetUserWalletUseCase,
moshi: Moshi,
@ -322,8 +320,6 @@ internal class DefaultStakingRepository(
cryptoCurrency: CryptoCurrency,
refresh: Boolean,
) = withContext(dispatchers.io) {
if (!stakingFeatureToggle.isStakingEnabled) return@withContext
cacheRegistry.invokeOnExpire(
key = getYieldBalancesKey(userWalletId),
skipCache = refresh,
@ -363,30 +359,26 @@ internal class DefaultStakingRepository(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): Flow<YieldBalance> = channelFlow {
if (!stakingFeatureToggle.isStakingEnabled) {
send(YieldBalance.Empty)
} else {
launch(dispatchers.io) {
val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network).orEmpty()
val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)]
?: error("Could not get integrationId")
stakingBalanceStore.get(userWalletId, address, integrationId)
.distinctUntilChanged()
.collectLatest {
if (it != null) {
send(yieldBalanceConverter.convert(it))
} else {
error("No yield balance available for currency ${cryptoCurrency.id.value}")
}
launch(dispatchers.io) {
val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network).orEmpty()
val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)]
?: error("Could not get integrationId")
stakingBalanceStore.get(userWalletId, address, integrationId)
.distinctUntilChanged()
.collectLatest {
if (it != null) {
send(yieldBalanceConverter.convert(it))
} else {
error("No yield balance available for currency ${cryptoCurrency.id.value}")
}
}
}
}
withContext(dispatchers.io) {
fetchSingleYieldBalance(
userWalletId,
cryptoCurrency,
)
}
withContext(dispatchers.io) {
fetchSingleYieldBalance(
userWalletId,
cryptoCurrency,
)
}
}.cancellable()
@ -394,21 +386,17 @@ internal class DefaultStakingRepository(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): YieldBalance = withContext(dispatchers.io) {
if (!stakingFeatureToggle.isStakingEnabled) {
YieldBalance.Empty
} else {
fetchSingleYieldBalance(userWalletId, cryptoCurrency)
fetchSingleYieldBalance(userWalletId, cryptoCurrency)
val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network).orEmpty()
val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network).orEmpty()
val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)]
?: error("Could not get integrationId")
val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)]
?: error("Could not get integrationId")
val result = stakingBalanceStore.getSyncOrNull(userWalletId, address, integrationId)
?: return@withContext YieldBalance.Error
val result = stakingBalanceStore.getSyncOrNull(userWalletId, address, integrationId)
?: return@withContext YieldBalance.Error
yieldBalanceConverter.convert(result)
}
yieldBalanceConverter.convert(result)
}
override suspend fun fetchMultiYieldBalance(
@ -416,8 +404,6 @@ internal class DefaultStakingRepository(
cryptoCurrencies: List<CryptoCurrency>,
refresh: Boolean,
) = withContext(dispatchers.io) {
if (!stakingFeatureToggle.isStakingEnabled) return@withContext
cacheRegistry.invokeOnExpire(
key = getYieldBalancesKey(userWalletId),
skipCache = refresh,
@ -474,19 +460,15 @@ internal class DefaultStakingRepository(
userWalletId: UserWalletId,
cryptoCurrencies: List<CryptoCurrency>,
): Flow<YieldBalanceList> = channelFlow {
if (!stakingFeatureToggle.isStakingEnabled) {
send(YieldBalanceList.Empty)
} else {
stakingBalanceStore.get(userWalletId)
.onEach {
val balances = yieldBalanceListConverter.convert(it)
send(balances)
}
.launchIn(scope = this + dispatchers.io)
withContext(dispatchers.io) {
fetchMultiYieldBalance(userWalletId, cryptoCurrencies, refresh = false)
stakingBalanceStore.get(userWalletId)
.onEach {
val balances = yieldBalanceListConverter.convert(it)
send(balances)
}
.launchIn(scope = this + dispatchers.io)
withContext(dispatchers.io) {
fetchMultiYieldBalance(userWalletId, cryptoCurrencies, refresh = false)
}
}
@ -494,13 +476,9 @@ internal class DefaultStakingRepository(
userWalletId: UserWalletId,
cryptoCurrencies: List<CryptoCurrency>,
): YieldBalanceList = withContext(dispatchers.io) {
if (!stakingFeatureToggle.isStakingEnabled) {
YieldBalanceList.Empty
} else {
fetchMultiYieldBalance(userWalletId, cryptoCurrencies)
val result = stakingBalanceStore.getSyncOrNull(userWalletId) ?: return@withContext YieldBalanceList.Error
yieldBalanceListConverter.convert(result)
}
fetchMultiYieldBalance(userWalletId, cryptoCurrencies)
val result = stakingBalanceStore.getSyncOrNull(userWalletId) ?: return@withContext YieldBalanceList.Error
yieldBalanceListConverter.convert(result)
}
override suspend fun isAnyTokenStaked(userWalletId: UserWalletId): Boolean {

View file

@ -18,7 +18,6 @@ import com.tangem.datasource.local.token.StakingYieldsStore
import com.tangem.domain.staking.repositories.*
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -38,7 +37,6 @@ internal object StakingDataModule {
stakingBalanceStore: StakingBalanceStore,
cacheRegistry: CacheRegistry,
dispatchers: CoroutineDispatcherProvider,
stakingFeatureToggle: StakingFeatureToggles,
walletManagersFacade: WalletManagersFacade,
getUserWalletUseCase: GetUserWalletUseCase,
@NetworkMoshi moshi: Moshi,
@ -49,7 +47,6 @@ internal object StakingDataModule {
stakingBalanceStore = stakingBalanceStore,
cacheRegistry = cacheRegistry,
dispatchers = dispatchers,
stakingFeatureToggle = stakingFeatureToggle,
walletManagersFacade = walletManagersFacade,
getUserWalletUseCase = getUserWalletUseCase,
moshi = moshi,

View file

@ -2,12 +2,11 @@ package com.tangem.data.tokens.di
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.tokens.repository.*
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.exchangeservice.swap.SwapServiceLoader
import com.tangem.datasource.local.network.NetworksStatusesStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.quote.QuotesStore
import com.tangem.datasource.local.token.ExpressAssetsStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.tokens.repository.*
import com.tangem.domain.walletmanager.WalletManagersFacade
@ -26,23 +25,21 @@ internal object TokensDataModule {
@Singleton
fun provideCurrenciesRepository(
tangemTechApi: TangemTechApi,
tangemExpressApi: TangemExpressApi,
appPreferencesStore: AppPreferencesStore,
userWalletsStore: UserWalletsStore,
walletManagersFacade: WalletManagersFacade,
expressAssetsStore: ExpressAssetsStore,
cacheRegistry: CacheRegistry,
dispatchers: CoroutineDispatcherProvider,
swapServiceLoader: SwapServiceLoader,
): CurrenciesRepository {
return DefaultCurrenciesRepository(
tangemTechApi = tangemTechApi,
tangemExpressApi = tangemExpressApi,
appPreferencesStore = appPreferencesStore,
walletManagersFacade = walletManagersFacade,
userWalletsStore = userWalletsStore,
expressAssetsStore = expressAssetsStore,
walletManagersFacade = walletManagersFacade,
cacheRegistry = cacheRegistry,
appPreferencesStore = appPreferencesStore,
dispatchers = dispatchers,
swapServiceLoader = swapServiceLoader,
)
}
@ -84,15 +81,6 @@ internal object TokensDataModule {
)
}
@Provides
@Singleton
fun provideDefaultMarketCoinsRepository(
expressAssetsStore: ExpressAssetsStore,
coroutineDispatcherProvider: CoroutineDispatcherProvider,
): MarketCryptoCurrencyRepository {
return DefaultMarketCryptoCurrencyRepository(expressAssetsStore, coroutineDispatcherProvider)
}
@Provides
@Singleton
fun provideCurrencyChecksRepository(

View file

@ -12,18 +12,14 @@ import com.tangem.data.tokens.utils.CustomTokensMerger
import com.tangem.data.tokens.utils.UserTokensBackwardCompatibility
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE
import com.tangem.datasource.api.express.models.request.AssetsRequestBody
import com.tangem.datasource.api.express.models.request.LeastTokenInfo
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.exchangeservice.swap.SwapServiceLoader
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
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.datasource.local.token.ExpressAssetsStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.core.error.DataError
import com.tangem.domain.demo.DemoConfig
@ -46,12 +42,11 @@ import com.tangem.blockchain.common.FeePaidCurrency as FeePaidSdkCurrency
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions")
internal class DefaultCurrenciesRepository(
private val tangemTechApi: TangemTechApi,
private val tangemExpressApi: TangemExpressApi,
private val userWalletsStore: UserWalletsStore,
private val walletManagersFacade: WalletManagersFacade,
private val expressAssetsStore: ExpressAssetsStore,
private val cacheRegistry: CacheRegistry,
private val appPreferencesStore: AppPreferencesStore,
private val swapServiceLoader: SwapServiceLoader,
private val dispatchers: CoroutineDispatcherProvider,
) : CurrenciesRepository {
@ -565,27 +560,7 @@ internal class DefaultCurrenciesRepository(
userWalletId: UserWalletId,
userTokens: UserTokensResponse,
) {
try {
val tokensList = userTokens.tokens
.map {
LeastTokenInfo(
contractAddress = it.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE,
network = it.networkId,
)
}
if (tokensList.isNotEmpty()) {
val response = tangemExpressApi.getAssets(
AssetsRequestBody(
tokensList = tokensList,
),
)
expressAssetsStore.store(userWalletId, response.getOrThrow())
}
} catch (e: Throwable) {
Timber.e(e, "Unable to fetch assets for: ${userWalletId.stringValue}")
}
swapServiceLoader.update(userWalletId, userTokens)
}
private suspend fun handleFetchTokensError(userWallet: UserWallet, e: ApiResponseError): UserTokensResponse {

View file

@ -1,33 +0,0 @@
package com.tangem.data.tokens.repository
import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE
import com.tangem.datasource.local.token.ExpressAssetsStore
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
class DefaultMarketCryptoCurrencyRepository(
private val expressAssetsStore: ExpressAssetsStore,
private val dispatchers: CoroutineDispatcherProvider,
) : MarketCryptoCurrencyRepository {
override suspend fun isExchangeable(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean {
return getExchangeableFlag(userWalletId, cryptoCurrency) && !cryptoCurrency.isCustom
}
private suspend fun getExchangeableFlag(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean {
return withContext(dispatchers.io) {
val contractAddress =
(cryptoCurrency as? CryptoCurrency.Token)?.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE
val asset = expressAssetsStore.getSyncOrNull(userWalletId)?.find {
it.network == cryptoCurrency.network.backendId &&
it.contractAddress.equals(contractAddress, ignoreCase = true)
}
asset?.exchangeAvailable ?: false
}
}
}

View file

@ -14,6 +14,7 @@ dependencies {
implementation(projects.common)
implementation(projects.libs.auth)
implementation(projects.libs.blockchainSdk)
implementation(projects.domain.core)
implementation(projects.domain.demo)
implementation(projects.domain.models)
implementation(projects.domain.tokens.models)

View file

@ -1,7 +1,10 @@
package com.tangem.domain.exchange
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
/**
* Manager that holds info about available actions as Sell and Buy
@ -11,4 +14,16 @@ interface RampStateManager {
fun availableForBuy(scanResponse: ScanResponse, cryptoCurrency: CryptoCurrency): Boolean
fun availableForSell(cryptoCurrency: CryptoCurrency): Boolean
suspend fun availableForSwap(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean
suspend fun fetchBuyServiceData()
fun getBuyInitializationStatus(): Flow<Lce<Throwable, Any>>
suspend fun fetchSellServiceData()
fun getSellInitializationStatus(): Flow<Lce<Throwable, Any>>
fun getSwapInitializationStatus(userWalletId: UserWalletId): Flow<Lce<Throwable, Any>>
}

View file

@ -0,0 +1,8 @@
package com.tangem.domain.onramp.model
sealed interface OnrampAvailability {
data object Available : OnrampAvailability
data class NotSupported(val country: OnrampCountry) : OnrampAvailability
data class ConfirmResidency(val country: OnrampCountry) : OnrampAvailability
}

View file

@ -1,6 +1,10 @@
package com.tangem.domain.onramp.model
import kotlinx.serialization.Serializable
@Serializable
data class OnrampCountry(
val id: String,
val name: String,
val code: String,
val image: String,

View file

@ -0,0 +1,3 @@
package com.tangem.domain.onramp.model
data class OnrampCurrencies(val populars: List<OnrampCurrency>, val others: List<OnrampCurrency>)

View file

@ -1,5 +1,8 @@
package com.tangem.domain.onramp.model
import kotlinx.serialization.Serializable
@Serializable
data class OnrampCurrency(
val name: String,
val code: String,

View file

@ -0,0 +1,24 @@
package com.tangem.domain.onramp
import arrow.core.Either
import com.tangem.domain.onramp.model.OnrampAvailability
import com.tangem.domain.onramp.repositories.OnrampRepository
class CheckOnrampAvailabilityUseCase(private val repository: OnrampRepository) {
suspend operator fun invoke(): Either<Throwable, OnrampAvailability> {
return Either.catch {
return@catch OnrampAvailability.Available
repository.getDefaultCountrySync()?.let { savedCountry ->
return@catch if (savedCountry.onrampAvailable) {
OnrampAvailability.Available
} else {
OnrampAvailability.NotSupported(savedCountry)
}
}
val detectedCountry = repository.getCountryByIp()
OnrampAvailability.ConfirmResidency(detectedCountry)
}
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.domain.onramp
import arrow.core.Either
import com.tangem.domain.onramp.model.OnrampCountry
import com.tangem.domain.onramp.repositories.OnrampRepository
class GetOnrampCountriesUseCase(private val onrampRepository: OnrampRepository) {
suspend operator fun invoke(): Either<Throwable, List<OnrampCountry>> {
return Either.catch { onrampRepository.getCountries() }
}
}

View file

@ -0,0 +1,23 @@
package com.tangem.domain.onramp
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.domain.onramp.model.OnrampCountry
import com.tangem.domain.onramp.repositories.OnrampRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
class GetOnrampCountryUseCase(private val repository: OnrampRepository) {
operator fun invoke(): Flow<Either<Throwable, OnrampCountry?>> {
return repository.getDefaultCountry()
.map<OnrampCountry?, Either<Throwable, OnrampCountry?>> { it.right() }
.catch { emit(it.left()) }
}
suspend fun invokeSync(): Either<Throwable, OnrampCountry> {
return Either.catch { repository.getDefaultCountrySync() ?: repository.getCountryByIp() }
}
}

View file

@ -0,0 +1,23 @@
package com.tangem.domain.onramp
import arrow.core.Either
import com.tangem.domain.onramp.model.OnrampCurrencies
import com.tangem.domain.onramp.repositories.OnrampRepository
class GetOnrampCurrenciesUseCase(
private val onrampRepository: OnrampRepository,
) {
suspend operator fun invoke(): Either<Throwable, OnrampCurrencies> {
return Either.catch {
val currenciesList = onrampRepository.getCurrencies()
val (populars, others) = currenciesList.toSet()
.partition { popularFiatCodes.contains(it.code.uppercase()) }
OnrampCurrencies(populars = populars, others = others)
}
}
private companion object {
val popularFiatCodes = setOf("USD", "EUR", "GBP", "CAD", "AUD", "HKD")
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.domain.onramp
import com.tangem.domain.onramp.model.OnrampCountry
import com.tangem.domain.onramp.repositories.OnrampRepository
class OnrampSaveDefaultCountryUseCase(private val repository: OnrampRepository) {
suspend operator fun invoke(country: OnrampCountry) {
repository.saveDefaultCountry(country)
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.domain.onramp
import com.tangem.domain.onramp.model.OnrampCurrency
import com.tangem.domain.onramp.repositories.OnrampRepository
class OnrampSaveDefaultCurrencyUseCase(private val repository: OnrampRepository) {
suspend operator fun invoke(currency: OnrampCurrency) {
repository.saveDefaultCurrency(currency)
}
}

View file

@ -2,8 +2,16 @@ package com.tangem.domain.onramp.repositories
import com.tangem.domain.onramp.model.OnrampCountry
import com.tangem.domain.onramp.model.OnrampCurrency
import kotlinx.coroutines.flow.Flow
interface OnrampRepository {
suspend fun getCurrencies(): List<OnrampCurrency>
suspend fun getCountries(): List<OnrampCountry>
suspend fun getCountryByIp(): OnrampCountry
suspend fun saveDefaultCurrency(currency: OnrampCurrency)
suspend fun getDefaultCurrencySync(): OnrampCurrency?
fun getDefaultCurrency(): Flow<OnrampCurrency?>
suspend fun saveDefaultCountry(country: OnrampCountry)
suspend fun getDefaultCountrySync(): OnrampCountry?
fun getDefaultCountry(): Flow<OnrampCountry?>
}

View file

@ -7,12 +7,10 @@ import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.model.*
import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.isNullOrZero
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -27,12 +25,10 @@ import kotlinx.coroutines.flow.*
class GetCryptoCurrencyActionsUseCase(
private val rampManager: RampStateManager,
private val walletManagersFacade: WalletManagersFacade,
private val marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository,
private val currenciesRepository: CurrenciesRepository,
private val quotesRepository: QuotesRepository,
private val networksRepository: NetworksRepository,
private val stakingRepository: StakingRepository,
private val stakingFeatureToggles: StakingFeatureToggles,
private val dispatchers: CoroutineDispatcherProvider,
) {
@ -134,28 +130,26 @@ class GetCryptoCurrencyActionsUseCase(
}
// staking
if (stakingFeatureToggles.isStakingEnabled) {
if (isStakingAvailable(userWallet, cryptoCurrency)) {
val yield = kotlin.runCatching {
stakingRepository.getYield(
cryptoCurrencyId = cryptoCurrency.id,
symbol = cryptoCurrency.symbol,
)
}.getOrNull()
activeList.add(
TokenActionsState.ActionState.Stake(
unavailabilityReason = ScenarioUnavailabilityReason.None,
yield = yield,
),
if (isStakingAvailable(userWallet, cryptoCurrency)) {
val yield = kotlin.runCatching {
stakingRepository.getYield(
cryptoCurrencyId = cryptoCurrency.id,
symbol = cryptoCurrency.symbol,
)
} else {
disabledList.add(
TokenActionsState.ActionState.Stake(
unavailabilityReason = ScenarioUnavailabilityReason.StakingUnavailable(cryptoCurrency.name),
yield = null,
),
)
}
}.getOrNull()
activeList.add(
TokenActionsState.ActionState.Stake(
unavailabilityReason = ScenarioUnavailabilityReason.None,
yield = yield,
),
)
} else {
disabledList.add(
TokenActionsState.ActionState.Stake(
unavailabilityReason = ScenarioUnavailabilityReason.StakingUnavailable(cryptoCurrency.name),
yield = null,
),
)
}
// send
@ -172,7 +166,7 @@ class GetCryptoCurrencyActionsUseCase(
// swap
if (userWallet.isMultiCurrency) {
if (
marketCryptoCurrencyRepository.isExchangeable(userWallet.walletId, cryptoCurrency) &&
rampManager.availableForSwap(userWallet.walletId, cryptoCurrency) &&
cryptoCurrencyStatus.value !is CryptoCurrencyStatus.NoQuote
) {
activeList.add(TokenActionsState.ActionState.Swap(ScenarioUnavailabilityReason.None))
@ -271,9 +265,7 @@ class GetCryptoCurrencyActionsUseCase(
}
actionsList.add(TokenActionsState.ActionState.Receive(scenario))
}
if (stakingFeatureToggles.isStakingEnabled) {
actionsList.add(TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.Unreachable, null))
}
actionsList.add(TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.Unreachable, null))
actionsList.add(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None))
return actionsList

View file

@ -1,5 +1,6 @@
package com.tangem.domain.tokens
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.model.*
@ -26,7 +27,7 @@ class GetCurrencyWarningsUseCase(
private val quotesRepository: QuotesRepository,
private val networksRepository: NetworksRepository,
private val swapRepository: SwapRepository,
private val marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository,
private val rampStateManager: RampStateManager,
private val stakingRepository: StakingRepository,
private val promoRepository: PromoRepository,
private val showSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase,
@ -90,10 +91,10 @@ class GetCurrencyWarningsUseCase(
val cryptoStatuses = operations.getCurrenciesStatusesSync()
val promoBanner = promoRepository.getOkxPromoBanner()
return combine(
showSwapPromoTokenUseCase()
flow = showSwapPromoTokenUseCase()
.conflate()
.distinctUntilChanged(),
flowOf(marketCryptoCurrencyRepository.isExchangeable(userWalletId, currency))
flow2 = flowOf(rampStateManager.availableForSwap(userWalletId, currency))
.conflate()
.distinctUntilChanged(),
) { shouldShowSwapPromo, isExchangeable ->

View file

@ -1,12 +0,0 @@
package com.tangem.domain.tokens.repository
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
/**
* MarketCryptoCurrencyRepository works with data from Tangem coins backend, CoinMarketCap etc
*/
interface MarketCryptoCurrencyRepository {
suspend fun isExchangeable(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean
}

View file

@ -38,6 +38,8 @@ class GetCardImageUseCase(private val verifier: OnlineCardVerifier = OnlineCardV
}
}
fun getDefaultFallbackUrl(): String = Artwork.DEFAULT_IMG_URL
private fun getFallbackArtworkUrl(cardId: String): String {
return when {
cardId.startsWith(Artwork.SERGIO_CARD_ID) -> Artwork.SERGIO_CARD_URL
@ -45,7 +47,7 @@ class GetCardImageUseCase(private val verifier: OnlineCardVerifier = OnlineCardV
else -> when (TwinsHelper.getTwinCardNumber(cardId)) {
TwinCardNumber.First -> Artwork.TWIN_CARD_1_URL
TwinCardNumber.Second -> Artwork.TWIN_CARD_2_URL
else -> Artwork.DEFAULT_IMG_URL
else -> getDefaultFallbackUrl()
}
}
}

View file

@ -62,9 +62,9 @@ internal class OnboardingManageTokensModel @Inject constructor(
.launchIn(modelScope)
combine(
manageTokensListManager.currenciesToAdd,
manageTokensListManager.currenciesToRemove,
::handleChangedCurrencies,
flow = manageTokensListManager.currenciesToAdd,
flow2 = manageTokensListManager.currenciesToRemove,
transform = ::handleChangedCurrencies,
).launchIn(modelScope)
observeSearchQueryChanges()

View file

@ -531,11 +531,11 @@ internal class MarketsTokenDetailsModel @Inject constructor(
showBottomSheet(content = ExchangesBottomSheetContent.Loading(exchangesCount))
// Delay to show the bottom sheet
delay(timeMillis = 800L)
val maybeExchanges = getTokenExchangesUseCase(tokenId = params.token.id)
// Delay to show the bottom sheet
delay(timeMillis = 400L)
updateExchangeBSContent(maybeExchanges = maybeExchanges, exchangesCount = exchangesCount)
}
}

View file

@ -4,6 +4,7 @@ import com.tangem.core.ui.components.audits.AuditLabelUM
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.markets.TokenMarketExchange
import com.tangem.domain.markets.TokenMarketExchange.TrustScore
@ -26,7 +27,7 @@ internal object ExchangeItemStateConverter : Converter<TokenMarketExchange, Toke
isGrayscale = false,
showCustomBadge = false,
),
titleState = TokenItemState.TitleState.Content(text = value.name),
titleState = TokenItemState.TitleState.Content(text = stringReference(value.name)),
fiatAmountState = TokenItemState.FiatAmountState.Content(
text = BigDecimalFormatter.formatFiatPriceUncapped(
fiatAmount = value.volumeInUsd,
@ -35,7 +36,7 @@ internal object ExchangeItemStateConverter : Converter<TokenMarketExchange, Toke
),
),
subtitleState = TokenItemState.SubtitleState.TextContent(
value = if (value.isCentralized) "CEX" else "DEX",
value = stringReference(value = if (value.isCentralized) "CEX" else "DEX"),
),
subtitle2State = TokenItemState.Subtitle2State.LabelContent(
auditLabelUM = value.trustScore.toAuditLabelUM(),

View file

@ -176,9 +176,9 @@ private class ExchangesBottomSheetContentProvider : CollectionPreviewParameterPr
isGrayscale = false,
showCustomBadge = false,
),
titleState = TokenItemState.TitleState.Content(text = "OKX"),
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "OKX")),
fiatAmountState = TokenItemState.FiatAmountState.Content(text = "$67.52M"),
subtitleState = TokenItemState.SubtitleState.TextContent(value = "CEX"),
subtitleState = TokenItemState.SubtitleState.TextContent(value = stringReference(value = "CEX")),
subtitle2State = TokenItemState.Subtitle2State.LabelContent(
auditLabelUM = AuditLabelUM(
text = stringReference("Caution"),

View file

@ -2,6 +2,7 @@ package com.tangem.features.markets.portfolio.impl.model
import com.tangem.common.ui.tokens.TokenItemStateConverter
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
@ -29,11 +30,11 @@ internal class PortfolioTokenUMConverter(
override fun convert(value: PortfolioData.CryptoCurrencyData): PortfolioTokenUM {
val tokenItemStateConverter = TokenItemStateConverter(
appCurrency = appCurrency,
titleStateProvider = { TokenItemState.TitleState.Content(text = value.userWallet.name) },
titleStateProvider = { TokenItemState.TitleState.Content(text = stringReference(value.userWallet.name)) },
subtitleStateProvider = {
TokenItemState.SubtitleState.TextContent(value = value.status.currency.name)
TokenItemState.SubtitleState.TextContent(value = stringReference(value.status.currency.name))
},
onItemClick = onTokenItemClick,
onItemClick = { _, status -> onTokenItemClick(status) },
)
return PortfolioTokenUM(

View file

@ -149,7 +149,7 @@ internal class TokenActionsHandler @AssistedInject constructor(
private fun onExchangeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
router.push(
AppRoute.Swap(
currency = cryptoCurrencyData.status.currency,
currencyFrom = cryptoCurrencyData.status.currency,
userWalletId = cryptoCurrencyData.userWallet.walletId,
isInitialReverseOrder = true,
),

View file

@ -32,7 +32,7 @@ internal fun PortfolioItem(state: PortfolioTokenUM, lastInList: Boolean, modifie
val onClick = state.tokenItemState.onItemClick
if (onClick != null) {
hapticManager.perform(TangemHapticEffect.View.ContextClick)
onClick.invoke()
onClick.invoke(it)
}
},
)
@ -81,8 +81,8 @@ private fun Preview(@PreviewParameter(PortfolioTokenUMProvider::class) tokenUM:
PortfolioItem(
state = tokenUM.copy(
tokenItemState = when (tokenUM.tokenItemState) {
is TokenItemState.Content -> tokenUM.tokenItemState.copy(onItemClick = onItemClick)
is TokenItemState.Unreachable -> tokenUM.tokenItemState.copy(onItemClick = onItemClick)
is TokenItemState.Content -> tokenUM.tokenItemState.copy(onItemClick = { onItemClick() })
is TokenItemState.Unreachable -> tokenUM.tokenItemState.copy(onItemClick = { onItemClick() })
else -> tokenUM.tokenItemState
},
isQuickActionsShown = quickActionsShown,

View file

@ -4,6 +4,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM
import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM
@ -49,10 +50,12 @@ internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider<MyPortfol
tokenItemState = TokenItemState.Content(
id = "",
iconState = CurrencyIconState.Locked,
titleState = TokenItemState.TitleState.Content(text = "My wallet"),
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "My wallet")),
fiatAmountState = TokenItemState.FiatAmountState.Content(text = "486,65 \$"),
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "733,71097 MATIC"),
subtitleState = TokenItemState.SubtitleState.TextContent(value = "XRP Ledger token"),
subtitleState = TokenItemState.SubtitleState.TextContent(
value = stringReference(value = "XRP Ledger token"),
),
onItemClick = {},
onItemLongClick = {},
),

View file

@ -8,6 +8,7 @@ interface OnboardingEntryComponent : ComposableContentComponent {
data class Params(
val scanResponse: ScanResponse,
val startBackupFlow: Boolean,
)
interface Factory : ComponentFactory<Params, OnboardingEntryComponent>

View file

@ -28,6 +28,8 @@ dependencies {
implementation(projects.domain.models)
implementation(projects.domain.feedback)
implementation(projects.domain.core)
implementation(projects.domain.card)
implementation(projects.domain.wallets)
/** Tangem libraries */
implementation(projects.libs.tangemSdkApi)
@ -54,6 +56,7 @@ dependencies {
implementation(deps.kotlin.immutable.collections)
implementation(deps.kotlin.serialization)
implementation(deps.timber)
implementation(deps.firebase.crashlytics)
/** DI */
implementation(deps.hilt.android)

View file

@ -105,6 +105,14 @@ internal class DefaultOnboardingEntryComponent @AssistedInject constructor(
}.saveIn(innerNavigationLinkJobHolder)
}
}
componentScope.launch {
model.titleProvider.currentTitle.collect { title ->
stepperComponent.state.update {
it.copy(title = title)
}
}
}
}
@Composable

View file

@ -40,12 +40,12 @@ internal class OnboardingEntryModel @Inject constructor(
return when (scanResponse.productType) {
ProductType.Note -> TODO()
ProductType.Twins -> TODO()
ProductType.Wallet -> OnboardingRoute.Wallet12(
ProductType.Wallet -> OnboardingRoute.MultiWallet(
scanResponse = scanResponse,
withSeedPhraseFlow = false,
titleProvider = titleProvider,
)
ProductType.Wallet2 -> OnboardingRoute.Wallet12(
ProductType.Wallet2 -> OnboardingRoute.MultiWallet(
scanResponse = scanResponse,
withSeedPhraseFlow = true,
titleProvider = titleProvider,

View file

@ -5,12 +5,12 @@ import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletCo
import javax.inject.Inject
internal class OnboardingChildFactory @Inject constructor(
private val wallet12ComponentFactory: OnboardingMultiWalletComponent.Factory,
private val multiWalletComponentFactory: OnboardingMultiWalletComponent.Factory,
) {
fun createChild(route: OnboardingRoute, childContext: AppComponentContext): Any {
return when (route) {
is OnboardingRoute.Wallet12 -> wallet12ComponentFactory.create(
is OnboardingRoute.MultiWallet -> multiWalletComponentFactory.create(
context = childContext,
params = OnboardingMultiWalletComponent.Params(
scanResponse = route.scanResponse,

View file

@ -8,7 +8,7 @@ sealed class OnboardingRoute : Route {
data object None : OnboardingRoute()
data class Wallet12(
data class MultiWallet(
val titleProvider: TitleProvider,
val scanResponse: ScanResponse,
val withSeedPhraseFlow: Boolean,

View file

@ -31,7 +31,7 @@ internal inline fun OnboardingEntry(
animation = stackAnimation(slide()),
) {
when (it.configuration) {
is OnboardingRoute.Wallet12 -> {
is OnboardingRoute.MultiWallet -> {
(it.instance as OnboardingMultiWalletComponent).Content(
modifier = modifier,
)

View file

@ -4,12 +4,24 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.extensions.compose.jetpack.stack.Children
import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.slide
import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.stackAnimation
import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState
import com.arkivanov.decompose.router.stack.*
import com.arkivanov.decompose.value.Value
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.decompose.navigation.inner.InnerNavigation
import com.tangem.core.decompose.navigation.inner.InnerNavigationState
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.MultiWalletBackupComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.MultiWalletCreateWalletComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletModel
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState
import com.tangem.features.onboarding.v2.multiwallet.impl.ui.OnboardingMultiWallet
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@ -22,24 +34,82 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
) : OnboardingMultiWalletComponent, AppComponentContext by context {
private val model: OnboardingMultiWalletModel = getOrCreateModel(params)
private val stackNavigation = StackNavigation<OnboardingMultiWalletState.Step>()
override val innerNavigation: InnerNavigation = object : InnerNavigation {
override val state = MutableStateFlow(
Wallet12InnerNavigationState(1, 5), // TODO
MultiWalletInnerNavigationState(1, 5),
)
override fun pop(onComplete: (Boolean) -> Unit) {
// TODO
// TODO add warning dialog
stackNavigation.pop(onComplete)
}
}
private val childStack: Value<ChildStack<OnboardingMultiWalletState.Step, MultiWalletChildComponent>> = childStack(
key = "innerStack",
source = stackNavigation,
serializer = null,
initialConfiguration = model.state.value.currentStep,
handleBackButton = true,
childFactory = { configuration, factoryContext -> createChild(configuration, childByContext(factoryContext)) },
)
private fun createChild(
step: OnboardingMultiWalletState.Step,
childContext: AppComponentContext,
): MultiWalletChildComponent {
return when (step) {
OnboardingMultiWalletState.Step.CreateWallet -> MultiWalletCreateWalletComponent(
context = childContext,
params = MultiWalletChildParams(
multiWalletState = model.state,
parentParams = params,
),
onDone = ::onStepDone,
)
OnboardingMultiWalletState.Step.AddBackupDevice -> MultiWalletBackupComponent(
context = childContext,
params = MultiWalletChildParams(
multiWalletState = model.state,
parentParams = params,
),
onDone = ::onStepDone,
)
OnboardingMultiWalletState.Step.FinishBackup -> TODO()
OnboardingMultiWalletState.Step.Done -> TODO()
}
}
private fun onStepDone() {
when (model.state.value.currentStep) {
OnboardingMultiWalletState.Step.CreateWallet -> {
stackNavigation.push(OnboardingMultiWalletState.Step.AddBackupDevice)
// innerNavigation.state.value TODO
}
OnboardingMultiWalletState.Step.AddBackupDevice -> TODO()
OnboardingMultiWalletState.Step.FinishBackup -> TODO()
OnboardingMultiWalletState.Step.Done -> TODO()
}
}
@Composable
override fun Content(modifier: Modifier) {
val uiState by model.uiState.collectAsStateWithLifecycle()
val stackState by childStack.subscribeAsState()
val state by model.uiState.collectAsStateWithLifecycle()
OnboardingMultiWallet(
state = state,
modifier = modifier,
state = uiState,
childContent = { mdfr ->
Children(
stack = stackState,
animation = stackAnimation(slide()),
) {
it.instance.Content(mdfr)
}
},
)
}
@ -52,7 +122,7 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
}
}
data class Wallet12InnerNavigationState(
data class MultiWalletInnerNavigationState(
override val stackSize: Int,
override val stackMaxSize: Int?,
) : InnerNavigationState

View file

@ -0,0 +1,5 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child
import com.tangem.core.ui.decompose.ComposableContentComponent
interface MultiWalletChildComponent : ComposableContentComponent

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