Updated on 2026-08-14

This commit is contained in:
Tangem 2023-08-29 11:38:13 +03:00
commit 64fab92d06
231 changed files with 5317 additions and 1797 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

@ -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.feature.learn2earn.domain.api.Learn2earnInteractor
import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles
@ -42,6 +43,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
@ -165,6 +167,9 @@ class TapApplication : Application(), ImageLoaderFactory {
@Inject
lateinit var appCurrencyRepository: AppCurrencyRepository
@Inject
lateinit var walletManagersFacade: WalletManagersFacade
override fun onCreate() {
super.onCreate()
@ -184,12 +189,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.domain.redux.ReduxStateHolder
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.tap.proxy.AppStateHolder
import dagger.Binds
@ -20,4 +21,8 @@ internal interface AppStateHolderModule {
@Binds
@Singleton
fun bindsNavigationStateHolder(appStateHolder: AppStateHolder): NavigationStateHolder
@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

@ -51,10 +51,9 @@ internal object TokensDomainModule {
@Provides
@ViewModelScoped
fun provideToggleTokenListGroupingUseCase(
networksRepository: NetworksRepository,
dispatchers: CoroutineDispatcherProvider,
): ToggleTokenListGroupingUseCase {
return ToggleTokenListGroupingUseCase(networksRepository, dispatchers)
return ToggleTokenListGroupingUseCase(dispatchers)
}
@Provides
@ -71,4 +70,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
@ -249,60 +251,101 @@ private class ScanWalletProcessor(
val userTokensRepository = userTokensRepository ?: return emptyList()
val blockchainsToDerive = userTokensRepository.loadBlockchainsToDerive(card)
.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

@ -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

@ -103,13 +103,16 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
tvBody.isVisible = false
btnMainAction.text = ""
btnMainAction.icon = null
btnAlternativeAction.text = ""
btnAlternativeAction.icon = null
tvHeader.text = ""
tvBody.text = ""
}
private fun setupCreateWalletState(state: OnboardingNoteState) = with(mainBinding.onboardingActionContainer) {
btnMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet)
btnMainAction.setIconResource(R.drawable.ic_tangem_24)
btnMainAction.setOnClickListener {
Analytics.send(Onboarding.CreateWallet.ButtonCreateWallet())
store.dispatch(OnboardingNoteAction.CreateWallet)
@ -133,6 +136,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
private fun setupTopUpWalletState(state: OnboardingNoteState) = with(mainBinding.onboardingActionContainer) {
if (state.isBuyAllowed) {
btnMainAction.setText(R.string.onboarding_top_up_button_but_crypto)
btnMainAction.icon = null
btnMainAction.setOnClickListener {
store.dispatch(OnboardingNoteAction.TopUp)
}
@ -144,6 +148,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
}
} else {
btnMainAction.setText(R.string.onboarding_button_receive_crypto)
btnMainAction.icon = null
btnMainAction.setOnClickListener {
store.dispatch(OnboardingNoteAction.ShowAddressInfoDialog)
}
@ -179,6 +184,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
private fun setupDoneState(state: OnboardingNoteState) = with(mainBinding.onboardingActionContainer) {
btnMainAction.setText(R.string.common_continue)
btnMainAction.icon = null
btnMainAction.setOnClickListener {
showConfetti(false)
store.dispatch(OnboardingNoteAction.Done)

View file

@ -72,6 +72,7 @@ class OnboardingOtherCardsFragment : BaseOnboardingFragment<OnboardingOtherCards
private fun setupCreateWalletState() = with(mainBinding.onboardingActionContainer) {
btnMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet)
btnMainAction.setIconResource(R.drawable.ic_tangem_24)
btnMainAction.setOnClickListener {
Analytics.send(Onboarding.CreateWallet.ButtonCreateWallet())
store.dispatch(OnboardingOtherCardsAction.CreateWallet)
@ -92,6 +93,7 @@ class OnboardingOtherCardsFragment : BaseOnboardingFragment<OnboardingOtherCards
private fun setupDoneState() = with(mainBinding.onboardingActionContainer) {
btnMainAction.setText(R.string.common_continue)
btnMainAction.icon = null
btnMainAction.setOnClickListener {
showConfetti(false)
store.dispatch(OnboardingOtherCardsAction.Done)

View file

@ -5,6 +5,7 @@ import android.util.TypedValue
import android.view.View
import android.view.animation.OvershootInterpolator
import androidx.annotation.LayoutRes
import androidx.appcompat.content.res.AppCompatResources
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.isVisible
import androidx.transition.TransitionInflater
@ -15,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
@ -175,10 +176,12 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
}
private fun setupWelcomeState(state: TwinCardsState) {
setupWelcomeState(state) { store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.CreateFirstWallet)) }
setupWelcomeState(state) {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.CreateFirstWallet))
}
}
private fun setupWelcomeState(state: TwinCardsState, mainAction: VoidCallback) =
private fun setupWelcomeState(state: TwinCardsState, onMainButtonClick: VoidCallback) =
with(mainBinding.onboardingActionContainer) {
twinsWidget.toWelcome(false)
@ -192,7 +195,12 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
)
btnMainAction.setText(R.string.common_continue)
btnMainAction.setOnClickListener { mainAction() }
btnMainAction.icon = when (state.currentStep) {
is TwinCardsStep.WelcomeOnly -> null
is TwinCardsStep.Welcome -> AppCompatResources.getDrawable(requireContext(), R.drawable.ic_tangem_24)
else -> null
}
btnMainAction.setOnClickListener { onMainButtonClick() }
}
private fun setupWarningState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) {
@ -210,6 +218,7 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
}
btnMainAction.isEnabled = state.userWasUnderstandIfWalletRecreate
btnMainAction.setText(R.string.common_continue)
btnMainAction.icon = null
btnMainAction.setOnClickListener {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.CreateFirstWallet))
}
@ -251,6 +260,7 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
tvBody.setText(R.string.onboarding_twins_interrupt_warning)
btnMainAction.text = getString(R.string.twins_recreate_button_format, twinIndexNumber)
btnMainAction.setIconResource(R.drawable.ic_tangem_24)
btnMainAction.setOnClickListener {
Analytics.send(Onboarding.CreateWallet.ButtonCreateWallet())
store.dispatch(
@ -274,6 +284,7 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
btnMainAction.text =
getString(R.string.twins_recreate_button_format, twinPairIndexNumber)
btnMainAction.setIconResource(R.drawable.ic_tangem_24)
btnMainAction.setOnClickListener {
store.dispatch(
TwinCardsAction.Wallet.LaunchSecondStep(
@ -298,6 +309,7 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
tvBody.setText(R.string.onboarding_twins_interrupt_warning)
btnMainAction.text = getString(R.string.twins_recreate_button_format, twinIndexNumber)
btnMainAction.setIconResource(R.drawable.ic_tangem_24)
btnMainAction.setOnClickListener {
store.dispatch(
TwinCardsAction.Wallet.LaunchThirdStep(
@ -343,6 +355,7 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
}
} else {
btnMainAction.setText(R.string.onboarding_button_receive_crypto)
btnMainAction.icon = null
btnMainAction.setOnClickListener {
store.dispatch(TwinCardsAction.ShowAddressInfoDialog)
}
@ -367,6 +380,7 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
private fun setupDoneState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) {
btnMainAction.setText(R.string.common_continue)
btnMainAction.icon = null
btnMainAction.setOnClickListener {
store.dispatch(TwinCardsAction.Confetti.Hide)
store.dispatch(TwinCardsAction.Done)

View file

@ -204,6 +204,8 @@ class OnboardingWalletFragment :
private fun setupCreateWalletState() = with(binding) {
layoutButtonsCommon.btnWalletMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet)
layoutButtonsCommon.btnWalletMainAction.setIconResource(R.drawable.ic_tangem_24)
layoutButtonsCommon.btnWalletMainAction.setOnClickListener {
Analytics.send(Onboarding.CreateWallet.ButtonCreateWallet())
store.dispatch(OnboardingWalletAction.CreateWallet)
@ -243,6 +245,7 @@ class OnboardingWalletFragment :
with(layoutButtonsCommon) {
btnWalletMainAction.text = getText(R.string.onboarding_button_backup_now)
btnWalletMainAction.icon = null
btnWalletMainAction.setOnClickListener { store.dispatch(BackupAction.StartBackup) }
btnWalletAlternativeAction.text = getText(R.string.onboarding_button_skip_backup)
@ -261,6 +264,7 @@ class OnboardingWalletFragment :
with(layoutButtonsCommon) {
btnWalletMainAction.text = getString(R.string.onboarding_button_scan_origin_card)
btnWalletMainAction.setIconResource(R.drawable.ic_tangem_24)
btnWalletAlternativeAction.hide()
btnWalletMainAction.setOnClickListener { store.dispatch(BackupAction.ScanPrimaryCard) }
}
@ -277,6 +281,7 @@ class OnboardingWalletFragment :
layoutButtonsAddCards.root.show()
layoutButtonsCommon.root.hide()
layoutButtonsAddCards.btnAddCard.text = getText(R.string.onboarding_button_add_backup_card)
layoutButtonsAddCards.btnAddCard.setIconResource(R.drawable.ic_tangem_24)
if (state.backupCardsNumber < state.maxBackupCards) {
layoutButtonsAddCards.btnAddCard.setOnClickListener { store.dispatch(BackupAction.AddBackupCard) }
} else {
@ -358,6 +363,7 @@ class OnboardingWalletFragment :
state.primaryCardId?.let { cardIdFormatter.getFormattedCardId(it) },
)
layoutButtonsCommon.btnWalletMainAction.text = getText(R.string.onboarding_button_backup_origin)
layoutButtonsCommon.btnWalletMainAction.setIconResource(R.drawable.ic_tangem_24)
layoutButtonsCommon.btnWalletMainAction.setOnClickListener { store.dispatch(BackupAction.WritePrimaryCard) }
animator.showWritePrimaryCard(state)
@ -390,6 +396,7 @@ class OnboardingWalletFragment :
R.string.onboarding_button_backup_card_format,
cardNumber,
)
layoutButtonsCommon.btnWalletMainAction.setIconResource(R.drawable.ic_tangem_24)
layoutButtonsCommon.btnWalletMainAction.setOnClickListener {
store.dispatch(BackupAction.WriteBackupCard(cardNumber))
}
@ -408,6 +415,7 @@ class OnboardingWalletFragment :
tvBody.text = getText(R.string.onboarding_subtitle_success_tangem_wallet_onboarding)
layoutButtonsCommon.btnWalletMainAction.text = getText(R.string.onboarding_button_continue_wallet)
layoutButtonsCommon.btnWalletMainAction.icon = null
layoutButtonsCommon.btnWalletAlternativeAction.hide()
layoutButtonsCommon.btnWalletMainAction.setOnClickListener {
showConfetti(false)

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

@ -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

@ -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

@ -12,6 +12,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.*
@ -87,7 +88,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

@ -25,7 +25,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.res.TangemTheme
import com.tangem.core.ui.utils.OneTouchClickListener
import com.tangem.datasource.connection.NetworkConnectionManager

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,35 +1,36 @@
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
import androidx.compose.foundation.layout.systemBarsPadding
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.res.colorResource
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
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>()
@ -40,19 +41,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 {
@ -61,7 +58,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

@ -4,6 +4,7 @@ import com.tangem.core.navigation.NavigationAction
import com.tangem.core.navigation.NavigationStateHolder
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 +15,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 +23,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, NavigationStateHolder, ReduxStateHolder {
override var userWalletsListManager: UserWalletsListManager? = null
set(value) {
@ -50,4 +52,8 @@ class AppStateHolder @Inject constructor() : WalletsStateHolder, NavigationState
override fun navigate(action: NavigationAction) {
mainStore?.dispatch(action)
}
override fun dispatch(action: Action) {
mainStore?.dispatch(action)
}
}

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

@ -74,7 +74,8 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_main_action"
style="@style/TapPrimaryButton"
style="@style/TapPrimaryIconButton"
tools:icon="@drawable/ic_tangem_24"
tools:text="@string/welcome_unlock_card" />
<ProgressBar

View file

@ -112,7 +112,7 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/btnSend"
style="@style/TapPrimaryIconButton"
style="@style/TapPrimaryIconButtonLegacy"
android:layout_width="match_parent"
android:fontFamily="@font/saira_semi_condensed_regular"
android:text="@string/common_send"

View file

@ -7,14 +7,15 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_add_card"
style="@style/TapSecondaryButton"
style="@style/TapSecondaryIconButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="92dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/btn_continue"
app:layout_constraintStart_toStartOf="@id/btn_continue"
tools:text="+ Add backup card" />
tools:icon="@drawable/ic_tangem_24"
tools:text="Add backup card" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_continue"

View file

@ -7,13 +7,14 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_wallet_main_action"
style="@style/TapPrimaryButton"
style="@style/TapPrimaryIconButton"
android:layout_width="204dp"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/home_bottom_fl_scan_card_container_margin_bottom"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:icon="@drawable/ic_tangem_24"
tools:text="Main Action" />
<com.google.android.material.button.MaterialButton

View file

@ -74,7 +74,8 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_main_action"
style="@style/TapPrimaryButton"
style="@style/TapPrimaryIconButton"
tools:icon="@drawable/ic_tangem_24"
tools:text="@string/welcome_unlock_card" />
<ProgressBar

View file

@ -16,7 +16,7 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_trade"
style="@style/TapPrimaryIconButton"
style="@style/TapPrimaryIconButtonLegacy"
android:text="@string/wallet_button_actions"
android:visibility="gone"
app:icon="@drawable/ic_arrows_up_down"
@ -24,21 +24,21 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_buy"
style="@style/TapPrimaryIconButton"
style="@style/TapPrimaryIconButtonLegacy"
android:text="@string/common_buy"
android:visibility="gone"
app:icon="@drawable/ic_arrow_up" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_sell"
style="@style/TapPrimaryIconButton"
style="@style/TapPrimaryIconButtonLegacy"
android:text="@string/common_sell"
android:visibility="gone"
app:icon="@drawable/ic_arrow_down" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_swap"
style="@style/TapPrimaryIconButton"
style="@style/TapPrimaryIconButtonLegacy"
android:text="@string/swapping_swap_action"
android:visibility="gone"
app:icon="@drawable/ic_arrows_up_down"
@ -48,7 +48,7 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_send"
style="@style/TapPrimaryIconButton"
style="@style/TapPrimaryIconButtonLegacy"
android:layout_width="0dp"
android:layout_weight="1"
android:text="@string/common_send"

View file

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

View file

@ -1,7 +1,8 @@
<resources>
<!-- Base application theme. -->
<style name="BaseAppTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
<!-- Customize your theme here. -->
<style name="BaseAppTheme" parent="Theme.MaterialComponents.Light.NoActionBar" />
<style name="AppTheme" parent="BaseAppTheme">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorSecondary">@color/colorSecondary</item>
<item name="android:textColor">@color/darkGray6</item>
@ -10,8 +11,6 @@
<item name="materialCardViewStyle">@style/Widget.AppTheme.CardView</item>
</style>
<style name="AppTheme" parent="BaseAppTheme" />
<style name="SplashTheme" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/background_splash</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/inset_splash</item>
@ -46,38 +45,48 @@
<item name="android:minHeight">48dp</item>
<item name="android:insetTop">0dp</item>
<item name="android:insetBottom">0dp</item>
<item name="cornerRadius">14dp</item>
<item name="android:textAllCaps">false</item>
<item name="android:textSize">14sp</item>
<item name="android:textStyle">normal</item>
<item name="android:fontFamily">@font/roboto_medium</item>
<item name="android:fontFamily">@font/roboto_regular</item>
<item name="android:letterSpacing">0</item>
<item name="lineHeight">24sp</item>
<item name="cornerRadius">@dimen/btn_corner_radius_large</item>
<item name="fontWeight">500</item>
<item name="lineHeight">16sp</item>
</style>
<style name="TapPrimaryButton" parent="BaseTapButton">
<item name="android:backgroundTint">@color/selector_btn_primary</item>
<item name="android:textColor">@color/selector_btn_text_primary</item>
<item name="android:textColor">@color/selector_btn_content_primary</item>
</style>
<style name="TapSecondaryButton" parent="BaseTapButton">
<item name="android:backgroundTint">@color/selector_btn_secondary</item>
<item name="android:textColor">@color/selector_btn_text_secondary</item>
<item name="android:textColor">@color/selector_btn_content_secondary</item>
</style>
<style name="TapTextButton" parent="BaseTapButton">
<item name="android:minHeight">40dp</item>
<item name="cornerRadius">@dimen/btn_corner_radius</item>
<item name="android:backgroundTint">@android:color/transparent</item>
<item name="android:textColor">@color/selector_btn_text_secondary</item>
<item name="android:textColor">@color/selector_btn_content_secondary</item>
</style>
<style name="TapPrimaryIconButton" parent="TapPrimaryButton">
<item name="iconTint">@color/selector_btn_content_primary</item>
<item name="iconGravity">textEnd</item>
</style>
<style name="TapPrimaryIconButtonLegacy" parent="TapPrimaryButton">
<item name="android:gravity">start|center_vertical</item>
<item name="iconTint">@color/selector_btn_text_primary</item>
<item name="iconTint">@color/selector_btn_content_primary</item>
<item name="iconGravity">end</item>
</style>
<style name="TapSecondaryIconButton" parent="TapSecondaryButton">
<item name="iconTint">@color/selector_btn_content_secondary</item>
<item name="iconGravity">textEnd</item>
</style>
<style name="TapChip" parent="@style/Widget.MaterialComponents.Chip.Choice">
<item name="chipBackgroundColor">@color/selector_chip_background</item>
<item name="chipStrokeColor">@color/selector_chip_stroke</item>

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

@ -8,4 +8,6 @@ interface SelectedAppCurrencyStore {
fun get(): Flow<CurrenciesResponse.Currency>
suspend fun store(item: CurrenciesResponse.Currency)
suspend fun isEmpty(): Boolean
}

View file

@ -7,4 +7,9 @@ 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

@ -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

@ -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

@ -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
@ -164,7 +164,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

@ -88,7 +88,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

@ -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)
}
}
}

View file

@ -59,10 +59,10 @@ internal class ResponseCurrenciesFactory(private val demoConfig: DemoConfig) {
}
}
private fun createCoin(blockchain: Blockchain, responseToken: UserTokensResponse.Token): CryptoCurrency.Coin {
private fun createCoin(blockchain: Blockchain, responseToken: UserTokensResponse.Token): CryptoCurrency.Coin? {
return CryptoCurrency.Coin(
id = getCoinId(blockchain),
networkId = getNetworkId(blockchain),
network = getNetwork(blockchain) ?: return null,
name = responseToken.name,
symbol = responseToken.symbol,
decimals = responseToken.decimals,
@ -71,12 +71,12 @@ internal class ResponseCurrenciesFactory(private val demoConfig: DemoConfig) {
)
}
private fun createToken(blockchain: Blockchain, sdkToken: Token, derivationPath: String?): CryptoCurrency.Token {
private fun createToken(blockchain: Blockchain, sdkToken: Token, derivationPath: String?): CryptoCurrency.Token? {
val id = getTokenId(blockchain, sdkToken)
return CryptoCurrency.Token(
id = id,
networkId = getNetworkId(blockchain),
network = getNetwork(blockchain) ?: return null,
name = sdkToken.name,
symbol = sdkToken.symbol,
decimals = sdkToken.decimals,
@ -84,8 +84,6 @@ internal class ResponseCurrenciesFactory(private val demoConfig: DemoConfig) {
iconUrl = getTokenIconUrl(blockchain, sdkToken),
contractAddress = sdkToken.contractAddress,
isCustom = isCustomToken(id),
blockchainName = blockchain.fullName,
standardType = getTokenStandardType(blockchain, sdkToken),
)
}
}

View file

@ -5,7 +5,6 @@ import com.tangem.blockchain.common.IconsUtil
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.common.extensions.toCoinId
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.tokens.models.CryptoCurrency.ID
import com.tangem.domain.tokens.models.Network
import com.tangem.blockchain.common.Token as SdkToken
@ -31,12 +30,6 @@ internal fun getBlockchain(networkId: Network.ID): Blockchain {
return Blockchain.fromId(networkId.value)
}
internal fun getNetworkId(blockchain: Blockchain): Network.ID {
val value = blockchain.id
return Network.ID(value)
}
internal fun getCoinId(blockchain: Blockchain): ID {
return getTokenOrCoinId(blockchain, token = null)
}
@ -45,16 +38,6 @@ internal fun getTokenId(blockchain: Blockchain, token: SdkToken): ID {
return getTokenOrCoinId(blockchain, token)
}
internal fun getTokenStandardType(blockchain: Blockchain, token: SdkToken): CryptoCurrency.StandardType {
return when (blockchain) {
Blockchain.Ethereum, Blockchain.EthereumTestnet -> CryptoCurrency.StandardType.ERC20
Blockchain.BSC, Blockchain.BSCTestnet -> CryptoCurrency.StandardType.BEP20
Blockchain.Binance, Blockchain.BinanceTestnet -> CryptoCurrency.StandardType.BEP2
Blockchain.Tron, Blockchain.TronTestnet -> CryptoCurrency.StandardType.TRC20
else -> CryptoCurrency.StandardType.Unspecified(token.name)
}
}
internal fun getTokenIconUrl(blockchain: Blockchain, token: SdkToken): String? {
val tokenId = token.id
@ -83,7 +66,7 @@ private fun getTokenOrCoinId(blockchain: Blockchain, token: SdkToken?): ID {
else -> TOKEN_ID_PREFIX to CurrencyIdSuffix(rawId = sdkTokenId)
}
return ID(prefix, getNetworkId(blockchain), suffix)
return ID(prefix, Network.ID(blockchain.id), suffix)
}
private fun getTokenIconUrlFromDefaultHost(tokenId: String): String {

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