diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e72316ffd3..7f647efbd5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -92,6 +92,9 @@ dependencies { implementation(projects.features.tokendetails.api) implementation(projects.features.tokendetails.impl) implementation(projects.features.send.api) + implementation(projects.features.manageTokens.api) + implementation(projects.features.manageTokens.impl) + implementation(projects.features.send.impl) /** AndroidX libraries */ implementation(deps.androidx.core.ktx) @@ -183,4 +186,10 @@ dependencies { testImplementation(deps.test.truth) androidTestImplementation(deps.test.junit.android) androidTestImplementation(deps.test.espresso) + + /** Chucker */ + debugImplementation(deps.chucker) + externalImplementation(deps.chuckerStub) + internalImplementation(deps.chuckerStub) + releaseImplementation(deps.chuckerStub) } \ No newline at end of file diff --git a/app/src/main/assets/testnet_tokens.json b/app/src/main/assets/testnet_tokens.json index dd90c8b04c..140b02a645 100644 --- a/app/src/main/assets/testnet_tokens.json +++ b/app/src/main/assets/testnet_tokens.json @@ -511,8 +511,7 @@ "id": "aleph-zero", "symbol": "AZERO", "name": "Aleph Zero", - "networks": - [ + "networks": [ { "networkId": "aleph-zero/test" } @@ -522,12 +521,21 @@ "id": "near", "symbol": "NEAR", "name": "NEAR", - "networks": - [ + "networks": [ { "networkId": "near-protocol/test" } ] + }, + { + "id": "decimal", + "name": "Decimal", + "symbol": "tDEL", + "networks": [ + { + "networkId": "decimal/test" + } + ] } ] } diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 8d836dbb16..b1faa6cfad 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -1,14 +1,21 @@ package com.tangem.tap +import android.Manifest import android.annotation.SuppressLint import android.content.Intent import android.content.pm.ActivityInfo +import android.content.pm.PackageManager import android.content.res.Configuration +import android.os.Build import android.os.Bundle import android.view.View +import androidx.activity.viewModels +import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat import androidx.core.os.bundleOf import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.WindowCompat @@ -16,14 +23,20 @@ import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import arrow.core.getOrElse import by.kirich1409.viewbindingdelegate.viewBinding +import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.Snackbar import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference import com.tangem.data.card.sdk.CardSdkLifecycleObserver import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.features.managetokens.navigation.ManageTokensRouter +import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import com.tangem.features.wallet.navigation.WalletRouter @@ -46,12 +59,15 @@ import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandle import com.tangem.tap.features.intentHandler.handlers.BuyCurrencyIntentHandler import com.tangem.tap.features.intentHandler.handlers.SellCurrencyIntentHandler import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler +import com.tangem.tap.features.main.MainViewModel +import com.tangem.tap.features.main.model.Toast import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction import com.tangem.tap.features.shop.redux.ShopAction import com.tangem.tap.features.welcome.ui.WelcomeFragment import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.redux.DaggerGraphAction import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler +import com.tangem.wallet.BuildConfig import com.tangem.wallet.R import com.tangem.wallet.databinding.ActivityMainBinding import dagger.hilt.android.AndroidEntryPoint @@ -109,9 +125,17 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac @Inject lateinit var tokenDetailsRouter: TokenDetailsRouter + @Inject + lateinit var manageTokensRouter: ManageTokensRouter + @Inject lateinit var walletConnectInteractor: WalletConnectInteractor + @Inject + lateinit var sendRouter: SendRouter + + internal val viewModel: MainViewModel by viewModels() + private lateinit var appThemeModeFlow: SharedFlow // TODO: fixme: inject through DI @@ -136,6 +160,29 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac initContent() checkGooglePayAvailability() + + checkForNotificationPermission() + observeStateUpdates() + } + + private fun observeStateUpdates() { + viewModel.state + .flowWithLifecycle(lifecycle) + .onEach { state -> + if (state.toast is StateEvent.Triggered) { + showToast(state.toast.data) + state.toast.onConsume() + } + } + .launchIn(lifecycleScope) + } + + private fun showToast(toast: Toast) { + dismissSnackbar() + showSnackbar(toast.message, Snackbar.LENGTH_LONG, toast.action.text) { + toast.action.onClick() + dismissSnackbar() + } } private fun installActivityDependencies() { @@ -156,7 +203,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac walletRouter = walletRouter, walletConnectInteractor = walletConnectInteractor, tokenDetailsRouter = tokenDetailsRouter, + manageTokensRouter = manageTokensRouter, cardSdkConfigRepository = cardSdkConfigRepository, + sendRouter = sendRouter, ), ) } @@ -285,18 +334,22 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac } } - override fun showSnackbar(text: Int, buttonTitle: Int?, action: View.OnClickListener?) { - if (snackbar != null) return + override fun showSnackbar( + @StringRes text: Int, + length: Int, + @StringRes buttonTitle: Int?, + action: View.OnClickListener?, + ) { + showSnackbar(getString(text), length, buttonTitle?.let(::getString), action) + } - snackbar = Snackbar.make( - binding.fragmentContainer, - getString(text), - Snackbar.LENGTH_INDEFINITE, - ) - if (buttonTitle != null && action != null) { - snackbar?.setAction(getString(buttonTitle), action) - } - snackbar?.show() + override fun showSnackbar( + text: TextReference, + length: Int, + buttonTitle: TextReference?, + action: View.OnClickListener?, + ) { + showSnackbar(text.resolveReference(resources), length, buttonTitle?.resolveReference(resources), action) } override fun dismissSnackbar() { @@ -332,6 +385,33 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac lockUserWalletsTimer?.restart() } + private fun showSnackbar(text: String, length: Int, buttonTitle: String?, action: View.OnClickListener?) { + if (snackbar != null) return + + snackbar = Snackbar.make(binding.fragmentContainer, text, length).apply { + val textColor = getColor(R.color.text_primary_2) + + setBackgroundTint(getColor(R.color.button_primary)) + setActionTextColor(textColor) + setTextColor(textColor) + + if (buttonTitle != null && action != null) { + setAction(buttonTitle, action) + } + + addCallback( + object : BaseTransientBottomBar.BaseCallback() { + override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { + snackbar = null + removeCallback(this) + } + }, + ) + } + + snackbar?.show() + } + private fun navigateToInitialScreenIfNeeded(intentWhichStartedActivity: Intent?) { val backStackIsEmpty = supportFragmentManager.backStackEntryCount == 0 val isNotScannedBefore = store.state.globalState.scanResponse == null @@ -371,4 +451,14 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac store.dispatch(BackupAction.CheckForUnfinishedBackup) } + + private fun checkForNotificationPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + BuildConfig.DEBUG && + ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != + PackageManager.PERMISSION_GRANTED + ) { + ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 0) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 1f50712ace..c67f289238 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -5,6 +5,7 @@ import android.content.Context import android.content.pm.PackageManager import coil.ImageLoader import coil.ImageLoaderFactory +import com.chuckerteam.chucker.api.ChuckerInterceptor import com.orhanobut.logger.AndroidLogAdapter import com.orhanobut.logger.Logger import com.tangem.Log @@ -36,6 +37,8 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.WalletManagersRepository import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles +import com.tangem.features.send.api.featuretoggles.SendFeatureToggles import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.tap.common.analytics.AnalyticsFactory import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder @@ -170,6 +173,9 @@ internal class TapApplication : Application(), ImageLoaderFactory { // @Inject // lateinit var learn2earnInteractor: Learn2earnInteractor + @Inject + lateinit var manageTokensFeatureToggles: ManageTokensFeatureToggles + @Inject lateinit var scanCardProcessor: ScanCardProcessor @@ -211,6 +217,9 @@ internal class TapApplication : Application(), ImageLoaderFactory { @Inject lateinit var walletsRepository: WalletsRepository + + @Inject + lateinit var sendFeatureToggles: SendFeatureToggles // endregion Injected override fun onCreate() { @@ -255,6 +264,7 @@ internal class TapApplication : Application(), ImageLoaderFactory { if (LogConfig.network.blockchainSdkNetwork) { BlockchainSdkRetrofitBuilder.interceptors = listOf( createNetworkLoggingInterceptor(), + ChuckerInterceptor(this), ) } @@ -290,6 +300,7 @@ internal class TapApplication : Application(), ImageLoaderFactory { walletFeatureToggles = walletFeatureToggles, walletConnectRepository = walletConnect2Repository, walletConnectSessionsRepository = walletConnectSessionsRepository, + manageTokensFeatureToggles = manageTokensFeatureToggles, scanCardProcessor = scanCardProcessor, appCurrencyRepository = appCurrencyRepository, walletManagersFacade = walletManagersFacade, @@ -300,6 +311,7 @@ internal class TapApplication : Application(), ImageLoaderFactory { balanceHidingRepository = balanceHidingRepository, detailsFeatureToggles = detailsFeatureToggles, walletsRepository = walletsRepository, + sendFeatureToggles = sendFeatureToggles, ), ), ) diff --git a/app/src/main/java/com/tangem/tap/common/SnackbarHandler.kt b/app/src/main/java/com/tangem/tap/common/SnackbarHandler.kt index c10b8e6f37..53228bf674 100644 --- a/app/src/main/java/com/tangem/tap/common/SnackbarHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/SnackbarHandler.kt @@ -1,8 +1,25 @@ package com.tangem.tap.common import android.view.View +import androidx.annotation.StringRes +import com.google.android.material.snackbar.Snackbar +import com.tangem.core.ui.extensions.TextReference interface SnackbarHandler { - fun showSnackbar(text: Int, buttonTitle: Int? = null, action: View.OnClickListener? = null) + + fun showSnackbar( + @StringRes text: Int, + length: Int = Snackbar.LENGTH_INDEFINITE, + @StringRes buttonTitle: Int? = null, + action: View.OnClickListener? = null, + ) + + fun showSnackbar( + text: TextReference, + length: Int = Snackbar.LENGTH_INDEFINITE, + buttonTitle: TextReference? = null, + action: View.OnClickListener? = null, + ) + fun dismissSnackbar() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt b/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt index f1daaf34f6..a0cf53059d 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt @@ -1,56 +1,7 @@ package com.tangem.tap.common.extensions -import androidx.annotation.DrawableRes import com.tangem.blockchain.common.Blockchain import com.tangem.common.extensions.remove -import com.tangem.wallet.R - -@Suppress("ComplexMethod") -@DrawableRes -fun Blockchain.getGreyedOutIconRes(): Int { - return when (this) { - Blockchain.Arbitrum, Blockchain.ArbitrumTestnet -> R.drawable.ic_arbitrum_no_color - Blockchain.Bitcoin, Blockchain.BitcoinTestnet -> R.drawable.ic_bitcoin_no_color - Blockchain.BitcoinCash -> R.drawable.ic_bitcoin_cash_no_color - Blockchain.Litecoin -> R.drawable.ic_litecoin_no_color - Blockchain.Ethereum, Blockchain.EthereumTestnet -> R.drawable.ic_eth_no_color - Blockchain.EthereumClassic, Blockchain.EthereumClassicTestnet -> R.drawable.ic_eth_no_color - Blockchain.RSK -> R.drawable.ic_rsk_no_color - Blockchain.Cardano -> R.drawable.ic_cardano_no_color - Blockchain.Tezos -> R.drawable.ic_tezos_no_color - Blockchain.XRP -> R.drawable.ic_xrp_no_color - Blockchain.Stellar -> R.drawable.ic_stellar_no_color - Blockchain.Avalanche, Blockchain.AvalancheTestnet -> R.drawable.ic_avalanche_no_color - Blockchain.Polygon, Blockchain.PolygonTestnet -> R.drawable.ic_polygon_no_color - Blockchain.Solana, Blockchain.SolanaTestnet -> R.drawable.ic_solana_no_color - Blockchain.Fantom, Blockchain.FantomTestnet -> R.drawable.ic_fantom_no_color - Blockchain.BSC, Blockchain.BSCTestnet, Blockchain.Binance, Blockchain.BinanceTestnet -> - R.drawable.ic_bsc_no_color - Blockchain.Dogecoin -> R.drawable.ic_dogecoin_no_color - Blockchain.Tron, Blockchain.TronTestnet -> R.drawable.ic_tron_no_color - Blockchain.Gnosis -> R.drawable.ic_gnosis_no_color - Blockchain.EthereumPow, Blockchain.EthereumPowTestnet -> R.drawable.ic_ethereumpow_no_color - Blockchain.EthereumFair -> R.drawable.ic_ethereumfair_no_color - Blockchain.Polkadot, Blockchain.PolkadotTestnet -> R.drawable.ic_polkadot_no_color - Blockchain.Kusama -> R.drawable.ic_kusama_no_color - Blockchain.Optimism, Blockchain.OptimismTestnet -> R.drawable.ic_optimism_no_color - Blockchain.Dash -> R.drawable.ic_dash_no_color - Blockchain.Kaspa -> R.drawable.ic_kaspa_no_color - Blockchain.TON, Blockchain.TONTestnet -> R.drawable.ic_ton_no_color - Blockchain.Kava, Blockchain.KavaTestnet -> R.drawable.ic_kava_no_color - Blockchain.Ravencoin, Blockchain.RavencoinTestnet -> R.drawable.ic_ravencoin_no_color - Blockchain.Cosmos, Blockchain.CosmosTestnet -> R.drawable.ic_cosmos_no_color - Blockchain.TerraV1 -> R.drawable.ic_terra_no_color - Blockchain.TerraV2 -> R.drawable.ic_terra2_no_color - Blockchain.Cronos -> R.drawable.ic_cronos_no_color - Blockchain.Telos, Blockchain.TelosTestnet -> R.drawable.ic_telos_no_color - Blockchain.AlephZero, Blockchain.AlephZeroTestnet -> R.drawable.ic_azero_no_color - Blockchain.OctaSpace, Blockchain.OctaSpaceTestnet -> R.drawable.ic_octaspace_no_color - Blockchain.Chia, Blockchain.ChiaTestnet -> R.drawable.ic_chia_no_color - Blockchain.Near, Blockchain.NearTestnet -> R.drawable.ic_near_no_color - else -> R.drawable.ic_tangem_logo - } -} fun Blockchain.getNetworkName(): String { return when (this) { diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt index 5ba15310c9..51ac9818c4 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt @@ -18,6 +18,7 @@ import com.tangem.tap.features.details.ui.walletconnect.QrScanFragment import com.tangem.tap.features.details.ui.walletconnect.WalletConnectFragment import com.tangem.tap.features.disclaimer.ui.DisclaimerFragment import com.tangem.tap.features.home.HomeFragment +import com.tangem.tap.features.main.ui.ModalNotificationBottomSheetFragment import com.tangem.tap.features.onboarding.products.note.OnboardingNoteFragment import com.tangem.tap.features.onboarding.products.otherCards.OnboardingOtherCardsFragment import com.tangem.tap.features.onboarding.products.twins.ui.TwinsCardsFragment @@ -59,12 +60,8 @@ fun FragmentActivity.openFragment( } } - if (screen.isDialogFragment) { - val dialogFragment = requireNotNull(fragment as? DialogFragment) { - "If screen.isDialogFragment == true then fragment must be a DialogFragment" - } - - dialogFragment.showAllowingStateLoss( + if (screen.isDialogFragment && fragment is DialogFragment) { + fragment.showAllowingStateLoss( fragmentManager = supportFragmentManager, baseTransaction = transaction, tag = screen.name, @@ -125,7 +122,7 @@ fun FragmentActivity.getPreviousScreen(): AppScreen? { return tag?.let { AppScreen.valueOf(tag) } } -@Suppress("ComplexMethod") +@Suppress("ComplexMethod", "LongMethod") private fun fragmentFactory(screen: AppScreen): Fragment { return when (screen) { AppScreen.Home -> HomeFragment() @@ -146,7 +143,18 @@ private fun fragmentFactory(screen: AppScreen): Fragment { WalletFragment() } } - AppScreen.Send -> SendFragment() + AppScreen.Send -> { + val featureToggles = store.state.daggerGraphState.get( + getDependency = DaggerGraphState::sendFeatureToggles, + ) + if (featureToggles.isRedesignedSendEnabled) { + store.state.daggerGraphState + .get(getDependency = DaggerGraphState::sendRouter) + .getEntryFragment() + } else { + SendFragment() + } + } AppScreen.Details -> DetailsFragment() AppScreen.DetailsSecurity -> SecurityModeFragment() AppScreen.CardSettings -> CardSettingsFragment() @@ -154,7 +162,18 @@ private fun fragmentFactory(screen: AppScreen): Fragment { AppScreen.ResetToFactory -> ResetCardFragment() AppScreen.AccessCodeRecovery -> AccessCodeRecoveryFragment() AppScreen.Disclaimer -> DisclaimerFragment() - AppScreen.AddTokens -> TokensListFragment() + AppScreen.ManageTokens -> { + val featureToggles = store.state.daggerGraphState.get( + getDependency = DaggerGraphState::manageTokensFeatureToggles, + ) + if (featureToggles.isRedesignedScreenEnabled) { + store.state.daggerGraphState + .get(getDependency = DaggerGraphState::manageTokensRouter) + .getEntryFragment() + } else { + TokensListFragment() + } + } AppScreen.AddCustomToken -> AddCustomTokenFragment() AppScreen.WalletDetails -> { val featureToggles = store.state.daggerGraphState.get( @@ -176,5 +195,6 @@ private fun fragmentFactory(screen: AppScreen): Fragment { AppScreen.SaveWallet -> SaveWalletBottomSheetFragment() AppScreen.WalletSelector -> WalletSelectorBottomSheetFragment() AppScreen.AppCurrencySelector -> AppCurrencySelectorFragment() + AppScreen.ModalNotification -> ModalNotificationBottomSheetFragment() } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt index afe7009baa..17782946ce 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt @@ -1,8 +1,9 @@ package com.tangem.tap.di.domain import com.tangem.domain.balancehiding.DeviceFlipDetector -import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase +import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.balancehiding.ListenToFlipsUseCase +import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import com.tangem.domain.settings.* import com.tangem.domain.settings.repositories.AppRatingRepository @@ -63,8 +64,10 @@ internal object SettingsDomainModule { @Provides @ViewModelScoped - fun providesIsBalanceHiddenUseCase(balanceHidingRepository: BalanceHidingRepository): IsBalanceHiddenUseCase { - return IsBalanceHiddenUseCase( + fun providesGetBalanceHidingSettingsUseCase( + balanceHidingRepository: BalanceHidingRepository, + ): GetBalanceHidingSettingsUseCase { + return GetBalanceHidingSettingsUseCase( balanceHidingRepository = balanceHidingRepository, ) } @@ -80,4 +83,12 @@ internal object SettingsDomainModule { balanceHidingRepository = balanceHidingRepository, ) } + + @Provides + @ViewModelScoped + fun provideUpdateHideBalancesSettingsUseCase( + balanceHidingRepository: BalanceHidingRepository, + ): UpdateBalanceHidingSettingsUseCase { + return UpdateBalanceHidingSettingsUseCase(balanceHidingRepository) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index 39190b6354..4504a0bb43 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -13,7 +13,6 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.apptheme.model.AppThemeMode -import com.tangem.domain.balancehiding.BalanceHidingSettings import com.tangem.domain.common.TapWorkarounds.isTangemTwins import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ScanResponse @@ -280,7 +279,7 @@ class DetailsMiddleware { val repository = store.state.daggerGraphState.get(DaggerGraphState::balanceHidingRepository) scope.launch { - val newState = BalanceHidingSettings( + val newState = repository.getBalanceHidingSettings().copy( isHidingEnabledInSettings = hideBalance, isBalanceHidden = false, ) diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt index 7cff2f540e..6e8db8a3d0 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt @@ -94,7 +94,7 @@ class HomeFragment : ComposeFragment(), StoreSubscriber { }, onSearchTokensClick = { Analytics.send(IntroductionProcess.ButtonTokensList()) - store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens)) + store.dispatch(NavigationAction.NavigateTo(AppScreen.ManageTokens)) store.dispatch(TokensAction.SetArgs.ReadAccess) }, ) diff --git a/app/src/main/java/com/tangem/tap/features/main/MainIntents.kt b/app/src/main/java/com/tangem/tap/features/main/MainIntents.kt new file mode 100644 index 0000000000..bfbfb42379 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/main/MainIntents.kt @@ -0,0 +1,12 @@ +package com.tangem.tap.features.main + +internal interface MainIntents { + + fun onHiddenBalanceToastAction() + + fun onShownBalanceToastAction() + + fun onHiddenBalanceNotificationAction(isPermanent: Boolean) + + fun onDismissBottomSheet() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/main/MainScreenStateHolder.kt b/app/src/main/java/com/tangem/tap/features/main/MainScreenStateHolder.kt new file mode 100644 index 0000000000..9bbeded200 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/main/MainScreenStateHolder.kt @@ -0,0 +1,62 @@ +package com.tangem.tap.features.main + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.event.triggeredEvent +import com.tangem.tap.features.main.model.MainScreenState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update + +internal class MainScreenStateHolder(private val intents: MainIntents) { + + private val notificationsFactory = NotificationsFactory(intents) + + private val stateFlowInternal: MutableStateFlow = MutableStateFlow(getInitialState()) + + val stateFlow: StateFlow = stateFlowInternal + + fun updateWithHiddenBalancesToast(isBalanceHidden: Boolean) { + stateFlowInternal.update { state -> + state.copy( + toast = triggeredEvent( + data = if (isBalanceHidden) { + notificationsFactory.createBalancesAreHiddenToast() + } else { + notificationsFactory.createBalancesAreShownToast() + }, + onConsume = ::consumeToastEvent, + ), + ) + } + } + + fun updateWithHiddenBalancesNotification() { + stateFlowInternal.update { state -> + state.copy( + modalNotification = TangemBottomSheetConfig( + isShow = true, + onDismissRequest = intents::onDismissBottomSheet, + content = notificationsFactory.createBalancesAreHiddenModalNotification(), + ), + ) + } + } + + fun updateWithoutModalNotification() { + stateFlowInternal.update { state -> + state.copy(modalNotification = state.modalNotification?.copy(isShow = false)) + } + } + + private fun consumeToastEvent() { + stateFlowInternal.update { state -> + state.copy(toast = consumedEvent()) + } + } + + private fun getInitialState() = MainScreenState( + toast = consumedEvent(), + modalNotification = null, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt new file mode 100644 index 0000000000..e919fa41ec --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -0,0 +1,127 @@ +package com.tangem.tap.features.main + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.tangem.core.navigation.AppScreen +import com.tangem.core.navigation.NavigationAction +import com.tangem.core.navigation.ReduxNavController +import com.tangem.domain.balancehiding.BalanceHidingSettings +import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.balancehiding.ListenToFlipsUseCase +import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase +import com.tangem.tap.features.main.model.MainScreenState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +internal class MainViewModel @Inject constructor( + private val updateBalanceHidingSettingsUseCase: UpdateBalanceHidingSettingsUseCase, + private val listenToFlipsUseCase: ListenToFlipsUseCase, + private val reduxNavController: ReduxNavController, + getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, +) : ViewModel(), MainIntents { + + private val stateHolder = MainScreenStateHolder( + intents = this, + ) + + private val balanceHidingSettingsFlow: SharedFlow = getBalanceHidingSettingsUseCase() + .shareIn(viewModelScope, SharingStarted.WhileSubscribed()) + + val state: StateFlow = stateHolder.stateFlow + + init { + observeFlips() + displayBalancesHidingStatusToast() + displayHiddenBalancesModalNotification() + } + + private fun observeFlips() { + listenToFlipsUseCase().launchIn(viewModelScope) + } + + private fun displayHiddenBalancesModalNotification() { + balanceHidingSettingsFlow + .drop(count = 1) // Skip initial emit + .distinctUntilChanged() + .filter { + it.isBalanceHidingNotificationEnabled && it.isBalanceHidden + } + .onEach { + if (state.value.modalNotification?.isShow != true) { + stateHolder.updateWithHiddenBalancesNotification() + reduxNavController.navigate(NavigationAction.NavigateTo(AppScreen.ModalNotification)) + } + } + .launchIn(viewModelScope) + } + + private fun displayBalancesHidingStatusToast() { + var previousSettings: BalanceHidingSettings? = null + + balanceHidingSettingsFlow + .onEach { settings -> + if (previousSettings == null) { + previousSettings = settings + return@onEach + } + + /* + * Skip if the feature to hide balances has just been + * enabled or disabled in the settings, or if the hide balances status has not been changed + * */ + if (settings.isHidingEnabledInSettings != previousSettings?.isHidingEnabledInSettings || + settings.isBalanceHidden == previousSettings?.isBalanceHidden + ) { + previousSettings = settings + return@onEach + } + + displayBalancesHiddenStatusToast(settings) + + previousSettings = settings + } + .launchIn(viewModelScope) + } + + private fun displayBalancesHiddenStatusToast(settings: BalanceHidingSettings) { + // If modal notification is enabled and balances are hidden, the toast will not show + if (!settings.isBalanceHidingNotificationEnabled || !settings.isBalanceHidden) { + stateHolder.updateWithHiddenBalancesToast(settings.isBalanceHidden) + } + } + + override fun onHiddenBalanceToastAction() { + viewModelScope.launch { + updateBalanceHidingSettingsUseCase.invoke { + copy(isBalanceHidden = false) + } + } + } + + override fun onShownBalanceToastAction() { + viewModelScope.launch { + updateBalanceHidingSettingsUseCase.invoke { + copy(isBalanceHidden = true) + } + } + } + + override fun onHiddenBalanceNotificationAction(isPermanent: Boolean) { + onDismissBottomSheet() + + if (isPermanent) { + viewModelScope.launch { + updateBalanceHidingSettingsUseCase.invoke { + copy(isBalanceHidingNotificationEnabled = false) + } + } + } + } + + override fun onDismissBottomSheet() { + stateHolder.updateWithoutModalNotification() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/main/NotificationsFactory.kt b/app/src/main/java/com/tangem/tap/features/main/NotificationsFactory.kt new file mode 100644 index 0000000000..cc2dcc6c40 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/main/NotificationsFactory.kt @@ -0,0 +1,48 @@ +package com.tangem.tap.features.main + +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.tap.features.main.model.ActionConfig +import com.tangem.tap.features.main.model.ModalNotification +import com.tangem.tap.features.main.model.Toast +import com.tangem.wallet.R + +internal class NotificationsFactory( + private val intents: MainIntents, +) { + + fun createBalancesAreHiddenToast(): Toast { + return Toast( + message = resourceReference(R.string.toast_balances_hidden), + action = ActionConfig( + text = resourceReference(R.string.toast_undo), + onClick = intents::onHiddenBalanceToastAction, + ), + ) + } + + fun createBalancesAreShownToast(): Toast { + return Toast( + message = resourceReference(R.string.toast_balances_shown), + action = ActionConfig( + text = resourceReference(R.string.toast_undo), + onClick = intents::onShownBalanceToastAction, + ), + ) + } + + fun createBalancesAreHiddenModalNotification(): ModalNotification { + return ModalNotification( + iconResId = R.drawable.ic_eye_off_outline_24, + title = resourceReference(R.string.balance_hidden_title), + message = resourceReference(R.string.balance_hidden_description), + primaryAction = ActionConfig( + text = resourceReference(R.string.balance_hidden_got_it_button), + onClick = { intents.onHiddenBalanceNotificationAction(isPermanent = false) }, + ), + secondaryAction = ActionConfig( + text = resourceReference(R.string.balance_hidden_do_not_show_button), + onClick = { intents.onHiddenBalanceNotificationAction(isPermanent = true) }, + ), + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/main/model/ActionConfig.kt b/app/src/main/java/com/tangem/tap/features/main/model/ActionConfig.kt new file mode 100644 index 0000000000..6549c35d88 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/main/model/ActionConfig.kt @@ -0,0 +1,8 @@ +package com.tangem.tap.features.main.model + +import com.tangem.core.ui.extensions.TextReference + +internal data class ActionConfig( + val text: TextReference, + val onClick: () -> Unit, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/main/model/MainScreenState.kt b/app/src/main/java/com/tangem/tap/features/main/model/MainScreenState.kt new file mode 100644 index 0000000000..7a787bbc7b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/main/model/MainScreenState.kt @@ -0,0 +1,9 @@ +package com.tangem.tap.features.main.model + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.event.StateEvent + +internal data class MainScreenState( + val toast: StateEvent, + val modalNotification: TangemBottomSheetConfig?, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/main/model/ModalNotification.kt b/app/src/main/java/com/tangem/tap/features/main/model/ModalNotification.kt new file mode 100644 index 0000000000..eaf3d37833 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/main/model/ModalNotification.kt @@ -0,0 +1,13 @@ +package com.tangem.tap.features.main.model + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.extensions.TextReference + +internal data class ModalNotification( + @DrawableRes val iconResId: Int, + val title: TextReference, + val message: TextReference, + val primaryAction: ActionConfig, + val secondaryAction: ActionConfig?, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/main/model/Toast.kt b/app/src/main/java/com/tangem/tap/features/main/model/Toast.kt new file mode 100644 index 0000000000..9e959eb892 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/main/model/Toast.kt @@ -0,0 +1,8 @@ +package com.tangem.tap.features.main.model + +import com.tangem.core.ui.extensions.TextReference + +internal data class Toast( + val message: TextReference, + val action: ActionConfig, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/main/ui/ModalNotificationBottomSheetFragment.kt b/app/src/main/java/com/tangem/tap/features/main/ui/ModalNotificationBottomSheetFragment.kt new file mode 100644 index 0000000000..2a40e92ef8 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/main/ui/ModalNotificationBottomSheetFragment.kt @@ -0,0 +1,57 @@ +package com.tangem.tap.features.main.ui + +import android.content.DialogInterface +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.ui.screen.ComposeBottomSheetFragment +import com.tangem.core.ui.theme.AppThemeModeHolder +import com.tangem.tap.features.main.MainViewModel +import com.tangem.tap.features.main.model.ModalNotification +import com.tangem.tap.features.main.ui.components.ModalNotificationContent +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +internal class ModalNotificationBottomSheetFragment : ComposeBottomSheetFragment() { + + @Inject + override lateinit var appThemeModeHolder: AppThemeModeHolder + + private val viewModel: MainViewModel by activityViewModels() + + @Composable + override fun ScreenContent(modifier: Modifier) { + val state by viewModel.state.collectAsStateWithLifecycle() + val notification = state.modalNotification + if (notification == null) { + dismiss() + return + } + + BackHandler(onBack = notification.onDismissRequest) + + when (val content = notification.content) { + is ModalNotification -> ModalNotificationContent( + modifier = modifier, + notification = content, + ) + else -> Unit + } + + LaunchedEffect(notification) { + if (!notification.isShow) { + dismiss() + } + } + } + + override fun onDismiss(dialog: DialogInterface) { + viewModel.state.value.modalNotification?.onDismissRequest?.invoke() + super.onDismiss(dialog) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/main/ui/components/ModalNotificationScreen.kt b/app/src/main/java/com/tangem/tap/features/main/ui/components/ModalNotificationScreen.kt new file mode 100644 index 0000000000..3ebb87e8dd --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/main/ui/components/ModalNotificationScreen.kt @@ -0,0 +1,133 @@ +package com.tangem.tap.features.main.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.components.atoms.Hand +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.tap.features.main.model.ActionConfig +import com.tangem.tap.features.main.model.ModalNotification +import com.tangem.wallet.R + +@Composable +internal fun ModalNotificationContent(notification: ModalNotification, modifier: Modifier = Modifier) { + Column( + modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing16), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing40), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Hand() + Icon( + modifier = Modifier.size(TangemTheme.dimens.size48), + painter = painterResource(notification.iconResId), + tint = TangemTheme.colors.icon.primary1, + contentDescription = null, + ) + Column( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = notification.title.resolveReference(), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + Text( + text = notification.message.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + } + Column( + modifier = Modifier + .padding(bottom = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + PrimaryButton( + modifier = Modifier.fillMaxWidth(), + text = notification.primaryAction.text.resolveReference(), + onClick = notification.primaryAction.onClick, + ) + when (val secondaryAction = notification.secondaryAction) { + null -> Unit + else -> SecondaryButton( + modifier = Modifier.fillMaxWidth(), + text = secondaryAction.text.resolveReference(), + onClick = secondaryAction.onClick, + ) + } + } + } +} + +// region Preview +@Preview(widthDp = 360, heightDp = 404) +@Composable +private fun ModalNotificationContentPreview_Light( + @PreviewParameter(GlobalNotificationProvider::class) param: ModalNotification, +) { + TangemTheme(isDark = false) { + ModalNotificationContent( + param, + modifier = Modifier.background( + color = TangemTheme.colors.background.plain, + shape = TangemTheme.shapes.bottomSheet, + ), + ) + } +} + +@Preview(widthDp = 360, heightDp = 404) +@Composable +private fun ModalNotificationContentPreview_Dark( + @PreviewParameter(GlobalNotificationProvider::class) param: ModalNotification, +) { + TangemTheme(isDark = true) { + ModalNotificationContent( + param, + modifier = Modifier.background( + color = TangemTheme.colors.background.plain, + shape = TangemTheme.shapes.bottomSheet, + ), + ) + } +} + +private class GlobalNotificationProvider : CollectionPreviewParameterProvider( + collection = listOf( + ModalNotification( + iconResId = R.drawable.ic_eye_off_outline_24, + title = resourceReference(R.string.balance_hidden_title), + message = resourceReference(R.string.balance_hidden_description), + primaryAction = ActionConfig( + text = resourceReference(R.string.balance_hidden_got_it_button), + onClick = {}, + ), + secondaryAction = ActionConfig( + text = resourceReference(R.string.balance_hidden_do_not_show_button), + onClick = {}, + ), + ), + ), +) +// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupAnimator.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupAnimator.kt index 537ed92d90..94de063a63 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupAnimator.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupAnimator.kt @@ -4,9 +4,7 @@ import com.google.android.material.button.MaterialButton import com.tangem.tap.common.extensions.show import com.tangem.tap.common.postUiDelayBg import com.tangem.tap.features.onboarding.products.wallet.redux.BackupState -import com.tangem.tap.features.onboarding.products.wallet.ui.BackupCardType.FIRST_BACKUP -import com.tangem.tap.features.onboarding.products.wallet.ui.BackupCardType.ORIGIN -import com.tangem.tap.features.onboarding.products.wallet.ui.BackupCardType.SECOND_BACKUP +import com.tangem.tap.features.onboarding.products.wallet.ui.BackupCardType.* import com.tangem.wallet.databinding.FragmentOnboardingWalletBinding /** @@ -29,7 +27,7 @@ class WalletBackupAnimator( private val viewState: State = when (cardsWidget.leapfrogWidget.getViewsCount()) { 2 -> State.TwoCards - 3 -> State.TreeCards + 3 -> State.ThreeCards else -> throw UnsupportedOperationException() } @@ -131,7 +129,7 @@ class WalletBackupAnimator( } } } - State.TreeCards -> { + State.ThreeCards -> { when (backupCard) { 1 -> { if (firstBackupCardAnimated) return @@ -147,6 +145,15 @@ class WalletBackupAnimator( if (secondBackupCardAnimated) return secondBackupCardAnimated = true + /* + * !!! Workaround !!! + * For interrupted backup + * */ + if (!firstBackupCardAnimated) { + showWriteBackupCard(state, backupCard = 1) + return + } + SECOND_BACKUP.alpha(1f) cardsWidget.leapfrogWidget.leap { ORIGIN.alpha(0.4f) @@ -165,7 +172,7 @@ class WalletBackupAnimator( } private enum class State { - TreeCards, + ThreeCards, TwoCards, } @@ -186,7 +193,7 @@ class WalletBackupAnimator( } } SECOND_BACKUP -> { - if (viewState != State.TreeCards) return + if (viewState != State.ThreeCards) return cardsWidget.getSecondBackupCardView().animate().apply { alpha(alpha) this.startDelay = startDelay @@ -201,7 +208,7 @@ class WalletBackupAnimator( ORIGIN -> cardsWidget.getOriginCardView().alpha = alpha FIRST_BACKUP -> cardsWidget.getFirstBackupCardView().alpha = alpha SECOND_BACKUP -> { - if (viewState != State.TreeCards) return + if (viewState != State.ThreeCards) return cardsWidget.getSecondBackupCardView().alpha = alpha } } @@ -212,7 +219,7 @@ class WalletBackupAnimator( ORIGIN -> cardsWidget.getOriginCardView().show() FIRST_BACKUP -> cardsWidget.getFirstBackupCardView().show() SECOND_BACKUP -> { - if (viewState != State.TreeCards) return + if (viewState != State.ThreeCards) return cardsWidget.getSecondBackupCardView().show() } } diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt index 806cb70f74..fe7c000ae6 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt @@ -1,7 +1,7 @@ package com.tangem.tap.features.send.ui import androidx.lifecycle.* -import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase +import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.balancehiding.ListenToFlipsUseCase import com.tangem.domain.tokens.FetchPendingTransactionsUseCase import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase @@ -9,7 +9,7 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import com.tangem.features.send.navigation.SendRouter +import com.tangem.features.send.api.navigation.SendRouter import com.tangem.tap.di.DelayedWork import com.tangem.tap.features.send.redux.AmountAction import com.tangem.tap.proxy.AppStateHolder @@ -29,7 +29,7 @@ import javax.inject.Inject internal class SendViewModel @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, private val appStateHolder: AppStateHolder, - private val isBalanceHiddenUseCase: IsBalanceHiddenUseCase, + private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val listenToFlipsUseCase: ListenToFlipsUseCase, private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, @@ -41,11 +41,11 @@ internal class SendViewModel @Inject constructor( private val cryptoCurrency: CryptoCurrency? = savedStateHandle[SendRouter.CRYPTO_CURRENCY_KEY] override fun onCreate(owner: LifecycleOwner) { - isBalanceHiddenUseCase() + getBalanceHidingSettingsUseCase() .flowWithLifecycle(owner.lifecycle) - .onEach { isBalanceHidden -> + .onEach { withContext(dispatchers.main) { - appStateHolder.mainStore?.dispatch(AmountAction.HideBalance(isBalanceHidden)) + appStateHolder.mainStore?.dispatch(AmountAction.HideBalance(it.isBalanceHidden)) } } .launchIn(viewModelScope) diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/states/NetworkItemState.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/states/NetworkItemState.kt index bfcffbdf69..a78f074e16 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/states/NetworkItemState.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/states/NetworkItemState.kt @@ -3,7 +3,7 @@ package com.tangem.tap.features.tokens.impl.presentation.states import androidx.compose.runtime.MutableState import com.tangem.blockchain.common.Blockchain import com.tangem.core.ui.extensions.getActiveIconRes -import com.tangem.tap.common.extensions.getGreyedOutIconRes +import com.tangem.core.ui.extensions.getGreyedOutIconRes /** * Network item state @@ -76,7 +76,7 @@ sealed interface NetworkItemState { fun changeToggleState() { val reverseState = !isAdded.value isAdded.value = reverseState - iconResId.value = if (reverseState) getActiveIconRes(blockchain.id) else blockchain.getGreyedOutIconRes() + iconResId.value = if (reverseState) getActiveIconRes(blockchain.id) else getGreyedOutIconRes(blockchain.id) } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokenListPreviewData.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokenListPreviewData.kt index 819e5e4fb7..f5481080e5 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokenListPreviewData.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokenListPreviewData.kt @@ -40,7 +40,7 @@ object TokenListPreviewData { NetworkItemState.ManageContent( name = "Ethereum", protocolName = "MAIN", - iconResId = mutableStateOf(R.drawable.ic_eth_no_color), + iconResId = mutableStateOf(R.drawable.ic_eth_16), isMainNetwork = true, isAdded = mutableStateOf(true), id = "", @@ -53,7 +53,7 @@ object TokenListPreviewData { NetworkItemState.ManageContent( name = "BNB SMART CHAIN", protocolName = "BEP20", - iconResId = mutableStateOf(R.drawable.ic_bsc_no_color), + iconResId = mutableStateOf(R.drawable.ic_bsc_16), isMainNetwork = false, isAdded = mutableStateOf(false), id = "", @@ -71,13 +71,13 @@ object TokenListPreviewData { NetworkItemState.ReadContent( name = "Ethereum", protocolName = "MAIN", - iconResId = mutableStateOf(R.drawable.ic_eth_no_color), + iconResId = mutableStateOf(R.drawable.ic_eth_16), isMainNetwork = true, ), NetworkItemState.ReadContent( name = "BNB SMART CHAIN", protocolName = "BEP20", - iconResId = mutableStateOf(R.drawable.ic_bsc_no_color), + iconResId = mutableStateOf(R.drawable.ic_bsc_16), isMainNetwork = false, ), ) diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt index 06d6c0ce2e..a03a0aef85 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt @@ -9,6 +9,7 @@ import androidx.paging.* import com.tangem.blockchain.common.Blockchain import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.extensions.getActiveIconRes +import com.tangem.core.ui.extensions.getGreyedOutIconRes import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation import com.tangem.domain.common.extensions.canHandleBlockchain import com.tangem.domain.common.extensions.canHandleToken @@ -20,7 +21,6 @@ import com.tangem.domain.tokens.TokenWithBlockchain import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.tap.common.extensions.fullNameWithoutTestnet -import com.tangem.tap.common.extensions.getGreyedOutIconRes import com.tangem.tap.common.extensions.getNetworkName import com.tangem.tap.features.tokens.impl.domain.TokensListInteractor import com.tangem.tap.features.tokens.impl.domain.models.Token @@ -225,7 +225,7 @@ internal class TokensListViewModel @Inject constructor( return if (isAdded(address = address, blockchain = blockchain)) { getActiveIconRes(blockchain.id) } else { - blockchain.getGreyedOutIconRes() + getGreyedOutIconRes(blockchain.id) } } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index 70c30acd7f..c3092a3714 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -15,7 +15,7 @@ import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.feature.swap.presentation.SwapFragment -import com.tangem.features.send.navigation.SendRouter +import com.tangem.features.send.api.navigation.SendRouter import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Token import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder @@ -366,7 +366,10 @@ class TradeCryptoMiddleware { ), ) - val bundle = bundleOf(SendRouter.CRYPTO_CURRENCY_KEY to currency) + val bundle = bundleOf( + SendRouter.CRYPTO_CURRENCY_KEY to currency, + SendRouter.USER_WALLET_ID_KEY to action.userWallet.walletId.stringValue, + ) store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle)) } } @@ -416,7 +419,10 @@ class TradeCryptoMiddleware { is CryptoCurrency.Token -> error("Action.tokenStatus.currency is Token") } - val bundle = bundleOf(SendRouter.CRYPTO_CURRENCY_KEY to currency) + val bundle = bundleOf( + SendRouter.CRYPTO_CURRENCY_KEY to currency, + SendRouter.USER_WALLET_ID_KEY to action.userWallet.walletId.stringValue, + ) store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle)) } } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt index 290bd68881..37789a8792 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt @@ -106,7 +106,7 @@ class MultiWalletView : WalletView() { Analytics.send(Portfolio.ButtonManageTokens()) store.dispatch(action = TokensAction.SetArgs.ManageAccess) - store.dispatch(action = NavigationAction.NavigateTo(screen = AppScreen.AddTokens)) + store.dispatch(action = NavigationAction.NavigateTo(screen = AppScreen.ManageTokens)) } handleErrorStates(state = state, binding = binding, fragment = fragment) } diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt index 1b0f19c9fe..3392c82127 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt @@ -2,6 +2,8 @@ package com.tangem.tap.proxy.redux import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.features.managetokens.navigation.ManageTokensRouter +import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import com.tangem.features.wallet.navigation.WalletRouter @@ -16,6 +18,8 @@ sealed interface DaggerGraphAction : Action { val walletRouter: WalletRouter, val walletConnectInteractor: WalletConnectInteractor, val tokenDetailsRouter: TokenDetailsRouter, + val manageTokensRouter: ManageTokensRouter, val cardSdkConfigRepository: CardSdkConfigRepository, + val sendRouter: SendRouter, ) : DaggerGraphAction } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt index aec162a122..df08899bf0 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt @@ -18,7 +18,9 @@ object DaggerGraphReducer { walletRouter = action.walletRouter, walletConnectInteractor = action.walletConnectInteractor, tokenDetailsRouter = action.tokenDetailsRouter, + manageTokensRouter = action.manageTokensRouter, cardSdkConfigRepository = action.cardSdkConfigRepository, + sendRouter = action.sendRouter, ) } } diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index 08f259fa70..e7a479dfa0 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -12,6 +12,10 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles +import com.tangem.features.managetokens.navigation.ManageTokensRouter +import com.tangem.features.send.api.featuretoggles.SendFeatureToggles +import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles @@ -36,6 +40,8 @@ data class DaggerGraphState( val walletConnectSessionsRepository: WalletConnectSessionsRepository? = null, val walletConnectInteractor: WalletConnectInteractor? = null, val tokenDetailsRouter: TokenDetailsRouter? = null, + val manageTokensFeatureToggles: ManageTokensFeatureToggles? = null, + val manageTokensRouter: ManageTokensRouter? = null, val scanCardProcessor: ScanCardProcessor? = null, val cardSdkConfigRepository: CardSdkConfigRepository? = null, val appCurrencyRepository: AppCurrencyRepository? = null, @@ -46,6 +52,8 @@ data class DaggerGraphState( val detailsFeatureToggles: DetailsFeatureToggles? = null, val walletsRepository: WalletsRepository? = null, val networksRepository: NetworksRepository? = null, + val sendFeatureToggles: SendFeatureToggles? = null, + val sendRouter: SendRouter? = null, // FIXME: It is used only for TokensList screen. Remove after refactoring of TokensList val currenciesRepository: CurrenciesRepository? = null, diff --git a/app/src/main/res/drawable/ic_arbitrum_no_color.xml b/app/src/main/res/drawable/ic_arbitrum_no_color.xml deleted file mode 100644 index e7c20e844b..0000000000 --- a/app/src/main/res/drawable/ic_arbitrum_no_color.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_bitcoin_cash_no_color.xml b/app/src/main/res/drawable/ic_bitcoin_cash_no_color.xml deleted file mode 100644 index f9a1276588..0000000000 --- a/app/src/main/res/drawable/ic_bitcoin_cash_no_color.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_bsc_no_color.xml b/app/src/main/res/drawable/ic_bsc_no_color.xml deleted file mode 100644 index a7062f73c1..0000000000 --- a/app/src/main/res/drawable/ic_bsc_no_color.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/ic_cardano_no_color.xml b/app/src/main/res/drawable/ic_cardano_no_color.xml deleted file mode 100644 index 46a9df7709..0000000000 --- a/app/src/main/res/drawable/ic_cardano_no_color.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_dogecoin_no_color.xml b/app/src/main/res/drawable/ic_dogecoin_no_color.xml deleted file mode 100644 index 9d52af3530..0000000000 --- a/app/src/main/res/drawable/ic_dogecoin_no_color.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_eth_no_color.xml b/app/src/main/res/drawable/ic_eth_no_color.xml deleted file mode 100644 index 41071ba559..0000000000 --- a/app/src/main/res/drawable/ic_eth_no_color.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_fantom_no_color.xml b/app/src/main/res/drawable/ic_fantom_no_color.xml deleted file mode 100644 index f7498b5230..0000000000 --- a/app/src/main/res/drawable/ic_fantom_no_color.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_kusama_no_color.xml b/app/src/main/res/drawable/ic_kusama_no_color.xml deleted file mode 100644 index be4ff99b2e..0000000000 --- a/app/src/main/res/drawable/ic_kusama_no_color.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_polkadot_no_color.xml b/app/src/main/res/drawable/ic_polkadot_no_color.xml deleted file mode 100644 index 6e8ee64a05..0000000000 --- a/app/src/main/res/drawable/ic_polkadot_no_color.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/drawable/ic_polygon_no_color.xml b/app/src/main/res/drawable/ic_polygon_no_color.xml deleted file mode 100644 index eafe18126e..0000000000 --- a/app/src/main/res/drawable/ic_polygon_no_color.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_rsk_no_color.xml b/app/src/main/res/drawable/ic_rsk_no_color.xml deleted file mode 100644 index f09adebd5c..0000000000 --- a/app/src/main/res/drawable/ic_rsk_no_color.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_solana_no_color.xml b/app/src/main/res/drawable/ic_solana_no_color.xml deleted file mode 100644 index 31e6195232..0000000000 --- a/app/src/main/res/drawable/ic_solana_no_color.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_stellar_no_color.xml b/app/src/main/res/drawable/ic_stellar_no_color.xml deleted file mode 100644 index 61e3683e6f..0000000000 --- a/app/src/main/res/drawable/ic_stellar_no_color.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_tezos_no_color.xml b/app/src/main/res/drawable/ic_tezos_no_color.xml deleted file mode 100644 index a929bead46..0000000000 --- a/app/src/main/res/drawable/ic_tezos_no_color.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_tron_no_color.xml b/app/src/main/res/drawable/ic_tron_no_color.xml deleted file mode 100644 index 1281613a40..0000000000 --- a/app/src/main/res/drawable/ic_tron_no_color.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index 6d7e66c87e..88d82bd624 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -47,4 +47,11 @@ dependencies { /** Security */ implementation(deps.spongecastle.core) + + /** Chucker */ + /** Chucker */ + debugImplementation(deps.chucker) + externalImplementation(deps.chuckerStub) + internalImplementation(deps.chuckerStub) + releaseImplementation(deps.chuckerStub) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/paymentology/PaymentologyApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/paymentology/PaymentologyApi.kt deleted file mode 100644 index d3d092493f..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/paymentology/PaymentologyApi.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.tangem.datasource.api.paymentology - -import com.tangem.datasource.api.paymentology.models.request.CheckRegistrationRequests -import com.tangem.datasource.api.paymentology.models.request.RegisterKYCRequest -import com.tangem.datasource.api.paymentology.models.request.RegisterWalletRequest -import com.tangem.datasource.api.paymentology.models.response.AttestationResponse -import com.tangem.datasource.api.paymentology.models.response.RegisterWalletResponse -import com.tangem.datasource.api.paymentology.models.response.RegistrationResponse -import retrofit2.http.Body -import retrofit2.http.Headers -import retrofit2.http.POST - -/** - * Interface of Paymentology Api. - * - * IMPORTANT: Cannot replace [Headers] annotations with OkHttpClient.addHeader(), because OkHttp adds encoding to - * content type value. But Paymentology API expects a value without the charset. - * -[REDACTED_AUTHOR] - */ -interface PaymentologyApi { - - @Headers("Content-Type: application/json") - @POST("card/verify") - suspend fun checkRegistration(@Body request: CheckRegistrationRequests): RegistrationResponse - - @Headers("Content-Type: application/json") - @POST("card/get_challenge") - suspend fun requestAttestationChallenge(@Body request: CheckRegistrationRequests.Item): AttestationResponse - - @Headers("Content-Type: application/json") - @POST("card/set_pin") - suspend fun registerWallet(@Body request: RegisterWalletRequest): RegisterWalletResponse - - @Headers("Content-Type: application/json") - @POST("card/kyc") - suspend fun registerKYC(@Body request: RegisterKYCRequest): RegisterWalletResponse -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/paymentology/PaymentologyApiService.kt b/core/datasource/src/main/java/com/tangem/datasource/api/paymentology/PaymentologyApiService.kt deleted file mode 100644 index f660c1fc47..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/paymentology/PaymentologyApiService.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.tangem.datasource.api.paymentology - -import com.tangem.datasource.api.common.MoshiConverter -import com.tangem.datasource.utils.allowLogging -import okhttp3.OkHttpClient -import retrofit2.Retrofit - -/** -[REDACTED_AUTHOR] - */ -// TODO("Remove after removing Redux") -@Deprecated("Use PaymentologyApi") -object PaymentologyApiService { - val api = createApi() - - private const val PAYMENTOLOGY_BASE_URL: String = "https://paymentologygate.oa.r.appspot.com/" - - private fun createApi(): PaymentologyApi { - return Retrofit.Builder() - .addConverterFactory(MoshiConverter.networkMoshiConverter) - .baseUrl(PAYMENTOLOGY_BASE_URL) - .client( - OkHttpClient.Builder() - .allowLogging() - .build(), - ) - .build() - .create(PaymentologyApi::class.java) - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechService.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechService.kt index cf51d43c58..4c08c5ce73 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechService.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechService.kt @@ -6,7 +6,7 @@ import com.tangem.datasource.utils.RequestHeader import com.tangem.datasource.utils.RequestHeader.AuthenticationHeader import com.tangem.datasource.utils.RequestHeader.CacheControlHeader import com.tangem.datasource.utils.addHeaders -import com.tangem.datasource.utils.allowLogging +import com.tangem.datasource.utils.addLoggers import okhttp3.OkHttpClient import retrofit2.Retrofit @@ -35,7 +35,7 @@ object TangemTechService { .client( OkHttpClient.Builder() .addHeaders(*headers.toTypedArray()) - .allowLogging() + .addLoggers() .build(), ) .build() diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index 3a9580d2d0..f3e245e0d6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -1,17 +1,18 @@ package com.tangem.datasource.di +import android.content.Context import com.squareup.moshi.Moshi import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory -import com.tangem.datasource.api.paymentology.PaymentologyApi import com.tangem.datasource.api.promotion.PromotionApi import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.utils.RequestHeader.* import com.tangem.datasource.utils.addHeaders -import com.tangem.datasource.utils.allowLogging +import com.tangem.datasource.utils.addLoggers import com.tangem.lib.auth.AuthProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import okhttp3.OkHttpClient import retrofit2.Retrofit @@ -25,7 +26,7 @@ class NetworkModule { @Provides @Singleton - fun provideTangemTechApi(@NetworkMoshi moshi: Moshi): TangemTechApi { + fun provideTangemTechApi(@NetworkMoshi moshi: Moshi, @ApplicationContext context: Context): TangemTechApi { return Retrofit.Builder() .addConverterFactory(MoshiConverterFactory.create(moshi)) .addCallAdapterFactory(ApiResponseCallAdapterFactory.create()) @@ -37,35 +38,24 @@ class NetworkModule { // TODO("refactor header init") get auth data after biometric auth to avoid race condition // AuthenticationHeader(authProvider), ) - .allowLogging() + .addLoggers(context) .build(), ) .build() .create(TangemTechApi::class.java) } - @Provides - @Singleton - fun providePaymentologyApi(@NetworkMoshi moshi: Moshi): PaymentologyApi { - return Retrofit.Builder() - .addConverterFactory(MoshiConverterFactory.create(moshi)) - .baseUrl(PAYMENTOLOGY_BASE_URL) - .client( - OkHttpClient.Builder() - .allowLogging() - .build(), - ) - .build() - .create(PaymentologyApi::class.java) - } - @Provides @Singleton @PromotionOneInch - fun providePromotionOneInchApi(authProvider: AuthProvider, @NetworkMoshi moshi: Moshi): PromotionApi { + fun providePromotionOneInchApi( + authProvider: AuthProvider, + @NetworkMoshi moshi: Moshi, + @ApplicationContext context: Context, + ): PromotionApi { val okClient = OkHttpClient.Builder() .addHeaders(AuthenticationHeader(authProvider)) - .allowLogging() + .addLoggers(context) .callTimeout(API_ONE_INCH_TIMEOUT_MS, TimeUnit.MILLISECONDS) .connectTimeout(API_ONE_INCH_TIMEOUT_MS, TimeUnit.MILLISECONDS) .readTimeout(API_ONE_INCH_TIMEOUT_MS, TimeUnit.MILLISECONDS) diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/OneInchApisModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/OneInchApisModule.kt index b413b2aa14..68c9fdf683 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/OneInchApisModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/OneInchApisModule.kt @@ -1,12 +1,14 @@ package com.tangem.datasource.di +import android.content.Context import com.squareup.moshi.Moshi import com.tangem.datasource.api.oneinch.OneInchApi import com.tangem.datasource.api.oneinch.OneInchApiFactory -import com.tangem.datasource.utils.allowLogging +import com.tangem.datasource.utils.addLoggers import dagger.Module import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import okhttp3.OkHttpClient import retrofit2.Retrofit @@ -19,23 +21,31 @@ class OneInchApisModule { @Provides @Singleton - fun provideOneInchApiFactory(@NetworkMoshi moshi: Moshi): OneInchApiFactory { - val apiFactory = OneInchApiFactory() - apiFactory.putApi(ETH_NETWORK, createOneInchApiWithUrl(ONE_INCH_BASE_URL + ONE_INCH_ETH_PATH, moshi)) - apiFactory.putApi(BSC_NETWORK, createOneInchApiWithUrl(ONE_INCH_BASE_URL + ONE_INCH_BSC_PATH, moshi)) - apiFactory.putApi(POLYGON_NETWORK, createOneInchApiWithUrl(ONE_INCH_BASE_URL + ONE_INCH_POLYGON_PATH, moshi)) - apiFactory.putApi(OPTIMISM_NETWORK, createOneInchApiWithUrl(ONE_INCH_BASE_URL + ONE_INCH_OPTIMISM_PATH, moshi)) - apiFactory.putApi(ARBITRUM_NETWORK, createOneInchApiWithUrl(ONE_INCH_BASE_URL + ONE_INCH_ARBITRUM_PATH, moshi)) - apiFactory.putApi(GNOSIS_NETWORK, createOneInchApiWithUrl(ONE_INCH_BASE_URL + ONE_INCH_GNOSIS_PATH, moshi)) - apiFactory.putApi( - AVALANCHE_NETWORK, - createOneInchApiWithUrl(ONE_INCH_BASE_URL + ONE_INCH_AVALANCHE_PATH, moshi), + fun provideOneInchApiFactory(@NetworkMoshi moshi: Moshi, @ApplicationContext context: Context): OneInchApiFactory { + val networks = mapOf( + ETH_NETWORK to ONE_INCH_ETH_PATH, + BSC_NETWORK to ONE_INCH_BSC_PATH, + POLYGON_NETWORK to ONE_INCH_POLYGON_PATH, + OPTIMISM_NETWORK to ONE_INCH_OPTIMISM_PATH, + ARBITRUM_NETWORK to ONE_INCH_ARBITRUM_PATH, + GNOSIS_NETWORK to ONE_INCH_GNOSIS_PATH, + AVALANCHE_NETWORK to ONE_INCH_AVALANCHE_PATH, + FANTOM_NETWORK to ONE_INCH_FANTOM_PATH, ) - apiFactory.putApi(FANTOM_NETWORK, createOneInchApiWithUrl(ONE_INCH_BASE_URL + ONE_INCH_FANTOM_PATH, moshi)) + + val apiFactory = OneInchApiFactory() + + for ((network, path) in networks) { + apiFactory.putApi( + networkId = network, + api = createOneInchApiWithUrl("$ONE_INCH_BASE_URL$path", moshi, context), + ) + } + return apiFactory } - private fun createOneInchApiWithUrl(url: String, moshi: Moshi): OneInchApi { + private fun createOneInchApiWithUrl(url: String, moshi: Moshi, context: Context): OneInchApi { return Retrofit.Builder() .addConverterFactory( MoshiConverterFactory.create(moshi), @@ -43,7 +53,7 @@ class OneInchApisModule { .baseUrl(url) .client( OkHttpClient.Builder() - .allowLogging() + .addLoggers(context) .build(), ) .build() diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/implementation/BalanceStateHidingSettingsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/implementation/BalanceStateHidingSettingsStore.kt index 740005d56b..0314d6549d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/implementation/BalanceStateHidingSettingsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/implementation/BalanceStateHidingSettingsStore.kt @@ -13,6 +13,7 @@ internal class BalanceStateHidingSettingsStore( return getSyncOrNull() ?: BalanceHidingSettings( isHidingEnabledInSettings = false, isBalanceHidden = false, + isBalanceHidingNotificationEnabled = true, ) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt index 4a5a6a1b75..5490d5f8f3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt @@ -1,5 +1,7 @@ package com.tangem.datasource.utils +import android.content.Context +import com.chuckerteam.chucker.api.ChuckerInterceptor import com.tangem.datasource.BuildConfig import com.tangem.datasource.api.common.createNetworkLoggingInterceptor import okhttp3.Interceptor @@ -25,8 +27,11 @@ internal fun OkHttpClient.Builder.addHeaders(vararg requestHeaders: RequestHeade * * @param level logging level. By default, only the request body. */ -internal fun OkHttpClient.Builder.allowLogging(): OkHttpClient.Builder { - return if (BuildConfig.DEBUG) { +internal fun OkHttpClient.Builder.addLoggers(context: Context? = null): OkHttpClient.Builder { + return if (BuildConfig.LOG_ENABLED) { + context?.let { + addInterceptor(interceptor = ChuckerInterceptor(it)) + } addInterceptor(interceptor = createNetworkLoggingInterceptor()) } else { this diff --git a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json index f1ef728af6..9da64e96a0 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -26,5 +26,13 @@ { "name": "DARK_THEME_ENABLED", "version": "5.0.0" + }, + { + "name": "REDESIGNED_MANAGE_TOKENS_SCREEN_ENABLED", + "version": "undefined" + }, + { + "name": "REDESIGNED_SEND_SCREEN_ENABLED", + "version": "undefined" } ] diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/NavigationState.kt b/core/navigation/src/main/java/com/tangem/core/navigation/NavigationState.kt index 9aea608408..9f96640d74 100644 --- a/core/navigation/src/main/java/com/tangem/core/navigation/NavigationState.kt +++ b/core/navigation/src/main/java/com/tangem/core/navigation/NavigationState.kt @@ -19,14 +19,14 @@ enum class AppScreen(val isDialogFragment: Boolean = false) { OnboardingOther, Wallet, WalletDetails, - Send, + Send(isDialogFragment = true), Details, DetailsSecurity, CardSettings, AppSettings, ResetToFactory, AccessCodeRecovery, - AddTokens, + ManageTokens, AddCustomToken, WalletConnectSessions, QrScan, @@ -36,4 +36,5 @@ enum class AppScreen(val isDialogFragment: Boolean = false) { SaveWallet(isDialogFragment = true), WalletSelector(isDialogFragment = true), AppCurrencySelector, + ModalNotification(isDialogFragment = true), } \ No newline at end of file diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 44bf68de60..8532b9050c 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -28,6 +28,10 @@ Как в системе Тема Настройки приложения + Чтобы скрыть или показать баланс, просто поверните ваше устройство вниз или отключите опцию его в разделе \"Настройки\" + Больше не показывать + Понятно + Балансы скрыты Пожалуйста, отсканируйте карту Пожалуйста, попробуйте снова через 30 секунд или отсканируйте карту Слишком много попыток @@ -210,6 +214,24 @@ Токены Добавить Изменить + Основная сеть + Не основной или основной блокчейн, на котором размещен токен + Не основные сети + Сети + Выберите сети + Кошелек + Не удалось найти этот токен, вы можете добавить его вручную. + + %1$d из %2$d кошелька + %1$d из %2$d кошельков + %1$d из %2$d кошельков + %1$d из %2$d кошельков + + например Bitcoin + Рыночная капитализация + Выбранный токен не доступен в кошельке на данный момент. Но не переживайте, вы можете выразить свой интерес проголосовав за его добавление. + Голосовать + Выберите кошелек Вам необходимо установить единый код доступа для защиты всех ваших карт Защита Позже вы сможете установить индивидуальный код доступа для каждой карты @@ -434,6 +456,9 @@ Выберите токен Ваши токены не доступен + Балансы скрыты + Балансы показаны + Отменить %d токен %d токена diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 7282434764..ab48be1b10 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -28,6 +28,10 @@ System default Theme App Settings + To hide or show your balances, simply flip your device screen down, or switch it off in Settings + Don\'t show again + Got it + Balances are hidden Please scan the card Please try again in 30 seconds or scan the card Too many attempts @@ -222,10 +226,9 @@ Choose networks Wallet Couldn’t find this token, you can add it manually - %d of %#@total_wallets@ - - %d wallet - %d wallets + + %1$d of %2$d wallet + %1$d of %2$d wallets e.g. BTC I trust, hodl I must Coin market cap @@ -392,6 +395,7 @@ Normal Priority Maximum amount + Max Network fee Sending %s Total @@ -400,6 +404,7 @@ %s will be sent Transaction has been successfully signed and sent to the blockchain node. Wallet balance will be updated in a while Invalid address + Insufficient funds for transfer Buy now I have a promo code… Tangem Wallet @@ -452,6 +457,9 @@ Choose token Your tokens not available + Balances hidden + Balances shown + Undo %d token %d tokens diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 2f61a84a2b..5868a39017 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { implementation(deps.compose.paging) implementation(deps.compose.ui.tooling) implementation(deps.compose.ui.utils) + implementation(deps.compose.coil) /** Other libraries */ implementation(deps.compose.accompanist.systemUiController) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt index 3e60c55ef8..a9eddb52f7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt @@ -7,11 +7,10 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R -import com.tangem.core.ui.components.buttons.common.TangemButton -import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition -import com.tangem.core.ui.components.buttons.common.TangemButtonSize +import com.tangem.core.ui.components.buttons.common.* import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults import com.tangem.core.ui.res.TangemTheme @@ -118,6 +117,32 @@ fun PrimaryButtonIconEnd( ) } +/** + * [Show in Figma](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=68%3A20&mode=design&t=YXX8vqJcB9jn0wPp-1) + * */ +@Composable +fun PrimaryButtonIconEndTwoLines( + text: String, + @DrawableRes iconResId: Int, + onClick: () -> Unit, + modifier: Modifier = Modifier, + showProgress: Boolean = false, + enabled: Boolean = true, + additionalText: String? = null, +) { + TangemButton( + modifier = modifier, + text = text, + icon = TangemButtonIconPosition.End(iconResId), + onClick = onClick, + colors = TangemButtonsDefaults.primaryButtonColors, + enabled = enabled, + showProgress = showProgress, + additionalText = additionalText, + size = TangemButtonSize.TwoLines, + ) +} + /** * [Show in Figma](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?node-id=233%3A258&t=TmfD6UBHPg9uYfev-4) * */ @@ -148,9 +173,10 @@ fun SecondaryButton( text: String, onClick: () -> Unit, modifier: Modifier = Modifier, - size: TangemButtonSize = TangemButtonSize.Default, showProgress: Boolean = false, enabled: Boolean = true, + size: TangemButtonSize = TangemButtonSize.Default, + shape: Shape = size.toShape(), ) { TangemButton( modifier = modifier, @@ -161,6 +187,7 @@ fun SecondaryButton( enabled = enabled, showProgress = showProgress, size = size, + shape = shape, ) } @@ -267,6 +294,13 @@ private fun PrimaryButtonSample() { enabled = false, onClick = { }, ) + PrimaryButtonIconEndTwoLines( + modifier = Modifier.fillMaxWidth(), + text = "Manage tokens", + iconResId = R.drawable.ic_tangem_24, + onClick = { }, + additionalText = "Manage these tokens", + ) PrimaryButtonIconStart( modifier = Modifier.fillMaxWidth(), text = "Manage tokens", diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetDraggableHeader.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetDraggableHeader.kt index 5b1e13f57a..0d2cbe9d4e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetDraggableHeader.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetDraggableHeader.kt @@ -8,14 +8,15 @@ import androidx.compose.foundation.layout.size import androidx.compose.material.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import com.tangem.core.ui.res.TangemTheme @Composable -fun TangemBottomSheetDraggableHeader() { +fun TangemBottomSheetDraggableHeader(color: Color = TangemTheme.colors.background.primary) { Surface( modifier = Modifier .height(TangemTheme.dimens.size20), - color = TangemTheme.colors.background.primary, + color = color, ) { Box( modifier = Modifier diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/SmallButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/SmallButton.kt new file mode 100644 index 0000000000..5528c5dab9 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/SmallButton.kt @@ -0,0 +1,106 @@ +package com.tangem.core.ui.components.buttons + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme + +/** + * Small button config + * + * @property text text + * @property onClick lambda be invoked when action component is clicked + * + */ +data class SmallButtonConfig( + val text: TextReference, + val onClick: () -> Unit, +) + +/** + * Primary small button + * [Show in Figma](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=68%3A20&mode=design&t=fjwUkRtMUA4Q4s5r-1) + * + * @property config Config, containing parameters for a button + */ +@Composable +fun PrimarySmallButton(config: SmallButtonConfig, modifier: Modifier = Modifier) { + SmallButton(config = config, isPrimary = true, modifier = modifier) +} + +/** + * Secondary small button + * [Show in Figma](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=68%3A20&mode=design&t=fjwUkRtMUA4Q4s5r-1) + * + * @property config Config, containing parameters for a button + */ +@Composable +fun SecondarySmallButton(config: SmallButtonConfig, modifier: Modifier = Modifier) { + SmallButton(config = config, isPrimary = false, modifier = modifier) +} + +@Composable +private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier: Modifier = Modifier) { + val shape = RoundedCornerShape(size = TangemTheme.dimens.radius16) + Box( + modifier = modifier + .defaultMinSize(minWidth = TangemTheme.dimens.size46, minHeight = TangemTheme.dimens.size24) + .clip(shape) + .background( + color = if (isPrimary) TangemTheme.colors.button.primary else TangemTheme.colors.button.secondary, + shape = shape, + ) + .clickable(enabled = true, onClick = config.onClick) + .padding( + vertical = TangemTheme.dimens.spacing2, + ), + contentAlignment = Alignment.Center, + ) { + Text( + text = config.text.resolveReference(), + color = if (isPrimary) TangemTheme.colors.text.primary2 else TangemTheme.colors.text.primary1, + maxLines = 1, + style = TangemTheme.typography.button, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun Preview_SmallButton_Light() { + TangemTheme(isDark = false) { + ButtonsSample() + } +} + +@Preview(showBackground = true) +@Composable +private fun Preview_SmallButton_Dark() { + TangemTheme(isDark = true) { + ButtonsSample() + } +} + +@Composable +private fun ButtonsSample() { + Column( + modifier = Modifier.background(TangemTheme.colors.background.primary), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + ) { + val config = SmallButtonConfig( + text = TextReference.Str(value = "Add"), + onClick = {}, + ) + PrimarySmallButton(config = config) + SecondarySmallButton(config = config.copy(text = TextReference.Str(value = "Add"))) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt index fc114059fe..eda1d7fcac 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.Modifier import androidx.compose.ui.composed +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle @@ -26,16 +27,18 @@ fun TangemButton( showProgress: Boolean, enabled: Boolean, modifier: Modifier = Modifier, + additionalText: String? = null, size: TangemButtonSize = TangemButtonSize.Default, elevation: ButtonElevation = TangemButtonsDefaults.elevation, textStyle: TextStyle = TangemTheme.typography.button, + shape: Shape = size.toShape(), ) { Button( modifier = modifier.heightIn(min = size.toHeightDp()), onClick = { if (!showProgress) onClick() }, enabled = enabled, elevation = elevation, - shape = size.toShape(), + shape = shape, colors = colors, contentPadding = size.toContentPadding(icon = icon), ) { @@ -73,6 +76,20 @@ fun TangemButton( contentDescription = null, ) }, + additionalText = { + if (additionalText != null) { + Text( + modifier = Modifier + .fillMaxWidth(), + text = additionalText, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.disabled, + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + }, ) } } @@ -86,18 +103,27 @@ private inline fun RowScope.ButtonContentContainer( progressIndicator: @Composable RowScope.() -> Unit, text: @Composable RowScope.() -> Unit, icon: @Composable RowScope.(Int) -> Unit, + additionalText: @Composable () -> Unit, ) { if (showProgress) { progressIndicator() } else { - if (buttonIcon is TangemButtonIconPosition.Start) { - icon(buttonIcon.iconResId) - Spacer(modifier = Modifier.requiredWidth(iconPadding)) - } - text() - if (buttonIcon is TangemButtonIconPosition.End) { - Spacer(modifier = Modifier.requiredWidth(iconPadding)) - icon(buttonIcon.iconResId) + Column { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + ) { + if (buttonIcon is TangemButtonIconPosition.Start) { + icon(buttonIcon.iconResId) + Spacer(modifier = Modifier.requiredWidth(iconPadding)) + } + text() + if (buttonIcon is TangemButtonIconPosition.End) { + Spacer(modifier = Modifier.requiredWidth(iconPadding)) + icon(buttonIcon.iconResId) + } + } + additionalText() } } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonSize.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonSize.kt index 134ad1ddac..0370fe0efe 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonSize.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonSize.kt @@ -15,12 +15,13 @@ enum class TangemButtonSize { Action, RoundedAction, WideAction, + TwoLines, } @Composable @ReadOnlyComposable internal fun TangemButtonSize.toHeightDp(): Dp = when (this) { - TangemButtonSize.Default -> TangemTheme.dimens.size48 + TangemButtonSize.Default, TangemButtonSize.TwoLines -> TangemTheme.dimens.size48 TangemButtonSize.Text -> TangemTheme.dimens.size40 TangemButtonSize.Selector -> TangemTheme.dimens.size24 TangemButtonSize.Action, @@ -38,6 +39,7 @@ internal fun TangemButtonSize.toShape(): Shape = when (this) { TangemButtonSize.Text -> TangemTheme.shapes.roundedCornersSmall TangemButtonSize.Selector -> TangemTheme.shapes.roundedCornersSmall TangemButtonSize.Action -> TangemTheme.shapes.roundedCornersMedium + TangemButtonSize.TwoLines -> TangemTheme.shapes.roundedCornersXMedium TangemButtonSize.RoundedAction -> TangemTheme.shapes.roundedCornersLarge } @@ -46,6 +48,7 @@ internal fun TangemButtonSize.toShape(): Shape = when (this) { internal fun TangemButtonSize.toIconPadding(): Dp = when (this) { TangemButtonSize.Default, TangemButtonSize.WideAction, + TangemButtonSize.TwoLines, -> TangemTheme.dimens.spacing4 TangemButtonSize.Text -> TangemTheme.dimens.spacing8 TangemButtonSize.Selector -> 0.dp @@ -92,6 +95,12 @@ internal fun TangemButtonSize.toContentPadding(icon: TangemButtonIconPosition): start = horizontalPadding.first, end = horizontalPadding.second, ) + TangemButtonSize.TwoLines -> PaddingValues( + top = TangemTheme.dimens.spacing6, + bottom = TangemTheme.dimens.spacing6, + start = horizontalPadding.first, + end = horizontalPadding.second, + ) } } @@ -101,6 +110,7 @@ internal fun TangemButtonSize.toHorizontalContentPadding(icon: TangemButtonIconP return when (this) { TangemButtonSize.Default, TangemButtonSize.WideAction, + TangemButtonSize.TwoLines, -> TangemTheme.dimens.spacing32 to TangemTheme.dimens.spacing32 TangemButtonSize.Text -> when (icon) { is TangemButtonIconPosition.None -> TangemTheme.dimens.spacing16 to TangemTheme.dimens.spacing16 diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt new file mode 100644 index 0000000000..df18f78ab0 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt @@ -0,0 +1,161 @@ +package com.tangem.core.ui.components.buttons.segmentedbutton + +import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.res.TangemTheme +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf + +/** + * Segmented buttons + * + * [Figma component](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=1961-2004&mode=design&t=OFPQ18YhLHVAANab-4) + * + * @param config list of buttons in SegmentedButtons + * @param onClick button click + * @param modifier component modifier + * @param color default button color + * @param selectedColor selected button color + * @param dividerColor border and divider color + * @param showIndication show ripple indication + * @param buttonContent content as separate button + */ +@Composable +inline fun SegmentedButtons( + config: PersistentList, + crossinline onClick: (T) -> Unit, + modifier: Modifier = Modifier, + color: Color = TangemTheme.colors.background.tertiary, + selectedColor: Color = TangemTheme.colors.background.action, + dividerColor: Color = TangemTheme.colors.stroke.primary, + showIndication: Boolean = true, + crossinline buttonContent: @Composable (T) -> Unit, +) { + if (config.isEmpty() || config.size == 1) return + + var selected by remember { mutableIntStateOf(0) } + + Row( + modifier = modifier + .clip(RoundedCornerShape(TangemTheme.dimens.radius26)) + .background(dividerColor) + .padding(TangemTheme.dimens.spacing1), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing1), + ) { + repeat(config.size) { index -> + val leftRadius = if (index == 0) TangemTheme.dimens.radius26 else TangemTheme.dimens.radius0 + val rightRadius = if (index == config.lastIndex) TangemTheme.dimens.radius26 else TangemTheme.dimens.radius0 + + Box( + modifier = Modifier + .weight(1f) + .background( + color = if (index == selected) selectedColor else color, + shape = RoundedCornerShape( + topStart = leftRadius, + topEnd = rightRadius, + bottomEnd = rightRadius, + bottomStart = leftRadius, + ), + ) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = if (showIndication) LocalIndication.current else null, + ) { + onClick(config[index]) + selected = index + }, + ) { + buttonContent.invoke(config[index]) + } + } + } +} + +@Preview +@Composable +private fun SegmentedButtonsPreview_Light( + @PreviewParameter(SegmentedButtonsPreviewProvider::class) config: PersistentList, +) { + TangemTheme { + SegmentedButtons( + config = config, + onClick = {}, + ) { + Text( + text = it.text, + modifier = Modifier.padding(TangemTheme.dimens.spacing16), + ) + } + } +} + +@Preview +@Composable +private fun SegmentedButtonsPreview_Dark( + @PreviewParameter(SegmentedButtonsPreviewProvider::class) config: PersistentList, +) { + TangemTheme(isDark = true) { + SegmentedButtons( + config = config, + onClick = {}, + ) { + Text( + text = it.text, + modifier = Modifier.padding(TangemTheme.dimens.spacing16), + ) + } + } +} + +//region Preview config +/** + * Segmented buttons preview model + * + * @param text button title + */ +internal data class SegmentedButtonsConfigPreview( + val text: String, +) + +/** + * Segmented button preview provider + */ +internal class SegmentedButtonsPreviewProvider : + CollectionPreviewParameterProvider>( + collection = listOf( + persistentListOf( + SegmentedButtonsConfigPreview( + text = "Title 1", + ), + SegmentedButtonsConfigPreview( + text = "Title 2", + ), + SegmentedButtonsConfigPreview( + text = "Title 3", + ), + ), + persistentListOf( + SegmentedButtonsConfigPreview( + text = "Title 1", + ), + SegmentedButtonsConfigPreview( + text = "Title 2", + ), + ), + ), + ) + +//endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/DefaultCurrencyIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/DefaultCurrencyIcon.kt new file mode 100644 index 0000000000..792cbab8b0 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/DefaultCurrencyIcon.kt @@ -0,0 +1,60 @@ +package com.tangem.core.ui.components.currency + +import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.core.ui.components.currency.tokenicon.LoadingIcon +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.ImageBackgroundContrastChecker +import kotlinx.coroutines.launch + +@Composable +internal inline fun DefaultCurrencyIcon( + iconData: Any, + alpha: Float, + colorFilter: ColorFilter?, + crossinline errorIcon: @Composable () -> Unit, + modifier: Modifier = Modifier, +) { + var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) } + val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb() + val isDarkTheme = isSystemInDarkTheme() + val coroutineScope = rememberCoroutineScope() + + SubcomposeAsyncImage( + modifier = modifier + .background( + color = iconBackgroundColor, + shape = TangemTheme.shapes.roundedCorners8, + ), + model = ImageRequest.Builder(context = LocalContext.current) + .data(iconData) + .crossfade(enable = true) + .allowHardware(false) + .listener( + onSuccess = { _, result -> + if (isDarkTheme) { + coroutineScope.launch { + val color = ImageBackgroundContrastChecker( + drawable = result.drawable, + backgroundColor = itemBackgroundColor, + ).getContrastColorIfNeeded(isDarkTheme) + iconBackgroundColor = color + } + } + }, + ).build(), + loading = { LoadingIcon() }, + error = { errorIcon() }, + alpha = alpha, + colorFilter = colorFilter, + contentDescription = null, + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/fiaticon/FiatIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/fiaticon/FiatIcon.kt new file mode 100644 index 0000000000..879ea4575a --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/fiaticon/FiatIcon.kt @@ -0,0 +1,38 @@ +package com.tangem.core.ui.components.currency.fiaticon + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import com.tangem.core.ui.R +import com.tangem.core.ui.components.currency.DefaultCurrencyIcon + +/** + * Simple icon from network + * + * @param url link to icon + * @param fallbackResId fallback icon + * @param modifier component modifier + */ +@Composable +fun FiatIcon( + url: String?, + modifier: Modifier = Modifier, + @DrawableRes fallbackResId: Int = R.drawable.ic_shape_circle, +) { + val iconData: Any = if (url.isNullOrBlank()) fallbackResId else url + + DefaultCurrencyIcon( + modifier = modifier, + iconData = iconData, + errorIcon = { + Image( + painter = painterResource(id = fallbackResId), + contentDescription = null, + ) + }, + alpha = 1f, + colorFilter = null, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/ContentIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/ContentIcon.kt similarity index 55% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/ContentIcon.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/ContentIcon.kt index dd1be11f5a..dd3e892301 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/ContentIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/ContentIcon.kt @@ -1,44 +1,36 @@ -package com.tangem.feature.wallet.presentation.common.component.token.icon +package com.tangem.core.ui.components.currency.tokenicon import androidx.annotation.DrawableRes import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Icon -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource -import coil.compose.SubcomposeAsyncImage -import coil.request.ImageRequest -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.ImageBackgroundContrastChecker -import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.common.state.TokenItemState -import kotlinx.coroutines.launch +import com.tangem.core.ui.R +import com.tangem.core.ui.components.currency.DefaultCurrencyIcon @Composable internal fun ContentIcon( - icon: TokenItemState.IconState, + icon: TokenIconState, alpha: Float, colorFilter: ColorFilter?, modifier: Modifier = Modifier, ) { when (icon) { - is TokenItemState.IconState.CoinIcon -> CoinIcon( + is TokenIconState.CoinIcon -> CoinIcon( modifier = modifier, url = icon.url, fallbackResId = icon.fallbackResId, alpha = alpha, colorFilter = colorFilter, ) - is TokenItemState.IconState.TokenIcon -> TokenIcon( + is TokenIconState.TokenIcon -> TokenIcon( modifier = modifier, url = icon.url, alpha = alpha, @@ -52,14 +44,14 @@ internal fun ContentIcon( ) }, ) - is TokenItemState.IconState.CustomTokenIcon -> CustomTokenIcon( + is TokenIconState.CustomTokenIcon -> CustomTokenIcon( modifier = modifier, tint = icon.tint, background = icon.background, alpha = alpha, ) - TokenItemState.IconState.Loading, - TokenItemState.IconState.Locked, + TokenIconState.Loading, + TokenIconState.Locked, -> Unit } } @@ -128,48 +120,4 @@ private fun CustomTokenIcon(tint: Color, background: Color, alpha: Float, modifi contentDescription = null, ) } -} - -@Composable -private inline fun DefaultCurrencyIcon( - iconData: Any, - alpha: Float, - colorFilter: ColorFilter?, - crossinline errorIcon: @Composable () -> Unit, - modifier: Modifier = Modifier, -) { - var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) } - val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb() - val isDarkTheme = isSystemInDarkTheme() - val coroutineScope = rememberCoroutineScope() - - SubcomposeAsyncImage( - modifier = modifier - .background( - color = iconBackgroundColor, - shape = TangemTheme.shapes.roundedCorners8, - ), - model = ImageRequest.Builder(context = LocalContext.current) - .data(iconData) - .crossfade(enable = true) - .allowHardware(false) - .listener( - onSuccess = { _, result -> - if (isDarkTheme) { - coroutineScope.launch { - val color = ImageBackgroundContrastChecker( - drawable = result.drawable, - backgroundColor = itemBackgroundColor, - ).getContrastColorIfNeeded(isDarkTheme) - iconBackgroundColor = color - } - } - }, - ).build(), - loading = { LoadingIcon() }, - error = { errorIcon() }, - alpha = alpha, - colorFilter = colorFilter, - contentDescription = null, - ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/IconBadge.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/IconBadge.kt similarity index 96% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/IconBadge.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/IconBadge.kt index 6fd0339923..e21ce3274c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/IconBadge.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/IconBadge.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.common.component.token.icon +package com.tangem.core.ui.components.currency.tokenicon import androidx.annotation.DrawableRes import androidx.compose.foundation.Image diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/TokenIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIcon.kt similarity index 77% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/TokenIcon.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIcon.kt index 275c58d54f..07d299f583 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/TokenIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIcon.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.common.component.token.icon +package com.tangem.core.ui.components.currency.tokenicon import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -14,14 +14,22 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorMatrix import com.tangem.core.ui.components.CircleShimmer import com.tangem.core.ui.res.TangemTheme -import com.tangem.feature.wallet.presentation.common.state.TokenItemState.IconState as TokenIconState private const val GRAY_SCALE_SATURATION = 0f private const val GRAY_SCALE_ALPHA = 0.4f private const val NORMAL_ALPHA = 1f +/** + * Cryptocurrency icon with network badge + * + * TODO [separate domain from ui]([REDACTED_JIRA]) + * + * @param state cryptocurrency icon config + * @param modifier component modifier + * @param shouldDisplayNetwork specifies whether to display network badge + */ @Composable -internal fun TokenIcon(state: TokenIconState, modifier: Modifier = Modifier) { +fun TokenIcon(state: TokenIconState, modifier: Modifier = Modifier, shouldDisplayNetwork: Boolean = true) { BaseContainer(modifier = modifier) { val iconModifier = Modifier .align(Alignment.Center) @@ -34,7 +42,11 @@ internal fun TokenIcon(state: TokenIconState, modifier: Modifier = Modifier) { is TokenIconState.CustomTokenIcon, is TokenIconState.TokenIcon, -> { - ContentIconContainer(modifier = iconModifier, icon = state) + ContentIconContainer( + icon = state, + modifier = iconModifier, + shouldDisplayNetwork = shouldDisplayNetwork, + ) } } } @@ -60,7 +72,11 @@ private fun LockedIcon(modifier: Modifier = Modifier) { } @Composable -private fun BoxScope.ContentIconContainer(icon: TokenIconState, modifier: Modifier = Modifier) { +private fun BoxScope.ContentIconContainer( + icon: TokenIconState, + modifier: Modifier = Modifier, + shouldDisplayNetwork: Boolean = true, +) { val networkBadgeOffset = TangemTheme.dimens.spacing4 val (alpha, colorFilter) = remember(icon.isGrayscale) { if (icon.isGrayscale) { @@ -77,7 +93,7 @@ private fun BoxScope.ContentIconContainer(icon: TokenIconState, modifier: Modifi colorFilter = colorFilter, ) - if (icon.networkBadgeIconResId != null) { + if (icon.networkBadgeIconResId != null && shouldDisplayNetwork) { NetworkBadge( modifier = Modifier .offset(x = networkBadgeOffset, y = -networkBadgeOffset) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIconState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIconState.kt new file mode 100644 index 0000000000..f51adb19b3 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIconState.kt @@ -0,0 +1,85 @@ +package com.tangem.core.ui.components.currency.tokenicon + +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color + +/** + * Represents the various states an icon can be in. + * + * [REDACTED_TODO_COMMENT] + */ +@Immutable +sealed class TokenIconState { + + abstract val isGrayscale: Boolean + abstract val showCustomBadge: Boolean + abstract val networkBadgeIconResId: Int? + + /** + * Represents a coin icon. + * + * @property url The URL where the coin icon can be fetched from. May be `null` if not found. + * @property fallbackResId The drawable resource ID to be used as a fallback if the URL is not available. + * @property isGrayscale Specifies whether to show the icon in grayscale. + * @property showCustomBadge Specifies whether to show the custom token badge. + */ + data class CoinIcon( + val url: String?, + @DrawableRes val fallbackResId: Int, + override val isGrayscale: Boolean, + override val showCustomBadge: Boolean, + ) : TokenIconState() { + + override val networkBadgeIconResId: Int? = null + } + + /** + * Represents a token icon. + * + * @property url The URL where the token icon can be fetched from. May be `null` if not found. + * @property networkBadgeIconResId The drawable resource ID for the network badge. + * @property isGrayscale Specifies whether to show the icon in grayscale. + * @property showCustomBadge Specifies whether to show the custom token badge. + * @property fallbackTint The color to be used for tinting the fallback icon. + * @property fallbackBackground The background color to be used for the fallback icon. + */ + data class TokenIcon( + val url: String?, + @DrawableRes override val networkBadgeIconResId: Int, + override val isGrayscale: Boolean, + override val showCustomBadge: Boolean, + val fallbackTint: Color, + val fallbackBackground: Color, + ) : TokenIconState() + + /** + * Represents a custom token icon. + * + * @property tint The color to be used for tinting the icon. + * @property background The background color to be used for the icon. + * @property networkBadgeIconResId The drawable resource ID for the network badge. + * @property isGrayscale Specifies whether to show the icon in grayscale. + */ + data class CustomTokenIcon( + val tint: Color, + val background: Color, + @DrawableRes override val networkBadgeIconResId: Int, + override val isGrayscale: Boolean, + ) : TokenIconState() { + + override val showCustomBadge: Boolean = true + } + + object Loading : TokenIconState() { + override val isGrayscale: Boolean = false + override val showCustomBadge: Boolean = false + override val networkBadgeIconResId: Int? = null + } + + object Locked : TokenIconState() { + override val isGrayscale: Boolean = false + override val showCustomBadge: Boolean = false + override val networkBadgeIconResId: Int? = null + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/utils/CryptoCurrencyToIconStateConverter.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/converter/CryptoCurrencyToIconStateConverter.kt similarity index 69% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/utils/CryptoCurrencyToIconStateConverter.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/converter/CryptoCurrencyToIconStateConverter.kt index 0af718eb22..6509d4baf1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/utils/CryptoCurrencyToIconStateConverter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/converter/CryptoCurrencyToIconStateConverter.kt @@ -1,27 +1,27 @@ -package com.tangem.feature.wallet.presentation.common.utils +package com.tangem.core.ui.components.currency.tokenicon.converter +import com.tangem.common.Converter +import com.tangem.core.ui.components.currency.tokenicon.TokenIconState import com.tangem.core.ui.extensions.getTintForTokenIcon import com.tangem.core.ui.extensions.networkIconResId import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.feature.wallet.presentation.common.state.TokenItemState -import com.tangem.utils.converter.Converter -internal class CryptoCurrencyToIconStateConverter : Converter { +/** + * Converts [CryptoCurrencyStatus] to [TokenIconState] + */ +class CryptoCurrencyToIconStateConverter : Converter { - override fun convert(value: CryptoCurrencyStatus): TokenItemState.IconState { + override fun convert(value: CryptoCurrencyStatus): TokenIconState { return when (val currency = value.currency) { is CryptoCurrency.Coin -> getIconStateForCoin(currency, value.value.isError) is CryptoCurrency.Token -> getIconStateForToken(currency, value.value.isError) } } - private fun getIconStateForCoin( - coin: CryptoCurrency.Coin, - isUnreachable: Boolean, - ): TokenItemState.IconState.CoinIcon { - return TokenItemState.IconState.CoinIcon( + private fun getIconStateForCoin(coin: CryptoCurrency.Coin, isUnreachable: Boolean): TokenIconState.CoinIcon { + return TokenIconState.CoinIcon( url = coin.iconUrl, fallbackResId = coin.networkIconResId, isGrayscale = coin.network.isTestnet || isUnreachable, @@ -29,20 +29,20 @@ internal class CryptoCurrencyToIconStateConverter : Converter Unit, ) : ButtonsState() diff --git a/core/ui/src/main/java/com/tangem/core/ui/event/StateEvent.kt b/core/ui/src/main/java/com/tangem/core/ui/event/StateEvent.kt index 1860808b1d..fb215b168f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/event/StateEvent.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/event/StateEvent.kt @@ -35,7 +35,7 @@ sealed class StateEvent { */ data class Triggered( val data: A, - internal val onConsume: () -> Unit, + val onConsume: () -> Unit, ) : StateEvent() } diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt index 38da5c44af..3c29a58544 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt @@ -45,6 +45,7 @@ fun getActiveIconRes(blockchainId: String): Int { "aleph-zero", "aleph-zero/test" -> R.drawable.img_azero_22 "octaspace", "octaspace/test" -> R.drawable.img_octaspace_22 "chia", "chia/test" -> R.drawable.img_chia_22 + "decimal", "decimal/testnet" -> R.drawable.img_decimal_22 else -> R.drawable.ic_alert_24 } } @@ -88,6 +89,53 @@ fun getActiveIconResByCoinId(coinId: String): Int { "octaspace" -> R.drawable.img_octaspace_22 "chia" -> R.drawable.img_chia_22 "near" -> R.drawable.img_near_22 + "decimal" -> R.drawable.img_decimal_22 + else -> R.drawable.ic_alert_24 + } +} + +@Suppress("ComplexMethod") +@DrawableRes +fun getGreyedOutIconRes(blockchainId: String): Int { + return when (blockchainId) { + "ARBITRUM-ONE", "ARBITRUM/test" -> R.drawable.ic_arbitrum_22 + "BTC", "BTC/test" -> R.drawable.ic_bitcoin_16 + "BCH" -> R.drawable.ic_bitcoin_cash_16 + "LTC" -> R.drawable.ic_litecoin_22 + "ETH", "ETH/test" -> R.drawable.ic_eth_16 + "ETC", "ETC/test" -> R.drawable.ic_eth_16 + "RSK" -> R.drawable.ic_rsk_16 + "CARDANO", "CARDANO-S" -> R.drawable.ic_cardano_16 + "XTZ" -> R.drawable.ic_tezos_16 + "XRP" -> R.drawable.ic_xrp_22 + "XLM", "XLM/test" -> R.drawable.ic_stellar_16 + "AVALANCHE", "AVALANCHE/test" -> R.drawable.ic_avalanche_22 + "POLYGON", "POLYGON/test" -> R.drawable.ic_polygon_16 + "SOLANA", "SOLANA/test" -> R.drawable.ic_solana_16 + "FTM", "FTM/test" -> R.drawable.ic_fantom_22 + "BSC", "BSC/test", "BINANCE", "BINANCE/test" -> R.drawable.ic_bsc_16 + "DOGE" -> R.drawable.ic_dogecoin_16 + "TRON", "TRON/test" -> R.drawable.ic_tron_22 + "GNO" -> R.drawable.ic_gnosis_22 + "ETH-Pow", "ETH-Pow/test" -> R.drawable.ic_ethereumpow_22 + "ETH-Fair" -> R.drawable.ic_ethereumfair_22 + "Polkadot", "Polkadot/test" -> R.drawable.ic_polkadot_16 + "Kusama" -> R.drawable.ic_kusama_16 + "OPTIMISM", "OPTIMISM/test" -> R.drawable.ic_optimism_22 + "DASH" -> R.drawable.ic_dash_22 + "KAS" -> R.drawable.ic_kaspa_22 + "The-Open-Network", "The-Open-Network/test" -> R.drawable.ic_ton_22 + "KAVA", "KAVA/test" -> R.drawable.ic_kava_22 + "ravencoin", "ravencoin/test" -> R.drawable.ic_ravencoin_22 + "cosmos", "cosmos/test" -> R.drawable.ic_cosmos_22 + "terra", "terra-luna" -> R.drawable.ic_terra_22 + "terra-2", "terra-luna-2" -> R.drawable.ic_terra2_22 + "cronos" -> R.drawable.ic_cronos_22 + "TELOS", "TELOS/test" -> R.drawable.ic_telos_22 + "aleph-zero", "aleph-zero/test" -> R.drawable.ic_azero_22 + "octaspace", "octaspace/test" -> R.drawable.ic_octaspace_22 + "chia", "chia/test" -> R.drawable.ic_chia_22 + "decimal", "decimal/test" -> R.drawable.ic_decimal_22 else -> R.drawable.ic_alert_24 } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/ImageReference.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/ImageReference.kt new file mode 100644 index 0000000000..767065f05a --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/ImageReference.kt @@ -0,0 +1,30 @@ +package com.tangem.core.ui.extensions + +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable + +/** + * Utility class for keeping image reference when it can be an image resource or a URL. + * + * It necessary to use [Immutable] annotation because all sealed interface has runtime stability. + * All subclasses are stable. + */ +sealed class ImageReference { + + data class Url(val url: String) : ImageReference() + + data class Res(@DrawableRes val resId: Int) : ImageReference() + + /** + * Provides reference of any type. It can be used for image loading, + * when the type of reference is resolved further down the call site in the library. + * + * @return image reference either as an image resource ([Int]) or a URL ([String]) + */ + fun getReference(): Any { + return when (this) { + is Res -> resId + is Url -> url + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt index f8ad2530e8..e814d46e20 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt @@ -145,6 +145,7 @@ class TangemColors internal constructor( class Background internal constructor( primary: Color, secondary: Color, + tertiary: Color, plain: Color, action: Color, fade: Color, @@ -153,6 +154,8 @@ class TangemColors internal constructor( private set var secondary by mutableStateOf(secondary) private set + var tertiary by mutableStateOf(tertiary) + private set var plain by mutableStateOf(plain) private set var action by mutableStateOf(action) @@ -163,6 +166,7 @@ class TangemColors internal constructor( fun update(other: Background) { primary = other.primary secondary = other.secondary + tertiary = other.tertiary plain = other.plain action = other.action fade = other.fade diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt index 56a8145e05..b3820d111f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt @@ -32,7 +32,9 @@ data class TangemDimens internal constructor( val radius16: Dp = 16.dp, val radius18: Dp = 18.dp, val radius24: Dp = 24.dp, + val radius26: Dp = 26.dp, val radius28: Dp = 28.dp, + val radius36: Dp = 36.dp, // endregion Radius // region Size val size0: Dp = 0.dp, @@ -71,6 +73,7 @@ data class TangemDimens internal constructor( val size80: Dp = 80.dp, val size84: Dp = 84.dp, val size86: Dp = 86.dp, + val size90: Dp = 90.dp, val size93: Dp = 93.dp, val size96: Dp = 96.dp, val size102: Dp = 102.dp, @@ -82,10 +85,12 @@ data class TangemDimens internal constructor( val size164: Dp = 164.dp, val size200: Dp = 200.dp, val size248: Dp = 248.dp, + val size350: Dp = 350.dp, // endregion Size // region Spacing val spacing0: Dp = 0.dp, val spacing0_5: Dp = 0.5.dp, + val spacing1: Dp = 1.dp, val spacing2: Dp = 2.dp, val spacing3: Dp = 3.dp, val spacing4: Dp = 4.dp, diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt index a4f6721f5c..db286e6d51 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt @@ -3,12 +3,7 @@ package com.tangem.core.ui.res import androidx.compose.material.Colors import androidx.compose.material.MaterialTheme import androidx.compose.material.ProvideTextStyle -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.ReadOnlyComposable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.remember -import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.runtime.* // TODO: use isSystemInDarkTheme() for automatic color detection internal const val IS_SYSTEM_IN_DARK_THEME: Boolean = false @@ -116,12 +111,13 @@ private fun lightThemeColors(): TangemColors { background = TangemColors.Background( primary = TangemColorPalette.White, secondary = TangemColorPalette.Light1, + tertiary = TangemColorPalette.Light1, plain = TangemColorPalette.White, - action = TangemColorPalette.Black, + action = TangemColorPalette.White, fade = TangemColorPalette.White, ), control = TangemColors.Control( - checked = TangemColorPalette.Meadow, + checked = TangemColorPalette.Dark6, unchecked = TangemColorPalette.Light2, key = TangemColorPalette.White, ), @@ -168,14 +164,15 @@ private fun darkThemeColors(): TangemColors { background = TangemColors.Background( primary = TangemColorPalette.Dark6, secondary = TangemColorPalette.Black, + tertiary = TangemColorPalette.Dark6, plain = TangemColorPalette.Black, - action = TangemColorPalette.Light4, + action = TangemColorPalette.Dark5, fade = TangemColorPalette.Black, ), control = TangemColors.Control( - checked = TangemColorPalette.Meadow, + checked = TangemColorPalette.Azure, unchecked = TangemColorPalette.Dark4, - key = TangemColorPalette.Light1, + key = TangemColorPalette.White, ), stroke = TangemColors.Stroke( primary = TangemColorPalette.Dark5, diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt index 2350350cd0..d8e209f0ad 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt @@ -12,7 +12,9 @@ object BigDecimalFormatter { private const val TEMP_CURRENCY_CODE = "USD" - fun formatCryptoAmount(cryptoAmount: BigDecimal, cryptoCurrency: String, decimals: Int): String { + fun formatCryptoAmount(cryptoAmount: BigDecimal?, cryptoCurrency: String, decimals: Int): String { + if (cryptoAmount == null) return EMPTY_BALANCE_SIGN + val formatter = NumberFormat.getNumberInstance().apply { maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8) minimumFractionDigits = 2 @@ -23,11 +25,13 @@ object BigDecimalFormatter { } fun formatFiatAmount( - fiatAmount: BigDecimal, + fiatAmount: BigDecimal?, fiatCurrencyCode: String, fiatCurrencySymbol: String, locale: Locale = Locale.getDefault(), ): String { + if (fiatAmount == null) return EMPTY_BALANCE_SIGN + val formatterCurrency = getCurrency(fiatCurrencyCode) val formatter = NumberFormat.getCurrencyInstance(locale).apply { currency = formatterCurrency diff --git a/core/ui/src/main/res/drawable/ic_arbitrum_22.xml b/core/ui/src/main/res/drawable/ic_arbitrum_22.xml new file mode 100644 index 0000000000..af0adbd04c --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_arbitrum_22.xml @@ -0,0 +1,23 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_avalanche_no_color.xml b/core/ui/src/main/res/drawable/ic_avalanche_22.xml similarity index 97% rename from app/src/main/res/drawable/ic_avalanche_no_color.xml rename to core/ui/src/main/res/drawable/ic_avalanche_22.xml index 032533db9d..61518aacb9 100644 --- a/app/src/main/res/drawable/ic_avalanche_no_color.xml +++ b/core/ui/src/main/res/drawable/ic_avalanche_22.xml @@ -5,7 +5,7 @@ android:viewportWidth="22" android:viewportHeight="22"> diff --git a/app/src/main/res/drawable/ic_azero_no_color.xml b/core/ui/src/main/res/drawable/ic_azero_22.xml similarity index 97% rename from app/src/main/res/drawable/ic_azero_no_color.xml rename to core/ui/src/main/res/drawable/ic_azero_22.xml index 612a316f3f..ad6fb490e2 100644 --- a/app/src/main/res/drawable/ic_azero_no_color.xml +++ b/core/ui/src/main/res/drawable/ic_azero_22.xml @@ -5,5 +5,5 @@ android:viewportHeight="22"> + android:fillColor="#000000" /> diff --git a/app/src/main/res/drawable/ic_bitcoin_no_color.xml b/core/ui/src/main/res/drawable/ic_bitcoin_16.xml similarity index 100% rename from app/src/main/res/drawable/ic_bitcoin_no_color.xml rename to core/ui/src/main/res/drawable/ic_bitcoin_16.xml diff --git a/core/ui/src/main/res/drawable/ic_bitcoin_cash_16.xml b/core/ui/src/main/res/drawable/ic_bitcoin_cash_16.xml new file mode 100644 index 0000000000..91b3d5849a --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_bitcoin_cash_16.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_bsc_16.xml b/core/ui/src/main/res/drawable/ic_bsc_16.xml new file mode 100644 index 0000000000..dc8eea5d0b --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_bsc_16.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_cardano_16.xml b/core/ui/src/main/res/drawable/ic_cardano_16.xml new file mode 100644 index 0000000000..d7456cee94 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_cardano_16.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_chia_no_color.xml b/core/ui/src/main/res/drawable/ic_chia_22.xml similarity index 98% rename from app/src/main/res/drawable/ic_chia_no_color.xml rename to core/ui/src/main/res/drawable/ic_chia_22.xml index 4ed5d9574b..55ce695c84 100644 --- a/app/src/main/res/drawable/ic_chia_no_color.xml +++ b/core/ui/src/main/res/drawable/ic_chia_22.xml @@ -5,6 +5,6 @@ android:viewportHeight="22"> diff --git a/app/src/main/res/drawable/ic_cosmos_no_color.xml b/core/ui/src/main/res/drawable/ic_cosmos_22.xml similarity index 93% rename from app/src/main/res/drawable/ic_cosmos_no_color.xml rename to core/ui/src/main/res/drawable/ic_cosmos_22.xml index fbcaaabdd2..26d31ac8c3 100644 --- a/app/src/main/res/drawable/ic_cosmos_no_color.xml +++ b/core/ui/src/main/res/drawable/ic_cosmos_22.xml @@ -6,31 +6,31 @@ + android:strokeColor="#000000" /> + android:strokeColor="#000000" /> + android:strokeColor="#000000" /> + android:strokeColor="#000000" /> + android:strokeColor="#000000" /> diff --git a/app/src/main/res/drawable/ic_cronos_no_color.xml b/core/ui/src/main/res/drawable/ic_cronos_22.xml similarity index 85% rename from app/src/main/res/drawable/ic_cronos_no_color.xml rename to core/ui/src/main/res/drawable/ic_cronos_22.xml index 3764710b20..851b0ddb63 100644 --- a/app/src/main/res/drawable/ic_cronos_no_color.xml +++ b/core/ui/src/main/res/drawable/ic_cronos_22.xml @@ -4,15 +4,15 @@ android:viewportWidth="22" android:viewportHeight="22"> diff --git a/app/src/main/res/drawable/ic_dash_no_color.xml b/core/ui/src/main/res/drawable/ic_dash_22.xml similarity index 91% rename from app/src/main/res/drawable/ic_dash_no_color.xml rename to core/ui/src/main/res/drawable/ic_dash_22.xml index 0e7c3f29b6..319737dc56 100644 --- a/app/src/main/res/drawable/ic_dash_no_color.xml +++ b/core/ui/src/main/res/drawable/ic_dash_22.xml @@ -8,9 +8,9 @@ android:pathData="M0,0h22v22h-22z"/> + android:fillColor="#000000" /> + android:fillColor="#000000" /> diff --git a/core/ui/src/main/res/drawable/ic_decimal_22.xml b/core/ui/src/main/res/drawable/ic_decimal_22.xml new file mode 100644 index 0000000000..c4fc972f09 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_decimal_22.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_dogecoin_16.xml b/core/ui/src/main/res/drawable/ic_dogecoin_16.xml new file mode 100644 index 0000000000..e97bec6e4c --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_dogecoin_16.xml @@ -0,0 +1,11 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_eth_16.xml b/core/ui/src/main/res/drawable/ic_eth_16.xml new file mode 100644 index 0000000000..e2824ce570 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_eth_16.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_ethereumfair_no_color.xml b/core/ui/src/main/res/drawable/ic_ethereumfair_22.xml similarity index 78% rename from app/src/main/res/drawable/ic_ethereumfair_no_color.xml rename to core/ui/src/main/res/drawable/ic_ethereumfair_22.xml index 1379276e0d..d98f048426 100644 --- a/app/src/main/res/drawable/ic_ethereumfair_no_color.xml +++ b/core/ui/src/main/res/drawable/ic_ethereumfair_22.xml @@ -7,31 +7,31 @@ diff --git a/app/src/main/res/drawable/ic_ethereumpow_no_color.xml b/core/ui/src/main/res/drawable/ic_ethereumpow_22.xml similarity index 100% rename from app/src/main/res/drawable/ic_ethereumpow_no_color.xml rename to core/ui/src/main/res/drawable/ic_ethereumpow_22.xml diff --git a/core/ui/src/main/res/drawable/ic_eye_off_outline_24.xml b/core/ui/src/main/res/drawable/ic_eye_off_outline_24.xml new file mode 100644 index 0000000000..c7958120c8 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_eye_off_outline_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_fantom_22.xml b/core/ui/src/main/res/drawable/ic_fantom_22.xml new file mode 100644 index 0000000000..4a7fd00fb5 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_fantom_22.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_gnosis_no_color.xml b/core/ui/src/main/res/drawable/ic_gnosis_22.xml similarity index 78% rename from app/src/main/res/drawable/ic_gnosis_no_color.xml rename to core/ui/src/main/res/drawable/ic_gnosis_22.xml index 271d24efd6..fec5ed2c7d 100644 --- a/app/src/main/res/drawable/ic_gnosis_no_color.xml +++ b/core/ui/src/main/res/drawable/ic_gnosis_22.xml @@ -5,14 +5,14 @@ android:viewportHeight="22"> + android:fillColor="#000000" /> + android:fillColor="#000000" + android:pathData="M16.376,10.028C16.376,9.568 16.223,9.12 15.942,8.757L13.026,11.673C13.935,12.376 15.24,12.208 15.942,11.299C16.223,10.936 16.376,10.488 16.376,10.028Z" /> + android:fillColor="#000000" + android:pathData="M17.845,6.854L16.555,8.144C17.594,9.389 17.43,11.243 16.185,12.282C15.094,13.194 13.508,13.194 12.417,12.282L11,13.699L9.587,12.286C8.342,13.325 6.488,13.161 5.449,11.916C4.536,10.824 4.536,9.239 5.449,8.148L4.787,7.486L4.159,6.854C3.4,8.103 3,9.538 3,11C3,15.419 6.581,19 11,19C15.419,19 19,15.419 19,11C19.004,9.538 18.6,8.103 17.845,6.854Z" /> + android:fillColor="#000000" + android:pathData="M16.787,5.478C13.74,2.282 8.679,2.163 5.482,5.209C5.389,5.299 5.299,5.389 5.213,5.478C5.015,5.688 4.828,5.905 4.652,6.133L11,12.484L17.348,6.133C17.176,5.905 16.985,5.688 16.787,5.478ZM11,4.047C12.869,4.047 14.611,4.768 15.92,6.084L11,11.004L6.08,6.084C7.389,4.768 9.131,4.047 11,4.047Z" /> diff --git a/core/ui/src/main/res/drawable/ic_information_24.xml b/core/ui/src/main/res/drawable/ic_information_24.xml new file mode 100644 index 0000000000..2e97b7f85f --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_information_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_kaspa_no_color.xml b/core/ui/src/main/res/drawable/ic_kaspa_22.xml similarity index 91% rename from app/src/main/res/drawable/ic_kaspa_no_color.xml rename to core/ui/src/main/res/drawable/ic_kaspa_22.xml index 55b68fd581..bf99b9f327 100644 --- a/app/src/main/res/drawable/ic_kaspa_no_color.xml +++ b/core/ui/src/main/res/drawable/ic_kaspa_22.xml @@ -4,6 +4,6 @@ android:viewportWidth="22" android:viewportHeight="22"> diff --git a/app/src/main/res/drawable/ic_kava_no_color.xml b/core/ui/src/main/res/drawable/ic_kava_22.xml similarity index 91% rename from app/src/main/res/drawable/ic_kava_no_color.xml rename to core/ui/src/main/res/drawable/ic_kava_22.xml index 49e4647c33..8653dfdd6f 100644 --- a/app/src/main/res/drawable/ic_kava_no_color.xml +++ b/core/ui/src/main/res/drawable/ic_kava_22.xml @@ -5,6 +5,6 @@ android:viewportHeight="22"> diff --git a/core/ui/src/main/res/drawable/ic_kusama_16.xml b/core/ui/src/main/res/drawable/ic_kusama_16.xml new file mode 100644 index 0000000000..9327957356 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_kusama_16.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_litecoin_no_color.xml b/core/ui/src/main/res/drawable/ic_litecoin_22.xml similarity index 96% rename from app/src/main/res/drawable/ic_litecoin_no_color.xml rename to core/ui/src/main/res/drawable/ic_litecoin_22.xml index 5b3acc91e8..5148d512e6 100644 --- a/app/src/main/res/drawable/ic_litecoin_no_color.xml +++ b/core/ui/src/main/res/drawable/ic_litecoin_22.xml @@ -5,6 +5,6 @@ android:viewportWidth="22" android:viewportHeight="22"> diff --git a/app/src/main/res/drawable/ic_octaspace_no_color.xml b/core/ui/src/main/res/drawable/ic_octaspace_22.xml similarity index 77% rename from app/src/main/res/drawable/ic_octaspace_no_color.xml rename to core/ui/src/main/res/drawable/ic_octaspace_22.xml index c61e79358f..5cc1495ac4 100644 --- a/app/src/main/res/drawable/ic_octaspace_no_color.xml +++ b/core/ui/src/main/res/drawable/ic_octaspace_22.xml @@ -1,55 +1,55 @@ diff --git a/app/src/main/res/drawable/ic_optimism_no_color.xml b/core/ui/src/main/res/drawable/ic_optimism_22.xml similarity index 98% rename from app/src/main/res/drawable/ic_optimism_no_color.xml rename to core/ui/src/main/res/drawable/ic_optimism_22.xml index 7091ff4c7d..801fa42804 100644 --- a/app/src/main/res/drawable/ic_optimism_no_color.xml +++ b/core/ui/src/main/res/drawable/ic_optimism_22.xml @@ -6,7 +6,7 @@ diff --git a/core/ui/src/main/res/drawable/ic_polkadot_16.xml b/core/ui/src/main/res/drawable/ic_polkadot_16.xml new file mode 100644 index 0000000000..e052ddbe4f --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_polkadot_16.xml @@ -0,0 +1,25 @@ + + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_polygon_16.xml b/core/ui/src/main/res/drawable/ic_polygon_16.xml new file mode 100644 index 0000000000..ddc6ff8adf --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_polygon_16.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_ravencoin_no_color.xml b/core/ui/src/main/res/drawable/ic_ravencoin_22.xml similarity index 91% rename from app/src/main/res/drawable/ic_ravencoin_no_color.xml rename to core/ui/src/main/res/drawable/ic_ravencoin_22.xml index 7599b5c971..e048eea785 100644 --- a/app/src/main/res/drawable/ic_ravencoin_no_color.xml +++ b/core/ui/src/main/res/drawable/ic_ravencoin_22.xml @@ -6,13 +6,13 @@ diff --git a/core/ui/src/main/res/drawable/ic_rsk_16.xml b/core/ui/src/main/res/drawable/ic_rsk_16.xml new file mode 100644 index 0000000000..ff6427bdf7 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_rsk_16.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_shape_circle.xml b/core/ui/src/main/res/drawable/ic_shape_circle.xml new file mode 100644 index 0000000000..59a662d141 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_shape_circle.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_solana_16.xml b/core/ui/src/main/res/drawable/ic_solana_16.xml new file mode 100644 index 0000000000..812ff6051d --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_solana_16.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_stellar_16.xml b/core/ui/src/main/res/drawable/ic_stellar_16.xml new file mode 100644 index 0000000000..fab661b840 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_stellar_16.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_telos_no_color.xml b/core/ui/src/main/res/drawable/ic_telos_22.xml similarity index 96% rename from app/src/main/res/drawable/ic_telos_no_color.xml rename to core/ui/src/main/res/drawable/ic_telos_22.xml index bdd073cd40..b3a829d451 100644 --- a/app/src/main/res/drawable/ic_telos_no_color.xml +++ b/core/ui/src/main/res/drawable/ic_telos_22.xml @@ -8,5 +8,5 @@ android:pathData="M14.862,5.607C14.909,5.486 14.825,5.354 14.695,5.346L12.507,5.2C12.44,5.196 12.375,5.227 12.336,5.283L11.756,6.118C11.72,6.17 11.66,6.201 11.597,6.201L7.745,6.178C7.556,6.177 7.373,6.245 7.231,6.37L6.163,7.308C5.925,7.517 5.839,7.849 5.945,8.147L6.454,9.575C6.457,9.583 6.455,9.591 6.451,9.597C6.165,9.977 5.998,10.434 5.973,10.909L5.964,11.071C5.91,12.08 6.029,13.091 6.316,14.061C6.552,14.859 6.823,15.775 6.989,16.336C7.076,16.632 7.379,16.807 7.679,16.735C8.325,16.58 9.435,16.313 10.309,16.102C11.078,15.917 11.809,15.602 12.472,15.17L12.886,14.901C13.285,14.641 13.597,14.269 13.784,13.831C13.787,13.824 13.793,13.818 13.801,13.817L15.292,13.544C15.603,13.487 15.848,13.246 15.91,12.936L16.188,11.541C16.225,11.356 16.193,11.163 16.097,11L14.288,7.909C14.17,7.706 14.15,7.461 14.234,7.242L14.862,5.607Z" android:fillColor="#00000000" android:fillType="evenOdd" - android:strokeColor="#B0B0B0"/> + android:strokeColor="#000000" /> diff --git a/app/src/main/res/drawable/ic_terra2_no_color.xml b/core/ui/src/main/res/drawable/ic_terra2_22.xml similarity index 90% rename from app/src/main/res/drawable/ic_terra2_no_color.xml rename to core/ui/src/main/res/drawable/ic_terra2_22.xml index 990d30d7b5..d5d1a6c4b2 100644 --- a/app/src/main/res/drawable/ic_terra2_no_color.xml +++ b/core/ui/src/main/res/drawable/ic_terra2_22.xml @@ -5,14 +5,14 @@ android:viewportHeight="22"> + android:fillColor="#000000" /> + android:fillColor="#000000" /> + android:fillColor="#000000" /> + android:fillColor="#000000" /> diff --git a/app/src/main/res/drawable/ic_terra_no_color.xml b/core/ui/src/main/res/drawable/ic_terra_22.xml similarity index 90% rename from app/src/main/res/drawable/ic_terra_no_color.xml rename to core/ui/src/main/res/drawable/ic_terra_22.xml index f25adb981e..c362c152da 100644 --- a/app/src/main/res/drawable/ic_terra_no_color.xml +++ b/core/ui/src/main/res/drawable/ic_terra_22.xml @@ -5,11 +5,11 @@ android:viewportHeight="22"> + android:fillColor="#000000" /> + android:fillColor="#000000" /> + android:fillColor="#000000" /> diff --git a/core/ui/src/main/res/drawable/ic_tezos_16.xml b/core/ui/src/main/res/drawable/ic_tezos_16.xml new file mode 100644 index 0000000000..c769b70017 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_tezos_16.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_ton_no_color.xml b/core/ui/src/main/res/drawable/ic_ton_22.xml similarity index 97% rename from app/src/main/res/drawable/ic_ton_no_color.xml rename to core/ui/src/main/res/drawable/ic_ton_22.xml index e9ecd65434..b06fe8c7ad 100644 --- a/app/src/main/res/drawable/ic_ton_no_color.xml +++ b/core/ui/src/main/res/drawable/ic_ton_22.xml @@ -4,7 +4,7 @@ android:viewportWidth="22" android:viewportHeight="22"> diff --git a/core/ui/src/main/res/drawable/ic_tron_22.xml b/core/ui/src/main/res/drawable/ic_tron_22.xml new file mode 100644 index 0000000000..cf17b26c7b --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_tron_22.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_xrp_no_color.xml b/core/ui/src/main/res/drawable/ic_xrp_22.xml similarity index 96% rename from app/src/main/res/drawable/ic_xrp_no_color.xml rename to core/ui/src/main/res/drawable/ic_xrp_22.xml index 47a43000ca..6d31b60858 100644 --- a/app/src/main/res/drawable/ic_xrp_no_color.xml +++ b/core/ui/src/main/res/drawable/ic_xrp_22.xml @@ -5,6 +5,6 @@ android:viewportWidth="22" android:viewportHeight="22"> diff --git a/core/ui/src/main/res/drawable/img_decimal_22.xml b/core/ui/src/main/res/drawable/img_decimal_22.xml new file mode 100644 index 0000000000..02241418d2 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_decimal_22.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index cc7c128143..ca0202ae25 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -215,8 +215,6 @@ internal class DefaultCurrenciesRepository( override suspend fun getMultiCurrencyWalletCurrency( userWalletId: UserWalletId, id: CryptoCurrency.ID, - contractAddress: String?, - derivationPath: Network.DerivationPath, ): CryptoCurrency = withContext(dispatchers.io) { val userWallet = getUserWallet(userWalletId) ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true) @@ -227,10 +225,8 @@ internal class DefaultCurrenciesRepository( responseCurrenciesFactory.createCurrency( currencyId = id, - contractAddress = contractAddress, response = response, scanResponse = userWallet.scanResponse, - derivationPath = derivationPath.value, ) } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt index 1f10b3cdf5..ef55915ce2 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt @@ -4,10 +4,9 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.domain.common.DerivationStyleProvider -import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.extensions.toCoinId -import com.tangem.domain.common.extensions.toNetworkId +import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency @@ -18,26 +17,13 @@ internal class ResponseCryptoCurrenciesFactory { fun createCurrency( currencyId: CryptoCurrency.ID, - contractAddress: String?, response: UserTokensResponse, scanResponse: ScanResponse, - derivationPath: String?, ): CryptoCurrency { - val responseTokenId = currencyId.rawCurrencyId - val networkId = Blockchain.fromId(currencyId.rawNetworkId).toNetworkId() - - val token = requireNotNull( - value = response.tokens - .find { - it.id == responseTokenId && it.networkId == networkId && it.derivationPath == derivationPath && - it.contractAddress == contractAddress - }, - lazyMessage = { "Unable find a token with provided TokenID($responseTokenId) and NetworkID($networkId)" }, - ) - - return requireNotNull(createCurrency(token, scanResponse)) { - "Unable to create a currency with provided ID: $currencyId" - } + return response.tokens + .asSequence() + .mapNotNull { createCurrency(it, scanResponse) } + .first { it.id == currencyId } } fun createCurrencies(response: UserTokensResponse, scanResponse: ScanResponse): List { @@ -56,9 +42,8 @@ internal class ResponseCryptoCurrenciesFactory { } val cardDerivationStyleProvider = scanResponse.derivationStyleProvider - val card = scanResponse.card - if (card.isTestCard) { + if (scanResponse.cardTypesResolver.isTestCard()) { blockchain = blockchain.getTestnetVersion() ?: blockchain } diff --git a/domain/balance-hiding/build.gradle.kts b/domain/balance-hiding/build.gradle.kts index d9554a9615..1b932e1dc3 100644 --- a/domain/balance-hiding/build.gradle.kts +++ b/domain/balance-hiding/build.gradle.kts @@ -4,6 +4,7 @@ plugins { } dependencies { + implementation(projects.domain.core) implementation(deps.kotlin.coroutines) implementation(projects.domain.settings) implementation(projects.domain.balanceHiding.models) diff --git a/domain/balance-hiding/models/src/main/kotlin/com/tangem/domain/balancehiding/BalanceHidingSettings.kt b/domain/balance-hiding/models/src/main/kotlin/com/tangem/domain/balancehiding/BalanceHidingSettings.kt index 4aac92cad2..2432bbc2ec 100644 --- a/domain/balance-hiding/models/src/main/kotlin/com/tangem/domain/balancehiding/BalanceHidingSettings.kt +++ b/domain/balance-hiding/models/src/main/kotlin/com/tangem/domain/balancehiding/BalanceHidingSettings.kt @@ -3,4 +3,5 @@ package com.tangem.domain.balancehiding data class BalanceHidingSettings( val isHidingEnabledInSettings: Boolean, val isBalanceHidden: Boolean, + val isBalanceHidingNotificationEnabled: Boolean, ) \ No newline at end of file diff --git a/domain/balance-hiding/src/main/kotlin/com/tangem/domain/balancehiding/IsBalanceHiddenUseCase.kt b/domain/balance-hiding/src/main/kotlin/com/tangem/domain/balancehiding/GetBalanceHidingSettingsUseCase.kt similarity index 64% rename from domain/balance-hiding/src/main/kotlin/com/tangem/domain/balancehiding/IsBalanceHiddenUseCase.kt rename to domain/balance-hiding/src/main/kotlin/com/tangem/domain/balancehiding/GetBalanceHidingSettingsUseCase.kt index 9d22b8d762..6f4c08e5fa 100644 --- a/domain/balance-hiding/src/main/kotlin/com/tangem/domain/balancehiding/IsBalanceHiddenUseCase.kt +++ b/domain/balance-hiding/src/main/kotlin/com/tangem/domain/balancehiding/GetBalanceHidingSettingsUseCase.kt @@ -2,15 +2,12 @@ package com.tangem.domain.balancehiding import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -class IsBalanceHiddenUseCase( +class GetBalanceHidingSettingsUseCase( private val balanceHidingRepository: BalanceHidingRepository, ) { - operator fun invoke(): Flow { - return balanceHidingRepository.getBalanceHidingSettingsFlow().map { - it.isBalanceHidden - } + operator fun invoke(): Flow { + return balanceHidingRepository.getBalanceHidingSettingsFlow() } } \ No newline at end of file diff --git a/domain/balance-hiding/src/main/kotlin/com/tangem/domain/balancehiding/ListenToFlipsUseCase.kt b/domain/balance-hiding/src/main/kotlin/com/tangem/domain/balancehiding/ListenToFlipsUseCase.kt index f4c4eb1562..c80577e10e 100644 --- a/domain/balance-hiding/src/main/kotlin/com/tangem/domain/balancehiding/ListenToFlipsUseCase.kt +++ b/domain/balance-hiding/src/main/kotlin/com/tangem/domain/balancehiding/ListenToFlipsUseCase.kt @@ -1,28 +1,43 @@ package com.tangem.domain.balancehiding +import arrow.core.Either +import arrow.core.left +import arrow.core.raise.catch +import com.tangem.domain.balancehiding.error.HideBalancesError import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.collectLatest class ListenToFlipsUseCase( private val flipDetector: DeviceFlipDetector, private val balanceHidingRepository: BalanceHidingRepository, ) { - suspend operator fun invoke(): Flow { - return if (balanceHidingRepository.getBalanceHidingSettings().isHidingEnabledInSettings) { - flipDetector.getDeviceFlipFlow().onEach { - val balanceHidingSettings = balanceHidingRepository.getBalanceHidingSettings() + operator fun invoke(): Flow> = channelFlow { + flipDetector.getDeviceFlipFlow().collectLatest { + val balanceHidingSettings = catch( + block = { balanceHidingRepository.getBalanceHidingSettings() }, + catch = { + send(HideBalancesError.DataError(it).left()) + return@collectLatest + }, + ) - balanceHidingRepository.storeBalanceHidingSettings( - balanceHidingSettings.copy( - isBalanceHidden = !balanceHidingSettings.isBalanceHidden, - ), + if (balanceHidingSettings.isHidingEnabledInSettings) { + catch( + block = { + balanceHidingRepository.storeBalanceHidingSettings( + balanceHidingSettings.copy( + isBalanceHidden = !balanceHidingSettings.isBalanceHidden, + ), + ) + }, + catch = { send(HideBalancesError.DataError(it).left()) }, ) + } else { + send(HideBalancesError.HidingDisabled.left()) } - } else { - emptyFlow() } } } \ No newline at end of file diff --git a/domain/balance-hiding/src/main/kotlin/com/tangem/domain/balancehiding/UpdateBalanceHidingSettingsUseCase.kt b/domain/balance-hiding/src/main/kotlin/com/tangem/domain/balancehiding/UpdateBalanceHidingSettingsUseCase.kt new file mode 100644 index 0000000000..f2309fb23b --- /dev/null +++ b/domain/balance-hiding/src/main/kotlin/com/tangem/domain/balancehiding/UpdateBalanceHidingSettingsUseCase.kt @@ -0,0 +1,29 @@ +package com.tangem.domain.balancehiding + +import arrow.core.Either +import arrow.core.raise.catch +import arrow.core.raise.either +import com.tangem.domain.balancehiding.error.HideBalancesError +import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository + +class UpdateBalanceHidingSettingsUseCase( + private val balanceHidingRepository: BalanceHidingRepository, +) { + + suspend operator fun invoke( + update: BalanceHidingSettings.() -> BalanceHidingSettings, + ): Either = either { + val settings = catch({ balanceHidingRepository.getBalanceHidingSettings() }) { + raise(HideBalancesError.DataError(it)) + } + + if (settings.isHidingEnabledInSettings) { + catch( + block = { balanceHidingRepository.storeBalanceHidingSettings(update(settings)) }, + catch = { raise(HideBalancesError.DataError(it)) }, + ) + } else { + raise(HideBalancesError.HidingDisabled) + } + } +} \ No newline at end of file diff --git a/domain/balance-hiding/src/main/kotlin/com/tangem/domain/balancehiding/error/HideBalancesError.kt b/domain/balance-hiding/src/main/kotlin/com/tangem/domain/balancehiding/error/HideBalancesError.kt new file mode 100644 index 0000000000..7438c20918 --- /dev/null +++ b/domain/balance-hiding/src/main/kotlin/com/tangem/domain/balancehiding/error/HideBalancesError.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.balancehiding.error + +sealed class HideBalancesError { + + object HidingDisabled : HideBalancesError() + + data class DataError(val cause: Throwable) : HideBalancesError() +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt index 69fe697d8a..5c0fe2804c 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt @@ -72,6 +72,8 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "chia/test" -> Blockchain.ChiaTestnet "near-protocol" -> Blockchain.Near "near-protocol/test" -> Blockchain.NearTestnet + "decimal" -> Blockchain.Decimal + "decimal/test" -> Blockchain.DecimalTestnet else -> null } } @@ -145,6 +147,8 @@ fun Blockchain.toNetworkId(): String { Blockchain.ChiaTestnet -> "chia/test" Blockchain.Near -> "near-protocol" Blockchain.NearTestnet -> "near-protocol/test" + Blockchain.Decimal -> "decimal" + Blockchain.DecimalTestnet -> "decimal/test" } } @@ -188,10 +192,11 @@ fun Blockchain.toCoinId(): String { Blockchain.Telos, Blockchain.TelosTestnet -> "telos" Blockchain.AlephZero, Blockchain.AlephZeroTestnet -> "aleph-zero" Blockchain.OctaSpace, Blockchain.OctaSpaceTestnet -> "octaspace" - Blockchain.Chia -> "chia" - Blockchain.ChiaTestnet -> "chia/test" + Blockchain.Chia, Blockchain.ChiaTestnet -> "chia" Blockchain.Near -> "near" Blockchain.NearTestnet -> "near/test" + Blockchain.Decimal -> "decimal" + Blockchain.DecimalTestnet -> "decimal/test" Blockchain.Unknown -> "unknown" } } diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt index a424fbb683..bd6efc230c 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt @@ -1,6 +1,5 @@ package com.tangem.domain.redux.global -import com.tangem.datasource.api.paymentology.PaymentologyApiService import com.tangem.datasource.api.tangemTech.TangemTechService import com.tangem.domain.models.scan.ScanResponse @@ -17,5 +16,4 @@ data class DomainGlobalState( data class NetworkServices( val tangemTechService: TangemTechService = TangemTechService, - val paymentologyService: PaymentologyApiService = PaymentologyApiService, ) \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt index 8e9babd7f2..1e9c032f6c 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt @@ -57,9 +57,7 @@ internal class UpdateWalletManagerResultFactory { } private fun getTokensAmounts(amounts: Set): Set { - val mutableAmounts = hashSetOf() - - return amounts.mapNotNullTo(mutableAmounts, ::createCurrencyAmount) + return amounts.mapNotNullTo(hashSetOf(), ::createCurrencyAmount) } private fun getDemoTokensAmounts(demoAmount: Amount, tokens: Set): Set { diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkStatus.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkStatus.kt index fc2b955456..1a10993cfb 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkStatus.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkStatus.kt @@ -58,4 +58,14 @@ data class NetworkStatus( val amountToCreateAccount: BigDecimal, val errorMessage: String, ) : Status() + + /** + * Represents possible statuses of amount. + * + * This sealed class includes states as LoadedAmount, UnreachableAmount. + */ + sealed class AmountStatus + + data class LoadedAmount(val value: BigDecimal) : AmountStatus() + object UnreachableAmount : AmountStatus() } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt index b392da6d64..8fbd29d4f5 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt @@ -36,20 +36,16 @@ class FetchCurrencyStatusUseCase( * * @param userWalletId The ID of the user's wallet. * @param id The ID of the cryptocurrency. - * @param contractAddress The contract address of the crypto currency - * @param derivationPath currency derivation path. * @param refresh Indicates whether to force a refresh of the status data. * @return An [Either] representing success (Right) or an error (Left) in fetching the status. */ suspend operator fun invoke( userWalletId: UserWalletId, id: CryptoCurrency.ID, - contractAddress: String?, - derivationPath: Network.DerivationPath, refresh: Boolean = false, ): Either { return either { - val currency = getCurrency(userWalletId, id, contractAddress, derivationPath) + val currency = getCurrency(userWalletId, id) fetchCurrencyStatus(userWalletId, currency, refresh) } @@ -91,17 +87,10 @@ class FetchCurrencyStatusUseCase( private suspend fun Raise.getCurrency( userWalletId: UserWalletId, id: CryptoCurrency.ID, - contractAddress: String?, - derivationPath: Network.DerivationPath, ): CryptoCurrency { return catch( block = { - currenciesRepository.getMultiCurrencyWalletCurrency( - userWalletId = userWalletId, - id = id, - contractAddress = contractAddress, - derivationPath = derivationPath, - ) + currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, id) }, ) { raise(CurrencyStatusError.DataError(it)) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyUseCase.kt index 3959be4879..d0e47c2b62 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyUseCase.kt @@ -6,7 +6,6 @@ import arrow.core.raise.catch import arrow.core.raise.either import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.models.UserWalletId @@ -19,17 +18,13 @@ class GetCryptoCurrencyUseCase( * * @param userWalletId The ID of the user's wallet. * @param id The ID of the cryptocurrency. - * @param contractAddress The contract address of the crypto currency - * @param derivationPath currency derivation path. * @return An [Either] representing success (Right) or an error (Left) in fetching the status. */ suspend operator fun invoke( userWalletId: UserWalletId, id: CryptoCurrency.ID, - contractAddress: String?, - derivationPath: Network.DerivationPath, ): Either { - return either { getCurrency(userWalletId, id, contractAddress, derivationPath) } + return either { getCurrency(userWalletId, id) } } /** @@ -45,17 +40,10 @@ class GetCryptoCurrencyUseCase( private suspend fun Raise.getCurrency( userWalletId: UserWalletId, id: CryptoCurrency.ID, - contractAddress: String?, - derivationPath: Network.DerivationPath, ): CryptoCurrency { return catch( block = { - currenciesRepository.getMultiCurrencyWalletCurrency( - userWalletId, - id, - contractAddress, - derivationPath, - ) + currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, id) }, catch = { raise(CurrencyStatusError.DataError(it)) }, ) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt index 146a44bc4b..92e660af07 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt @@ -5,7 +5,6 @@ import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.error.mapper.mapToCurrencyError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository @@ -33,16 +32,12 @@ class GetCurrencyStatusUpdatesUseCase( * * @param userWalletId The unique identifier of the user's wallet. * @param currencyId The unique identifier of the cryptocurrency. - * @param contractAddress The contract address of the crypto currency - * @param derivationPath currency derivation path. * @param isSingleWalletWithTokens Indicates whether the user wallet contains only one token on card (old cards) * @return A [Flow] emitting either a [CurrencyStatusError] or a [CryptoCurrencyStatus], indicating the result of the fetch operation. */ operator fun invoke( userWalletId: UserWalletId, currencyId: CryptoCurrency.ID, - contractAddress: String?, - derivationPath: Network.DerivationPath, isSingleWalletWithTokens: Boolean, ): Flow> { return flow { @@ -50,8 +45,6 @@ class GetCurrencyStatusUpdatesUseCase( getCurrency( userWalletId = userWalletId, currencyId = currencyId, - contractAddress = contractAddress, - derivationPath = derivationPath, isSingleWalletWithTokens = isSingleWalletWithTokens, ), ) @@ -61,9 +54,7 @@ class GetCurrencyStatusUpdatesUseCase( private suspend fun getCurrency( userWalletId: UserWalletId, currencyId: CryptoCurrency.ID, - derivationPath: Network.DerivationPath, isSingleWalletWithTokens: Boolean, - contractAddress: String?, ): Flow> { val operations = CurrenciesStatusesOperations( currenciesRepository = currenciesRepository, @@ -75,7 +66,7 @@ class GetCurrencyStatusUpdatesUseCase( val currencyFlow = if (isSingleWalletWithTokens) { operations.getCurrencyStatusSingleWalletWithTokensFlow(currencyId) } else { - operations.getCurrencyStatusFlow(currencyId, contractAddress, derivationPath) + operations.getCurrencyStatusFlow(currencyId) } return currencyFlow.map { maybeCurrency -> maybeCurrency.mapLeft(CurrenciesStatusesOperations.Error::mapToCurrencyError) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt index 895ee2ee8e..0ff4f92ae7 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt @@ -35,7 +35,6 @@ class GetCurrencyWarningsUseCase( networkId = currency.network.id, currencyId = currency.id, derivationPath = derivationPath, - contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress, isSingleWalletWithTokens = isSingleWalletWithTokens, ), flowOf(walletManagersFacade.getRentInfo(userWalletId, currency.network)), @@ -63,7 +62,6 @@ class GetCurrencyWarningsUseCase( userWalletId: UserWalletId, networkId: Network.ID, currencyId: CryptoCurrency.ID, - contractAddress: String?, derivationPath: Network.DerivationPath, isSingleWalletWithTokens: Boolean, ): Flow> { @@ -77,7 +75,7 @@ class GetCurrencyWarningsUseCase( val currencyFlow = if (isSingleWalletWithTokens) { operations.getCurrencyStatusSingleWalletWithTokensFlow(currencyId) } else { - operations.getCurrencyStatusFlow(currencyId, contractAddress, derivationPath) + operations.getCurrencyStatusFlow(currencyId) } val networkFlow = if (isSingleWalletWithTokens) { operations.getNetworkCoinForSingleWalletWithTokenFlow(networkId) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index 3bbe439bc5..9bce6f0d35 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -105,13 +105,9 @@ internal class CurrenciesStatusesOperations( } } - suspend fun getCurrencyStatusFlow( - currencyId: CryptoCurrency.ID, - contractAddress: String?, - derivationPath: Network.DerivationPath, - ): Flow> { + suspend fun getCurrencyStatusFlow(currencyId: CryptoCurrency.ID): Flow> { val currency = recover( - block = { getMultiCurrencyWalletCurrency(currencyId, contractAddress, derivationPath) }, + block = { getMultiCurrencyWalletCurrency(currencyId) }, recover = { return flowOf(it.left()) }, ) @@ -245,17 +241,11 @@ internal class CurrenciesStatusesOperations( .onEmpty { emit(Error.EmptyCurrencies.left()) } } - private suspend fun Raise.getMultiCurrencyWalletCurrency( - currencyId: CryptoCurrency.ID, - contractAddress: String?, - derivationPath: Network.DerivationPath, - ): CryptoCurrency { + private suspend fun Raise.getMultiCurrencyWalletCurrency(currencyId: CryptoCurrency.ID): CryptoCurrency { return Either.catch { currenciesRepository.getMultiCurrencyWalletCurrency( userWalletId = userWalletId, id = currencyId, - contractAddress = contractAddress, - derivationPath = derivationPath, ) } .mapLeft { Error.DataError(it) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt index d2c1bd3b72..3901e5ad7a 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt @@ -127,18 +127,11 @@ interface CurrenciesRepository { * * @param userWalletId The unique identifier of the user wallet. * @param id The unique identifier of the cryptocurrency to be retrieved. - * @param contractAddress The contract address of the crypto currency - * @param derivationPath currency derivation path. * @return The cryptocurrency associated with the user wallet and ID. * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet * ID provided. */ - suspend fun getMultiCurrencyWalletCurrency( - userWalletId: UserWalletId, - id: CryptoCurrency.ID, - contractAddress: String?, - derivationPath: Network.DerivationPath, - ): CryptoCurrency + suspend fun getMultiCurrencyWalletCurrency(userWalletId: UserWalletId, id: CryptoCurrency.ID): CryptoCurrency /** * Get the coin for a specific network. diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt index 97e34fcc43..ffdd5c834d 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt @@ -80,8 +80,6 @@ internal class MockCurrenciesRepository( override suspend fun getMultiCurrencyWalletCurrency( userWalletId: UserWalletId, id: CryptoCurrency.ID, - contractAddress: String?, - derivationPath: Network.DerivationPath, ): CryptoCurrency { val token = token.getOrElse { e -> throw e } diff --git a/features/manage-tokens/.gitignore b/features/manage-tokens/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/manage-tokens/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/manage-tokens/api/.gitignore b/features/manage-tokens/api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/manage-tokens/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/manage-tokens/api/build.gradle.kts b/features/manage-tokens/api/build.gradle.kts new file mode 100644 index 0000000000..aa4ad2bd26 --- /dev/null +++ b/features/manage-tokens/api/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.managetokens.api" +} + +dependencies { + /** AndroidX */ + implementation(deps.androidx.fragment.ktx) +} \ No newline at end of file diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/featuretoggles/ManageTokensFeatureToggles.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/featuretoggles/ManageTokensFeatureToggles.kt new file mode 100644 index 0000000000..e29173b5fc --- /dev/null +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/featuretoggles/ManageTokensFeatureToggles.kt @@ -0,0 +1,5 @@ +package com.tangem.features.managetokens.featuretoggles + +interface ManageTokensFeatureToggles { + val isRedesignedScreenEnabled: Boolean +} \ No newline at end of file diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/navigation/ManageTokensRouter.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/navigation/ManageTokensRouter.kt new file mode 100644 index 0000000000..7feba2b807 --- /dev/null +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/navigation/ManageTokensRouter.kt @@ -0,0 +1,7 @@ +package com.tangem.features.managetokens.navigation + +import androidx.fragment.app.Fragment + +interface ManageTokensRouter { + fun getEntryFragment(): Fragment +} \ No newline at end of file diff --git a/features/manage-tokens/impl/.gitignore b/features/manage-tokens/impl/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/manage-tokens/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/manage-tokens/impl/build.gradle.kts b/features/manage-tokens/impl/build.gradle.kts new file mode 100644 index 0000000000..76bd921f71 --- /dev/null +++ b/features/manage-tokens/impl/build.gradle.kts @@ -0,0 +1,68 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.managetokens.impl" +} + +dependencies { + /** AndroidX */ + implementation(deps.androidx.activity.compose) + implementation(deps.material) + + /** Compose */ + implementation(deps.compose.accompanist.systemUiController) + implementation(deps.compose.coil) + implementation(deps.compose.constraintLayout) + implementation(deps.compose.foundation) + implementation(deps.compose.material) + implementation(deps.compose.material3) + implementation(deps.compose.navigation) + implementation(deps.compose.navigation.hilt) + implementation(deps.compose.paging) + implementation(deps.compose.reorderable) + implementation(deps.compose.shimmer) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + + /** Other libraries */ + implementation(deps.arrow.core) + implementation(deps.jodatime) + implementation(deps.kotlin.immutable.collections) + implementation(deps.tangem.card.core) + implementation(deps.tangem.blockchain) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + + /** Core modules */ + implementation(projects.core.featuretoggles) + implementation(projects.core.navigation) + implementation(projects.core.ui) + implementation(projects.core.utils) + + /** Domain modules */ + implementation(projects.common) + implementation(projects.domain.card) + implementation(projects.domain.demo) + implementation(projects.domain.legacy) + implementation(projects.domain.models) + implementation(projects.domain.settings) + implementation(projects.domain.tokens) + implementation(projects.domain.tokens.models) + implementation(projects.domain.txhistory) + implementation(projects.domain.txhistory.models) + implementation(projects.domain.wallets) + implementation(projects.domain.wallets.models) + implementation(projects.domain.appCurrency) + implementation(projects.domain.appCurrency.models) + + /** Feature Apis */ + implementation(projects.features.manageTokens.api) +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/ManageTokensFragment.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/ManageTokensFragment.kt new file mode 100644 index 0000000000..f4f7fa959b --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/ManageTokensFragment.kt @@ -0,0 +1,41 @@ +package com.tangem.managetokens + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.ui.components.SystemBarsEffect +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.core.ui.theme.AppThemeModeHolder +import com.tangem.features.managetokens.navigation.ManageTokensRouter +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +internal class ManageTokensFragment : ComposeFragment() { + + @Inject + override lateinit var appThemeModeHolder: AppThemeModeHolder + + @Inject + lateinit var manageTokensRouter: ManageTokensRouter + + // private val internalManageTokensRouter: InnerManageTokensRouter + // get() = requireNotNull(manageTokensRouter as? InnerManageTokensRouter) { + // "internalManageTokensRouter should be instance of InnerManageTokensRouter" + // } + + @Composable + override fun ScreenContent(modifier: Modifier) { + // val viewModel = hiltViewModel() + // viewModel.router = [REDACTED_EMAIL] + // + // LocalLifecycleOwner.current.lifecycle.addObserver(viewModel) + + val systemBarsColor = TangemTheme.colors.background.secondary + SystemBarsEffect { + setSystemBarsColor(systemBarsColor) + } + + // ManageTokensScreen(state = viewModel.uiState) + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/di/ManageTokensFeatureTogglesModule.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/di/ManageTokensFeatureTogglesModule.kt new file mode 100644 index 0000000000..7552eb21df --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/di/ManageTokensFeatureTogglesModule.kt @@ -0,0 +1,21 @@ +package com.tangem.managetokens.di + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles +import com.tangem.managetokens.featuretoggles.DefaultManageTokensFeatureToggles +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 ManageTokensFeatureTogglesModule { + + @Provides + @Singleton + fun provideWalletFeatureToggles(featureTogglesManager: FeatureTogglesManager): ManageTokensFeatureToggles { + return DefaultManageTokensFeatureToggles(featureTogglesManager = featureTogglesManager) + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/di/ManageTokensRouterModule.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/di/ManageTokensRouterModule.kt new file mode 100644 index 0000000000..73783601c0 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/di/ManageTokensRouterModule.kt @@ -0,0 +1,20 @@ +package com.tangem.managetokens.di + +import com.tangem.features.managetokens.navigation.ManageTokensRouter +import com.tangem.managetokens.presentation.router.DefaultManageTokensRouter +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.scopes.ActivityScoped + +@Module +@InstallIn(ActivityComponent::class) +internal object ManageTokensRouterModule { + + @Provides + @ActivityScoped + fun provideManageTokensRouter(): ManageTokensRouter { + return DefaultManageTokensRouter() + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/featuretoggles/DefaultManageTokensFeatureToggles.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/featuretoggles/DefaultManageTokensFeatureToggles.kt new file mode 100644 index 0000000000..967a54e950 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/featuretoggles/DefaultManageTokensFeatureToggles.kt @@ -0,0 +1,11 @@ +package com.tangem.managetokens.featuretoggles + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles + +internal class DefaultManageTokensFeatureToggles( + private val featureTogglesManager: FeatureTogglesManager, +) : ManageTokensFeatureToggles { + override val isRedesignedScreenEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "REDESIGNED_MANAGE_TOKENS_SCREEN_ENABLED") +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/DerivationNotificationState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/DerivationNotificationState.kt new file mode 100644 index 0000000000..8856ab58c5 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/DerivationNotificationState.kt @@ -0,0 +1,33 @@ +package com.tangem.managetokens.presentation.managetokens.state + +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.pluralReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.features.managetokens.impl.R + +data class DerivationNotificationState( + val totalNeeded: Int, + val missingAddressesCount: Int, + val onGenerateClick: () -> Unit, +) { + val config = NotificationConfig( + title = resourceReference(id = R.string.warning_missing_derivation_title), + subtitle = pluralReference( + id = R.plurals.warning_missing_derivation_message, + count = totalNeeded, + formatArgs = wrappedList(totalNeeded), + ), + iconResId = R.drawable.ic_alert_circle_24, + buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig( + text = resourceReference(id = R.string.common_generate_addresses), + iconResId = R.drawable.ic_tangem_24, + onClick = onGenerateClick, + additionalText = pluralReference( + id = R.plurals.manage_tokens_number_of_wallets_android, + count = totalNeeded, + formatArgs = wrappedList(missingAddressesCount, totalNeeded), + ), + ), + ) +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/QuotesState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/QuotesState.kt new file mode 100644 index 0000000000..9e255ebc77 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/QuotesState.kt @@ -0,0 +1,17 @@ +package com.tangem.managetokens.presentation.managetokens.state + +import kotlinx.collections.immutable.ImmutableList + +internal sealed class QuotesState { + object Unknown : QuotesState() + + data class Content( + val priceChange: String, + val changeType: PriceChangeType, + val chartData: ImmutableList, + ) : QuotesState() +} + +enum class PriceChangeType { + UP, DOWN +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenButtonType.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenButtonType.kt new file mode 100644 index 0000000000..6c47a870ec --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenButtonType.kt @@ -0,0 +1,5 @@ +package com.tangem.managetokens.presentation.managetokens.state + +internal enum class TokenButtonType { + ADD, EDIT, NOT_AVAILABLE +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenIconState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenIconState.kt new file mode 100644 index 0000000000..0bd0c0d25d --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenIconState.kt @@ -0,0 +1,10 @@ +package com.tangem.managetokens.presentation.managetokens.state + +import androidx.compose.ui.graphics.Color +import com.tangem.core.ui.extensions.ImageReference + +internal data class TokenIconState( + val iconReference: ImageReference?, + val placeholderTint: Color, + val placeholderBackground: Color, +) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenItemState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenItemState.kt new file mode 100644 index 0000000000..ae386cc7c1 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenItemState.kt @@ -0,0 +1,19 @@ +package com.tangem.managetokens.presentation.managetokens.state + +internal sealed class TokenItemState { + + abstract val id: String + + data class Loading(override val id: String) : TokenItemState() + + data class Loaded( + override val id: String, + val name: String, + val currencyId: String, + val tokenIcon: TokenIconState, + val quotes: QuotesState, + val rate: String?, + val availableAction: TokenButtonType, + val onButtonClick: (String) -> Unit, + ) : TokenItemState() +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/DerivationNotificationStatePreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/DerivationNotificationStatePreviewData.kt new file mode 100644 index 0000000000..207ea75cb8 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/DerivationNotificationStatePreviewData.kt @@ -0,0 +1,11 @@ +package com.tangem.managetokens.presentation.managetokens.state.previewdata + +import com.tangem.managetokens.presentation.managetokens.state.DerivationNotificationState + +object DerivationNotificationStatePreviewData { + val state = DerivationNotificationState( + totalNeeded = 3, + missingAddressesCount = 2, + onGenerateClick = {}, + ) +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/TokenItemStatePreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/TokenItemStatePreviewData.kt new file mode 100644 index 0000000000..bdfedacd3e --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/TokenItemStatePreviewData.kt @@ -0,0 +1,50 @@ +package com.tangem.managetokens.presentation.managetokens.state.previewdata + +import androidx.compose.ui.graphics.Color +import com.tangem.managetokens.presentation.managetokens.state.* +import kotlinx.collections.immutable.persistentListOf + +internal object TokenItemStatePreviewData { + + val tokenLoading: TokenItemState + get() = TokenItemState.Loading("id") + + val loadedPriceDown: TokenItemState + get() = TokenItemState.Loaded( + id = "BTC", + name = "Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin", + currencyId = "BTC", + tokenIcon = tokenIconState, + quotes = QuotesState.Content( + priceChange = "0.43%", + changeType = PriceChangeType.DOWN, + chartData = persistentListOf(10f, 2f, 5f, 3f, 4f, 8f, 9f, 7f, 4f), + ), + rate = "31 285.72$", + availableAction = TokenButtonType.ADD, + onButtonClick = {}, + ) + + val loadedPriceUp: TokenItemState + get() = TokenItemState.Loaded( + id = "BTC", + name = "Bitcoin", + currencyId = "BTC", + tokenIcon = tokenIconState, + quotes = QuotesState.Content( + priceChange = "0.43%", + changeType = PriceChangeType.UP, + chartData = persistentListOf(1f, 3f, 4f, 8f, 12f, 10f, 8f, 3f, 5f, 7f), + ), + rate = "31 285.72$", + availableAction = TokenButtonType.NOT_AVAILABLE, + onButtonClick = {}, + ) + + private val tokenIconState: TokenIconState + get() = TokenIconState( + iconReference = null, + placeholderTint = Color.White, + placeholderBackground = Color.Black, + ) +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/DerivationNotification.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/DerivationNotification.kt new file mode 100644 index 0000000000..a18fffaf56 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/DerivationNotification.kt @@ -0,0 +1,143 @@ +package com.tangem.managetokens.presentation.managetokens.ui.components + +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.material.Card +import androidx.compose.material.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.PrimaryButtonIconEndTwoLines +import com.tangem.core.ui.components.SpacerW +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.managetokens.impl.R +import com.tangem.managetokens.presentation.managetokens.state.previewdata.DerivationNotificationStatePreviewData + +@Composable +internal fun DerivationNotification(config: NotificationConfig, modifier: Modifier = Modifier) { + BaseContainer( + modifier = modifier, + ) { + Column( + modifier = Modifier.padding(all = TangemTheme.dimens.spacing16), + verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing16), + ) { + MainContent( + iconResId = config.iconResId, + iconTint = TangemTheme.colors.icon.accent, + title = config.title, + subtitle = config.subtitle, + ) + val buttonConfig = config.buttonsState + if (buttonConfig is NotificationConfig.ButtonsState.PrimaryButtonConfig) { + PrimaryButtonIconEndTwoLines( + text = buttonConfig.text.resolveReference(), + iconResId = buttonConfig.iconResId ?: R.drawable.ic_tangem_24, + onClick = buttonConfig.onClick, + modifier = Modifier + .fillMaxWidth(), + additionalText = buttonConfig.additionalText?.resolveReference(), + ) + } + } + } +} + +@Composable +private fun BaseContainer(modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit) { + Card( + modifier = modifier + .defaultMinSize(minHeight = TangemTheme.dimens.size62) + .fillMaxWidth(), + shape = RoundedCornerShape( + topStart = TangemTheme.dimens.radius16, + topEnd = TangemTheme.dimens.radius16, + ), + elevation = TangemTheme.dimens.elevation12, + backgroundColor = TangemTheme.colors.background.action, + ) { + Box(content = content) + } +} + +@Composable +private fun MainContent(iconResId: Int, iconTint: Color, title: TextReference, subtitle: TextReference) { + Row { + NotificationIcon(iconResId = iconResId, iconTint = iconTint) + SpacerW(width = TangemTheme.dimens.spacing10) + TextsBlock(title = title, subtitle = subtitle) + } +} + +@Composable +private fun RowScope.NotificationIcon(iconResId: Int, iconTint: Color) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .align(alignment = Alignment.CenterVertically), + ) { + Box( + modifier = Modifier + .size(TangemTheme.dimens.size36) + .background( + color = iconTint.copy(alpha = 0.12f), + shape = CircleShape, + ), + ) + Box( + modifier = Modifier + .size(TangemTheme.dimens.size16) + .background( + color = TangemTheme.colors.background.action, + shape = CircleShape, + ), + ) + Icon( + painter = painterResource(id = iconResId), + contentDescription = null, + tint = iconTint, + ) + } +} + +@Composable +private fun TextsBlock(title: TextReference, subtitle: TextReference) { + Column(verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing2)) { + Text( + text = title.resolveReference(), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.button, + ) + + Text( + text = subtitle.resolveReference(), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + ) + } +} + +@Preview +@Composable +private fun Preview_ManageTokensScreen_LightTheme() { + TangemTheme(isDark = false) { + DerivationNotification(DerivationNotificationStatePreviewData.state.config) + } +} + +@Preview +@Composable +private fun Preview_ManageTokensScreen_DarkTheme() { + TangemTheme(isDark = true) { + DerivationNotification(DerivationNotificationStatePreviewData.state.config) + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/PriceChangesChart.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/PriceChangesChart.kt new file mode 100644 index 0000000000..9909709a8d --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/PriceChangesChart.kt @@ -0,0 +1,123 @@ +package com.tangem.managetokens.presentation.managetokens.ui.components + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.res.TangemTheme +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +/** + * A chart with a solid line and gradient underneath. It can accept values of any range and number. + * If the last value is bigger or equal to the first, the chart is of accent color, otherwise it's warning color. + * + * @param values a list of float values for a chart. + **/ +@Composable +fun PriceChangesChart(values: ImmutableList, modifier: Modifier = Modifier) { + Row(modifier = modifier) { + if (values.size < 2) return // escape without drawing when there are not enough points + + val lineColor = if (values.last() >= values.first()) { + TangemTheme.colors.icon.accent + } else { + TangemTheme.colors.icon.warning + } + val gradient = Brush.verticalGradient( + colors = listOf(lineColor.copy(alpha = 0.21f), lineColor.copy(alpha = 0.0f)), + ) + Chart(list = values, lineColor = lineColor, gradient = gradient, modifier = Modifier.weight(1f)) + } +} + +@Composable +private fun Chart(list: ImmutableList, lineColor: Color, gradient: Brush, modifier: Modifier = Modifier) { + val max = list.max() + val min = list.min() + val zipList: List> = list.zipWithNext() + + for (pair in zipList) { + val fromValuePercentage = getValuePercentageForRange(pair.first, max, min) + val toValuePercentage = getValuePercentageForRange(pair.second, max, min) + + Canvas( + modifier = modifier.fillMaxHeight(), + onDraw = { + val fromPoint = Offset(x = 0f, y = size.height.times(1 - fromValuePercentage)) + val toPoint = Offset(x = size.width, y = size.height.times(1 - toValuePercentage)) + + val path = drawChartLineAndCreatePath(fromPoint = fromPoint, toPoint = toPoint, lineColor = lineColor) + + fillChart( + path = path, + fromPoint = fromPoint, + toPoint = toPoint, + size = size, + gradient = gradient, + ) + }, + ) + } +} + +private fun DrawScope.drawChartLineAndCreatePath(fromPoint: Offset, toPoint: Offset, lineColor: Color): Path { + val path = Path() + path.moveTo(fromPoint.x, fromPoint.y) + path.lineTo(toPoint.x, toPoint.y) + drawPath( + path = path, + color = lineColor, + style = Stroke(width = 1f), + ) + return path +} + +private fun DrawScope.fillChart(path: Path, fromPoint: Offset, toPoint: Offset, size: Size, gradient: Brush) { + path.lineTo(toPoint.x, size.height) + path.lineTo(fromPoint.x, size.height) + path.lineTo(0f, fromPoint.y) + drawPath( + path = path, + brush = gradient, + ) +} + +private fun getValuePercentageForRange(value: Float, max: Float, min: Float): Float { + return if (max == min) { // to draw a straight line when all values are the same + val modifiedMax = max + 1 + val modifiedMin = min - 1 + (value - modifiedMin) / (modifiedMax - modifiedMin) + } else { + (value - min) / (max - min) + } +} + +@Preview(widthDp = 150, heightDp = 150, showBackground = true) +@Composable +private fun Chart_Positive_Preview() { + TangemTheme(isDark = true) { + PriceChangesChart( + persistentListOf(1f, 2f, 4f, 1f, 5f), + ) + } +} + +@Preview(widthDp = 150, heightDp = 150, showBackground = true) +@Composable +private fun Chart_Negative_Preview() { + TangemTheme(isDark = true) { + PriceChangesChart( + persistentListOf(10f, 2f, 4f, 1f, 5f), + ) + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenButton.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenButton.kt new file mode 100644 index 0000000000..f3f76effd3 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenButton.kt @@ -0,0 +1,90 @@ +package com.tangem.managetokens.presentation.managetokens.ui.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.material.Icon +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.core.ui.components.buttons.PrimarySmallButton +import com.tangem.core.ui.components.buttons.SecondarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.managetokens.impl.R +import com.tangem.managetokens.presentation.managetokens.state.TokenButtonType + +@Composable +internal fun TokenButton(type: TokenButtonType, onClick: () -> Unit, modifier: Modifier = Modifier) { + when (type) { + TokenButtonType.ADD -> PrimarySmallButton( + config = SmallButtonConfig( + text = resourceReference(R.string.manage_tokens_add), + onClick = onClick, + ), + modifier = modifier, + ) + TokenButtonType.EDIT -> SecondarySmallButton( + config = SmallButtonConfig( + text = resourceReference(R.string.manage_tokens_edit), + onClick = onClick, + ), + modifier = modifier, + ) + TokenButtonType.NOT_AVAILABLE -> { + Box( + modifier = modifier + .size(height = TangemTheme.dimens.size24, width = TangemTheme.dimens.size46) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple( + bounded = false, + radius = TangemTheme.dimens.size20, + ), + onClick = onClick, + ), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier + .size(TangemTheme.dimens.size20), + painter = painterResource(id = R.drawable.ic_information_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } + } + } +} + +@Preview(backgroundColor = 0xffffff, showBackground = true) +@Composable +private fun TokenButton_Light_Preview(@PreviewParameter(TokenButtonTypeProvider::class) type: TokenButtonType) { + TangemTheme(isDark = false) { + TokenButton(type = type, {}) + } +} + +@Preview(showBackground = true) +@Composable +private fun TokenButton_Dark_Preview(@PreviewParameter(TokenButtonTypeProvider::class) type: TokenButtonType) { + TangemTheme(isDark = true) { + TokenButton(type = type, {}) + } +} + +private class TokenButtonTypeProvider : PreviewParameterProvider { + override val values = sequenceOf( + TokenButtonType.ADD, + TokenButtonType.EDIT, + TokenButtonType.NOT_AVAILABLE, + ) +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenIcon.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenIcon.kt new file mode 100644 index 0000000000..bc7701328b --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenIcon.kt @@ -0,0 +1,114 @@ +package com.tangem.managetokens.presentation.managetokens.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.core.ui.components.CircleShimmer +import com.tangem.core.ui.extensions.ImageReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.ImageBackgroundContrastChecker +import com.tangem.features.managetokens.impl.R +import com.tangem.managetokens.presentation.managetokens.state.TokenIconState +import kotlinx.coroutines.launch + +@Composable +internal fun TokenIcon(state: TokenIconState, modifier: Modifier = Modifier) { + val iconModifier = modifier + .size(TangemTheme.dimens.size36) + if (state.iconReference != null) { + DefaultCurrencyIcon( + modifier = iconModifier, + iconReference = state.iconReference, + errorIcon = { + PlaceholderIcon( + modifier = iconModifier, + tint = state.placeholderTint, + background = state.placeholderBackground, + ) + }, + ) + } else { + PlaceholderIcon( + modifier = iconModifier, + tint = state.placeholderTint, + background = state.placeholderBackground, + ) + } +} + +@Composable +private fun PlaceholderIcon(tint: Color, background: Color, modifier: Modifier = Modifier) { + Box( + modifier = modifier + .background( + color = background, + shape = CircleShape, + ), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier.matchParentSize(), + painter = painterResource(id = R.drawable.ic_custom_token_44), + tint = tint, + contentDescription = null, + ) + } +} + +@Composable +private inline fun DefaultCurrencyIcon( + iconReference: ImageReference, + crossinline errorIcon: @Composable () -> Unit, + modifier: Modifier = Modifier, +) { + val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb() + var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) } + val isDarkTheme = isSystemInDarkTheme() + val coroutineScope = rememberCoroutineScope() + + SubcomposeAsyncImage( + modifier = modifier + .background( + color = iconBackgroundColor, + shape = TangemTheme.shapes.roundedCorners8, + ), + model = ImageRequest.Builder(context = LocalContext.current) + .data(iconReference.getReference()) + .crossfade(enable = true) + .allowHardware(false) + .listener( + onSuccess = { _, result -> + if (isDarkTheme) { + coroutineScope.launch { + val color = ImageBackgroundContrastChecker( + drawable = result.drawable, + backgroundColor = itemBackgroundColor, + ).getContrastColorIfNeeded(isDarkTheme) + iconBackgroundColor = color + } + } + }, + ) + .build(), + loading = { LoadingIcon() }, + error = { errorIcon() }, + contentDescription = null, + ) +} + +@Composable +internal fun LoadingIcon(modifier: Modifier = Modifier) { + CircleShimmer(modifier = modifier) +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenPriceChange.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenPriceChange.kt new file mode 100644 index 0000000000..1b2d04c7a9 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenPriceChange.kt @@ -0,0 +1,79 @@ +package com.tangem.managetokens.presentation.managetokens.ui.components + +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import com.tangem.core.ui.components.SpacerW4 +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.managetokens.impl.R +import com.tangem.managetokens.presentation.managetokens.state.PriceChangeType +import com.tangem.managetokens.presentation.managetokens.state.QuotesState + +@Composable +internal fun TokenPriceChange(state: QuotesState, modifier: Modifier = Modifier) { + when (state) { + is QuotesState.Content -> + PriceChangeBlock(modifier = modifier, type = state.changeType, text = state.priceChange) + QuotesState.Unknown -> PriceChangeBlock(modifier = modifier) + } +} + +@Composable +private fun PriceChangeBlock(modifier: Modifier = Modifier, type: PriceChangeType? = null, text: String? = null) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + ) { + PriceChangeIcon(type = type) + SpacerW4() + PriceChangeText(type = type, text = text) + } +} + +@Composable +private fun PriceChangeIcon(type: PriceChangeType?) { + AnimatedContent(targetState = type, label = "Update the price change's arrow") { animatedType -> + animatedType ?: return@AnimatedContent + + Icon( + painter = painterResource( + id = when (animatedType) { + PriceChangeType.UP -> R.drawable.ic_arrow_up_8 + PriceChangeType.DOWN -> R.drawable.ic_arrow_down_8 + }, + ), + tint = when (animatedType) { + PriceChangeType.UP -> TangemTheme.colors.icon.accent + PriceChangeType.DOWN -> TangemTheme.colors.icon.warning + }, + contentDescription = null, + ) + } +} + +@Composable +private fun PriceChangeText(type: PriceChangeType?, text: String?) { + AnimatedContent(targetState = text, label = "Update the price change's text") { animatedText -> + animatedText ?: return@AnimatedContent + + Text( + text = animatedText, + color = when (type) { + PriceChangeType.UP -> TangemTheme.colors.text.accent + PriceChangeType.DOWN -> TangemTheme.colors.text.warning + null -> TangemTheme.colors.text.primary1 + }, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = TangemTheme.typography.body2, + ) + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenRowItem.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenRowItem.kt new file mode 100644 index 0000000000..139e4f9652 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenRowItem.kt @@ -0,0 +1,205 @@ +package com.tangem.managetokens.presentation.managetokens.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.Dp +import com.tangem.core.ui.components.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.managetokens.presentation.managetokens.state.QuotesState +import com.tangem.managetokens.presentation.managetokens.state.TokenItemState +import com.tangem.managetokens.presentation.managetokens.state.previewdata.TokenItemStatePreviewData + +private val TOKEN_ITEM_HEIGHT: Dp + @Composable + @ReadOnlyComposable + get() = TangemTheme.dimens.size68 + +@Composable +internal fun TokenRowItem(state: TokenItemState, modifier: Modifier = Modifier) { + when (state) { + is TokenItemState.Loading -> LoadingTokenItem(modifier) + is TokenItemState.Loaded -> LoadedTokenItem(state, modifier) + } +} + +@Composable +private fun LoadedTokenItem(state: TokenItemState.Loaded, modifier: Modifier = Modifier) { + BoxWithConstraints( + modifier = modifier + .fillMaxWidth() + .defaultMinSize(minHeight = TOKEN_ITEM_HEIGHT) + .background(TangemTheme.colors.background.primary), + contentAlignment = Alignment.CenterStart, + ) { + val width = maxWidth + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), + ) { + TokenIcon(state = state.tokenIcon) + SpacerW12() + + Column( + modifier = Modifier + .weight(weight = 1f), + ) { + TokenName(name = state.name, currencyId = state.currencyId) + TokenPriceData(price = state.rate, quotesState = state.quotes) + } + SpacerW24() + + if (width > TangemTheme.dimens.size350 && // hide chart for small screens + state.quotes is QuotesState.Content + ) { + Chart(quotes = state.quotes) + SpacerW24() + } + + TokenButton( + type = state.availableAction, + onClick = { state.onButtonClick(state.currencyId) }, + ) + } + } +} + +@Composable +private fun TokenName(name: String, currencyId: String) { + Row { + Text( + text = name, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle2, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .weight(weight = 1f, fill = false), + ) + SpacerW(width = TangemTheme.dimens.spacing6) + Text( + text = currencyId, + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.body2, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } +} + +@Composable +private fun TokenPriceData(price: String?, quotesState: QuotesState) { + if (price != null) { + Row { + Text( + text = price, + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.body2, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .weight(weight = 1f, fill = false), + ) + SpacerW(width = TangemTheme.dimens.spacing6) + TokenPriceChange(state = quotesState) + } + } +} + +@Composable +private fun Chart(quotes: QuotesState.Content) { + Box( + modifier = Modifier + .size(width = TangemTheme.dimens.size50, height = TangemTheme.dimens.size28), + ) { + PriceChangesChart(values = quotes.chartData) + } +} + +@Composable +private fun LoadingTokenItem(modifier: Modifier = Modifier) { + BaseSurface(modifier) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = TangemTheme.dimens.spacing16, + vertical = TangemTheme.dimens.spacing4, + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + CircleShimmer(modifier = Modifier.size(size = TangemTheme.dimens.size36)) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing10)) { + RectangleShimmer( + modifier = Modifier.size( + width = TangemTheme.dimens.size70, + height = TangemTheme.dimens.size12, + ), + ) + RectangleShimmer( + modifier = Modifier.size( + width = TangemTheme.dimens.size52, + height = TangemTheme.dimens.size12, + ), + ) + } + RectangleShimmer( + modifier = Modifier.size( + width = TangemTheme.dimens.size46, + height = TangemTheme.dimens.size12, + ), + ) + } + } + } +} + +@Composable +private fun BaseSurface(modifier: Modifier = Modifier, content: @Composable () -> Unit) { + Surface( + modifier = modifier.defaultMinSize(minHeight = TOKEN_ITEM_HEIGHT), + color = TangemTheme.colors.background.primary, + ) { + content() + } +} + +@Preview() +@Composable +private fun Preview_Tokens_LightTheme(@PreviewParameter(TokenConfigProvider::class) state: TokenItemState) { + TangemTheme(isDark = false) { + TokenRowItem(state) + } +} + +@Preview +@Composable +private fun Preview_Tokens_DarkTheme(@PreviewParameter(TokenConfigProvider::class) state: TokenItemState) { + TangemTheme(isDark = true) { + TokenRowItem(state) + } +} + +private class TokenConfigProvider : CollectionPreviewParameterProvider( + collection = listOf( + TokenItemStatePreviewData.tokenLoading, + TokenItemStatePreviewData.loadedPriceDown, + TokenItemStatePreviewData.loadedPriceUp, + ), +) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/router/DefaultManageTokensRouter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/router/DefaultManageTokensRouter.kt new file mode 100644 index 0000000000..593da2a10a --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/router/DefaultManageTokensRouter.kt @@ -0,0 +1,9 @@ +package com.tangem.managetokens.presentation.router + +import androidx.fragment.app.Fragment +import com.tangem.features.managetokens.navigation.ManageTokensRouter +import com.tangem.managetokens.ManageTokensFragment + +internal class DefaultManageTokensRouter : ManageTokensRouter { + override fun getEntryFragment(): Fragment = ManageTokensFragment() +} \ No newline at end of file diff --git a/features/send/api/src/main/kotlin/com/tangem/features/send/api/featuretoggles/SendFeatureToggles.kt b/features/send/api/src/main/kotlin/com/tangem/features/send/api/featuretoggles/SendFeatureToggles.kt new file mode 100644 index 0000000000..d80f08cf56 --- /dev/null +++ b/features/send/api/src/main/kotlin/com/tangem/features/send/api/featuretoggles/SendFeatureToggles.kt @@ -0,0 +1,10 @@ +package com.tangem.features.send.api.featuretoggles + +/** + * Send feature toggles + */ +interface SendFeatureToggles { + + /** Availability of redesigned send screen */ + val isRedesignedSendEnabled: Boolean +} \ No newline at end of file diff --git a/features/send/api/src/main/kotlin/com/tangem/features/send/navigation/SendRouter.kt b/features/send/api/src/main/kotlin/com/tangem/features/send/api/navigation/SendRouter.kt similarity index 64% rename from features/send/api/src/main/kotlin/com/tangem/features/send/navigation/SendRouter.kt rename to features/send/api/src/main/kotlin/com/tangem/features/send/api/navigation/SendRouter.kt index 0d0676b23b..5a44a6614d 100644 --- a/features/send/api/src/main/kotlin/com/tangem/features/send/navigation/SendRouter.kt +++ b/features/send/api/src/main/kotlin/com/tangem/features/send/api/navigation/SendRouter.kt @@ -1,4 +1,4 @@ -package com.tangem.features.send.navigation +package com.tangem.features.send.api.navigation import androidx.fragment.app.Fragment @@ -8,5 +8,6 @@ interface SendRouter { companion object { const val CRYPTO_CURRENCY_KEY = "send_crypto_currency" + const val USER_WALLET_ID_KEY = "send_user_wallet_id" } } \ No newline at end of file diff --git a/features/send/impl/.gitignore b/features/send/impl/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/send/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts new file mode 100644 index 0000000000..9c3a10e456 --- /dev/null +++ b/features/send/impl/build.gradle.kts @@ -0,0 +1,56 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.send.impl" +} + +dependencies { + /** AndroidX */ + implementation(deps.androidx.fragment.ktx) + implementation(deps.androidx.appCompat) + + /** Other dependencies */ + implementation(deps.kotlin.immutable.collections) + implementation(deps.material) + implementation(deps.arrow.core) + implementation(deps.tangem.card.core) + + /** Compose */ + implementation(deps.compose.accompanist.systemUiController) + implementation(deps.compose.material3) + implementation(deps.compose.foundation) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.navigation) + implementation(deps.compose.navigation.hilt) + + /** Common */ + implementation(projects.common) + + /** Core modules */ + implementation(projects.core.featuretoggles) + implementation(projects.core.ui) + implementation(projects.core.utils) + + /** Domain modules */ + implementation(projects.domain.models) + implementation(projects.domain.tokens) + implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets) + implementation(projects.domain.wallets.models) + implementation(projects.domain.appCurrency) + implementation(projects.domain.appCurrency.models) + + /** Feature modules */ + implementation(projects.features.send.api) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendFeatureTogglesModule.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendFeatureTogglesModule.kt new file mode 100644 index 0000000000..6b1465762c --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendFeatureTogglesModule.kt @@ -0,0 +1,24 @@ +package com.tangem.features.send.impl.di + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.features.send.api.featuretoggles.SendFeatureToggles +import com.tangem.features.send.impl.featuretoggles.DefaultSendFeatureToggles +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +/** + * DI module provides implementation of [SendFeatureToggles] + */ +@Module +@InstallIn(SingletonComponent::class) +internal object SendFeatureTogglesModule { + + @Provides + @Singleton + fun provideSendFeatureToggles(featureTogglesManager: FeatureTogglesManager): SendFeatureToggles { + return DefaultSendFeatureToggles(featureTogglesManager = featureTogglesManager) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendRouterModule.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendRouterModule.kt new file mode 100644 index 0000000000..8bf19609d8 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendRouterModule.kt @@ -0,0 +1,23 @@ +package com.tangem.features.send.impl.di + +import com.tangem.features.send.api.navigation.SendRouter +import com.tangem.features.send.impl.navigation.DefaultSendRouter +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.scopes.ActivityScoped + +/** + * DI module provides implementation of [SendRouter] + */ +@Module +@InstallIn(ActivityComponent::class) +internal object SendRouterModule { + + @Provides + @ActivityScoped + fun provideSendRouter(): SendRouter { + return DefaultSendRouter() + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/featuretoggles/DefaultSendFeatureToggles.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/featuretoggles/DefaultSendFeatureToggles.kt new file mode 100644 index 0000000000..71f11c480d --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/featuretoggles/DefaultSendFeatureToggles.kt @@ -0,0 +1,16 @@ +package com.tangem.features.send.impl.featuretoggles + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.features.send.api.featuretoggles.SendFeatureToggles + +/** + * Default implementation of Send feature toggles + * + * @property featureTogglesManager manager for getting information about the availability of feature toggles + */ +internal class DefaultSendFeatureToggles( + private val featureTogglesManager: FeatureTogglesManager, +) : SendFeatureToggles { + override val isRedesignedSendEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "REDESIGNED_SEND_SCREEN_ENABLED") +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt new file mode 100644 index 0000000000..4db1a5a674 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt @@ -0,0 +1,9 @@ +package com.tangem.features.send.impl.navigation + +import androidx.fragment.app.Fragment +import com.tangem.features.send.api.navigation.SendRouter +import com.tangem.features.send.impl.presentation.SendFragment + +internal class DefaultSendRouter : SendRouter { + override fun getEntryFragment(): Fragment = SendFragment.create() +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt new file mode 100644 index 0000000000..d33b82b860 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt @@ -0,0 +1,48 @@ +package com.tangem.features.send.impl.presentation + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.hilt.navigation.compose.hiltViewModel +import com.tangem.core.ui.components.SystemBarsEffect +import com.tangem.core.ui.screen.ComposeBottomSheetFragment +import com.tangem.core.ui.theme.AppThemeModeHolder +import com.tangem.features.send.impl.presentation.send.state.SendUiState +import com.tangem.features.send.impl.presentation.send.ui.SendScreen +import com.tangem.features.send.impl.presentation.send.viewmodel.SendViewModel +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +/** + * Send fragment + */ +@AndroidEntryPoint +internal class SendFragment : ComposeBottomSheetFragment() { + + @Inject + override lateinit var appThemeModeHolder: AppThemeModeHolder + + override val expandedHeightFraction: Float = 1f + + @Composable + override fun ScreenContent(modifier: Modifier) { + val viewModel = hiltViewModel() + LocalLifecycleOwner.current.lifecycle.addObserver(viewModel) + + SystemBarsEffect { setSystemBarsColor(color = Color.Transparent) } + BackHandler { dismiss() } + + when (val state = viewModel.uiState) { + is SendUiState.Content -> SendScreen(state) + SendUiState.Dismiss -> dismiss() + } + } + + companion object { + + /** Create send fragment instance */ + fun create(): SendFragment = SendFragment() + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendAmountSegmentedButtonsConfig.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendAmountSegmentedButtonsConfig.kt new file mode 100644 index 0000000000..ea3220b6e6 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendAmountSegmentedButtonsConfig.kt @@ -0,0 +1,21 @@ +package com.tangem.features.send.impl.presentation.send.state + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.extensions.TextReference + +/** + * Segmented buttons config + * + * @param title button title + * @param iconState currency icon state + * @param iconUrl currency icon url + * @param isFiat is fiat currency + */ +@Immutable +internal data class SendAmountSegmentedButtonsConfig( + val title: TextReference, + val iconState: TokenIconState? = null, + val iconUrl: String? = null, + val isFiat: Boolean, +) \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendAmountStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendAmountStateConverter.kt new file mode 100644 index 0000000000..c1a17d6011 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendAmountStateConverter.kt @@ -0,0 +1,63 @@ +package com.tangem.features.send.impl.presentation.send.state + +import arrow.core.Either +import com.tangem.common.Provider +import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.utils.BigDecimalFormatter.formatCryptoAmount +import com.tangem.core.ui.utils.BigDecimalFormatter.formatFiatAmount +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.send.impl.presentation.send.state.fields.SendAmountFieldConverter +import com.tangem.features.send.impl.presentation.send.viewmodel.SendClickIntents +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.persistentListOf + +internal class SendAmountStateConverter( + private val currentStateProvider: Provider, + private val appCurrencyProvider: Provider, + private val userWalletProvider: Provider, + private val clickIntents: SendClickIntents, + private val iconStateConverter: CryptoCurrencyToIconStateConverter, + private val sendAmountFieldConverter: SendAmountFieldConverter, +) : Converter, SendUiState> { + + override fun convert(value: Either): SendUiState { + val userWallet = userWalletProvider() ?: return currentStateProvider() + val appCurrency = appCurrencyProvider() + return value.fold( + ifLeft = { + // TODO add error handling + currentStateProvider() + }, + ifRight = { + val fiat = formatFiatAmount(it.value.fiatAmount, appCurrency.code, appCurrency.symbol) + val crypto = formatCryptoAmount(it.value.amount, it.currency.symbol, it.currency.decimals) + SendUiState.Content.AmountState( + cryptoCurrencyStatus = it, + walletName = userWallet.name, + walletBalance = "$crypto ($fiat)", + tokenIconState = iconStateConverter.convert(it), + appCurrency = appCurrency, + amountTextField = sendAmountFieldConverter.convert(Unit), + isFiatValue = false, + clickIntents = clickIntents, + segmentedButtonConfig = persistentListOf( + SendAmountSegmentedButtonsConfig( + title = stringReference(it.currency.symbol), + iconState = iconStateConverter.convert(it), + isFiat = false, + ), + SendAmountSegmentedButtonsConfig( + title = stringReference(appCurrency.code), + iconState = iconStateConverter.convert(it), + isFiat = true, + ), + ), + ) + }, + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendStateFactory.kt new file mode 100644 index 0000000000..bfca56ad1d --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendStateFactory.kt @@ -0,0 +1,58 @@ +package com.tangem.features.send.impl.presentation.send.state + +import arrow.core.Either +import com.tangem.common.Provider +import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.send.impl.presentation.send.state.fields.SendAmountFieldChangeConverter +import com.tangem.features.send.impl.presentation.send.state.fields.SendAmountFieldConverter +import com.tangem.features.send.impl.presentation.send.viewmodel.SendClickIntents + +internal class SendStateFactory( + private val clickIntents: SendClickIntents, + private val currentStateProvider: Provider, + private val appCurrencyProvider: Provider, + private val userWalletProvider: Provider, +) { + + private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) + + private val amountFieldConverter by lazy { SendAmountFieldConverter(clickIntents) } + + private val amountFieldChangeConverter by lazy { SendAmountFieldChangeConverter(currentStateProvider) } + + private val amountStateConverter by lazy { + SendAmountStateConverter( + currentStateProvider = currentStateProvider, + appCurrencyProvider = appCurrencyProvider, + clickIntents = clickIntents, + iconStateConverter = iconStateConverter, + userWalletProvider = userWalletProvider, + sendAmountFieldConverter = amountFieldConverter, + ) + } + + fun getInitialState(): SendUiState = SendUiState.Content.Initial(clickIntents = clickIntents) + + fun getAmountState(cryptoCurrencyStatus: Either): SendUiState { + return amountStateConverter.convert(cryptoCurrencyStatus) + } + + fun getOnReceiveState(): SendUiState = SendUiState.Content.Initial(clickIntents = clickIntents) + + fun getOnAmountValueChange(value: String) = amountFieldChangeConverter.convert(value) + + fun getOnCurrencyChangedState(isFiat: Boolean): SendUiState { + val state = currentStateProvider() + val amountState = state as? SendUiState.Content.AmountState ?: return state + + return if (amountState.isFiatValue == isFiat) { + state + } else { + return state.copy(isFiatValue = isFiat) + } + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendUiState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendUiState.kt new file mode 100644 index 0000000000..0c48ca14b0 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendUiState.kt @@ -0,0 +1,70 @@ +package com.tangem.features.send.impl.presentation.send.state + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.impl.presentation.send.state.fields.SendTextField +import com.tangem.features.send.impl.presentation.send.viewmodel.SendClickIntents +import kotlinx.collections.immutable.PersistentList + +/** + * Ui states of the send screen + */ +@Immutable +internal sealed class SendUiState { + + /** States with content */ + sealed class Content : SendUiState() { + + /** Is primary button enabled */ + abstract val isPrimaryButtonEnabled: Boolean + + /** Click intents */ + abstract val clickIntents: SendClickIntents + + /** Initial state */ + data class Initial( + override val isPrimaryButtonEnabled: Boolean = false, + override val clickIntents: SendClickIntents, + ) : Content() + + /** Amount state */ + data class AmountState( + override val isPrimaryButtonEnabled: Boolean = false, + override val clickIntents: SendClickIntents, + val walletName: String, + val walletBalance: String, + val tokenIconState: TokenIconState, + val cryptoCurrencyStatus: CryptoCurrencyStatus, + val appCurrency: AppCurrency, + val isFiatValue: Boolean, + val segmentedButtonConfig: PersistentList, + val amountTextField: SendTextField.Amount, + ) : Content() + + // todo [REDACTED_JIRA] + /** Recipient state */ + data class RecipientState( + override val isPrimaryButtonEnabled: Boolean = false, + override val clickIntents: SendClickIntents, + ) : Content() + + // todo [REDACTED_JIRA] + /** Fee and speed state */ + data class FeeState( + override val isPrimaryButtonEnabled: Boolean = false, + override val clickIntents: SendClickIntents, + ) : Content() + + // todo [REDACTED_JIRA] + /** Send state */ + data class SendState( + override val isPrimaryButtonEnabled: Boolean = true, + override val clickIntents: SendClickIntents, + ) : Content() + } + + /** Dismiss screen */ + object Dismiss : SendUiState() +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/fields/SendAmountFieldChangeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/fields/SendAmountFieldChangeConverter.kt new file mode 100644 index 0000000000..0775989341 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/fields/SendAmountFieldChangeConverter.kt @@ -0,0 +1,97 @@ +package com.tangem.features.send.impl.presentation.send.state.fields + +import com.tangem.common.Provider +import com.tangem.features.send.impl.presentation.send.state.SendUiState +import com.tangem.utils.converter.Converter +import java.text.DecimalFormatSymbols +import java.text.NumberFormat + +internal class SendAmountFieldChangeConverter( + private val currentStateProvider: Provider, +) : Converter { + override fun convert(value: String): SendUiState { + val state = currentStateProvider() + + if ( + state !is SendUiState.Content.AmountState || + value.checkDecimalSeparatorDuplicate() + ) { + return state + } + + if (value.isEmpty()) return state.emptyState() + + val fiatRate = state.cryptoCurrencyStatus.value.fiatRate + + val trimmedValue = value.trim() + + val cryptoValue = if (state.isFiatValue) { + if (value.isNotBlank()) { + trimmedValue.toBigDecimal().divide(fiatRate)?.stripTrailingZeros()?.toPlainString().orEmpty() + } else { + DEFAULT_VALUE + } + } else { + trimmedValue + } + + val fiatValue = if (!state.isFiatValue) { + if (value.isNotBlank()) { + trimmedValue.toBigDecimal().multiply(fiatRate)?.stripTrailingZeros()?.toPlainString().orEmpty() + } else { + DEFAULT_VALUE + } + } else { + trimmedValue + } + + val isExceedBalance = value.checkExceedBalance(state) + return state.copy( + amountTextField = state.amountTextField.copy( + value = cryptoValue, + fiatValue = fiatValue, + isError = isExceedBalance, + ), + isPrimaryButtonEnabled = !isExceedBalance, + ) + } + + private fun SendUiState.Content.AmountState.emptyState(): SendUiState { + return copy( + amountTextField = amountTextField.copy( + value = if (!isFiatValue) "" else DEFAULT_VALUE, + fiatValue = if (isFiatValue) "" else DEFAULT_VALUE, + isError = false, + ), + isPrimaryButtonEnabled = false, + ) + } + + private fun String.checkDecimalSeparatorDuplicate(): Boolean { + val regex = "[\\.\\,]".toRegex() + val decimalSeparatorCount = regex.findAll(this).count() + + return decimalSeparatorCount > 1 + } + + private fun String.checkExceedBalance(state: SendUiState.Content.AmountState): Boolean { + val currencyStatus = state.cryptoCurrencyStatus.value + return if (state.isFiatValue) { + toBigDecimal() > currencyStatus.fiatAmount + } else { + toBigDecimal() > currencyStatus.amount + } + } + + private fun String.trim(): String { + var trimmedValue = this + if (length > 1 && firstOrNull() == '0' && get(1).isDigit()) trimmedValue = drop(1) + + val separatorChar = DecimalFormatSymbols.getInstance().decimalSeparator.toString() + return trimmedValue.replace("[\\.\\,]".toRegex(), separatorChar) + } + + companion object { + private val DEFAULT_VALUE = NumberFormat.getInstance().format(0.00) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/fields/SendAmountFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/fields/SendAmountFieldConverter.kt new file mode 100644 index 0000000000..8f09a10a04 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/fields/SendAmountFieldConverter.kt @@ -0,0 +1,35 @@ +package com.tangem.features.send.impl.presentation.send.state.fields + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.send.viewmodel.SendClickIntents +import com.tangem.utils.converter.Converter +import java.text.NumberFormat + +internal class SendAmountFieldConverter( + private val clickIntents: SendClickIntents, +) : Converter { + + override fun convert(value: Unit): SendTextField.Amount { + return SendTextField.Amount( + value = "", + fiatValue = DEFAULT_VALUE, + onValueChange = clickIntents::onAmountValueChange, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Number, + ), + label = TextReference.Str(""), + placeholder = TextReference.Str(DEFAULT_VALUE), + isError = false, + error = TextReference.Res(R.string.send_insufficient_funds), + ) + } + + companion object { + private val DEFAULT_VALUE = NumberFormat.getInstance().format(0.00) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/fields/SendTextField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/fields/SendTextField.kt new file mode 100644 index 0000000000..e7677e747d --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/fields/SendTextField.kt @@ -0,0 +1,35 @@ +package com.tangem.features.send.impl.presentation.send.state.fields + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference + +@Immutable +internal sealed class SendTextField { + + /** Current value */ + abstract val value: String + + /** Lambda be invoked when value is been changed */ + abstract val onValueChange: (String) -> Unit + + /** Keyboard options */ + abstract val keyboardOptions: KeyboardOptions + + /** Label */ + abstract val label: TextReference + + /** Placeholder (hint) */ + abstract val placeholder: TextReference + + data class Amount( + override val value: String, + override val onValueChange: (String) -> Unit, + override val keyboardOptions: KeyboardOptions, + override val label: TextReference, + override val placeholder: TextReference, + val fiatValue: String, + val isError: Boolean, + val error: TextReference, + ) : SendTextField() +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendAmountContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendAmountContent.kt new file mode 100644 index 0000000000..8f6dbcabc9 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendAmountContent.kt @@ -0,0 +1,109 @@ +package com.tangem.features.send.impl.presentation.send.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.components.buttons.common.TangemButtonSize +import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons +import com.tangem.core.ui.components.currency.fiaticon.FiatIcon +import com.tangem.core.ui.components.currency.tokenicon.TokenIcon +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.send.state.SendAmountSegmentedButtonsConfig +import com.tangem.features.send.impl.presentation.send.state.SendUiState +import com.tangem.features.send.impl.presentation.send.ui.amount.AmountFieldContainer + +@Composable +internal fun SendAmountContent(amountState: SendUiState.Content.AmountState) { + Column( + modifier = Modifier + .background(TangemTheme.colors.background.tertiary), + ) { + Text( + text = stringResource(R.string.common_send), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + modifier = Modifier + .padding(vertical = TangemTheme.dimens.spacing16) + .align(CenterHorizontally), + ) + AmountFieldContainer(amountState = amountState) + Row( + modifier = Modifier + .fillMaxWidth() + .padding( + top = TangemTheme.dimens.spacing12, + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + ), + ) { + SegmentedButtons( + modifier = Modifier + .height(TangemTheme.dimens.size40) + .weight(1f), + config = amountState.segmentedButtonConfig, + onClick = { amountState.clickIntents.onCurrencyChangeClick(it.isFiat) }, + ) { + SendAmountCurrencyButton(it) + } + SecondaryButton( + text = stringResource(R.string.send_max_amount), + onClick = amountState.clickIntents::onMaxValueClick, + size = TangemButtonSize.Text, + shape = RoundedCornerShape(TangemTheme.dimens.radius26), + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing8) + .height(TangemTheme.dimens.size40), + ) + } + } +} + +@Composable +private fun SendAmountCurrencyButton(button: SendAmountSegmentedButtonsConfig) { + Row( + modifier = Modifier + .fillMaxSize() + .padding( + horizontal = TangemTheme.dimens.spacing10, + ), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + if (button.isFiat) { + FiatIcon( + url = button.iconUrl, + modifier = Modifier + .size(TangemTheme.dimens.size18), + ) + } else { + button.iconState?.let { + TokenIcon( + state = it, + shouldDisplayNetwork = false, + modifier = Modifier + .size(TangemTheme.dimens.size18), + ) + } + } + Text( + text = button.title.resolveReference(), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.button, + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing8, + ), + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendNavigationButtons.kt new file mode 100644 index 0000000000..cf453d487a --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendNavigationButtons.kt @@ -0,0 +1,94 @@ +package com.tangem.features.send.impl.presentation.send.ui + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import com.tangem.core.ui.R +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.PrimaryButtonIconEnd +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.impl.presentation.send.state.SendUiState + +@Composable +internal fun SendNavigationButtons(uiState: SendUiState.Content) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = TangemTheme.dimens.spacing12), + ) { + SendSecondaryNavigationButton(uiState) + SendPrimaryNavigationButton( + uiState = uiState, + modifier = Modifier + .weight(1f) + .padding(horizontal = TangemTheme.dimens.spacing16), + ) + } +} + +@Composable +private fun SendSecondaryNavigationButton(uiState: SendUiState.Content) { + AnimatedVisibility( + visible = uiState is SendUiState.Content.RecipientState || uiState is SendUiState.Content.FeeState, + ) { + Icon( + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing16) + .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) + .background(TangemTheme.colors.button.secondary) + .clickable { + // todo add prev click + } + .padding(TangemTheme.dimens.spacing12), + painter = painterResource(R.drawable.ic_back_24), + contentDescription = null, + ) + } +} + +@Composable +private fun SendPrimaryNavigationButton(uiState: SendUiState.Content, modifier: Modifier = Modifier) { + val buttonTextId = when (uiState) { + is SendUiState.Content.AmountState, + is SendUiState.Content.RecipientState, + is SendUiState.Content.FeeState, + -> R.string.common_next + is SendUiState.Content.SendState -> R.string.common_send + else -> R.string.common_close + } + AnimatedContent( + targetState = buttonTextId, + label = "Update send screen state", + modifier = modifier, + ) { textId -> + if (uiState is SendUiState.Content.SendState) { + PrimaryButtonIconEnd( + text = stringResource(textId), + iconResId = R.drawable.ic_tangem_24, + enabled = uiState.isPrimaryButtonEnabled, + onClick = { + // todo add next click + }, + ) + } else { + PrimaryButton( + text = stringResource(textId), + enabled = uiState.isPrimaryButtonEnabled, + onClick = { + // todo add next click + }, + ) + } + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendScreen.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendScreen.kt new file mode 100644 index 0000000000..1e52cf291a --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendScreen.kt @@ -0,0 +1,53 @@ +package com.tangem.features.send.impl.presentation.send.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.scrollable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetDraggableHeader +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.impl.presentation.send.state.SendUiState + +@Composable +internal fun SendScreen(uiState: SendUiState.Content) { + Column( + modifier = Modifier + .imePadding() + .background( + color = TangemTheme.colors.background.tertiary, + shape = RoundedCornerShape( + topStart = TangemTheme.dimens.radius24, + topEnd = TangemTheme.dimens.radius24, + ), + ), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + TangemBottomSheetDraggableHeader( + color = TangemTheme.colors.background.tertiary, + ) + Box( + modifier = Modifier + .weight(1f) + .scrollable(state = rememberScrollState(), orientation = Orientation.Vertical), + ) { + SendScreenContent(uiState) + } + SendNavigationButtons(uiState) + } +} + +@Composable +private fun SendScreenContent(uiState: SendUiState.Content) { + when (uiState) { + is SendUiState.Content.AmountState -> SendAmountContent(uiState) + else -> { /* [REDACTED_TODO_COMMENT]*/ + } + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/amount/AmountField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/amount/AmountField.kt new file mode 100644 index 0000000000..4eda379a95 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/amount/AmountField.kt @@ -0,0 +1,168 @@ +package com.tangem.features.send.impl.presentation.send.ui.amount + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.BottomCenter +import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.TransformedText +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.impl.presentation.send.state.fields.SendTextField + +@Composable +internal fun ColumnScope.AmountField( + sendField: SendTextField.Amount, + cryptoSymbol: String, + fiatSymbol: String, + isFiat: Boolean, +) { + val value = if (isFiat) sendField.fiatValue else sendField.value + val secondaryValue = if (!isFiat) sendField.fiatValue else sendField.value + val symbol = if (isFiat) fiatSymbol else cryptoSymbol + val secondarySymbol = if (!isFiat) fiatSymbol else cryptoSymbol + + AmountFieldInner( + value = value, + placeholder = sendField.placeholder, + symbol = symbol, + onValueChange = sendField.onValueChange, + keyboardOptions = sendField.keyboardOptions, + modifier = Modifier + .align(CenterHorizontally) + .padding( + top = TangemTheme.dimens.spacing24, + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + ), + ) + + Box( + modifier = Modifier + .align(CenterHorizontally) + .padding( + top = TangemTheme.dimens.spacing8, + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + ), + ) { + Text( + text = "$secondaryValue $secondarySymbol", + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center, + modifier = Modifier + .align(BottomCenter) + .padding(bottom = TangemTheme.dimens.spacing32), + ) + AmountFieldError( + isError = sendField.isError, + error = sendField.error, + modifier = Modifier + .align(BottomCenter) + .padding(bottom = TangemTheme.dimens.spacing12), + ) + } +} + +@Composable +private fun AmountFieldInner( + value: String, + placeholder: TextReference, + symbol: String, + onValueChange: (String) -> Unit, + keyboardOptions: KeyboardOptions, + modifier: Modifier = Modifier, +) { + val focusRequester = remember { FocusRequester() } + BasicTextField( + value = value, + onValueChange = onValueChange, + modifier = modifier + .focusRequester(focusRequester) + .background(TangemTheme.colors.background.action), + textStyle = TangemTheme.typography.h2.copy( + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ), + keyboardOptions = keyboardOptions, + singleLine = true, + visualTransformation = AmountVisualTransformation(symbol), + decorationBox = { innerTextField -> + Box { + if (value.isBlank()) { + Text( + text = "${placeholder.resolveReference()} $symbol", + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.disabled, + textAlign = TextAlign.Center, + modifier = Modifier + .align(Alignment.TopCenter), + ) + } + innerTextField() + } + }, + ) +} + +@Composable +private fun AmountFieldError(isError: Boolean, error: TextReference, modifier: Modifier = Modifier) { + AnimatedVisibility( + visible = isError, + enter = fadeIn(), + exit = fadeOut(), + modifier = modifier, + ) { + Text( + text = error.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.warning, + textAlign = TextAlign.Center, + ) + } +} + +private class AmountVisualTransformation( + private val symbol: String, +) : VisualTransformation { + override fun filter(text: AnnotatedString): TransformedText { + return TransformedText( + buildAnnotatedString { + append(text) + if (text.isNotBlank()) { + append(" ") + append(symbol) + } + }, + object : OffsetMapping { + override fun originalToTransformed(offset: Int): Int { + return text.length + } + + override fun transformedToOriginal(offset: Int): Int { + return text.length + } + }, + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/amount/AmountFieldContainer.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/amount/AmountFieldContainer.kt new file mode 100644 index 0000000000..b7810d5acb --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/amount/AmountFieldContainer.kt @@ -0,0 +1,61 @@ +package com.tangem.features.send.impl.presentation.send.ui.amount + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.style.TextAlign +import com.tangem.core.ui.components.currency.tokenicon.TokenIcon +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.impl.presentation.send.state.SendUiState + +@Composable +internal fun AmountFieldContainer(amountState: SendUiState.Content.AmountState, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .fillMaxWidth() + .padding( + top = TangemTheme.dimens.spacing4, + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + ) + .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) + .background(TangemTheme.colors.background.action), + ) { + Text( + text = amountState.walletName, + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing14) + .align(Alignment.CenterHorizontally), + ) + Text( + text = amountState.walletBalance, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing2) + .align(Alignment.CenterHorizontally), + ) + TokenIcon( + state = amountState.tokenIconState, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing32) + .align(Alignment.CenterHorizontally), + ) + AmountField( + sendField = amountState.amountTextField, + cryptoSymbol = amountState.cryptoCurrencyStatus.currency.symbol, + fiatSymbol = amountState.appCurrency.symbol, + isFiat = amountState.isFiatValue, + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/viewmodel/SendClickIntents.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/viewmodel/SendClickIntents.kt new file mode 100644 index 0000000000..3e6885543a --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/viewmodel/SendClickIntents.kt @@ -0,0 +1,14 @@ +package com.tangem.features.send.impl.presentation.send.viewmodel + +interface SendClickIntents { + + fun onNextClick() + + fun onPrevClick() + + fun onAmountValueChange(value: String) + + fun onCurrencyChangeClick(isFiat: Boolean) + + fun onMaxValueClick() +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/viewmodel/SendViewModel.kt new file mode 100644 index 0000000000..eac86c94a9 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/viewmodel/SendViewModel.kt @@ -0,0 +1,154 @@ +package com.tangem.features.send.impl.presentation.send.viewmodel + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.* +import arrow.core.getOrElse +import com.tangem.common.Provider +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.features.send.api.navigation.SendRouter +import com.tangem.features.send.impl.presentation.send.state.SendStateFactory +import com.tangem.features.send.impl.presentation.send.state.SendUiState +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +internal class SendViewModel @Inject constructor( + private val dispatchers: CoroutineDispatcherProvider, + private val getUserWalletUseCase: GetUserWalletUseCase, + private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + savedStateHandle: SavedStateHandle, +) : ViewModel(), DefaultLifecycleObserver, SendClickIntents { + + private val userWalletId: UserWalletId = savedStateHandle.get(SendRouter.USER_WALLET_ID_KEY) + ?.let { stringValue -> UserWalletId(stringValue) } + ?: error("This screen can't open without `UserWalletId`") + + private val cryptoCurrency: CryptoCurrency = savedStateHandle[SendRouter.CRYPTO_CURRENCY_KEY] + ?: error("This screen can't open without `CryptoCurrency`") + + private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() + + private val stateFactory = SendStateFactory( + clickIntents = this, + currentStateProvider = Provider { uiState }, + userWalletProvider = Provider { userWallet }, + appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), + ) + + var uiState: SendUiState by mutableStateOf(stateFactory.getInitialState()) + private set + + private var userWallet: UserWallet? = null + + private var balanceJobHolder = JobHolder() + + override fun onCreate(owner: LifecycleOwner) { + subscribeOnCurrencyStatusUpdates(owner) + } + + private fun subscribeOnCurrencyStatusUpdates(owner: LifecycleOwner) { + viewModelScope.launch(dispatchers.io) { + getUserWalletUseCase(userWalletId).fold( + ifRight = { wallet -> + userWallet = wallet + getCurrencyStatusUpdates(owner, wallet) + }, + ifLeft = { + // TODO add error handling + return@launch + }, + ) + } + } + + private fun getCurrencyStatusUpdates(owner: LifecycleOwner, wallet: UserWallet) { + val isSingleWallet = wallet.scanResponse.walletData?.token != null && !wallet.isMultiCurrency + getCurrencyStatusUpdatesUseCase( + userWalletId = userWalletId, + currencyId = cryptoCurrency.id, + isSingleWalletWithTokens = isSingleWallet, + ) + .flowWithLifecycle(owner.lifecycle) + .conflate() + .distinctUntilChanged() + .onEach { either -> + uiState = stateFactory.getAmountState( + cryptoCurrencyStatus = either, + ) + } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + .saveIn(balanceJobHolder) + } + + private fun createSelectedAppCurrencyFlow(): StateFlow { + return getSelectedAppCurrencyUseCase() + .map { maybeAppCurrency -> + maybeAppCurrency.getOrElse { AppCurrency.Default } + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = AppCurrency.Default, + ) + } + + // region screen state navigation + override fun onNextClick() { + when (uiState) { + is SendUiState.Content.AmountState -> onRecipientStateClick() + is SendUiState.Content.RecipientState -> onFeeStateClick() + else -> { + // todo implement + } + } + } + + override fun onPrevClick() { + // todo implement + } + + private fun onRecipientStateClick() { + stateFactory.getOnReceiveState() + } + + private fun onFeeStateClick() { + // todo implement + } + // endregion + + // region amount state clicks + override fun onCurrencyChangeClick(isFiat: Boolean) { + uiState = stateFactory.getOnCurrencyChangedState(isFiat) + } + + override fun onAmountValueChange(value: String) { + uiState = stateFactory.getOnAmountValueChange(value) + } + + override fun onMaxValueClick() { + val amountState = uiState as? SendUiState.Content.AmountState ?: return + + val amount = if (amountState.isFiatValue) { + amountState.cryptoCurrencyStatus.value.fiatAmount + } else { + amountState.cryptoCurrencyStatus.value.amount + } + onAmountValueChange(amount?.toPlainString() ?: "0.00") + } + // endregion +} \ No newline at end of file diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index 63f2d6414e..1068c49d28 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -18,6 +18,7 @@ dependencies { /** Domain modules **/ implementation(projects.domain.balanceHiding) + implementation(projects.domain.balanceHiding.models) /** AndroidX */ implementation(deps.androidx.activity.compose) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index b1be139f19..c5e7f74bc7 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -7,8 +7,7 @@ import androidx.lifecycle.* import com.tangem.common.Provider import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.utils.InputNumberFormatter -import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase -import com.tangem.domain.balancehiding.ListenToFlipsUseCase +import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.tokens.model.Network import com.tangem.feature.swap.analytics.SwapEvents import com.tangem.feature.swap.domain.BlockchainInteractor @@ -29,7 +28,6 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.Debouncer import com.tangem.utils.coroutines.runCatching import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -49,8 +47,7 @@ internal class SwapViewModel @Inject constructor( private val blockchainInteractor: BlockchainInteractor, private val dispatchers: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, - private val isBalanceHiddenUseCase: IsBalanceHiddenUseCase, - private val listenToFlipsUseCase: ListenToFlipsUseCase, + private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver { @@ -96,21 +93,15 @@ internal class SwapViewModel @Inject constructor( } override fun onCreate(owner: LifecycleOwner) { - isBalanceHiddenUseCase() + getBalanceHidingSettingsUseCase() .flowWithLifecycle(owner.lifecycle) - .onEach { hidden -> - isBalanceHidden = hidden + .onEach { + isBalanceHidden = it.isBalanceHidden withContext(dispatchers.main) { uiState = stateBuilder.updateBalanceHiddenState(uiState, isBalanceHidden) } } .launchIn(viewModelScope) - - viewModelScope.launch { - listenToFlipsUseCase() - .flowWithLifecycle(owner.lifecycle) - .collect() - } } override fun onCleared() { diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index c8d97e576c..bc5b2cda79 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -63,6 +63,7 @@ dependencies { implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) implementation(projects.domain.balanceHiding) + implementation(projects.domain.balanceHiding.models) /** Feature Apis */ implementation(projects.features.tokendetails.api) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index cece9f729a..b1496b2e0c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -15,8 +15,7 @@ import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase -import com.tangem.domain.balancehiding.ListenToFlipsUseCase +import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.redux.ReduxStateHolder @@ -65,8 +64,7 @@ internal class TokenDetailsViewModel @Inject constructor( private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, private val removeCurrencyUseCase: RemoveCurrencyUseCase, private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase, - private val isBalanceHiddenUseCase: IsBalanceHiddenUseCase, - private val listenToFlipsUseCase: ListenToFlipsUseCase, + private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val getCurrencyWarningsUseCase: GetCurrencyWarningsUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val walletManagersFacade: WalletManagersFacade, @@ -117,20 +115,14 @@ internal class TokenDetailsViewModel @Inject constructor( } private fun handleBalanceHiding(owner: LifecycleOwner) { - isBalanceHiddenUseCase() + getBalanceHidingSettingsUseCase() .flowWithLifecycle(owner.lifecycle) - .onEach { hidden -> + .onEach { uiState = stateFactory.getStateWithUpdatedHidden( - isBalanceHidden = hidden, + isBalanceHidden = it.isBalanceHidden, ) } .launchIn(viewModelScope) - - viewModelScope.launch { - listenToFlipsUseCase() - .flowWithLifecycle(owner.lifecycle) - .collect() - } } private suspend fun updateButtons(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus) { @@ -168,8 +160,6 @@ internal class TokenDetailsViewModel @Inject constructor( getCurrencyStatusUpdatesUseCase( userWalletId = userWalletId, currencyId = cryptoCurrency.id, - contractAddress = (cryptoCurrency as? CryptoCurrency.Token)?.contractAddress, - derivationPath = cryptoCurrency.network.derivationPath, isSingleWalletWithTokens = wallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(), ) .distinctUntilChanged() @@ -464,8 +454,6 @@ internal class TokenDetailsViewModel @Inject constructor( fetchCurrencyStatusUseCase( userWalletId = userWalletId, id = cryptoCurrency.id, - contractAddress = (cryptoCurrency as? CryptoCurrency.Token)?.contractAddress, - derivationPath = cryptoCurrency.network.derivationPath, refresh = true, ) }, diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index d95c4421f7..f96ee8b723 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -65,6 +65,7 @@ dependencies { implementation(projects.domain.appCurrency) implementation(projects.domain.appCurrency.models) implementation(projects.domain.balanceHiding) + implementation(projects.domain.balanceHiding.models) //TODO: Create api/impl modules for onboarding [REDACTED_JIRA] implementation(projects.features.onboarding) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index 30b447e54c..d0829385fe 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -5,6 +5,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.marketprice.PriceChangeState import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.components.currency.tokenicon.TokenIconState import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.event.consumedEvent @@ -88,7 +89,7 @@ internal object WalletPreviewData { } val coinIconState - get() = TokenItemState.IconState.CoinIcon( + get() = TokenIconState.CoinIcon( url = null, fallbackResId = R.drawable.img_polygon_22, isGrayscale = false, @@ -96,7 +97,7 @@ internal object WalletPreviewData { ) private val tokenIconState - get() = TokenItemState.IconState.TokenIcon( + get() = TokenIconState.TokenIcon( url = null, networkBadgeIconResId = R.drawable.img_polygon_22, fallbackTint = TangemColorPalette.Black, @@ -106,7 +107,7 @@ internal object WalletPreviewData { ) private val customTokenIconState - get() = TokenItemState.IconState.CustomTokenIcon( + get() = TokenIconState.CustomTokenIcon( tint = TangemColorPalette.Black, background = TangemColorPalette.Meadow, networkBadgeIconResId = R.drawable.img_polygon_22, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt index 9e0ea7e0cd..fbc0f7af7b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt @@ -14,11 +14,11 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.Constraints import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.components.currency.tokenicon.TokenIcon import com.tangem.core.ui.extensions.rememberHapticFeedback import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.common.component.token.* -import com.tangem.feature.wallet.presentation.common.component.token.icon.TokenIcon import com.tangem.feature.wallet.presentation.common.state.TokenItemState import org.burnoutcrew.reorderable.ReorderableLazyListState import kotlin.math.max diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt index b9f7b9d127..52bdd7abed 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt @@ -1,9 +1,8 @@ package com.tangem.feature.wallet.presentation.common.state -import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable -import androidx.compose.ui.graphics.Color import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.components.currency.tokenicon.TokenIconState /** Token item state */ @Immutable @@ -11,7 +10,7 @@ internal sealed class TokenItemState { abstract val id: String - abstract val iconState: IconState + abstract val iconState: TokenIconState abstract val titleState: TitleState @@ -24,7 +23,7 @@ internal sealed class TokenItemState { /** Loading token state */ data class Loading( override val id: String, - override val iconState: IconState, + override val iconState: TokenIconState, override val titleState: TitleState.Content, ) : TokenItemState() { override val fiatAmountState: FiatAmountState = FiatAmountState.Loading @@ -34,7 +33,7 @@ internal sealed class TokenItemState { /** Locked token state */ data class Locked(override val id: String) : TokenItemState() { - override val iconState: IconState = IconState.Locked + override val iconState: TokenIconState = TokenIconState.Locked override val titleState: TitleState = TitleState.Locked override val fiatAmountState: FiatAmountState = FiatAmountState.Locked override val cryptoAmountState: CryptoAmountState = CryptoAmountState.Locked @@ -52,7 +51,7 @@ internal sealed class TokenItemState { */ data class Content( override val id: String, - override val iconState: IconState, + override val iconState: TokenIconState, override val titleState: TitleState, override val fiatAmountState: FiatAmountState, override val cryptoAmountState: CryptoAmountState.Content, @@ -70,7 +69,7 @@ internal sealed class TokenItemState { */ data class Draggable( override val id: String, - override val iconState: IconState, + override val iconState: TokenIconState, override val titleState: TitleState, override val cryptoAmountState: CryptoAmountState, ) : TokenItemState() { @@ -89,7 +88,7 @@ internal sealed class TokenItemState { */ data class Unreachable( override val id: String, - override val iconState: IconState, + override val iconState: TokenIconState, override val titleState: TitleState, val onItemClick: () -> Unit, val onItemLongClick: () -> Unit, @@ -109,7 +108,7 @@ internal sealed class TokenItemState { */ data class NoAddress( override val id: String, - override val iconState: IconState, + override val iconState: TokenIconState, override val titleState: TitleState, val onItemLongClick: () -> Unit, ) : TokenItemState() { @@ -118,84 +117,6 @@ internal sealed class TokenItemState { override val priceChangeState: PriceChangeState? = null } - /** - * Represents the various states an icon can be in. - */ - @Immutable - sealed class IconState { - - abstract val isGrayscale: Boolean - abstract val showCustomBadge: Boolean - abstract val networkBadgeIconResId: Int? - - /** - * Represents a coin icon. - * - * @property url The URL where the coin icon can be fetched from. May be `null` if not found. - * @property fallbackResId The drawable resource ID to be used as a fallback if the URL is not available. - * @property isGrayscale Specifies whether to show the icon in grayscale. - * @property showCustomBadge Specifies whether to show the custom token badge. - */ - data class CoinIcon( - val url: String?, - @DrawableRes val fallbackResId: Int, - override val isGrayscale: Boolean, - override val showCustomBadge: Boolean, - ) : IconState() { - - override val networkBadgeIconResId: Int? = null - } - - /** - * Represents a token icon. - * - * @property url The URL where the token icon can be fetched from. May be `null` if not found. - * @property networkBadgeIconResId The drawable resource ID for the network badge. - * @property isGrayscale Specifies whether to show the icon in grayscale. - * @property showCustomBadge Specifies whether to show the custom token badge. - * @property fallbackTint The color to be used for tinting the fallback icon. - * @property fallbackBackground The background color to be used for the fallback icon. - */ - data class TokenIcon( - val url: String?, - @DrawableRes override val networkBadgeIconResId: Int, - override val isGrayscale: Boolean, - override val showCustomBadge: Boolean, - val fallbackTint: Color, - val fallbackBackground: Color, - ) : IconState() - - /** - * Represents a custom token icon. - * - * @property tint The color to be used for tinting the icon. - * @property background The background color to be used for the icon. - * @property networkBadgeIconResId The drawable resource ID for the network badge. - * @property isGrayscale Specifies whether to show the icon in grayscale. - */ - data class CustomTokenIcon( - val tint: Color, - val background: Color, - @DrawableRes override val networkBadgeIconResId: Int, - override val isGrayscale: Boolean, - ) : IconState() { - - override val showCustomBadge: Boolean = true - } - - object Loading : IconState() { - override val isGrayscale: Boolean = false - override val showCustomBadge: Boolean = false - override val networkBadgeIconResId: Int? = null - } - - object Locked : IconState() { - override val isGrayscale: Boolean = false - override val showCustomBadge: Boolean = false - override val networkBadgeIconResId: Int? = null - } - } - @Immutable sealed class TitleState { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt index a522081e21..89a9cce078 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt @@ -7,8 +7,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase -import com.tangem.domain.balancehiding.ListenToFlipsUseCase +import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.ToggleTokenListGroupingUseCase @@ -38,8 +37,7 @@ internal class OrganizeTokensViewModel @Inject constructor( private val toggleTokenListSortingUseCase: ToggleTokenListSortingUseCase, private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val isBalanceHiddenUseCase: IsBalanceHiddenUseCase, - private val listenToFlipsUseCase: ListenToFlipsUseCase, + private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val analyticsEventsHandler: AnalyticsEventHandler, private val dispatchers: CoroutineDispatcherProvider, savedStateHandle: SavedStateHandle, @@ -74,20 +72,14 @@ internal class OrganizeTokensViewModel @Inject constructor( override fun onCreate(owner: LifecycleOwner) { analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ScreenOpened) - isBalanceHiddenUseCase() + getBalanceHidingSettingsUseCase() .flowWithLifecycle(owner.lifecycle) - .onEach { hidden -> - isBalanceHidden = hidden + .onEach { + isBalanceHidden = it.isBalanceHidden stateHolder.updateHiddenState(isBalanceHidden) } .launchIn(viewModelScope) - viewModelScope.launch { - listenToFlipsUseCase() - .flowWithLifecycle(owner.lifecycle) - .collect() - } - bootstrapTokenList() bootstrapDragAndDropUpdates() } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt index aa518deba2..60445f1928 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt @@ -5,7 +5,7 @@ import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.wallet.presentation.common.state.TokenItemState -import com.tangem.feature.wallet.presentation.common.utils.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getTokenItemId diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 6a9365ff4d..646cec18f5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -134,7 +134,7 @@ internal class DefaultWalletRouter(private val reduxNavController: ReduxNavContr override fun isWalletLastScreen(): Boolean = reduxNavController.getBackStack().lastOrNull() == AppScreen.Wallet override fun openManageTokensScreen() { - reduxNavController.navigate(action = NavigationAction.NavigateTo(AppScreen.AddTokens)) + reduxNavController.navigate(action = NavigationAction.NavigateTo(AppScreen.ManageTokens)) } override fun openScanFailedDialog() { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt index 59b3b14b1e..df51275678 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt @@ -6,7 +6,7 @@ import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.wallet.presentation.common.state.TokenItemState -import com.tangem.feature.wallet.presentation.common.utils.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import com.tangem.utils.converter.Converter import java.math.BigDecimal diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 05d1796f2b..d943c0f76a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -27,8 +27,7 @@ import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.analytics.ChangeCardAnalyticsContextUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase -import com.tangem.domain.balancehiding.ListenToFlipsUseCase +import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.card.DerivePublicKeysUseCase import com.tangem.domain.card.SetCardWasScannedUseCase import com.tangem.domain.common.CardTypesResolver @@ -115,8 +114,7 @@ internal class WalletViewModel @Inject constructor( private val shouldShowSaveWalletScreenUseCase: ShouldShowSaveWalletScreenUseCase, private val canUseBiometryUseCase: CanUseBiometryUseCase, private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, - private val isBalanceHiddenUseCase: IsBalanceHiddenUseCase, - private val listenToFlipsUseCase: ListenToFlipsUseCase, + private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val removeCurrencyUseCase: RemoveCurrencyUseCase, private val isCryptoCurrencyCoinCouldHide: IsCryptoCurrencyCoinCouldHideUseCase, private val walletManagersFacade: WalletManagersFacade, @@ -212,20 +210,14 @@ internal class WalletViewModel @Inject constructor( } } - isBalanceHiddenUseCase() + getBalanceHidingSettingsUseCase() .flowWithLifecycle(owner.lifecycle) - .onEach { hidden -> - isBalanceHidden = hidden - WalletStateCache.updateAll { copySealed(isBalanceHidden = hidden) } - uiState = stateFactory.getHiddenBalanceState(isBalanceHidden = hidden) + .onEach { + isBalanceHidden = it.isBalanceHidden + WalletStateCache.updateAll { copySealed(isBalanceHidden = it.isBalanceHidden) } + uiState = stateFactory.getHiddenBalanceState(isBalanceHidden = it.isBalanceHidden) } .launchIn(viewModelScope) - - viewModelScope.launch { - listenToFlipsUseCase() - .flowWithLifecycle(owner.lifecycle) - .collect() - } } private fun updateWallets(sourceList: List) { diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index a4408cd2b2..0303bdfd94 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -77,13 +77,14 @@ walletConnectCore = "1.18.0" walletConnectWeb3 = "1.11.0" prettyLogger = "2.2.0" okHttp-prettyLogging = "3.1.0" +chucker = "4.0.0" spr-client = "3.6.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.0-379" +tangemBlockchainSdk = "develop-380" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.0-309" +tangemCardSdk = "develop-310" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds # endregion Tangem @@ -228,4 +229,6 @@ walletConnectCore = { module = "com.walletconnect:android-core", version.ref = " walletConnectWeb3 = { module = "com.walletconnect:web3wallet", version.ref = "walletConnectWeb3" } prettyLogger = { module = "com.orhanobut:logger", version.ref = "prettyLogger" } sprClient = { module = "com.spr:messengerclient", version.ref = "spr-client" } +chucker = { module = "com.github.chuckerteam.chucker:library", version.ref = "chucker" } +chuckerStub = { module = "com.github.chuckerteam.chucker:library-no-op", version.ref = "chucker" } # endregion Other diff --git a/settings.gradle.kts b/settings.gradle.kts index e2478e48b5..008c0c57ff 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -93,6 +93,10 @@ include(":features:learn2earn:api") include(":features:learn2earn:impl") include(":features:send:api") +include(":features:send:impl") + +include(":features:manage-tokens:api") +include(":features:manage-tokens:impl") // endregion Feature modules // region Domain modules