Updated on 2026-08-14

This commit is contained in:
Tangem 2023-09-11 11:03:50 +03:00
commit 4c97630a6e
266 changed files with 6858 additions and 2386 deletions

View file

@ -35,6 +35,8 @@ dependencies {
implementation(projects.domain.txhistory)
implementation(projects.domain.appCurrency)
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.appTheme)
implementation(projects.domain.appTheme.models)
implementation(project(":common"))
implementation(project(":core:analytics"))
@ -48,13 +50,15 @@ dependencies {
implementation(project(":libs:crypto"))
implementation(project(":libs:auth"))
implementation(project(":data:source:preferences"))
implementation(projects.data.appCurrency)
implementation(projects.data.appTheme)
implementation(projects.data.card)
implementation(projects.data.common)
implementation(projects.data.settings)
implementation(projects.data.source.preferences)
implementation(projects.data.tokens)
implementation(projects.data.txhistory)
implementation(projects.data.appCurrency)
implementation(projects.data.wallets)
/** Features */
implementation(project(":features:onboarding"))
@ -86,6 +90,7 @@ dependencies {
implementation(deps.lifecycle.runtime.ktx)
implementation(deps.lifecycle.common.java8)
implementation(deps.lifecycle.viewModel.ktx)
implementation(deps.lifecycle.compose)
/** Compose libraries */
implementation(deps.compose.constraintLayout)

View file

@ -0,0 +1,12 @@
package com.tangem.tap
import com.tangem.domain.apptheme.model.AppThemeMode
internal sealed class GlobalSettingsState {
object Loading : GlobalSettingsState()
data class Content(
val appThemeMode: AppThemeMode,
) : GlobalSettingsState()
}

View file

@ -4,10 +4,10 @@ import android.content.Intent
import android.content.pm.ActivityInfo
import android.os.Bundle
import android.view.View
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.lifecycle.lifecycleScope
import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.android.material.snackbar.Snackbar
@ -26,6 +26,7 @@ import com.tangem.tap.common.ActivityResultCallbackHolder
import com.tangem.tap.common.DialogManager
import com.tangem.tap.common.OnActivityResultCallback
import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.NotificationsHandler
import com.tangem.tap.common.redux.global.GlobalAction
@ -53,6 +54,8 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import java.lang.ref.WeakReference
import javax.inject.Inject
@ -109,6 +112,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
lateinit var walletConnectInteractor: WalletConnectInteractor
private val viewModel: MainViewModel by viewModels()
private var isInitializing: Boolean = true
// TODO: fixme: inject through DI
private val intentProcessor: IntentProcessor = IntentProcessor()
@ -119,11 +125,17 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
private val onActivityResultCallbacks = mutableListOf<OnActivityResultCallback>()
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState)
bootstrapMainStateUpdates()
splashScreen.setKeepOnScreenCondition { isInitializing }
setContentView(R.layout.activity_main)
systemActions()
store.dispatch(NavigationAction.ActivityCreated(WeakReference(this)))
cardSdkLifecycleObserver.onCreate(context = this)
@ -204,13 +216,24 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
store.dispatch(GlobalAction.UpdateUserWalletsListManager(manager))
}
private fun bootstrapMainStateUpdates() {
viewModel.state
.onEach { state ->
isInitializing = state is GlobalSettingsState.Loading
when (state) {
is GlobalSettingsState.Content -> {
MutableAppThemeModeHolder.value = state.appThemeMode
}
is GlobalSettingsState.Loading -> Unit
}
}
.launchIn(lifecycleScope)
}
private fun systemActions() {
WindowCompat.setDecorFitsSystemWindows(window, false)
val windowInsetsController = WindowInsetsControllerCompat(window, binding.root)
windowInsetsController.isAppearanceLightStatusBars = true
windowInsetsController.isAppearanceLightNavigationBars = true
supportFragmentManager.registerFragmentLifecycleCallbacks(
NavBarInsetsFragmentLifecycleCallback(),
true,

View file

@ -0,0 +1,35 @@
package com.tangem.tap
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import arrow.core.getOrElse
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
import com.tangem.domain.apptheme.model.AppThemeMode
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject
@HiltViewModel
internal class MainViewModel @Inject constructor(
private val getAppThemeModeUseCase: GetAppThemeModeUseCase,
) : ViewModel() {
val state: StateFlow<GlobalSettingsState> = createMainStateFlow()
private fun createMainStateFlow(): StateFlow<GlobalSettingsState> {
return getAppThemeModeUseCase()
.map { maybeMode ->
val mode = maybeMode.getOrElse { AppThemeMode.DEFAULT }
GlobalSettingsState.Content(appThemeMode = mode)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5_000),
initialValue = GlobalSettingsState.Loading,
)
}
}

View file

@ -26,6 +26,7 @@ import com.tangem.domain.DomainLayer
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.common.LogConfig
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.WalletManagersRepository
import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
@ -41,6 +42,7 @@ import com.tangem.tap.common.feedback.AdditionalFeedbackInfo
import com.tangem.tap.common.feedback.FeedbackManager
import com.tangem.tap.common.images.createCoilImageLoader
import com.tangem.tap.common.log.TangemLogCollector
import com.tangem.tap.common.log.TimberFormatStrategy
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.appReducer
import com.tangem.tap.common.redux.global.GlobalAction
@ -164,6 +166,9 @@ class TapApplication : Application(), ImageLoaderFactory {
@Inject
lateinit var appCurrencyRepository: AppCurrencyRepository
@Inject
lateinit var walletManagersFacade: WalletManagersFacade
override fun onCreate() {
super.onCreate()
@ -183,12 +188,13 @@ class TapApplication : Application(), ImageLoaderFactory {
tokenDetailsFeatureToggles = tokenDetailsFeatureToggles,
scanCardProcessor = scanCardProcessor,
appCurrencyRepository = appCurrencyRepository,
walletManagersFacade = walletManagersFacade,
),
),
)
if (BuildConfig.DEBUG) {
Logger.addLogAdapter(AndroidLogAdapter())
Logger.addLogAdapter(AndroidLogAdapter(TimberFormatStrategy()))
Timber.plant(
object : Timber.DebugTree() {
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {

View file

@ -0,0 +1,17 @@
package com.tangem.tap.common.apptheme
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import com.tangem.core.ui.theme.AppThemeModeHolder
import com.tangem.domain.apptheme.model.AppThemeMode
internal object MutableAppThemeModeHolder : AppThemeModeHolder {
override val appThemeMode: MutableState<AppThemeMode> = mutableStateOf(AppThemeMode.DEFAULT)
var value: AppThemeMode
set(value) {
appThemeMode.value = value
}
get() = appThemeMode.value
}

View file

@ -82,7 +82,7 @@ fun WalletManager?.getAddressData(): WalletDataModel.AddressData? {
}
fun <T> WalletManager.Companion.stub(): T {
val wallet = Wallet(Blockchain.Unknown, setOf(), Wallet.PublicKey(byteArrayOf(), null, null), setOf())
val wallet = Wallet(Blockchain.Unknown, setOf(), Wallet.PublicKey(byteArrayOf(), null), setOf())
return object : WalletManager(wallet) {
override val currentHost: String = ""
override suspend fun update() {}

View file

@ -0,0 +1,63 @@
package com.tangem.tap.common.log
import com.orhanobut.logger.FormatStrategy
import com.orhanobut.logger.LogStrategy
import com.orhanobut.logger.LogcatLogStrategy
class TimberFormatStrategy : FormatStrategy {
private val logStrategy: LogStrategy = LogcatLogStrategy()
override fun log(priority: Int, tag: String?, message: String) {
logTopBorder(priority, tag)
val bytes = message.toByteArray()
val length = bytes.size
if (length <= CHUNK_SIZE) {
logContent(priority, tag, message)
logBottomBorder(priority, tag)
return
}
var i = 0
while (i < length) {
val count = (length - i).coerceAtMost(CHUNK_SIZE)
// create a new String with system's default charset (which is UTF-8 for Android)
logContent(priority, tag, String(bytes, i, count))
i += CHUNK_SIZE
}
logBottomBorder(priority, tag)
}
private fun logTopBorder(logType: Int, tag: String?) {
logChunk(logType, tag, TOP_BORDER)
}
private fun logBottomBorder(logType: Int, tag: String?) {
logChunk(logType, tag, BOTTOM_BORDER)
}
private fun logContent(logType: Int, tag: String?, chunk: String) {
chunk.split(System.lineSeparator()).forEach { line ->
logChunk(logType, tag, "$HORIZONTAL_LINE $line")
}
}
private fun logChunk(priority: Int, tag: String?, chunk: String) {
logStrategy.log(priority, tag, chunk)
}
private companion object {
/**
* Android's max limit for a log entry is ~4076 bytes,
* so 4000 bytes is used as chunk size since default charset
* is UTF-8
*/
private const val CHUNK_SIZE = 4000
const val TOP_LEFT_CORNER = ""
const val BOTTOM_LEFT_CORNER = ""
const val HORIZONTAL_LINE = ""
const val DOUBLE_DIVIDER = "────────────────────────────────────────────────────────"
const val TOP_BORDER = TOP_LEFT_CORNER + DOUBLE_DIVIDER + DOUBLE_DIVIDER
const val BOTTOM_BORDER = BOTTOM_LEFT_CORNER + DOUBLE_DIVIDER + DOUBLE_DIVIDER
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.di
import com.tangem.core.navigation.NavigationStateHolder
import com.tangem.core.navigation.ReduxNavController
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.tap.proxy.AppStateHolder
import dagger.Binds
@ -19,5 +20,9 @@ internal interface AppStateHolderModule {
@Binds
@Singleton
fun bindsNavigationStateHolder(appStateHolder: AppStateHolder): NavigationStateHolder
fun bindsNavigationStateHolder(appStateHolder: AppStateHolder): ReduxNavController
@Binds
@Singleton
fun bindsReduxStateHolder(appStateHolder: AppStateHolder): ReduxStateHolder
}

View file

@ -0,0 +1,18 @@
package com.tangem.tap.di
import com.tangem.core.ui.theme.AppThemeModeHolder
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
@Module
@InstallIn(ActivityComponent::class)
internal object ThemeModule {
@Provides
fun provideAppThemeModeHolder(): AppThemeModeHolder {
return MutableAppThemeModeHolder
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.tap.di.domain
import com.tangem.domain.apptheme.ChangeAppThemeModeUseCase
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
@Module
@InstallIn(ViewModelComponent::class)
internal object AppThemeDomainModule {
@Provides
fun provideGetAppThemeModeUpdatesUseCase(appThemeModeRepository: AppThemeModeRepository): GetAppThemeModeUseCase {
return GetAppThemeModeUseCase(appThemeModeRepository)
}
@Provides
fun provideChangeAppThemeModeUseCase(appThemeModeRepository: AppThemeModeRepository): ChangeAppThemeModeUseCase {
return ChangeAppThemeModeUseCase(appThemeModeRepository)
}
}

View file

@ -1,7 +1,11 @@
package com.tangem.tap.di.domain
import com.tangem.domain.settings.CanUseBiometryUseCase
import com.tangem.domain.settings.IsUserAlreadyRateAppUseCase
import com.tangem.domain.settings.ShouldShowSaveWalletScreenUseCase
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.settings.DefaultLegacySettingsRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -17,4 +21,20 @@ internal object SettingsDomainModule {
fun providesGetWalletsUseCase(settingsRepository: SettingsRepository): IsUserAlreadyRateAppUseCase {
return IsUserAlreadyRateAppUseCase(settingsRepository = settingsRepository)
}
@Provides
@ViewModelScoped
fun providesShouldShowSaveWalletScreenUseCase(
settingsRepository: SettingsRepository,
): ShouldShowSaveWalletScreenUseCase {
return ShouldShowSaveWalletScreenUseCase(settingsRepository = settingsRepository)
}
@Provides
@ViewModelScoped
fun providesCanUseBiometryUseCase(tangemSdkManager: TangemSdkManager): CanUseBiometryUseCase {
return CanUseBiometryUseCase(
legacySettingsRepository = DefaultLegacySettingsRepository(tangemSdkManager = tangemSdkManager),
)
}
}

View file

@ -15,6 +15,16 @@ import dagger.hilt.android.scopes.ViewModelScoped
@InstallIn(ViewModelComponent::class)
internal object TokensDomainModule {
@Provides
@ViewModelScoped
fun provideFetchTokenListUseCase(
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
): FetchTokenListUseCase {
return FetchTokenListUseCase(currenciesRepository, networksRepository, quotesRepository)
}
@Provides
@ViewModelScoped
fun provideGetTokenListUseCase(
@ -26,6 +36,15 @@ internal object TokensDomainModule {
return GetTokenListUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers)
}
@Provides
@ViewModelScoped
fun provideRemoveCurrencyUseCase(
currenciesRepository: CurrenciesRepository,
dispatchers: CoroutineDispatcherProvider,
): RemoveCurrencyUseCase {
return RemoveCurrencyUseCase(currenciesRepository, dispatchers)
}
@Provides
@ViewModelScoped
fun provideGetCurrencyUseCase(
@ -33,8 +52,8 @@ internal object TokensDomainModule {
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
dispatchers: CoroutineDispatcherProvider,
): GetCurrencyUseCase {
return GetCurrencyUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers)
): GetCurrencyStatusUpdatesUseCase {
return GetCurrencyStatusUpdatesUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers)
}
@Provides
@ -44,17 +63,31 @@ internal object TokensDomainModule {
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
dispatchers: CoroutineDispatcherProvider,
): GetPrimaryCurrencyUseCase {
return GetPrimaryCurrencyUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers)
): GetPrimaryCurrencyStatusUpdatesUseCase {
return GetPrimaryCurrencyStatusUpdatesUseCase(
currenciesRepository,
quotesRepository,
networksRepository,
dispatchers,
)
}
@Provides
@ViewModelScoped
fun provideFetchCurrencyStatusUseCase(
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
): FetchCurrencyStatusUseCase {
return FetchCurrencyStatusUseCase(currenciesRepository, networksRepository, quotesRepository)
}
@Provides
@ViewModelScoped
fun provideToggleTokenListGroupingUseCase(
networksRepository: NetworksRepository,
dispatchers: CoroutineDispatcherProvider,
): ToggleTokenListGroupingUseCase {
return ToggleTokenListGroupingUseCase(networksRepository, dispatchers)
return ToggleTokenListGroupingUseCase(dispatchers)
}
@Provides
@ -71,4 +104,12 @@ internal object TokensDomainModule {
): ApplyTokenListSortingUseCase {
return ApplyTokenListSortingUseCase(currenciesRepository, dispatchers)
}
@Provides
@ViewModelScoped
fun provideGetCryptoCurrencyActionsUseCase(
dispatchers: CoroutineDispatcherProvider,
): GetCryptoCurrencyActionsUseCase {
return GetCryptoCurrencyActionsUseCase(dispatchers)
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.tap.di.domain
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.*
import dagger.Module
import dagger.Provides
@ -48,4 +49,22 @@ internal object WalletsDomainModule {
fun providesSelectWalletUseCase(walletsStateHolder: WalletsStateHolder): SelectWalletUseCase {
return SelectWalletUseCase(walletsStateHolder = walletsStateHolder)
}
@Provides
@ViewModelScoped
fun providesUpdateWalletUseCase(walletsStateHolder: WalletsStateHolder): UpdateWalletUseCase {
return UpdateWalletUseCase(walletsStateHolder = walletsStateHolder)
}
@Provides
@ViewModelScoped
fun providesDeleteWalletUseCase(walletsStateHolder: WalletsStateHolder): DeleteWalletUseCase {
return DeleteWalletUseCase(walletsStateHolder = walletsStateHolder)
}
@Provides
@ViewModelScoped
fun providesShouldSaveUserWalletsUseCase(walletsRepository: WalletsRepository): ShouldSaveUserWalletsUseCase {
return ShouldSaveUserWalletsUseCase(walletsRepository = walletsRepository)
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.tap.domain.settings
import com.tangem.domain.settings.repositories.LegacySettingsRepository
import com.tangem.tap.domain.TangemSdkManager
internal class DefaultLegacySettingsRepository(
private val tangemSdkManager: TangemSdkManager,
) : LegacySettingsRepository {
override fun canUseBiometry(): Boolean = tangemSdkManager.canUseBiometry
}

View file

@ -1,5 +1,6 @@
package com.tangem.tap.domain.tasks.product
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
@ -224,7 +225,8 @@ private class ScanWalletProcessor(
walletData = session.environment.walletData,
primaryCard = primaryCard,
)
val derivations = collectDerivations(card, config, scanResponse.derivationStyleProvider)
val derivations =
collectDerivations(card, config, scanResponse.derivationStyleProvider)
if (derivations.isEmpty() || !card.settings.isHDWalletAllowed) {
callback(CompletionResult.Success(scanResponse))
return@launch
@ -252,60 +254,101 @@ private class ScanWalletProcessor(
derivationStyleProvider.getDerivationStyle(),
)
.toMutableList()
.ifEmpty {
mutableListOf(
BlockchainNetwork(
blockchain = Blockchain.Bitcoin,
derivationStyleProvider = derivationStyleProvider,
),
BlockchainNetwork(
blockchain = Blockchain.Ethereum,
derivationStyleProvider = derivationStyleProvider,
),
)
}
.ifEmpty { getDefaultBlockchains(derivationStyleProvider) }
if (card.settings.isHDWalletAllowed) {
blockchainsToDerive.addAll(
listOf(
BlockchainNetwork(
blockchain = Blockchain.Ethereum,
derivationStyleProvider = derivationStyleProvider,
),
BlockchainNetwork(
blockchain = Blockchain.EthereumTestnet,
derivationStyleProvider = derivationStyleProvider,
),
),
)
blockchainsToDerive += getEthereumBlockchains(derivationStyleProvider)
}
if (additionalBlockchainsToDerive != null) {
blockchainsToDerive.addAll(
additionalBlockchainsToDerive.map {
BlockchainNetwork(
blockchain = it,
derivationStyleProvider = derivationStyleProvider,
)
},
)
additionalBlockchainsToDerive?.let {
blockchainsToDerive += getAdditionalBlockchainToDerive(derivationStyleProvider, it)
}
// we should generate second key for cardano
// because cardano address generation for wallet2 requires keys from 2 derivations
// https://developers.cardano.org/docs/get-started/cardano-serialization-lib/generating-keys/
val secondCardanoNetwork = blockchainsToDerive
.find { it.blockchain == Blockchain.Cardano }
?.let { getCardanoSecondNetwork(it) }
secondCardanoNetwork?.let { blockchainsToDerive.add(it) }
// pay attention to this
if (!card.useOldStyleDerivation) {
blockchainsToDerive.removeAll(
listOf(
Blockchain.BSC, Blockchain.BSCTestnet,
Blockchain.Polygon, Blockchain.PolygonTestnet,
Blockchain.RSK,
Blockchain.Fantom, Blockchain.FantomTestnet,
Blockchain.Avalanche, Blockchain.AvalancheTestnet,
).map {
BlockchainNetwork(
blockchain = it,
derivationStyleProvider = derivationStyleProvider,
)
},
removeUnnecessaryBlockchains(blockchainsToDerive, derivationStyleProvider)
}
return blockchainsToDerive.distinct()
}
private fun getDefaultBlockchains(
derivationStyleProvider: DerivationStyleProvider,
): MutableList<BlockchainNetwork> {
return mutableListOf(
BlockchainNetwork(
blockchain = Blockchain.Bitcoin,
derivationStyleProvider = derivationStyleProvider,
),
BlockchainNetwork(
blockchain = Blockchain.Ethereum,
derivationStyleProvider = derivationStyleProvider,
),
)
}
private fun getEthereumBlockchains(derivationStyleProvider: DerivationStyleProvider): List<BlockchainNetwork> {
return listOf(
BlockchainNetwork(
blockchain = Blockchain.Ethereum,
derivationStyleProvider = derivationStyleProvider,
),
BlockchainNetwork(
blockchain = Blockchain.EthereumTestnet,
derivationStyleProvider = derivationStyleProvider,
),
)
}
private fun getAdditionalBlockchainToDerive(
derivationStyleProvider: DerivationStyleProvider,
collection: Collection<Blockchain>,
): List<BlockchainNetwork> {
return collection.map {
BlockchainNetwork(
blockchain = it,
derivationStyleProvider = derivationStyleProvider,
)
}
return blockchainsToDerive.distinct()
}
private fun getCardanoSecondNetwork(cardanoBlockchainNetwork: BlockchainNetwork): BlockchainNetwork? {
val cardanoStandardDerivation = cardanoBlockchainNetwork.derivationPath?.let { DerivationPath(it) }
?: return null
val cardanoPatchedDerivation = CardanoUtils.extendedDerivationPath(cardanoStandardDerivation)
return BlockchainNetwork(
blockchain = Blockchain.Cardano,
derivationPath = cardanoPatchedDerivation.rawPath,
tokens = emptyList(),
)
}
private fun removeUnnecessaryBlockchains(
blockchainsToDerive: MutableList<BlockchainNetwork>,
derivationStyleProvider: DerivationStyleProvider,
) {
blockchainsToDerive.removeAll(
listOf(
Blockchain.BSC, Blockchain.BSCTestnet,
Blockchain.Polygon, Blockchain.PolygonTestnet,
Blockchain.RSK,
Blockchain.Fantom, Blockchain.FantomTestnet,
Blockchain.Avalanche, Blockchain.AvalancheTestnet,
).map {
BlockchainNetwork(
blockchain = it,
derivationStyleProvider = derivationStyleProvider,
)
},
)
}
private suspend fun collectDerivations(

View file

@ -251,7 +251,7 @@ class WalletConnectRepositoryImpl @Inject constructor(
}.groupBy { pair -> pair.first }
.mapValues { entry -> entry.value.map { pair -> pair.second }.toSet() }
val preparedNamespaces = sessionProposal.requiredNamespaces
val preparedRequiredNamespaces = sessionProposal.requiredNamespaces
.map { requiredNamespace ->
val accountsRequired = requiredNamespace.value.chains
?.mapNotNull { chain -> userChains[chain] }
@ -272,7 +272,9 @@ class WalletConnectRepositoryImpl @Inject constructor(
val sessionApproval = Wallet.Params.SessionApprove(
proposerPublicKey = sessionProposal.proposerPublicKey,
namespaces = preparedNamespaces,
namespaces = preparedRequiredNamespaces.ifEmpty {
sessionProposal.createPreparedOptionalNamespaces(userChains)
},
)
Timber.d("Session approval is prepared for sending: $sessionApproval")
@ -301,6 +303,25 @@ class WalletConnectRepositoryImpl @Inject constructor(
)
}
private fun Wallet.Model.SessionProposal.createPreparedOptionalNamespaces(
userChains: Map<String, Set<String>>,
): Map<String, Wallet.Model.Namespace.Session> {
return optionalNamespaces
.map { optionalNamespace ->
val accountsOptional = optionalNamespace.value.chains
?.mapNotNull { chain -> userChains[chain] }
?.flatten() ?: emptyList()
val methods = optionalNamespace.value.methods
optionalNamespace.key to Wallet.Model.Namespace.Session(
accounts = accountsOptional.distinct(),
methods = methods,
events = optionalNamespace.value.events,
)
}
.toMap()
}
override fun sendRequest(requestData: RequestData, result: String) {
val session = currentSessions.find { it.topic == requestData.topic }
// Add Ethereum Chain method is processed without user input, skip logging it

View file

@ -1,5 +1,6 @@
package com.tangem.tap.features.customtoken.impl.domain
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
@ -123,9 +124,15 @@ class DefaultCustomTokenInteractor(
it.blockchain.getSupportedCurves().contains(curve)
}.mapNotNull { it.derivationPath }.map { DerivationPath(it) }
val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct()
val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct().toMutableList()
if (bothCandidates.isEmpty()) return null
currencyList.find { it is Currency.Blockchain && it.blockchain == Blockchain.Cardano }?.let { currency ->
currency.derivationPath?.let {
bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it)))
}
}
val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey()
val alreadyDerivedKeys: ExtendedPublicKeysMap =
scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap())

View file

@ -2,6 +2,7 @@ package com.tangem.tap.features.demo
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.tap.common.extensions.dispatchNotification
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.details.redux.DetailsAction
@ -21,8 +22,8 @@ object DemoHelper {
private val disabledActionFeatures = listOf(
WalletConnectAction.StartWalletConnect::class.java,
WalletAction.TradeCryptoAction.Buy::class.java,
WalletAction.TradeCryptoAction.Sell::class.java,
TradeCryptoAction.Buy::class.java,
TradeCryptoAction.Sell::class.java,
BackupAction.StartBackup::class.java,
WalletAction.ExploreAddress::class.java,
DetailsAction.ResetToFactory.Start::class.java,

View file

@ -7,7 +7,7 @@ import android.webkit.WebView
import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import com.tangem.core.navigation.AppScreen
import com.tangem.core.ui.fragments.setStatusBarColor
import com.tangem.core.ui.extensions.setStatusBarColor
import com.tangem.tap.common.extensions.beginDelayedTransition
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show

View file

@ -1,9 +1,9 @@
package com.tangem.tap.features.intentHandler.handlers
import android.content.Intent
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.features.intentHandler.IntentHandler
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import timber.log.Timber
@ -22,7 +22,7 @@ class SellCurrencyIntentHandler : IntentHandler {
Timber.d("MoonPay Sell: $amount $currency to $destinationAddress")
store.dispatchWithMain(
WalletAction.TradeCryptoAction.SendCrypto(
TradeCryptoAction.SendCrypto(
currencyId = currency,
amount = amount,
destinationAddress = destinationAddress,

View file

@ -16,7 +16,7 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.VoidCallback
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.ShareElement
import com.tangem.core.ui.fragments.setStatusBarColor
import com.tangem.core.ui.extensions.setStatusBarColor
import com.tangem.datasource.asset.AssetReader
import com.tangem.domain.common.TwinCardNumber
import com.tangem.domain.models.scan.ScanResponse

View file

@ -6,24 +6,27 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.SnackbarHost
import androidx.compose.material.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.fragment.app.viewModels
import com.tangem.core.ui.fragments.ComposeBottomSheetFragment
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeBottomSheetFragment
import com.tangem.core.ui.theme.AppThemeModeHolder
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
import com.tangem.tap.features.saveWallet.ui.components.EnrollBiometricsDialogContent
import com.tangem.tap.features.saveWallet.ui.components.SaveWalletScreenContent
import com.tangem.tap.features.saveWallet.ui.models.EnrollBiometricsDialog
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
internal class SaveWalletBottomSheetFragment : ComposeBottomSheetFragment() {
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
internal class SaveWalletBottomSheetFragment : ComposeBottomSheetFragment<SaveWalletScreenState>() {
override val expandedHeightFraction: Float = .98f
private val viewModel by viewModels<SaveWalletViewModel>()
@ -34,12 +37,8 @@ internal class SaveWalletBottomSheetFragment : ComposeBottomSheetFragment<SaveWa
}
@Composable
override fun provideState(): State<SaveWalletScreenState> {
return viewModel.state.collectAsState()
}
@Composable
override fun ScreenContent(state: SaveWalletScreenState, modifier: Modifier) {
override fun ScreenContent(modifier: Modifier) {
val state by viewModel.state.collectAsStateWithLifecycle()
val snackbarHostState = remember { SnackbarHostState() }
val errorMessage by rememberUpdatedState(newValue = state.error?.resolveReference())
val enrollBiometricsDialog by rememberUpdatedState(newValue = state.enrollBiometricsDialog)

View file

@ -19,6 +19,7 @@ import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.extensions.minimalAmount
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.tap.*
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Basic
@ -38,7 +39,6 @@ import com.tangem.tap.features.send.redux.*
import com.tangem.tap.features.send.redux.FeeAction.RequestFee
import com.tangem.tap.features.send.redux.states.*
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
@ -254,7 +254,7 @@ private fun sendTransaction(
),
)
Analytics.sendSelectedCurrencyEvent(mainCurrencyType)
dispatch(WalletAction.TradeCryptoAction.FinishSelling(externalTransactionData.transactionId))
dispatch(TradeCryptoAction.FinishSelling(externalTransactionData.transactionId))
} else {
Analytics.send(
Basic.TransactionSent(

View file

@ -18,6 +18,7 @@ import com.google.android.material.textfield.TextInputEditText
import com.tangem.Message
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.sdk.extensions.hideSoftKeyboard
import com.tangem.tap.common.KeyboardObserver
import com.tangem.tap.common.analytics.events.Token
@ -39,7 +40,6 @@ import com.tangem.tap.features.send.redux.FeeActionUi.*
import com.tangem.tap.features.send.redux.states.FeeType
import com.tangem.tap.features.send.redux.states.MainCurrencyType
import com.tangem.tap.features.send.ui.stateSubscribers.SendStateSubscriber
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter
import com.tangem.tap.mainScope
import com.tangem.tap.store
@ -337,9 +337,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
if (externalTransactionData == null) {
store.dispatch(NavigationAction.PopBackTo())
} else {
store.dispatch(
WalletAction.TradeCryptoAction.FinishSelling(externalTransactionData.transactionId),
)
store.dispatch(TradeCryptoAction.FinishSelling(externalTransactionData.transactionId))
}
}

View file

@ -6,14 +6,24 @@ import androidx.activity.viewModels
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.fragments.ComposeActivity
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeActivity
import com.tangem.core.ui.theme.AppThemeModeHolder
import com.tangem.tap.common.analytics.events.Chat
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
internal class SprinklrActivity : ComposeActivity() {
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
internal class SprinklrActivity : ComposeActivity<SprinklrScreenState>() {
private val viewModel by viewModels<SprinklrViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
@ -25,12 +35,14 @@ internal class SprinklrActivity : ComposeActivity<SprinklrScreenState>() {
}
@Composable
override fun provideState(): State<SprinklrScreenState> {
return viewModel.state.collectAsState()
}
override fun ScreenContent(modifier: Modifier) {
val state by viewModel.state.collectAsStateWithLifecycle()
val systemBarsColor = TangemTheme.colors.background.primary
SystemBarsEffect {
setSystemBarsColor(systemBarsColor)
}
@Composable
override fun ScreenContent(state: SprinklrScreenState, modifier: Modifier) {
BackHandler(onBack = state.onNavigateBack)
SprinklrScreenContent(
modifier = modifier

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.tokens.impl.domain
import androidx.paging.PagingData
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.common.CompletionResult
@ -190,9 +191,15 @@ internal class DefaultTokensListInteractor(
.mapNotNull(Currency::derivationPath)
.map(::DerivationPath)
val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct()
val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct().toMutableList()
if (bothCandidates.isEmpty()) return null
currencyList.find { it is Currency.Blockchain && it.blockchain == Blockchain.Cardano }?.let { currency ->
currency.derivationPath?.let {
bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it)))
}
}
val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey()
val alreadyDerivedKeys = scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap())
val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList()

View file

@ -7,17 +7,14 @@ import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.FabPosition
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
@ -32,9 +29,7 @@ import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType
import androidx.compose.ui.unit.dp
import androidx.paging.PagingData
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.items
import androidx.paging.compose.*
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
@ -134,8 +129,11 @@ private fun TokensListContent(
item { DifferentAddressesWarning() }
}
items(items = tokens, key = TokenItemState::composedId) {
it?.let { TokenItem(model = it) }
tokens.itemKey(TokenItemState::composedId)
tokens.itemContentType(TokenItemState::composedId)
items(items = tokens.itemSnapshotList.items, key = TokenItemState::composedId) {
TokenItem(model = it)
}
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.tap.features.tokens.legacy.redux
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.common.CompletionResult
@ -179,9 +180,15 @@ object TokensMiddleware {
it.blockchain.getSupportedCurves().contains(curve)
}.mapNotNull { it.derivationPath }.map { DerivationPath(it) }
val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct()
val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct().toMutableList()
if (bothCandidates.isEmpty()) return null
currencyList.find { it is Currency.Blockchain && it.blockchain == Blockchain.Cardano }?.let { currency ->
currency.derivationPath?.let {
bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it)))
}
}
val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey()
val alreadyDerivedKeys: ExtendedPublicKeysMap =
scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap())

View file

@ -7,7 +7,7 @@ import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.store
import com.tangem.utils.converter.Converter
class CryptoCurrencyConverter : Converter<Currency, CryptoCurrency> {
internal class CryptoCurrencyConverter : Converter<Currency, CryptoCurrency> {
private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory() }

View file

@ -4,12 +4,16 @@ import androidx.core.os.bundleOf
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.common.extensions.toCoinId
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.feature.swap.presentation.SwapFragment
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Token
@ -27,6 +31,7 @@ import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.buyErc20TestnetTokens
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.launch
@ -35,19 +40,27 @@ import kotlinx.serialization.json.Json
import com.tangem.feature.swap.domain.models.domain.Currency as SwapCurrency
class TradeCryptoMiddleware {
fun handle(state: () -> AppState?, action: WalletAction.TradeCryptoAction) {
fun handle(state: () -> AppState?, action: TradeCryptoAction) {
if (DemoHelper.tryHandle(state, action)) return
when (action) {
is WalletAction.TradeCryptoAction.Buy -> proceedBuyAction(state, action)
is WalletAction.TradeCryptoAction.Sell -> proceedSellAction()
is WalletAction.TradeCryptoAction.SendCrypto -> preconfigureAndOpenSendScreen(action)
is WalletAction.TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId)
is WalletAction.TradeCryptoAction.Swap -> openSwap()
is TradeCryptoAction.Buy -> proceedBuyAction(state, action)
is TradeCryptoAction.Sell -> proceedSellAction()
is TradeCryptoAction.SendCrypto -> preconfigureAndOpenSendScreen(action)
is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId)
is TradeCryptoAction.Swap -> {
openSwap(currency = store.state.walletState.selectedWalletData?.currency?.toSwapCurrency())
}
is TradeCryptoAction.New.Buy -> proceedNewBuyAction(state, action)
TradeCryptoAction.New.Send -> store.dispatch(WalletAction.Send())
is TradeCryptoAction.New.Sell -> proceedNewSellAction(action)
is TradeCryptoAction.New.Swap -> {
openSwap(currency = action.cryptoCurrency.toSwapCurrency())
}
}
}
private fun proceedBuyAction(state: () -> AppState?, action: WalletAction.TradeCryptoAction.Buy) {
private fun proceedBuyAction(state: () -> AppState?, action: TradeCryptoAction.Buy) {
val selectedWalletData = store.state.walletState.selectedWalletData ?: return
val currency = chooseAppropriateCurrency(store.state.walletState) ?: return
@ -75,7 +88,7 @@ class TradeCryptoMiddleware {
buyErc20TestnetTokens(
card = card,
walletManager = walletManager,
token = currency.token,
destinationAddress = currency.token.contractAddress,
)
}
return
@ -93,6 +106,56 @@ class TradeCryptoMiddleware {
}
}
private fun proceedNewBuyAction(state: () -> AppState?, action: TradeCryptoAction.New.Buy) {
val networkAddress = action.cryptoCurrencyStatus.value.networkAddress?.defaultAddress ?: return
if (action.checkUserLocation && state()?.globalState?.userCountryCode == RUSSIA_COUNTRY_CODE) {
store.dispatchOnMain(WalletAction.DialogAction.RussianCardholdersWarningDialog())
return
}
val status = action.cryptoCurrencyStatus
val currency = status.currency
val blockchain = Blockchain.fromId(currency.network.id.value)
if (currency is CryptoCurrency.Token && currency.network.isTestnet) {
scope.launch {
val walletManager = store.state.daggerGraphState
.get(DaggerGraphState::walletManagersFacade)
.getOrCreateWalletManager(
userWallet = action.userWallet,
blockchain = blockchain,
derivationPath = blockchain.derivationPath(
style = action.userWallet.scanResponse.derivationStyleProvider.getDerivationStyle(),
),
)
if (walletManager !is EthereumWalletManager) {
store.dispatchDebugErrorNotification("Testnet tokens available only for the Ethereum")
return@launch
}
buyErc20TestnetTokens(
card = action.userWallet.scanResponse.card,
walletManager = walletManager,
destinationAddress = currency.contractAddress,
)
}
return
}
val exchangeManager = store.state.globalState.exchangeManager
exchangeManager.getUrl(
action = CurrencyExchangeManager.Action.Buy,
blockchain = blockchain,
cryptoCurrencyName = currency.symbol,
fiatCurrencyName = action.appCurrencyCode,
walletAddress = networkAddress,
)?.let {
store.dispatchOpenUrl(it)
Analytics.send(Token.Topup.ScreenOpened())
}
}
private fun proceedSellAction() {
val selectedWalletData = store.state.walletState.selectedWalletData ?: return
val currency = chooseAppropriateCurrency(store.state.walletState) ?: return
@ -115,6 +178,22 @@ class TradeCryptoMiddleware {
}
}
private fun proceedNewSellAction(action: TradeCryptoAction.New.Sell) {
val networkAddress = action.cryptoCurrencyStatus.value.networkAddress?.defaultAddress ?: return
val currency = action.cryptoCurrencyStatus.currency
store.state.globalState.exchangeManager.getUrl(
action = CurrencyExchangeManager.Action.Sell,
blockchain = Blockchain.fromId(currency.network.id.value),
cryptoCurrencyName = currency.symbol,
fiatCurrencyName = action.appCurrencyCode,
walletAddress = networkAddress,
)?.let {
store.dispatchOpenUrl(it)
Analytics.send(Token.Withdraw.ScreenOpened())
}
}
private fun chooseAppropriateCurrency(walletState: WalletState): Currency? {
return if (walletState.primaryTokenData == null) {
walletState.selectedWalletData?.currency
@ -126,7 +205,7 @@ class TradeCryptoMiddleware {
}
}
private fun preconfigureAndOpenSendScreen(action: WalletAction.TradeCryptoAction.SendCrypto) {
private fun preconfigureAndOpenSendScreen(action: TradeCryptoAction.SendCrypto) {
val selectedWalletData = store.state.walletState.selectedWalletData ?: return
Analytics.send(Token.ButtonSend(AnalyticsParam.CurrencyType.Currency(selectedWalletData.currency)))
@ -160,16 +239,44 @@ class TradeCryptoMiddleware {
)?.let { store.dispatchOpenUrl(it) }
}
private fun openSwap() {
val currency = store.state.walletState.selectedWalletData?.currency?.toSwapCurrency()
val bundle =
bundleOf(
SwapFragment.CURRENCY_BUNDLE_KEY to Json.encodeToString(currency),
SwapFragment.DERIVATION_PATH to store.state.walletState.selectedWalletData?.currency?.derivationPath,
)
private fun openSwap(currency: SwapCurrency?) {
val bundle = bundleOf(
SwapFragment.CURRENCY_BUNDLE_KEY to Json.encodeToString(currency),
SwapFragment.DERIVATION_PATH to store.state.walletState.selectedWalletData?.currency?.derivationPath,
)
store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Swap, bundle = bundle))
}
private fun CryptoCurrency.toSwapCurrency(): SwapCurrency {
val blockchain = Blockchain.fromId(network.id.value)
return when (this) {
is CryptoCurrency.Coin -> {
SwapCurrency.NativeToken(
id = blockchain.toCoinId(),
name = name,
symbol = symbol,
networkId = blockchain.toNetworkId(),
// no need to set logoUrl for blockchain cause
// error when form url with coinId, coinId of eth and arbitrum the same
logoUrl = "",
)
}
is CryptoCurrency.Token -> {
SwapCurrency.NonNativeToken(
id = id.value,
name = name,
symbol = symbol,
networkId = blockchain.toNetworkId(),
logoUrl = getIconUrl(id.value),
contractAddress = contractAddress,
decimalCount = decimals,
)
}
}
}
private fun Currency.toSwapCurrency(): SwapCurrency {
return when (this) {
is Currency.Blockchain -> {

View file

@ -13,6 +13,7 @@ import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.userwallets.GetCardImageUseCase
import com.tangem.domain.wallets.legacy.lockIfLockable
import com.tangem.tap.*
@ -88,7 +89,7 @@ class WalletMiddleware {
val walletState = store.state.walletState
when (action) {
is WalletAction.TradeCryptoAction -> tradeCryptoMiddleware.handle(state, action)
is TradeCryptoAction -> tradeCryptoMiddleware.handle(state, action)
is WalletAction.Warnings -> warningsMiddleware.handle(action, globalState)
is WalletAction.MultiWallet -> multiWalletMiddleware.handle(action, walletState)
is WalletAction.AppCurrencyAction -> appCurrencyMiddleware.handle(action)

View file

@ -68,7 +68,6 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS
error = null,
)
}
is WalletAction.TradeCryptoAction -> return newState
is WalletAction.AppCurrencyAction -> {
newState = appCurrencyReducer.reduce(action, newState)
}

View file

@ -21,6 +21,7 @@ import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.feature.swap.api.SwapFeatureToggleManager
import com.tangem.feature.swap.domain.SwapInteractor
import com.tangem.sdk.extensions.dpToPx
@ -276,9 +277,9 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), SafeSt
) {
val exchangeManager = store.state.globalState.exchangeManager
binding.rowButtons.apply {
onBuyClick = { store.dispatch(WalletAction.TradeCryptoAction.Buy()) }
onSellClick = { store.dispatch(WalletAction.TradeCryptoAction.Sell) }
onSwapClick = { store.dispatch(WalletAction.TradeCryptoAction.Swap) }
onBuyClick = { store.dispatch(TradeCryptoAction.Buy()) }
onSellClick = { store.dispatch(TradeCryptoAction.Sell) }
onSwapClick = { store.dispatch(TradeCryptoAction.Swap) }
onTradeClick = {
store.dispatch(
WalletAction.DialogAction.ChooseTradeActionDialog(

View file

@ -23,7 +23,7 @@ import com.badoo.mvicore.modelWatcher
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.ui.fragments.setStatusBarColor
import com.tangem.core.ui.extensions.setStatusBarColor
import com.tangem.core.ui.utils.OneTouchClickListener
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.feature.swap.api.SwapFeatureToggleManager

View file

@ -4,6 +4,7 @@ import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.tap.common.extensions.show
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.models.WalletDialog
@ -39,15 +40,15 @@ class ChooseTradeActionBottomSheetDialog(
dialogBtnBuy.setOnClickListener {
dismiss()
store.dispatch(WalletAction.TradeCryptoAction.Buy())
store.dispatch(TradeCryptoAction.Buy())
}
dialogBtnSell.setOnClickListener {
dismiss()
store.dispatch(WalletAction.TradeCryptoAction.Sell)
store.dispatch(TradeCryptoAction.Sell)
}
dialogBtnSwap.setOnClickListener {
dismiss()
store.dispatch(WalletAction.TradeCryptoAction.Swap)
store.dispatch(TradeCryptoAction.Swap)
}
}
}

View file

@ -6,10 +6,10 @@ import android.view.LayoutInflater
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.tap.store
import com.tangem.wallet.databinding.DialogRussiansCardholdersWarningBinding
@ -40,7 +40,7 @@ class RussianCardholdersWarningBottomSheetDialog(
if (dialogData != null) {
store.dispatchOpenUrl(dialogData.topUpUrl)
} else {
store.dispatch(WalletAction.TradeCryptoAction.Buy(checkUserLocation = false))
store.dispatch(TradeCryptoAction.Buy(checkUserLocation = false))
}
dismiss()
}

View file

@ -4,6 +4,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import com.tangem.core.analytics.Analytics
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.*
import com.tangem.tap.domain.model.WalletDataModel
@ -151,9 +152,9 @@ class SingleWalletView : WalletView() {
val exchangeManager = store.state.globalState.exchangeManager
binding?.rowButtons?.apply {
onBuyClick = { store.dispatch(WalletAction.TradeCryptoAction.Buy()) }
onSellClick = { store.dispatch(WalletAction.TradeCryptoAction.Sell) }
onSwapClick = { store.dispatch(WalletAction.TradeCryptoAction.Swap) }
onBuyClick = { store.dispatch(TradeCryptoAction.Buy()) }
onSellClick = { store.dispatch(TradeCryptoAction.Sell) }
onSwapClick = { store.dispatch(TradeCryptoAction.Swap) }
onTradeClick = {
store.dispatch(
WalletAction.DialogAction.ChooseTradeActionDialog(

View file

@ -7,35 +7,32 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.SnackbarHost
import androidx.compose.material.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.fragments.ComposeBottomSheetFragment
import com.tangem.core.ui.components.wallets.RenameWalletDialogContent
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeBottomSheetFragment
import com.tangem.core.ui.theme.AppThemeModeHolder
import com.tangem.tap.common.analytics.events.MyWallets
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
import com.tangem.tap.features.walletSelector.ui.components.BiometricsDisabledWarningContent
import com.tangem.tap.features.walletSelector.ui.components.BiometricsLockoutWarningContent
import com.tangem.tap.features.walletSelector.ui.components.KeyInvalidatedWarningContent
import com.tangem.tap.features.walletSelector.ui.components.RemoveWalletDialogContent
import com.tangem.tap.features.walletSelector.ui.components.RenameWalletDialogContent
import com.tangem.tap.features.walletSelector.ui.components.WalletSelectorScreenContent
import com.tangem.tap.features.walletSelector.ui.components.*
import com.tangem.tap.features.walletSelector.ui.model.DialogModel
import com.tangem.tap.features.walletSelector.ui.model.WarningModel
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment<WalletSelectorScreenState>() {
internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment() {
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
private val viewModel by viewModels<WalletSelectorViewModel>()
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
@ -45,10 +42,8 @@ internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment<Wa
}
@Composable
override fun provideState(): State<WalletSelectorScreenState> = viewModel.state.collectAsState()
@Composable
override fun ScreenContent(state: WalletSelectorScreenState, modifier: Modifier) {
override fun ScreenContent(modifier: Modifier) {
val state by viewModel.state.collectAsStateWithLifecycle()
val snackbarHostState = remember { SnackbarHostState() }
val errorMessage by rememberUpdatedState(newValue = state.error?.resolveReference())
val dialog by rememberUpdatedState(newValue = state.dialog)
@ -90,7 +85,13 @@ internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment<Wa
if (dialog == null) return
when (dialog) {
is DialogModel.RemoveWalletDialog -> RemoveWalletDialogContent(dialog)
is DialogModel.RenameWalletDialog -> RenameWalletDialogContent(dialog)
is DialogModel.RenameWalletDialog -> {
RenameWalletDialogContent(
name = dialog.currentName,
onConfirm = dialog.onConfirm,
onDismiss = dialog.onDismiss,
)
}
is WarningModel.BiometricsLockoutWarning -> BiometricsLockoutWarningContent(dialog)
is WarningModel.KeyInvalidatedWarning -> KeyInvalidatedWarningContent(dialog)
is WarningModel.BiometricsDisabledWarning -> BiometricsDisabledWarningContent(dialog)

View file

@ -1,69 +0,0 @@
package com.tangem.tap.features.walletSelector.ui.components
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.AdditionalTextInputDialogParams
import com.tangem.core.ui.components.DialogButton
import com.tangem.core.ui.components.TextInputDialog
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.walletSelector.ui.model.DialogModel
import com.tangem.wallet.R
@Composable
internal fun RenameWalletDialogContent(dialog: DialogModel.RenameWalletDialog) {
var value by remember {
mutableStateOf(TextFieldValue(text = dialog.currentName))
}
TextInputDialog(
fieldValue = value,
confirmButton = DialogButton(
title = stringResource(id = R.string.common_ok),
enabled = value.text.isNotEmpty() && value.text != dialog.currentName,
onClick = { dialog.onConfirm(value.text) },
),
onDismissDialog = dialog.onDismiss,
onValueChange = { newValue ->
value = newValue
},
title = stringResource(R.string.user_wallet_list_rename_popup_title),
dismissButton = DialogButton(
title = stringResource(id = R.string.common_cancel),
onClick = dialog.onDismiss,
),
textFieldParams = AdditionalTextInputDialogParams(
label = stringResource(R.string.user_wallet_list_rename_popup_placeholder),
),
)
}
// region Preview
@Composable
private fun RenameWalletDialogContentSample(modifier: Modifier = Modifier) {
Column(
modifier = modifier,
) {
RenameWalletDialogContent(dialog = DialogModel.RenameWalletDialog("", {}, {}))
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun RenameWalletDialogContentPreview_Light() {
TangemTheme {
RenameWalletDialogContentSample()
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun RenameWalletDialogContentPreview_Dark() {
TangemTheme(isDark = true) {
RenameWalletDialogContentSample()
}
}
// endregion Preview

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.welcome.ui
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@ -10,21 +11,27 @@ import androidx.compose.material.SnackbarHostState
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.fragments.ComposeFragment
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.core.ui.theme.AppThemeModeHolder
import com.tangem.tap.common.analytics.events.SignIn
import com.tangem.tap.common.extensions.eraseContext
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
import com.tangem.tap.features.welcome.ui.components.WarningDialog
import com.tangem.tap.features.welcome.ui.components.WelcomeScreenContent
import com.tangem.wallet.R
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
internal class WelcomeFragment : ComposeFragment<WelcomeScreenState>() {
@AndroidEntryPoint
internal class WelcomeFragment : ComposeFragment() {
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
private val viewModel by viewModels<WelcomeViewModel>()
@ -35,19 +42,15 @@ internal class WelcomeFragment : ComposeFragment<WelcomeScreenState>() {
}
@Composable
override fun provideState(): State<WelcomeScreenState> {
return viewModel.state.collectAsState()
}
@Composable
override fun ScreenContent(state: WelcomeScreenState, modifier: Modifier) {
override fun ScreenContent(modifier: Modifier) {
val state by viewModel.state.collectAsStateWithLifecycle()
val snackbarHostState = remember { SnackbarHostState() }
val errorMessage by rememberUpdatedState(newValue = state.error?.resolveReference())
val warning by rememberUpdatedState(newValue = state.warning)
val backgroundColor = colorResource(id = R.color.background_primary)
val backgroundColor = TangemTheme.colors.background.primary
SystemBarsEffect {
setSystemBarsColor(color = backgroundColor)
setSystemBarsColor(backgroundColor)
}
BackHandler {
@ -56,7 +59,8 @@ internal class WelcomeFragment : ComposeFragment<WelcomeScreenState>() {
Box(
modifier = modifier
.systemBarsPadding(),
.systemBarsPadding()
.background(backgroundColor),
) {
WelcomeScreenContent(
showUnlockProgress = state.showUnlockWithBiometricsProgress,

View file

@ -5,7 +5,6 @@ import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchain.extensions.Result
import com.tangem.domain.models.scan.CardDTO
@ -87,17 +86,12 @@ class CurrencyExchangeManager(
}
}
suspend fun buyErc20TestnetTokens(card: CardDTO, walletManager: EthereumWalletManager, token: Token) {
suspend fun buyErc20TestnetTokens(card: CardDTO, walletManager: EthereumWalletManager, destinationAddress: String) {
walletManager.safeUpdate()
val amountToSend = Amount(walletManager.wallet.blockchain)
val destinationAddress = token.contractAddress
val feeResult = walletManager.getFee(
amountToSend,
destinationAddress,
) as? Result.Success ?: return
val feeResult = walletManager.getFee(amountToSend, destinationAddress) as? Result.Success ?: return
val fee = when (val feeForTx = feeResult.data) {
is TransactionFee.Choosable -> feeForTx.minimum
is TransactionFee.Single -> feeForTx.normal
@ -106,8 +100,6 @@ suspend fun buyErc20TestnetTokens(card: CardDTO, walletManager: EthereumWalletMa
val coinValue = walletManager.wallet.amounts[AmountType.Coin]?.value ?: BigDecimal.ZERO
if (coinValue < fee.amount.value) return
val transaction = walletManager.createTransaction(amountToSend, fee, destinationAddress)
val signer = TangemSigner(
card = card,
tangemSdk = store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).sdk,
@ -121,5 +113,13 @@ suspend fun buyErc20TestnetTokens(card: CardDTO, walletManager: EthereumWalletMa
),
)
}
walletManager.send(transaction, signer)
walletManager.send(
transactionData = walletManager.createTransaction(
amount = amountToSend,
fee = fee,
destination = destinationAddress,
),
signer = signer,
)
}

View file

@ -1,9 +1,11 @@
package com.tangem.tap.proxy
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.navigation.NavigationStateHolder
import com.tangem.core.navigation.ReduxNavController
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.tap.common.entities.FiatCurrency
@ -14,6 +16,7 @@ import com.tangem.tap.domain.walletStores.WalletStoresManager
import com.tangem.tap.features.wallet.redux.WalletState
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import org.rekotlin.Action
import org.rekotlin.Store
import javax.inject.Inject
@ -21,7 +24,7 @@ import javax.inject.Inject
* Holds objects from old modules, that missing in DI graph.
* Object sets manually to use in new modules and [AppStateHolder] proxies its to DI.
*/
class AppStateHolder @Inject constructor() : WalletsStateHolder, NavigationStateHolder {
class AppStateHolder @Inject constructor() : WalletsStateHolder, ReduxNavController, ReduxStateHolder {
override var userWalletsListManager: UserWalletsListManager? = null
set(value) {
@ -50,4 +53,10 @@ class AppStateHolder @Inject constructor() : WalletsStateHolder, NavigationState
override fun navigate(action: NavigationAction) {
mainStore?.dispatch(action)
}
override fun getBackStack(): List<AppScreen> = mainStore?.state?.navigationState?.backStack.orEmpty()
override fun dispatch(action: Action) {
mainStore?.dispatch(action)
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.tap.proxy
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.common.CompletionResult
@ -29,6 +30,7 @@ import com.tangem.tap.scope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.coroutines.suspendCoroutine
import com.tangem.tap.features.wallet.models.Currency as WalletModelCurrency
class DerivationManagerImpl(
private val appStateHolder: AppStateHolder,
@ -50,7 +52,7 @@ class DerivationManagerImpl(
val scanResponse = appStateHolder.scanResponse
if (scanResponse != null) {
val blockchainNetwork = BlockchainNetwork(blockchain, scanResponse.derivationStyleProvider)
val appCurrency = com.tangem.tap.features.wallet.models.Currency.fromBlockchainNetwork(
val appCurrency = WalletModelCurrency.fromBlockchainNetwork(
blockchainNetwork,
appToken,
)
@ -90,7 +92,7 @@ class DerivationManagerImpl(
private fun deriveMissingBlockchains(
scanResponse: ScanResponse,
currencyList: List<com.tangem.tap.features.wallet.models.Currency>,
currencyList: List<WalletModelCurrency>,
onSuccess: (ScanResponse) -> Unit,
onFailure: (Exception) -> Unit,
) {
@ -157,7 +159,7 @@ class DerivationManagerImpl(
private fun getDerivations(
curve: EllipticCurve,
scanResponse: ScanResponse,
currencyList: List<com.tangem.tap.features.wallet.models.Currency>,
currencyList: List<WalletModelCurrency>,
): DerivationData? {
val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null
@ -171,7 +173,7 @@ class DerivationManagerImpl(
it.blockchain.getSupportedCurves().contains(curve)
}.mapNotNull { it.derivationPath }.map { DerivationPath(it) }
val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct()
val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct().toMutableList()
if (bothCandidates.isEmpty()) return null
val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey()
@ -182,6 +184,13 @@ class DerivationManagerImpl(
val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) }
if (toDerive.isEmpty()) return null
currencyList.find { it is WalletModelCurrency.Blockchain && it.blockchain == Blockchain.Cardano }
?.let { currency ->
currency.derivationPath?.let {
bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it)))
}
}
return DerivationData(
derivations = mapKeyOfWalletPublicKey to toDerive,
)

View file

@ -6,6 +6,7 @@ import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.features.tester.api.TesterRouter
import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
@ -33,6 +34,7 @@ data class DaggerGraphState(
val scanCardProcessor: ScanCardProcessor? = null,
val cardSdkConfigRepository: CardSdkConfigRepository? = null,
val appCurrencyRepository: AppCurrencyRepository? = null,
val walletManagersFacade: WalletManagersFacade? = null,
) : StateType {
inline fun <reified T> get(getDependency: DaggerGraphState.() -> T?): T {

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="BaseAppTheme">
<item name="android:forceDarkAllowed">false</item>
</style>
</resources>
</resources>

View file

@ -37,5 +37,8 @@ fun createNetworkLoggingInterceptor(): Interceptor {
return LoggingInterceptor.Builder()
.setLevel(Level.BODY)
.log(Log.VERBOSE)
.tag(NETWORK_LOGS_TAG)
.build()
}
}
private const val NETWORK_LOGS_TAG = "NetworkLogs"

View file

@ -1,26 +0,0 @@
package com.tangem.datasource.local.appcurrency
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
// TODO: Will be implemented in [REDACTED_TASK_KEY] task
internal class MockSelectedAppCurrencyStore : SelectedAppCurrencyStore {
override fun get(): Flow<CurrenciesResponse.Currency> {
return flowOf(
CurrenciesResponse.Currency(
id = "usd",
code = "USD",
name = "US Dollar",
unit = "$",
type = "fiat",
rateBTC = "",
),
)
}
override suspend fun store(item: CurrenciesResponse.Currency) {
/* no-op */
}
}

View file

@ -7,5 +7,9 @@ interface SelectedAppCurrencyStore {
fun get(): Flow<CurrenciesResponse.Currency>
suspend fun getSyncOrNull(): CurrenciesResponse.Currency?
suspend fun store(item: CurrenciesResponse.Currency)
suspend fun isEmpty(): Boolean
}

View file

@ -7,4 +7,8 @@ import com.tangem.datasource.local.datastore.core.StringKeyDataStore
internal class DefaultSelectedAppCurrencyStore(
dataStore: StringKeyDataStore<CurrenciesResponse.Currency>,
) : SelectedAppCurrencyStore, KeylessDataStoreDecorator<CurrenciesResponse.Currency>(dataStore)
) : SelectedAppCurrencyStore, KeylessDataStoreDecorator<CurrenciesResponse.Currency>(dataStore) {
override suspend fun isEmpty(): Boolean {
return getSyncOrNull() == null
}
}

View file

@ -10,15 +10,15 @@ internal abstract class KeylessDataStoreDecorator<Value : Any>(
return STRING_KEY
}
fun get(): Flow<Value> {
open fun get(): Flow<Value> {
return get(Unit)
}
suspend fun getSyncOrNull(): Value? {
open suspend fun getSyncOrNull(): Value? {
return getSyncOrNull(Unit)
}
suspend fun store(item: Value) {
open suspend fun store(item: Value) {
store(Unit, item)
}

View file

@ -1,12 +1,14 @@
package com.tangem.core.navigation
/**
* Navigation state holder
* Navigation controller that based on redux actions
*
[REDACTED_AUTHOR]
*/
interface NavigationStateHolder {
interface ReduxNavController {
/** Navigate by [action] */
fun navigate(action: NavigationAction)
fun getBackStack(): List<AppScreen>
}

View file

@ -95,6 +95,7 @@
<string name="common_receive">Получить</string>
<string name="common_reject">Отклонить</string>
<string name="common_reload">Перезагрузить</string>
<string name="common_rename">Переименовать</string>
<string name="common_reset">Сбросить</string>
<string name="common_save_changes">Сохранить изменения</string>
<string name="common_search">Искать</string>
@ -116,6 +117,7 @@
<string name="common_transactions">Транзакции</string>
<string name="common_transfer">Перевод</string>
<string name="common_understand">Я понял</string>
<string name="common_unlock_needed">Необходима разблокировка</string>
<string name="common_unreachable">Недоступно</string>
<string name="common_yes">Да</string>
<string name="contract_address_copied_message">Адрес контракта скопирован!</string>
@ -457,6 +459,7 @@
<string name="transaction_history_contract_address">контракт: %s</string>
<string name="transaction_history_empty_transactions">У вас еще нет транзакций</string>
<string name="transaction_history_error_failed_to_load">Не удалось загрузить историю транзакций.\nНажмите на кнопку перезагрузки, чтобы обновить информацию.</string>
<string name="transaction_history_multiple_addresses">Несколько адресов</string>
<string name="transaction_history_not_supported_description">История транзакций в настоящее время не поддерживается для этого блокчейна. Но не волнуйтесь, мы работаем над этим! А пока вы можете проверить ее в обозревателе.</string>
<string name="transaction_history_transaction_from_address">от: %s</string>
<string name="transaction_history_transaction_to_address">на: %s</string>
@ -472,6 +475,8 @@
<string name="twins_recreate_toolbar">Tangem Twin</string>
<string name="twins_recreate_warning">Это действие необратимо. У вас не будет доступа к старому кошельку.</string>
<string name="twins_scan_twin_with_number">Приложите twin-карту с номером %s и не убирайте до окончания операции</string>
<string name="unlock_wallet_description_full">Используйте %s или отсканируйте карту, чтобы получить доступ к своему кошельку</string>
<string name="unlock_wallet_description_short">Используйте %s или отсканируйте карту</string>
<string name="user_wallet_list_add_button">Добавить новый кошелек</string>
<string name="user_wallet_list_delete_prompt">Вы уверены, что хотите удалить этот кошелек?</string>
<string name="user_wallet_list_editing_count">%d выбрано</string>

View file

@ -79,6 +79,7 @@
<string name="common_ok">OK</string>
<string name="common_origin_card">主卡片</string>
<string name="common_reject">拒絕</string>
<string name="common_rename">重新命名</string>
<string name="common_reset">重置</string>
<string name="common_save_changes">保存設置</string>
<string name="common_search">搜索</string>

View file

@ -94,6 +94,7 @@
<string name="common_receive">Receive</string>
<string name="common_reject">Reject</string>
<string name="common_reload">Reload</string>
<string name="common_rename">Rename</string>
<string name="common_reset">Reset</string>
<string name="common_save_changes">Save changes</string>
<string name="common_search">Search</string>
@ -115,6 +116,7 @@
<string name="common_transactions">Transactions</string>
<string name="common_transfer">Transfer</string>
<string name="common_understand">I understand</string>
<string name="common_unlock_needed">Unlock needed</string>
<string name="common_unreachable">Unreachable</string>
<string name="common_yes">Yes</string>
<string name="contract_address_copied_message">Contract address copied!</string>
@ -448,6 +450,7 @@
<string name="transaction_history_contract_address">contract: %s</string>
<string name="transaction_history_empty_transactions">You don\'t have any transactions yet</string>
<string name="transaction_history_error_failed_to_load">Failed to load transaction history.\nClick on reload button to update the information.</string>
<string name="transaction_history_multiple_addresses">Multiple addresses</string>
<string name="transaction_history_not_supported_description">Transaction history is currently not supported for this blockchain. But don\'t worry, we\'re working on it! In the meantime you can check it in the explorer.</string>
<string name="transaction_history_transaction_from_address">from: %s</string>
<string name="transaction_history_transaction_to_address">to: %s</string>
@ -463,6 +466,8 @@
<string name="twins_recreate_toolbar">Tangem Twin</string>
<string name="twins_recreate_warning">This action is irreversible. You will not have access to the old wallet.</string>
<string name="twins_scan_twin_with_number">Tap the twin card with number %s and do not remove until the end of the operation</string>
<string name="unlock_wallet_description_full">Use %s or scan a card to have an access to your wallet</string>
<string name="unlock_wallet_description_short">Use %s or scan a card</string>
<string name="user_wallet_list_add_button">Add new wallet</string>
<string name="user_wallet_list_delete_prompt">Are you sure you want to delete this wallet?</string>
<string name="user_wallet_list_editing_count">%d selected</string>

View file

@ -5,6 +5,13 @@ plugins {
}
dependencies {
/** Project - Domain */
implementation(projects.domain.tokens.models)
implementation(projects.domain.appTheme.models)
/** Project - Core */
implementation(projects.core.res)
/** AndroidX libraries */
implementation(deps.androidx.fragment.ktx)
implementation(deps.androidx.paging.runtime)
@ -22,6 +29,4 @@ dependencies {
implementation(deps.material)
implementation(deps.compose.shimmer)
implementation(deps.kotlin.immutable.collections)
implementation(project(":core:res"))
}

View file

@ -0,0 +1,74 @@
package com.tangem.core.ui.components
import androidx.annotation.DrawableRes
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import com.tangem.core.ui.res.TangemTheme
/**
* [Show in Figma](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=281-248&mode=design&t=bXqehWPHyATKcZEW-4)
* */
@Composable
fun SimpleSettingsRow(
title: String,
@DrawableRes icon: Int,
onItemsClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
subtitle: String? = null,
) {
Row(
modifier = modifier
.height(TangemTheme.dimens.size56)
.fillMaxWidth()
.clickable(
onClick = {
if (enabled) {
onItemsClick()
}
},
),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
) {
val textColor: Color by animateColorAsState(
targetValue = if (enabled) {
TangemTheme.colors.text.primary1
} else {
TangemTheme.colors.text.secondary
},
)
Icon(
painter = painterResource(id = icon),
contentDescription = null,
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing20),
tint = textColor,
)
Column(modifier = Modifier.padding(end = TangemTheme.dimens.spacing20)) {
Text(
text = title,
style = TangemTheme.typography.subtitle1,
color = textColor,
)
AnimatedVisibility(
visible = !subtitle.isNullOrEmpty(),
) {
Text(
text = subtitle ?: "",
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
)
}
}
}
}

View file

@ -7,6 +7,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import com.tangem.core.ui.res.TangemTheme
import com.valentinilk.shimmer.shimmer
@ -14,13 +15,13 @@ import com.valentinilk.shimmer.shimmer
* Rectangle shimmer item with rounded shape from DS
*/
@Composable
fun RectangleShimmer(modifier: Modifier = Modifier) {
fun RectangleShimmer(modifier: Modifier = Modifier, radius: Dp = TangemTheme.dimens.radius6) {
Box(
modifier = modifier
.shimmer()
.background(
color = TangemTheme.colors.button.secondary,
shape = RoundedCornerShape(TangemTheme.dimens.radius6),
shape = RoundedCornerShape(size = radius),
),
)
}
@ -31,16 +32,14 @@ fun RectangleShimmer(modifier: Modifier = Modifier) {
*/
@Composable
fun CircleShimmer(modifier: Modifier = Modifier) {
Box(modifier = modifier.shimmer()) {
Box(
modifier = Modifier
.matchParentSize()
.background(
color = TangemTheme.colors.button.secondary,
shape = CircleShape,
),
)
}
Box(
modifier = modifier
.shimmer()
.background(
color = TangemTheme.colors.button.secondary,
shape = CircleShape,
),
)
}
// region preview

View file

@ -33,7 +33,7 @@ fun HorizontalActionChips(
) {
items(
items = buttons,
key = { config -> "${config.text.hashCode()} ${config.iconResId}" },
key = { config -> config.text.hashCode() },
itemContent = { ActionButton(config = it) },
)
}

View file

@ -1,5 +1,6 @@
package com.tangem.core.ui.components.buttons.actions
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@ -7,6 +8,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -78,43 +80,52 @@ private fun Button(
modifier: Modifier = Modifier,
color: Color = TangemTheme.colors.button.secondary,
) {
val backgroundColor by animateColorAsState(
targetValue = if (config.enabled) color else TangemTheme.colors.button.disabled,
label = "Update background color",
)
Row(
modifier = modifier
.heightIn(min = TangemTheme.dimens.size36)
.clip(shape)
.background(
color = if (config.enabled) color else TangemTheme.colors.button.disabled,
shape = shape,
)
.background(color = backgroundColor, shape = shape)
.clickable(enabled = config.enabled, onClick = config.onClick)
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing24,
)
.padding(start = TangemTheme.dimens.spacing16, end = TangemTheme.dimens.spacing24)
.padding(vertical = TangemTheme.dimens.spacing8),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = painterResource(id = config.iconResId),
contentDescription = null,
modifier = Modifier.size(size = TangemTheme.dimens.size20),
tint = when {
val iconTint by animateColorAsState(
targetValue = when {
!config.enabled -> TangemTheme.colors.icon.informative
config.dimContent -> TangemTheme.colors.icon.secondary
else -> TangemTheme.colors.icon.primary1
},
label = "Update tint color",
)
Icon(
painter = painterResource(id = config.iconResId),
contentDescription = null,
modifier = Modifier.size(size = TangemTheme.dimens.size20),
tint = iconTint,
)
SpacerW8()
Text(
text = config.text.resolveReference(),
color = when {
val textColor by animateColorAsState(
targetValue = when {
!config.enabled -> TangemTheme.colors.text.disabled
config.dimContent -> TangemTheme.colors.text.secondary
else -> TangemTheme.colors.text.primary1
},
label = "Update text color",
)
Text(
text = config.text.resolveReference(),
color = textColor,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = TangemTheme.typography.button,

View file

@ -167,7 +167,7 @@ private fun LoadingContent() {
RectangleShimmer(
modifier = Modifier.size(
width = TangemTheme.dimens.size158,
height = TangemTheme.dimens.size20,
height = TangemTheme.dimens.size18,
),
)

View file

@ -4,6 +4,7 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
@ -22,6 +23,8 @@ import com.tangem.core.ui.R
import com.tangem.core.ui.components.CircleShimmer
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import java.util.UUID
@ -158,6 +161,18 @@ private fun Icon(state: TransactionState, modifier: Modifier = Modifier) {
is TransactionState.Loading -> {
CircleShimmer(modifier = modifier.size(TangemTheme.dimens.size40))
}
is TransactionState.Locked -> {
Box(modifier = modifier.size(TangemTheme.dimens.size40)) {
Box(
modifier = Modifier
.matchParentSize()
.background(
color = TangemTheme.colors.button.secondary,
shape = CircleShape,
),
)
}
}
}
}
@ -206,6 +221,11 @@ private fun Title(state: TransactionState, modifier: Modifier = Modifier) {
modifier = modifier.size(width = TangemTheme.dimens.size70, height = TangemTheme.dimens.size12),
)
}
is TransactionState.Locked -> {
LockedContent(
modifier = modifier.size(width = TangemTheme.dimens.size70, height = TangemTheme.dimens.size12),
)
}
}
}
@ -219,7 +239,7 @@ private fun Subtitle(state: TransactionState, modifier: Modifier = Modifier) {
is TransactionState.Send,
-> stringResource(
id = R.string.transaction_history_transaction_to_address,
state.address,
state.address.resolveReference(),
)
is TransactionState.Receiving,
is TransactionState.Receive,
@ -227,13 +247,13 @@ private fun Subtitle(state: TransactionState, modifier: Modifier = Modifier) {
is TransactionState.Approved,
-> stringResource(
id = R.string.transaction_history_transaction_from_address,
state.address,
state.address.resolveReference(),
)
is TransactionState.Swapping,
is TransactionState.Swapped,
-> stringResource(
id = R.string.transaction_history_contract_address,
state.address,
state.address.resolveReference(),
)
},
modifier = modifier,
@ -247,6 +267,11 @@ private fun Subtitle(state: TransactionState, modifier: Modifier = Modifier) {
modifier = modifier.size(width = TangemTheme.dimens.size52, height = TangemTheme.dimens.size12),
)
}
is TransactionState.Locked -> {
LockedContent(
modifier = modifier.size(width = TangemTheme.dimens.size52, height = TangemTheme.dimens.size12),
)
}
}
}
@ -267,6 +292,11 @@ private fun Amount(state: TransactionState, modifier: Modifier = Modifier) {
modifier = modifier.size(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12),
)
}
is TransactionState.Locked -> {
LockedContent(
modifier = modifier.size(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12),
)
}
}
}
@ -287,9 +317,24 @@ private fun Timestamp(state: TransactionState, modifier: Modifier = Modifier) {
modifier = modifier.size(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12),
)
}
is TransactionState.Locked -> {
LockedContent(
modifier = modifier.size(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12),
)
}
}
}
@Composable
private fun LockedContent(modifier: Modifier = Modifier) {
Box(
modifier = modifier.background(
color = TangemTheme.colors.field.primary,
shape = RoundedCornerShape(TangemTheme.dimens.radius6),
),
)
}
@Preview
@Composable
private fun Preview_TransactionItem_LightTheme(
@ -314,49 +359,49 @@ private class TransactionItemStateProvider : CollectionPreviewParameterProvider<
collection = listOf(
TransactionState.Sending(
txHash = UUID.randomUUID().toString(),
address = "33BddS...ga2B",
address = TextReference.Str("33BddS...ga2B"),
amount = "-0.500913 BTC",
timestamp = "8:41",
),
TransactionState.Receiving(
txHash = UUID.randomUUID().toString(),
address = "33BddS...ga2B",
address = TextReference.Str("33BddS...ga2B"),
amount = "+0.500913 BTC",
timestamp = "8:41",
),
TransactionState.Approving(
txHash = UUID.randomUUID().toString(),
address = "33BddS...ga2B",
address = TextReference.Str("33BddS...ga2B"),
amount = "+0.500913 BTC",
timestamp = "8:41",
),
TransactionState.Swapping(
txHash = UUID.randomUUID().toString(),
address = "33BddS...ga2B",
address = TextReference.Str("33BddS...ga2B"),
amount = "+0.500913 BTC",
timestamp = "8:41",
),
TransactionState.Send(
txHash = UUID.randomUUID().toString(),
address = "33BddS...ga2B",
address = TextReference.Str("33BddS...ga2B"),
amount = "-0.500913 BTC",
timestamp = "8:41",
),
TransactionState.Receive(
txHash = UUID.randomUUID().toString(),
address = "33BddS...ga2B",
address = TextReference.Str("33BddS...ga2B"),
amount = "+0.500913 BTC",
timestamp = "8:41",
),
TransactionState.Approved(
txHash = UUID.randomUUID().toString(),
address = "33BddS...ga2B",
address = TextReference.Str("33BddS...ga2B"),
amount = "+0.500913 BTC",
timestamp = "8:41",
),
TransactionState.Swapped(
txHash = UUID.randomUUID().toString(),
address = "33BddS...ga2B",
address = TextReference.Str("33BddS...ga2B"),
amount = "+0.500913 BTC",
timestamp = "8:41",
),

View file

@ -4,9 +4,11 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.ui.Modifier
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.itemsIndexed
import androidx.paging.compose.itemContentType
import androidx.paging.compose.itemKey
import com.tangem.core.ui.components.transactions.empty.EmptyTransactionBlock
import com.tangem.core.ui.components.transactions.empty.EmptyTransactionsBlockState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
@ -26,7 +28,7 @@ fun LazyListScope.txHistoryItems(
modifier: Modifier = Modifier,
) {
when (state) {
is TxHistoryState.ContentState -> {
is TxHistoryState.Content -> {
contentItems(
txHistoryItems = requireNotNull(txHistoryItems),
modifier = modifier,
@ -58,8 +60,18 @@ private fun LazyListScope.contentItems(
txHistoryItems: LazyPagingItems<TxHistoryState.TxHistoryItemState>,
modifier: Modifier = Modifier,
) {
txHistoryItems.itemKey { item ->
when (item) {
is TxHistoryState.TxHistoryItemState.GroupTitle -> item.title
is TxHistoryState.TxHistoryItemState.Title -> item.onExploreClick.hashCode()
is TxHistoryState.TxHistoryItemState.Transaction -> item.state.txHash
}
}
txHistoryItems.itemContentType { it::class.java }
itemsIndexed(
items = txHistoryItems,
items = txHistoryItems.itemSnapshotList.items,
key = { _, item ->
when (item) {
is TxHistoryState.TxHistoryItemState.GroupTitle -> item.title
@ -68,8 +80,6 @@ private fun LazyListScope.contentItems(
}
},
) { index, item ->
if (item == null) return@itemsIndexed
TxHistoryListItem(
state = item,
modifier = modifier

View file

@ -0,0 +1,10 @@
package com.tangem.core.ui.components.transactions.intents
interface TxHistoryClickIntents {
fun onBuyClick()
fun onReloadClick()
fun onExploreClick()
}

View file

@ -1,5 +1,7 @@
package com.tangem.core.ui.components.transactions.state
import com.tangem.core.ui.extensions.TextReference
/**
* Transaction component state
*
@ -20,14 +22,14 @@ sealed interface TransactionState {
*/
sealed class Content(
override val txHash: String,
open val address: String,
open val address: TextReference,
open val amount: String,
open val timestamp: String,
) : TransactionState {
fun copySealed(
txHash: String = this.txHash,
address: String = this.address,
address: TextReference = this.address,
amount: String = this.amount,
timestamp: String = this.timestamp,
): Content {
@ -54,7 +56,7 @@ sealed interface TransactionState {
*/
sealed class ProcessedTransactionContent(
override val txHash: String,
override val address: String,
override val address: TextReference,
override val amount: String,
override val timestamp: String,
) : Content(txHash, address, amount, timestamp)
@ -69,7 +71,7 @@ sealed interface TransactionState {
*/
sealed class CompletedTransactionContent(
override val txHash: String,
override val address: String,
override val address: TextReference,
override val amount: String,
override val timestamp: String,
) : Content(txHash, address, amount, timestamp)
@ -84,7 +86,7 @@ sealed interface TransactionState {
*/
data class Sending(
override val txHash: String,
override val address: String,
override val address: TextReference,
override val amount: String,
override val timestamp: String,
) : ProcessedTransactionContent(txHash, address, amount, timestamp)
@ -99,7 +101,7 @@ sealed interface TransactionState {
*/
data class Receiving(
override val txHash: String,
override val address: String,
override val address: TextReference,
override val amount: String,
override val timestamp: String,
) : ProcessedTransactionContent(txHash, address, amount, timestamp)
@ -114,7 +116,7 @@ sealed interface TransactionState {
*/
data class Approving(
override val txHash: String,
override val address: String,
override val address: TextReference,
override val amount: String,
override val timestamp: String,
) : ProcessedTransactionContent(txHash, address, amount, timestamp)
@ -129,7 +131,7 @@ sealed interface TransactionState {
*/
data class Swapping(
override val txHash: String,
override val address: String,
override val address: TextReference,
override val amount: String,
override val timestamp: String,
) : ProcessedTransactionContent(txHash, address, amount, timestamp)
@ -144,7 +146,7 @@ sealed interface TransactionState {
*/
data class Send(
override val txHash: String,
override val address: String,
override val address: TextReference,
override val amount: String,
override val timestamp: String,
) : CompletedTransactionContent(txHash, address, amount, timestamp)
@ -159,7 +161,7 @@ sealed interface TransactionState {
*/
data class Receive(
override val txHash: String,
override val address: String,
override val address: TextReference,
override val amount: String,
override val timestamp: String,
) : CompletedTransactionContent(txHash, address, amount, timestamp)
@ -174,7 +176,7 @@ sealed interface TransactionState {
*/
data class Approved(
override val txHash: String,
override val address: String,
override val address: TextReference,
override val amount: String,
override val timestamp: String,
) : CompletedTransactionContent(txHash, address, amount, timestamp)
@ -189,7 +191,7 @@ sealed interface TransactionState {
*/
data class Swapped(
override val txHash: String,
override val address: String,
override val address: TextReference,
override val amount: String,
override val timestamp: String,
) : CompletedTransactionContent(txHash, address, amount, timestamp)
@ -200,4 +202,11 @@ sealed interface TransactionState {
* @property txHash transaction hash
*/
data class Loading(override val txHash: String) : TransactionState
/**
* Locked state
*
* @property txHash transaction hash
*/
data class Locked(override val txHash: String) : TransactionState
}

View file

@ -1,77 +1,17 @@
package com.tangem.core.ui.components.transactions.state
import androidx.paging.PagingData
import com.tangem.core.ui.components.wallet.WalletLockedContentState
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.MutableStateFlow
/**
* Wallet transaction history state
*/
/** Wallet transaction history state */
sealed interface TxHistoryState {
/**
* Wallet transaction history state with content
*
* @property items content items
* @property contentItems content items
*/
sealed class ContentState(open val items: Flow<PagingData<TxHistoryItemState>>) : TxHistoryState
/**
* Loading state
*
* @property onExploreClick lambda be invoke when explore button was clicked
*/
data class Loading(val onExploreClick: () -> Unit) : ContentState(
items = flowOf(
PagingData.from(
listOf(
TxHistoryItemState.Title(onExploreClick = onExploreClick),
TxHistoryItemState.Transaction(state = TransactionState.Loading(txHash = LOADING_TX_HASH)),
),
),
),
)
/**
* Wallet transaction history state with loading transactions
*
* @property itemsCount count of loading transactions
*/
data class ContentWithLoadingItems(val itemsCount: Int) : ContentState(
items = flowOf(
value = PagingData.from(
data = buildList(capacity = itemsCount) {
add(TxHistoryItemState.Transaction(state = TransactionState.Loading(txHash = LOADING_TX_HASH)))
},
),
),
)
/**
* Wallet transaction history state with content
*
* @property items content items
*/
data class Content(override val items: Flow<PagingData<TxHistoryItemState>>) : ContentState(items)
/**
* Locked state
*
* @property onExploreClick lambda be invoke when explore button was clicked
*/
data class Locked(val onExploreClick: () -> Unit) :
ContentState(
items = flowOf(
PagingData.from(
listOf(
TxHistoryItemState.Title(onExploreClick = onExploreClick),
TxHistoryItemState.Transaction(state = TransactionState.Loading(txHash = LOADING_TX_HASH)),
),
),
),
),
WalletLockedContentState
data class Content(val contentItems: MutableStateFlow<PagingData<TxHistoryItemState>>) : TxHistoryState
/**
* Empty state
@ -119,7 +59,18 @@ sealed interface TxHistoryState {
data class Transaction(val state: TransactionState) : TxHistoryItemState
}
private companion object {
const val LOADING_TX_HASH = "LOADING_TX_HASH"
companion object {
private const val LOADING_TX_HASH = "LOADING_TX_HASH"
fun getDefaultLoadingTransactions(onExploreClick: () -> Unit): PagingData<TxHistoryItemState> {
return PagingData.from(
data = listOf(
TxHistoryItemState.Title(onExploreClick = onExploreClick),
TxHistoryItemState.Transaction(
state = TransactionState.Loading(txHash = LOADING_TX_HASH),
),
),
)
}
}
}

View file

@ -1,7 +0,0 @@
package com.tangem.core.ui.components.wallet
/**
* Wallet locked content state.
* It allows to divide the locked content of multi-currency and single-currency wallets.
*/
interface WalletLockedContentState

View file

@ -0,0 +1,59 @@
package com.tangem.core.ui.components.wallets
import androidx.compose.runtime.*
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.components.AdditionalTextInputDialogParams
import com.tangem.core.ui.components.DialogButton
import com.tangem.core.ui.components.TextInputDialog
import com.tangem.core.ui.res.TangemTheme
/**
* Rename a wallet dialog
*
* @param name wallet name
* @param onConfirm lambda be invoked when Confirm button is clicked
* @param onDismiss lambda be invoked when dialog is dismissed
*/
@Composable
fun RenameWalletDialogContent(name: String, onConfirm: (newName: String) -> Unit, onDismiss: () -> Unit) {
var value by remember { mutableStateOf(TextFieldValue(text = name)) }
TextInputDialog(
fieldValue = value,
confirmButton = DialogButton(
title = stringResource(id = R.string.common_ok),
enabled = value.text.isNotEmpty() && value.text != name,
onClick = { onConfirm(value.text) },
),
onDismissDialog = onDismiss,
onValueChange = { value = it },
title = stringResource(R.string.user_wallet_list_rename_popup_title),
dismissButton = DialogButton(title = stringResource(id = R.string.common_cancel), onClick = onDismiss),
textFieldParams = AdditionalTextInputDialogParams(
label = stringResource(R.string.user_wallet_list_rename_popup_placeholder),
),
)
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun RenameWalletDialogContentPreview_Light() {
TangemTheme(isDark = false) {
RenameWalletDialogContent(name = "", onConfirm = {}, onDismiss = {})
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun RenameWalletDialogContentPreview_Dark() {
TangemTheme(isDark = true) {
RenameWalletDialogContent(name = "", onConfirm = {}, onDismiss = {})
}
}
// endregion Preview

View file

@ -0,0 +1,23 @@
package com.tangem.core.ui.event
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.NonRestartableComposable
/**
* A Composable function that reacts to a given [StateEvent], executing the provided action only once when the event
* is triggered.
*
* @param event The [StateEvent] to listen to.
* @param onTrigger The action to execute when the event is triggered.
*/
@Composable
@NonRestartableComposable
fun EventEffect(event: StateEvent, onTrigger: suspend () -> Unit) {
LaunchedEffect(event) {
if (event is StateEvent.Triggered) {
onTrigger()
event.consume()
}
}
}

View file

@ -0,0 +1,51 @@
package com.tangem.core.ui.event
import androidx.compose.runtime.Immutable
/**
* Represents compose state event, which can be consumed or triggered.
*
* This is especially useful for handling one-off UI events like showing snack bars or navigation which should not be
* re-triggered on recompositions or state changes.
*/
@Immutable
sealed class StateEvent {
/** Defines the action to be executed when the event is consumed. */
protected abstract val onConsume: () -> Unit
/**
* Represents an already consumed state event.
* Events of this type will not trigger any further actions.
*/
object Consumed : StateEvent() {
override val onConsume: () -> Unit = {}
}
/**
* Represents a state event that has been triggered but not yet consumed.
*
* @property onConsume The action to be executed when the event is consumed.
*/
data class Triggered(override val onConsume: () -> Unit) : StateEvent()
/**
* Consumes the event, triggering any associated action.
*/
fun consume() {
onConsume()
}
}
/**
* Creates a [StateEvent.Triggered] instance.
*
* @param onConsume The action to be executed when the event is consumed.
* @return A triggered state event.
*/
fun triggered(onConsume: () -> Unit): StateEvent.Triggered = StateEvent.Triggered(onConsume)
/**
* Represents a statically defined [StateEvent.Consumed] event.
*/
val consumed: StateEvent.Consumed = StateEvent.Consumed

View file

@ -0,0 +1,45 @@
package com.tangem.core.ui.extensions
import androidx.annotation.DrawableRes
import com.tangem.core.ui.R
import com.tangem.domain.tokens.models.CryptoCurrency
/**
* Retrieves the resource ID for the network badge of a [CryptoCurrency].
*
* This property provides a way to fetch the appropriate drawable resource ID
* for the network badge of a given cryptocurrency. For coins, this will typically
* return null as they do not have network badges, while tokens will fetch the icon
* based on their associated network ID.
*
* @return Drawable resource ID for the network badge or null if the cryptocurrency is a coin.
*/
@get:DrawableRes
val CryptoCurrency.networkBadgeIconResId: Int?
get() = when (this) {
is CryptoCurrency.Coin -> null
is CryptoCurrency.Token -> getActiveIconRes(network.id.value)
}
/**
* Retrieves the resource ID for the icon of a [CryptoCurrency].
*
* This property provides a way to fetch the appropriate drawable resource ID
* for the icon of a given cryptocurrency.
*
* @return Drawable resource ID for the cryptocurrency icon.
*/
@get:DrawableRes
val CryptoCurrency.iconResId: Int
get() = when (this) {
is CryptoCurrency.Coin -> {
val rawCoinId = id.rawCurrencyId
if (rawCoinId != null) {
getActiveIconResByCoinId(rawCoinId, network.id.value)
} else {
R.drawable.ic_alert_24
}
}
is CryptoCurrency.Token -> R.drawable.ic_alert_24
}

View file

@ -1,4 +1,4 @@
package com.tangem.core.ui.fragments
package com.tangem.core.ui.extensions
import android.view.WindowManager
import androidx.annotation.ColorRes

View file

@ -20,12 +20,19 @@ sealed interface TextReference {
* Text resource id
*
* @property id resource id
* @property formatArgs arguments
*
* Impossible to use [kotlinx.collections.immutable.ImmutableList] because [Any] is unstable.
* @property formatArgs arguments. Impossible to use [kotlinx.collections.immutable.ImmutableList] because [Any] is
* unstable.
*/
data class Res(@StringRes val id: Int, val formatArgs: WrappedList<Any> = WrappedList(emptyList())) : TextReference
/**
* Plural resource id
*
* @property id resource id
* @property count count
* @property formatArgs arguments. Impossible to use [kotlinx.collections.immutable.ImmutableList] because [Any] is
* unstable.
*/
data class PluralRes(@PluralsRes val id: Int, val count: Int, val formatArgs: WrappedList<Any>) : TextReference
/**
@ -34,9 +41,16 @@ sealed interface TextReference {
* @property value value
*/
data class Str(val value: String) : TextReference
/**
* Combined reference. It concatenates all [refs].
*
* @see [TextReference.plus] method
*/
data class Combined(val refs: WrappedList<TextReference>) : TextReference
}
/** Get text */
/** Resolve [TextReference] as [String] */
@Composable
@ReadOnlyComposable
fun TextReference.resolveReference(): String {
@ -44,5 +58,23 @@ fun TextReference.resolveReference(): String {
is TextReference.Res -> stringResource(id, *formatArgs.toTypedArray())
is TextReference.PluralRes -> pluralStringResource(id, count, *formatArgs.toTypedArray())
is TextReference.Str -> value
is TextReference.Combined -> {
buildString {
refs.forEach {
append(it.resolveReference())
}
}
}
}
}
/** Concatenate [this] reference with [ref] */
operator fun TextReference.plus(ref: TextReference): TextReference {
return when (this) {
is TextReference.Combined -> copy(refs = (refs.data + ref).toWrappedList())
is TextReference.PluralRes,
is TextReference.Res,
is TextReference.Str,
-> TextReference.Combined(refs = wrappedList(this, ref))
}
}

View file

@ -7,4 +7,8 @@ import androidx.compose.runtime.Immutable
*/
@JvmInline
@Immutable
value class WrappedList<T>(val data: List<T>) : List<T> by data
value class WrappedList<T>(val data: List<T>) : List<T> by data
fun <T> List<T>.toWrappedList(): WrappedList<T> = WrappedList(data = this)
fun <T> wrappedList(vararg elements: T): WrappedList<T> = WrappedList(data = listOf(*elements))

View file

@ -1,11 +0,0 @@
package com.tangem.core.ui.fragments
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
abstract class ComposeActivity<ScreenState> : AppCompatActivity(), ComposeScreen<ScreenState> {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(createComposeView(context = this))
}
}

View file

@ -1,13 +0,0 @@
package com.tangem.core.ui.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
abstract class ComposeFragment<ScreenState> : Fragment(), ComposeScreen<ScreenState> {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return createComposeView(inflater.context)
}
}

View file

@ -1,44 +0,0 @@
package com.tangem.core.ui.fragments
import android.content.Context
import android.view.View
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme
internal interface ComposeScreen<ScreenState> {
fun createComposeView(context: Context): View {
return ComposeView(context).apply {
setContent {
TangemTheme {
val backgroundColor = TangemTheme.colors.background.primary
SystemBarsEffect {
setSystemBarsColor(
color = backgroundColor,
)
}
ScreenContent(
state = provideState().value,
modifier = Modifier
.fillMaxSize()
.background(color = backgroundColor),
)
}
}
}
}
@Suppress("TopLevelComposableFunctions")
@Composable
fun provideState(): State<ScreenState>
@Suppress("TopLevelComposableFunctions")
@Composable
fun ScreenContent(state: ScreenState, modifier: Modifier)
}

View file

@ -46,6 +46,7 @@ data class TangemDimens internal constructor(
val size10: Dp = 10.dp,
val size11: Dp = 11.dp,
val size12: Dp = 12.dp,
val size14: Dp = 14.dp,
val size16: Dp = 16.dp,
val size18: Dp = 18.dp,
val size20: Dp = 20.dp,

View file

@ -0,0 +1,17 @@
package com.tangem.core.ui.screen
import android.os.Bundle
import androidx.activity.ComponentActivity
/**
* An abstract base class for activities that use Compose for UI rendering.
* Extends [ComponentActivity] and implements [ComposeScreen] interface.
*/
abstract class ComposeActivity : ComponentActivity(), ComposeScreen {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(createComposeView(context = this))
}
}

View file

@ -1,7 +1,6 @@
package com.tangem.core.ui.fragments
package com.tangem.core.ui.screen
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@ -10,20 +9,46 @@ import androidx.annotation.FloatRange
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.tangem.core.ui.R
import com.tangem.core.ui.res.TangemTheme
abstract class ComposeBottomSheetFragment<ScreenState> : BottomSheetDialogFragment(), ComposeScreen<ScreenState> {
/**
* An abstract base class for bottom sheet dialogs that use Compose for UI rendering.
* Extends [BottomSheetDialogFragment] and implements [ComposeScreen] interface.
*/
abstract class ComposeBottomSheetFragment : BottomSheetDialogFragment(), ComposeScreen {
/**
* The initial state of the bottom sheet. Default is [BottomSheetBehavior.STATE_EXPANDED].
*/
open val initialBottomSheetState = BottomSheetBehavior.STATE_EXPANDED
/**
* The fraction of the screen height that the bottom sheet should take when expanded.
* Default is `null`, indicating that the height will be determined by the content.
*/
@FloatRange(from = 0.0, to = 1.0)
open val expandedHeightFraction: Float? = null
override val screenModifier: Modifier
@Composable
@ReadOnlyComposable
get() = Modifier
.fillMaxWidth()
.let {
if (expandedHeightFraction != null) it.fillMaxHeight(expandedHeightFraction!!) else it
}
.background(
color = TangemTheme.colors.background.plain,
shape = TangemTheme.shapes.bottomSheet,
)
override fun getTheme(): Int = R.style.AppTheme_TransparentBottomSheetDialog
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
@ -40,25 +65,4 @@ abstract class ComposeBottomSheetFragment<ScreenState> : BottomSheetDialogFragme
return dialog
}
override fun createComposeView(context: Context): View {
return ComposeView(context).apply {
setContent {
TangemTheme {
ScreenContent(
state = provideState().value,
modifier = Modifier
.fillMaxWidth()
.let {
if (expandedHeightFraction != null) it.fillMaxHeight(expandedHeightFraction!!) else it
}
.background(
color = TangemTheme.colors.background.plain,
shape = TangemTheme.shapes.bottomSheet,
),
)
}
}
}
}
}

View file

@ -0,0 +1,37 @@
package com.tangem.core.ui.screen
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.tangem.core.ui.R
/**
* An abstract base class for fragments that use Compose for UI rendering.
* Extends [Fragment] and implements [ComposeScreen] interface.
*/
abstract class ComposeFragment : Fragment(), ComposeScreen {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val isTransitionsInflated = TransitionInflater.from(requireContext()).inflateTransitions()
return createComposeView(inflater.context).also {
it.isTransitionGroup = isTransitionsInflated
}
}
/**
* Inflates transitions for the fragment. Override this method to customize
* enter and exit transitions for the fragment.
*
* @return `true` if transitions were inflated; `false` otherwise.
*/
protected open fun TransitionInflater.inflateTransitions(): Boolean {
enterTransition = inflateTransition(R.transition.slide_right)
exitTransition = inflateTransition(R.transition.fade)
return true
}
}

View file

@ -0,0 +1,79 @@
package com.tangem.core.ui.screen
import android.content.Context
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.theme.AppThemeModeHolder
import com.tangem.domain.apptheme.model.AppThemeMode
/**
* Interface representing a Compose screen with common theming and content composition properties.
*
* This interface defines properties and functions that allow a Compose screen to manage its theme,
* background color, and screen modifier. It also provides a composable function to define the content
* of the screen.
*/
internal interface ComposeScreen {
/**
* The holder for managing the current application theme mode.
*/
val appThemeModeHolder: AppThemeModeHolder
/**
* The screen modifier.
*/
val screenModifier: Modifier
@Composable
@ReadOnlyComposable
get() = Modifier.fillMaxSize()
/**
* Composable function to define the content of the screen.
*
* @param modifier The modifier to apply to the screen content.
*/
@Suppress("TopLevelComposableFunctions")
@Composable
fun ScreenContent(modifier: Modifier)
}
/**
* Creates a [ComposeView] with the defined content for the Compose screen.
*
* @param context The context.
* @return A [ComposeView] instance with the defined screen content.
*/
internal fun ComposeScreen.createComposeView(context: Context): ComposeView {
return ComposeView(context).apply {
setContent {
val appThemeMode by appThemeModeHolder.appThemeMode
TangemTheme(isDark = shouldUseDarkTheme(appThemeMode)) {
ScreenContent(modifier = screenModifier)
}
}
}
}
/**
* Determines whether the dark theme should be used based on the given [AppThemeMode].
*
* @param appThemeMode The application theme mode.
* @return `true` if the dark theme should be used, `false` otherwise.
*/
@Composable
@ReadOnlyComposable
private fun shouldUseDarkTheme(appThemeMode: AppThemeMode): Boolean {
return when (appThemeMode) {
AppThemeMode.FORCE_DARK -> true
AppThemeMode.FORCE_LIGHT -> false
AppThemeMode.FOLLOW_SYSTEM -> isSystemInDarkTheme()
}
}

View file

@ -0,0 +1,17 @@
package com.tangem.core.ui.theme
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import com.tangem.domain.apptheme.model.AppThemeMode
/**
* Representing a holder for the application theme mode.
*/
@Stable
interface AppThemeModeHolder {
/**
* A [State] representing the current application theme mode.
*/
val appThemeMode: State<AppThemeMode>
}

View file

@ -12,22 +12,14 @@ object BigDecimalFormatter {
private const val TEMP_CURRENCY_CODE = "USD"
fun formatCryptoAmount(
cryptoAmount: BigDecimal,
cryptoCurrency: String,
decimals: Int,
locale: Locale = Locale.getDefault(),
): String {
val formatterCurrency = getCurrency(cryptoCurrency)
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
currency = formatterCurrency
fun formatCryptoAmount(cryptoAmount: BigDecimal, cryptoCurrency: String, decimals: Int): String {
val formatter = NumberFormat.getNumberInstance().apply {
maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8)
minimumFractionDigits = 2
roundingMode = RoundingMode.DOWN
}
return formatter.format(cryptoAmount)
.replace(formatterCurrency.getSymbol(locale), cryptoCurrency)
return formatter.format(cryptoAmount) + "\u2009$cryptoCurrency"
}
fun formatFiatAmount(

View file

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Before After
Before After

View file

@ -10,9 +10,9 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEmpty
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.joda.time.Duration
import timber.log.Timber
@ -27,11 +27,18 @@ internal class DefaultAppCurrencyRepository(
private val appCurrencyConverter = AppCurrencyConverter()
override fun getSelectedAppCurrency(): Flow<AppCurrency> {
return selectedAppCurrencyStore.get()
.onEmpty { fetchDefaultAppCurrency() }
.map(appCurrencyConverter::convert)
.flowOn(dispatchers.io)
override fun getSelectedAppCurrency(): Flow<AppCurrency> = channelFlow {
launch(dispatchers.io) {
selectedAppCurrencyStore.get()
.map(appCurrencyConverter::convert)
.collect(::send)
}
launch(dispatchers.io) {
if (selectedAppCurrencyStore.isEmpty()) {
fetchDefaultAppCurrency()
}
}
}
override suspend fun getAvailableAppCurrencies(): List<AppCurrency> {

1
data/app-theme/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,34 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
id("configuration")
}
android {
namespace = "com.tangem.data.apptheme"
}
dependencies {
/** Project - Domain */
implementation(projects.domain.core)
implementation(projects.domain.appTheme)
implementation(projects.domain.appTheme.models)
/** Project - Data */
implementation(projects.core.datasource)
implementation(projects.data.common)
/** Project - Utils */
implementation(projects.core.utils)
/** DI */
implementation(deps.hilt.core)
kapt(deps.hilt.kapt)
/** Other */
implementation(deps.kotlin.coroutines)
implementation(deps.timber)
implementation(deps.jodatime)
}

View file

@ -0,0 +1,19 @@
package com.tangem.data.apptheme
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
internal class MockAppThemeModeRepository : AppThemeModeRepository {
private val appThemeModeFlow = MutableStateFlow(AppThemeMode.DEFAULT)
override fun getAppThemeMode(): Flow<AppThemeMode> {
return appThemeModeFlow
}
override suspend fun changeAppThemeMode(mode: AppThemeMode) {
appThemeModeFlow.value = mode
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.data.apptheme.di
import com.tangem.data.apptheme.MockAppThemeModeRepository
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
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 AppThemeModeDataModule {
@Provides
@Singleton
fun provideAppThemeModeRepository(): AppThemeModeRepository {
return MockAppThemeModeRepository()
}
}

View file

@ -15,4 +15,8 @@ internal class DefaultSettingsRepository(
preferencesDataSource.appRatingLaunchObserver.isReadyToShow()
}
}
override suspend fun shouldShowSaveUserWalletScreen(): Boolean {
return withContext(dispatchers.io) { preferencesDataSource.shouldShowSaveUserWalletScreen }
}
}

View file

@ -8,8 +8,8 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.token.UserTokensStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.core.error.DataError
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.core.error.DataError
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.tokens.repository.CurrenciesRepository
@ -53,6 +53,22 @@ internal class DefaultCurrenciesRepository(
storeAndPushTokens(userWalletId, response)
}
override suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency) =
withContext(dispatchers.io) {
val savedCurrencies = requireNotNull(
value = userTokensStore.getSyncOrNull(userWalletId),
lazyMessage = { "Saved tokens empty. Can not perform remove currency action" },
)
val token = userTokensResponseFactory.createResponseToken(currency)
storeAndPushTokens(
userWalletId = userWalletId,
response = savedCurrencies.copy(
tokens = savedCurrencies.tokens.filter { it != token },
),
)
}
override suspend fun getSingleCurrencyWalletPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency {
return withContext(dispatchers.io) {
val userWallet = getUserWallet(userWalletId)
@ -62,20 +78,38 @@ internal class DefaultCurrenciesRepository(
}
}
override fun getMultiCurrencyWalletCurrencies(
override fun getMultiCurrencyWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow<List<CryptoCurrency>> {
return channelFlow {
val userWallet = getUserWallet(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true)
launch(dispatchers.io) {
getMultiCurrencyWalletCurrencies(userWallet).collect(::send)
}
launch(dispatchers.io) {
fetchTokensIfCacheExpired(userWallet, refresh = false)
}
}
}
override suspend fun getMultiCurrencyWalletCurrenciesSync(
userWalletId: UserWalletId,
refresh: Boolean,
): Flow<List<CryptoCurrency>> = channelFlow {
): List<CryptoCurrency> {
val userWallet = getUserWallet(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true)
launch(dispatchers.io) {
getMultiCurrencyWalletCurrencies(userWallet).collect(::send)
fetchTokensIfCacheExpired(userWallet, refresh)
val storedTokens = requireNotNull(userTokensStore.getSyncOrNull(userWallet.walletId)) {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
}
launch(dispatchers.io) {
fetchTokensIfCacheExpired(userWallet, refresh)
}
return responseCurrenciesFactory.createCurrencies(
response = storedTokens,
card = userWallet.scanResponse.card,
)
}
override suspend fun getMultiCurrencyWalletCurrency(
@ -164,7 +198,7 @@ internal class DefaultCurrenciesRepository(
tangemTechApi.saveUserTokens(userWallet.walletId.stringValue, response)
} else {
throw error
Timber.e(error, "Unable to fetch currencies for: ${userWallet.walletId}")
}
}

View file

@ -16,14 +16,8 @@ import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.addOrReplace
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
internal class DefaultNetworksRepository(
private val walletManagersFacade: WalletManagersFacade,
@ -45,10 +39,9 @@ internal class DefaultNetworksRepository(
return networkConverter.convertSet(networksIds)
}
override fun getNetworkStatuses(
override fun getNetworkStatusesUpdates(
userWalletId: UserWalletId,
networks: Set<Network.ID>,
refresh: Boolean,
): Flow<Set<NetworkStatus>> = channelFlow {
launch(dispatchers.io) {
networksStatuses.collect {
@ -57,10 +50,19 @@ internal class DefaultNetworksRepository(
}
launch(dispatchers.io) {
fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh)
fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh = false)
}
}
override suspend fun getNetworkStatusesSync(
userWalletId: UserWalletId,
networks: Set<Network.ID>,
refresh: Boolean,
): Set<NetworkStatus> = withContext(dispatchers.io) {
fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh)
networksStatuses.first().toSet()
}
private suspend fun fetchNetworksStatusesIfCacheExpired(
userWalletId: UserWalletId,
networks: Set<Network.ID>,
@ -88,7 +90,7 @@ internal class DefaultNetworksRepository(
private suspend fun fetchNetworkStatus(userWalletId: UserWalletId, networkId: Network.ID) {
val currencies = getCurrencies(userWalletId)
.asSequence()
.filter { it.networkId == networkId }
.filter { it.network.id == networkId }
val result = walletManagersFacade.update(
userWalletId = userWalletId,

View file

@ -9,11 +9,9 @@ import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.tokens.models.Quote
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
internal class DefaultQuotesRepository(
@ -28,7 +26,7 @@ internal class DefaultQuotesRepository(
private var quotesFetchedForAppCurrency: String? = null
override fun getQuotes(currenciesIds: Set<CryptoCurrency.ID>, refresh: Boolean): Flow<Set<Quote>> {
override fun getQuotesUpdates(currenciesIds: Set<CryptoCurrency.ID>): Flow<Set<Quote>> {
return channelFlow {
launch(dispatchers.io) {
quotesStore.get(currenciesIds)
@ -38,12 +36,26 @@ internal class DefaultQuotesRepository(
launch(dispatchers.io) {
selectedAppCurrencyStore.get().collectLatest { appCurrency ->
fetchExpiredQuotes(currenciesIds, appCurrency.id, refresh)
fetchExpiredQuotes(currenciesIds, appCurrency.id, refresh = false)
}
}
}
}
override suspend fun getQuotesSync(currenciesIds: Set<CryptoCurrency.ID>, refresh: Boolean): Set<Quote> {
return withContext(dispatchers.io) {
val selectedAppCurrency = requireNotNull(selectedAppCurrencyStore.getSyncOrNull()) {
"Unable to get selected application currency to update quotes"
}
fetchExpiredQuotes(currenciesIds, selectedAppCurrency.id, refresh)
val quotes = quotesStore.get(currenciesIds).first()
quotesConverter.convertSet(quotes)
}
}
private suspend fun fetchExpiredQuotes(
currenciesIds: Set<CryptoCurrency.ID>,
appCurrencyId: String,

View file

@ -1,11 +1,12 @@
package com.tangem.data.tokens.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token as SdkToken
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.tokens.models.CryptoCurrency
import timber.log.Timber
import com.tangem.blockchain.common.Token as SdkToken
// FIXME: Make internal
class CryptoCurrencyFactory {
fun createToken(
@ -19,9 +20,10 @@ class CryptoCurrencyFactory {
}
val id = getTokenId(blockchain, sdkToken)
return CryptoCurrency.Token(
id = id,
networkId = getNetworkId(blockchain),
network = getNetwork(blockchain) ?: return null,
name = sdkToken.name,
symbol = sdkToken.symbol,
iconUrl = getTokenIconUrl(blockchain, sdkToken),
@ -29,8 +31,6 @@ class CryptoCurrencyFactory {
isCustom = isCustomToken(id),
contractAddress = sdkToken.contractAddress,
derivationPath = getDerivationPath(blockchain, derivationStyleProvider),
blockchainName = blockchain.fullName,
standardType = getTokenStandardType(blockchain, sdkToken),
)
}
@ -42,7 +42,7 @@ class CryptoCurrencyFactory {
return CryptoCurrency.Coin(
id = getCoinId(blockchain),
networkId = getNetworkId(blockchain),
network = getNetwork(blockchain) ?: return null,
name = blockchain.fullName,
symbol = blockchain.currency,
iconUrl = getCoinIconUrl(blockchain),

View file

@ -3,22 +3,13 @@ package com.tangem.data.tokens.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.tokens.models.Network
import com.tangem.utils.converter.Converter
import timber.log.Timber
internal class NetworkConverter : Converter<Network.ID, Network?> {
override fun convert(value: Network.ID): Network? {
val blockchain = Blockchain.fromId(value.value)
if (blockchain == Blockchain.Unknown) {
Timber.e("Unable to convert Unknown blockchain to the domain network model")
return null
}
return Network(
id = value,
name = blockchain.fullName,
)
return getNetwork(blockchain)
}
override fun convertList(input: Collection<Network.ID>): List<Network> {

View file

@ -0,0 +1,29 @@
package com.tangem.data.tokens.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.tokens.models.Network
import timber.log.Timber
internal fun getNetwork(blockchain: Blockchain): Network? {
if (blockchain == Blockchain.Unknown) {
Timber.e("Unable to convert Unknown blockchain to the domain network model")
return null
}
return Network(
id = Network.ID(blockchain.id),
name = blockchain.fullName,
isTestnet = blockchain.isTestnet(),
standardType = getNetworkStandardType(blockchain),
)
}
private fun getNetworkStandardType(blockchain: Blockchain): Network.StandardType {
return when (blockchain) {
Blockchain.Ethereum, Blockchain.EthereumTestnet -> Network.StandardType.ERC20
Blockchain.BSC, Blockchain.BSCTestnet -> Network.StandardType.BEP20
Blockchain.Binance, Blockchain.BinanceTestnet -> Network.StandardType.BEP2
Blockchain.Tron, Blockchain.TronTestnet -> Network.StandardType.TRC20
else -> Network.StandardType.Unspecified(blockchain.name)
}
}

View file

@ -1,10 +1,14 @@
package com.tangem.data.tokens.utils
import com.tangem.domain.tokens.model.NetworkAddress
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.tokens.model.PendingTransaction
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.tokens.models.Network
import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount
import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction
import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
import timber.log.Timber
import java.math.BigDecimal
internal class NetworkStatusFactory {
@ -19,10 +23,18 @@ internal class NetworkStatusFactory {
value = when (result) {
is UpdateWalletManagerResult.MissedDerivation -> NetworkStatus.MissedDerivation
is UpdateWalletManagerResult.Unreachable -> NetworkStatus.Unreachable
is UpdateWalletManagerResult.NoAccount -> NetworkStatus.NoAccount(result.amountToCreateAccount)
is UpdateWalletManagerResult.NoAccount -> NetworkStatus.NoAccount(
address = getNetworkAddress(result.defaultAddress, result.addresses),
amountToCreateAccount = result.amountToCreateAccount,
)
is UpdateWalletManagerResult.Verified -> NetworkStatus.Verified(
amounts = formatAmounts(result.tokensAmounts, currencies),
hasTransactionsInProgress = result.hasTransactionsInProgress,
address = getNetworkAddress(result.defaultAddress, result.addresses),
amounts = formatAmounts(result.currenciesAmounts, currencies),
pendingTransactions = formatTransactions(
networksAddresses = result.addresses,
transactions = result.currentTransactions,
currencies = currencies,
),
)
},
)
@ -39,13 +51,91 @@ internal class NetworkStatusFactory {
is CryptoCurrencyAmount.Coin -> currencies.singleOrNull { it is CryptoCurrency.Coin }
is CryptoCurrencyAmount.Token -> currencies.firstOrNull {
it is CryptoCurrency.Token &&
it.id.rawCurrencyId == amount.id &&
it.id.rawCurrencyId == amount.tokenId &&
it.contractAddress == amount.tokenContractAddress
}
}
currency?.id?.let { it to amount.value }
if (currency == null) {
Timber.e("Unable to find cryptocurrency for amount: $amount")
null
} else {
currency.id to amount.value
}
}
.toMap()
}
private fun formatTransactions(
networksAddresses: Set<String>,
transactions: Set<CryptoCurrencyTransaction>,
currencies: Set<CryptoCurrency>,
): Map<CryptoCurrency.ID, Set<PendingTransaction>> {
if (transactions.isEmpty()) return emptyMap()
return currencies
.asSequence()
.map { currency ->
val currencyTransactions = when (currency) {
is CryptoCurrency.Coin -> transactions.filterTo(hashSetOf()) { transaction ->
transaction is CryptoCurrencyTransaction.Coin
}
is CryptoCurrency.Token -> transactions.filterTo(hashSetOf()) { transaction ->
transaction is CryptoCurrencyTransaction.Token &&
transaction.tokenId == currency.id.rawCurrencyId &&
transaction.tokenContractAddress == currency.contractAddress
}
}
currency.id to createCurrentTransactions(networksAddresses, currencyTransactions)
}
.toMap()
}
private fun createCurrentTransactions(
networksAddresses: Set<String>,
transactions: Set<CryptoCurrencyTransaction>,
): Set<PendingTransaction> {
return transactions.mapNotNullTo(hashSetOf()) { createCurrentTransaction(networksAddresses, it) }
}
private fun createCurrentTransaction(
networksAddresses: Set<String>,
transaction: CryptoCurrencyTransaction,
): PendingTransaction? {
val direction = when {
transaction.toAddress in networksAddresses -> PendingTransaction.Direction.Incoming(
fromAddress = transaction.fromAddress,
)
transaction.fromAddress in networksAddresses -> PendingTransaction.Direction.Outgoing(
toAddress = transaction.toAddress,
)
else -> {
Timber.e(
"""
Unable to find transaction direction
|- To address: ${transaction.toAddress}
|- From address: ${transaction.fromAddress}
|- Network addresses: $networksAddresses
""".trimIndent(),
)
return null
}
}
return PendingTransaction(
amount = transaction.amount,
direction = direction,
sentAt = transaction.sentAt,
)
}
private fun getNetworkAddress(defaultAddress: String, availableAddresses: Set<String>): NetworkAddress {
return if (availableAddresses.size != 1) {
NetworkAddress.Selectable(defaultAddress, availableAddresses)
} else {
NetworkAddress.Single(defaultAddress)
}
}
}

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