diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e72316ffd3..9a66725eb7 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) @@ -102,6 +105,7 @@ dependencies { implementation(deps.androidx.activity.compose) implementation(deps.androidx.browser) implementation(deps.androidx.paging.runtime) + implementation(deps.androidx.swipeRefreshLayout) implementation(deps.lifecycle.runtime.ktx) implementation(deps.lifecycle.common.java8) implementation(deps.lifecycle.viewModel.ktx) @@ -150,7 +154,6 @@ dependencies { implementation(deps.timber) implementation(deps.reKotlin) implementation(deps.zxing.qrCore) - implementation(deps.zxing.qrBarcodeScanner) implementation(deps.otaliastudiosCameraView) implementation(deps.coil) implementation(deps.appsflyer) @@ -183,4 +186,25 @@ 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) + + /** Camera */ + implementation(deps.camera.camera2) + implementation(deps.camera.lifecycle) + implementation(deps.camera.view) + + implementation(deps.listenableFuture) + implementation(deps.mlKit.barcodeScanning) + + /** Excluded dependencies */ + implementation("com.google.guava:guava:30.0-android") { + // excludes version 9999.0-empty-to-avoid-conflict-with-guava + exclude(group="com.google.guava", module = "listenablefuture") + } + } \ 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/common/qrCodeScan/MLKitBarcodeAnalyzer.kt b/app/src/main/java/com/tangem/tap/common/qrCodeScan/MLKitBarcodeAnalyzer.kt new file mode 100644 index 0000000000..d85d5f44c2 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/qrCodeScan/MLKitBarcodeAnalyzer.kt @@ -0,0 +1,39 @@ +package com.tangem.tap.common.qrCodeScan + +import androidx.camera.core.ExperimentalGetImage +import androidx.camera.core.ImageAnalysis +import androidx.camera.core.ImageProxy +import com.google.mlkit.vision.barcode.BarcodeScanning +import com.google.mlkit.vision.common.InputImage + +class MLKitBarcodeAnalyzer(private val onScanned: (String) -> Unit) : ImageAnalysis.Analyzer { + + private var isScanning: Boolean = false + + @ExperimentalGetImage + override fun analyze(imageProxy: ImageProxy) { + val mediaImage = imageProxy.image + if (mediaImage != null && !isScanning) { + val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) + val scanner = BarcodeScanning.getClient() + + isScanning = true + scanner.process(image) + .addOnSuccessListener { codes -> + codes.firstOrNull().let { barcode -> + val rawValue = barcode?.rawValue + rawValue?.let { + onScanned.invoke(it) + } + } + + isScanning = false + imageProxy.close() + } + .addOnFailureListener { + isScanning = false + imageProxy.close() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/qrCodeScan/ScanQrCodeActivity.kt b/app/src/main/java/com/tangem/tap/common/qrCodeScan/ScanQrCodeActivity.kt index be7a6e4330..f49613149a 100644 --- a/app/src/main/java/com/tangem/tap/common/qrCodeScan/ScanQrCodeActivity.kt +++ b/app/src/main/java/com/tangem/tap/common/qrCodeScan/ScanQrCodeActivity.kt @@ -3,48 +3,63 @@ package com.tangem.tap.common.qrCodeScan import android.Manifest import android.content.Intent import android.content.pm.PackageManager -import android.os.Build import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import androidx.camera.lifecycle.ProcessCameraProvider import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat -import com.google.zxing.BarcodeFormat -import com.google.zxing.Result +import by.kirich1409.viewbindingdelegate.viewBinding +import com.google.common.util.concurrent.ListenableFuture import com.otaliastudios.cameraview.CameraView.PERMISSION_REQUEST_CODE -import me.dm7.barcodescanner.zxing.ZXingScannerView +import com.tangem.tap.features.details.ui.walletconnect.dialogs.PreviewBinder +import com.tangem.wallet.R +import com.tangem.wallet.databinding.LayoutQrScanningBinding +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors /** [REDACTED_AUTHOR] */ -class ScanQrCodeActivity : AppCompatActivity(), ZXingScannerView.ResultHandler { +class ScanQrCodeActivity : AppCompatActivity() { - private lateinit var mScannerView: ZXingScannerView + private val binding: LayoutQrScanningBinding by viewBinding(LayoutQrScanningBinding::bind) + + private var cameraProviderFuture: ListenableFuture? = null + private var cameraExecutor: ExecutorService? = null + + private val binder = PreviewBinder() override fun onCreate(state: Bundle?) { super.onCreate(state) - mScannerView = ZXingScannerView(this).apply { - setFormats(listOf(BarcodeFormat.QR_CODE)) - } - setContentView(mScannerView) - if (!permissionIsGranted()) requestPermission() - } - override fun onResume() { - super.onResume() - mScannerView.setResultHandler(this) - mScannerView.startCamera() - } + setContentView(R.layout.layout_qr_scanning) - override fun onPause() { - super.onPause() - mScannerView.stopCamera() - } + cameraProviderFuture = ProcessCameraProvider.getInstance(this) + cameraExecutor = Executors.newSingleThreadExecutor() - override fun handleResult(result: Result) { - setResult(SCAN_QR_REQUEST_CODE, Intent().apply { putExtra(SCAN_RESULT, result.text) }) - finish() + cameraProviderFuture?.addListener( + { + val cameraProvider = cameraProviderFuture?.get() + binder.bindPreview( + context = this, + binding = binding, + lifecycleOwner = this, + cameraProvider = requireNotNull(cameraProvider), + cameraExecutor = requireNotNull(cameraExecutor), + onScanned = { result -> + setResult(SCAN_QR_REQUEST_CODE, Intent().apply { putExtra(SCAN_RESULT, result) }) + finish() + }, + ) + }, + ContextCompat.getMainExecutor(this), + ) + + binding.overlay.post { + binding.overlay.setViewFinder() + } } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { @@ -56,12 +71,8 @@ class ScanQrCodeActivity : AppCompatActivity(), ZXingScannerView.ResultHandler { } private fun permissionIsGranted(): Boolean { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val cameraPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) - cameraPermission == PackageManager.PERMISSION_GRANTED - } else { - true - } + val cameraPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) + return cameraPermission == PackageManager.PERMISSION_GRANTED } private fun requestPermission() { diff --git a/app/src/main/java/com/tangem/tap/common/qrCodeScan/ViewFinderOverlay.kt b/app/src/main/java/com/tangem/tap/common/qrCodeScan/ViewFinderOverlay.kt new file mode 100644 index 0000000000..f4939b828f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/qrCodeScan/ViewFinderOverlay.kt @@ -0,0 +1,42 @@ +package com.tangem.tap.common.qrCodeScan + +import android.content.Context +import android.graphics.* +import android.util.AttributeSet +import android.view.View +import androidx.core.content.ContextCompat +import com.tangem.wallet.R + +class ViewFinderOverlay(context: Context, attrs: AttributeSet) : View(context, attrs) { + + private val boxPaint: Paint = Paint().apply { + color = ContextCompat.getColor(context, R.color.white) + style = Paint.Style.STROKE + strokeWidth = context.resources.getDimensionPixelOffset(R.dimen.qr_border_stroke_width).toFloat() + } + + private val boxWidthRatio = 0.8F + private val boxCornerRadius: Float = + context.resources.getDimensionPixelOffset(R.dimen.qr_border_corner_radius).toFloat() + + private var boxRect: RectF? = null + + @Suppress("MagicNumber") + fun setViewFinder() { + val overlayWidth = width.toFloat() + val overlayHeight = height.toFloat() + val boxSize = overlayWidth * boxWidthRatio + val cx = overlayWidth / 2 + val cy = overlayHeight / 2 + boxRect = RectF(cx - boxSize / 2, cy - boxSize / 2, cx + boxSize / 2, cy + boxSize / 2) + + invalidate() + } + + override fun draw(canvas: Canvas) { + super.draw(canvas) + boxRect?.let { + canvas.drawRoundRect(it, boxCornerRadius, boxCornerRadius, boxPaint) + } + } +} \ 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/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt index 605b1318e7..0a38a59439 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt @@ -602,7 +602,8 @@ internal class AddCustomTokenViewModel @Inject constructor( val networkSelectorValue = uiState.form.networkSelectorField.selectedItem.blockchain val networkId = Blockchain.fromNetworkId(networkSelectorValue.toNetworkId())?.id - val savedTokenId = if (token.isCustom) null else token.id.value + // todo after move foundToken to CryptoCurrency model, use only id + val savedTokenId = if (token.isCustom) null else token.id.rawCurrencyId val sameId = foundToken?.id == savedTokenId val sameAddress = contractAddress == token.contractAddress @@ -804,7 +805,9 @@ internal class AddCustomTokenViewModel @Inject constructor( networkSelectorField = uiState.form.networkSelectorField.copy( selectedItem = selectedItem, ), - showTokenFields = selectedItem.blockchain.canHandleTokens(), + showTokenFields = selectedItem.blockchain.canHandleTokens() && + // workaround cause in Terra we support only 1 token + selectedItem.blockchain != Blockchain.TerraV1, ), ) onContactAddressValueChange(uiState.form.contractAddressInputField.value) 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/details/ui/walletconnect/QrScanFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/QrScanFragment.kt index ccfe000c5c..4ba97f4d51 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/QrScanFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/QrScanFragment.kt @@ -2,26 +2,33 @@ package com.tangem.tap.features.details.ui.walletconnect import android.Manifest import android.content.pm.PackageManager -import android.os.Build import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup +import android.view.* import androidx.activity.OnBackPressedCallback +import androidx.camera.lifecycle.ProcessCameraProvider import androidx.core.content.ContextCompat import androidx.core.view.WindowCompat import androidx.fragment.app.Fragment -import com.google.zxing.BarcodeFormat -import com.google.zxing.Result +import by.kirich1409.viewbindingdelegate.viewBinding +import com.google.common.util.concurrent.ListenableFuture import com.otaliastudios.cameraview.CameraView import com.tangem.core.navigation.NavigationAction import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction +import com.tangem.tap.features.details.ui.walletconnect.dialogs.PreviewBinder import com.tangem.tap.store -import me.dm7.barcodescanner.zxing.ZXingScannerView +import com.tangem.wallet.R +import com.tangem.wallet.databinding.LayoutQrScanningBinding +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors -class QrScanFragment : Fragment(0), ZXingScannerView.ResultHandler { +internal class QrScanFragment : Fragment(R.layout.layout_qr_scanning) { - private var scannerView: ZXingScannerView? = null + private val binding: LayoutQrScanningBinding by viewBinding(LayoutQrScanningBinding::bind) + + private val binder = PreviewBinder() + + private var cameraProviderFuture: ListenableFuture? = null + private var cameraExecutor: ExecutorService? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -36,25 +43,38 @@ class QrScanFragment : Fragment(0), ZXingScannerView.ResultHandler { ) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + if (!permissionIsGranted()) requestPermission() - scannerView = ZXingScannerView(activity).apply { - setFormats(listOf(BarcodeFormat.QR_CODE)) + cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext()) + cameraExecutor = Executors.newSingleThreadExecutor() + + cameraProviderFuture?.addListener( + { + val cameraProvider = cameraProviderFuture?.get() + binder.bindPreview( + context = requireContext(), + binding = binding, + lifecycleOwner = this, + cameraProvider = requireNotNull(cameraProvider), + cameraExecutor = requireNotNull(cameraExecutor), + onScanned = { result -> + store.dispatch(NavigationAction.PopBackTo()) + setFitSystemWindows(fit = false) + if (result.isNotBlank()) { + store.dispatch(WalletConnectAction.OpenSession(result)) + } + }, + ) + }, + ContextCompat.getMainExecutor(requireContext()), + ) + + binding.overlay.post { + binding.overlay.setViewFinder() } - - return scannerView - } - - override fun onResume() { - super.onResume() - scannerView?.setResultHandler(this) - scannerView?.startCamera() - } - - override fun onPause() { - super.onPause() - scannerView?.stopCamera() } override fun onDestroy() { @@ -62,14 +82,6 @@ class QrScanFragment : Fragment(0), ZXingScannerView.ResultHandler { setFitSystemWindows(fit = false) } - override fun handleResult(result: Result) { - store.dispatch(NavigationAction.PopBackTo()) - setFitSystemWindows(fit = false) - if (!result.text.isNullOrBlank()) { - store.dispatch(WalletConnectAction.OpenSession(result.text)) - } - } - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { if (requestCode != CameraView.PERMISSION_REQUEST_CODE) return @@ -80,13 +92,8 @@ class QrScanFragment : Fragment(0), ZXingScannerView.ResultHandler { } private fun permissionIsGranted(): Boolean { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val cameraPermission = - ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) - cameraPermission == PackageManager.PERMISSION_GRANTED - } else { - true - } + val cameraPermission = ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) + return cameraPermission == PackageManager.PERMISSION_GRANTED } private fun requestPermission() { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/PreviewBinder.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/PreviewBinder.kt new file mode 100644 index 0000000000..b958759c58 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/PreviewBinder.kt @@ -0,0 +1,69 @@ +package com.tangem.tap.features.details.ui.walletconnect.dialogs + +import android.content.Context +import android.util.Size +import android.view.OrientationEventListener +import android.view.Surface +import androidx.camera.core.CameraSelector +import androidx.camera.core.ImageAnalysis +import androidx.camera.core.Preview +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.lifecycle.LifecycleOwner +import com.tangem.tap.common.qrCodeScan.MLKitBarcodeAnalyzer +import com.tangem.wallet.databinding.LayoutQrScanningBinding +import java.util.concurrent.ExecutorService + +internal class PreviewBinder { + + @Suppress("LongParameterList") + fun bindPreview( + context: Context, + binding: LayoutQrScanningBinding, + lifecycleOwner: LifecycleOwner, + cameraProvider: ProcessCameraProvider, + cameraExecutor: ExecutorService, + onScanned: (String) -> Unit, + ) { + cameraProvider.unbindAll() + + val preview: Preview = Preview.Builder() + .build() + + val imageAnalysis = ImageAnalysis.Builder() + .setTargetResolution(Size(binding.cameraPreview.width, binding.cameraPreview.height)) + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .build() + + val orientationEventListener = object : OrientationEventListener(context) { + + @Suppress("MagicNumber") + override fun onOrientationChanged(orientation: Int) { + val rotation: Int = when (orientation) { + in 45..134 -> Surface.ROTATION_270 + in 135..224 -> Surface.ROTATION_180 + in 225..314 -> Surface.ROTATION_90 + else -> Surface.ROTATION_0 + } + + imageAnalysis.targetRotation = rotation + } + } + orientationEventListener.enable() + + val analyzer: ImageAnalysis.Analyzer = MLKitBarcodeAnalyzer { + imageAnalysis.clearAnalyzer() + onScanned.invoke(it) + } + + cameraExecutor.let { + imageAnalysis.setAnalyzer(it, analyzer) + } + + preview.setSurfaceProvider(binding.cameraPreview.surfaceProvider) + + val cameraSelector: CameraSelector = CameraSelector.Builder() + .requireLensFacing(CameraSelector.LENS_FACING_BACK) + .build() + cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, imageAnalysis, preview) + } +} \ No newline at end of file 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/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index e5238a1788..4a6035d3ae 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -4,6 +4,7 @@ import android.net.Uri import com.tangem.blockchain.common.Blockchain import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError +import com.tangem.common.extensions.guard import com.tangem.common.extensions.ifNotNull import com.tangem.common.extensions.toHexString import com.tangem.common.services.Result @@ -18,6 +19,7 @@ import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.userwallets.Artwork +import com.tangem.domain.userwallets.UserWalletBuilder import com.tangem.feature.onboarding.data.model.CreateWalletResponse import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource import com.tangem.operations.attestation.OnlineCardVerifier @@ -40,6 +42,7 @@ import com.tangem.wallet.R import kotlinx.coroutines.launch import org.rekotlin.Action import org.rekotlin.Middleware +import timber.log.Timber object OnboardingWalletMiddleware { val handler = onboardingWalletMiddleware @@ -501,10 +504,18 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) Analytics.send(Onboarding.Backup.Finished(backupState.backupCardsNumber)) } - userWalletsListManager.selectedUserWalletSync?.walletId?.let { + if (scanResponse != null) { scope.launch { + val userWallet = UserWalletBuilder(scanResponse) + .backupCardsIds(backupState.backupCardIds.toSet()) + .build() + .guard { + Timber.e("User wallet not created") + return@launch + } + userWalletsListManager.update( - userWalletId = it, + userWalletId = userWallet.walletId, update = { wallet -> wallet.copy( scanResponse = updateScanResponseAfterBackup( 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/send/ui/stateSubscribers/SendStateSubscriber.kt b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt index d71adc7cc8..15044dd26f 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt @@ -243,7 +243,7 @@ internal class SendStateSubscriber( } val filter = DecimalDigitsInputFilter( - digitsBeforeDecimal = 12, + digitsBeforeDecimal = 40, digitsAfterDecimal = state.maxLengthOfAmount, decimalSeparator = state.decimalSeparator, ) 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/TokenItem.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokenItem.kt index 862b0c874f..9107775b0a 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokenItem.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokenItem.kt @@ -10,6 +10,7 @@ import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext @@ -125,7 +126,9 @@ internal fun TokenItem(model: TokenItemState) { @Composable private fun Icon(name: String, iconUrl: String, onContrastCalculate: (Color) -> Unit, modifier: Modifier = Modifier) { - val iconModifier = modifier.size(size = TangemTheme.dimens.size46) + val iconModifier = modifier + .size(size = TangemTheme.dimens.size46) + .clip(TangemTheme.shapes.roundedCorners8) val screenBackgroundColor = TangemTheme.colors.background.primary.toArgb() val isDarkTheme = isSystemInDarkTheme() val coroutineScope = rememberCoroutineScope() 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/app/src/main/res/layout/layout_qr_scanning.xml b/app/src/main/res/layout/layout_qr_scanning.xml new file mode 100644 index 0000000000..964d9acae6 --- /dev/null +++ b/app/src/main/res/layout/layout_qr_scanning.xml @@ -0,0 +1,20 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_send_amount.xml b/app/src/main/res/layout/layout_send_amount.xml index fb744b5755..b8f987e234 100644 --- a/app/src/main/res/layout/layout_send_amount.xml +++ b/app/src/main/res/layout/layout_send_amount.xml @@ -7,63 +7,34 @@ android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@+id/tilAddress"> - - + android:layout_height="82dp" + android:background="@color/background_secondary" + android:fontFamily="sans-serif-light" + android:imeOptions="actionDone" + android:inputType="numberDecimal" + android:paddingStart="0dp" + android:paddingEnd="96dp" + android:textColor="@color/text_primary_1" + android:textSize="@dimen/text_size_amount_to_send" + tools:text="139" /> - - - - - - - - - - - + + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/tilAmountToSend" /> \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 8f75b4d5f4..63c85c5bfe 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -42,4 +42,7 @@ 16dp + 4dp + 8dp + diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index 6d7e66c87e..137ccdb922 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -47,4 +47,13 @@ dependencies { /** Security */ implementation(deps.spongecastle.core) + + /** Chucker */ + debugImplementation(deps.chucker) + externalImplementation(deps.chuckerStub) + internalImplementation(deps.chuckerStub) + releaseImplementation(deps.chuckerStub) + + /** Local storages */ + implementation(deps.androidx.datastore) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/ExpressApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/ExpressApi.kt new file mode 100644 index 0000000000..8574c38d72 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/ExpressApi.kt @@ -0,0 +1,60 @@ +package com.tangem.datasource.api.express + +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.express.models.request.AssetsRequestBody +import com.tangem.datasource.api.express.models.request.PairsRequestBody +import com.tangem.datasource.api.express.models.response.* +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.POST +import retrofit2.http.Query +import java.math.BigDecimal + +/** + * Interface of Tangem Express API (new swap mechanism) + */ +@Suppress("LongParameterList") +interface ExpressApi { + + // TODO move first three params to retrofit interceptor + @POST("assets") + suspend fun getAssets( + @Header("api-key") apiKey: String, + @Header("user-id") userId: String, + @Header("session-id") sessionId: String, + @Body body: AssetsRequestBody, + ): ApiResponse> + + @POST("pairs") + suspend fun getPairs(@Body body: PairsRequestBody): ApiResponse> + + @GET("providers") + suspend fun getProviders(): ApiResponse> + + @GET("exchange-quote") + suspend fun getExchangeQuote( + @Query("fromContractAddress") fromContractAddress: String, + @Query("fromNetwork") fromNetwork: String, + @Query("toContractAddress") toContractAddress: String, + @Query("toNetwork") toNetwork: String, + @Query("fromAmount") fromAmount: BigDecimal, + @Query("providerId") providerId: Int, + @Query("rateType") rateType: RateType, + ): ApiResponse + + @GET("exchange-data") + suspend fun getExchangeData( + @Query("fromContractAddress") fromContractAddress: String, + @Query("fromNetwork") fromNetwork: String, + @Query("toContractAddress") toContractAddress: String, + @Query("toNetwork") toNetwork: String, + @Query("fromAmount") fromAmount: BigDecimal, + @Query("providerId") providerId: Int, + @Query("rateType") rateType: RateType, + @Query("toAddress") toAddress: String, + ): ApiResponse + + @GET("exchange-results") + suspend fun getExchangeResults(@Query("txId") txId: String): ApiResponse +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/AssetsRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/AssetsRequestBody.kt new file mode 100644 index 0000000000..f1d31bd24e --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/AssetsRequestBody.kt @@ -0,0 +1,7 @@ +package com.tangem.datasource.api.express.models.request + +import com.squareup.moshi.Json + +data class AssetsRequestBody( + @Json(name = "filter") val filter: List?, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/LeastTokenInfo.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/LeastTokenInfo.kt new file mode 100644 index 0000000000..fd636c6682 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/LeastTokenInfo.kt @@ -0,0 +1,11 @@ +package com.tangem.datasource.api.express.models.request + +import com.squareup.moshi.Json + +data class LeastTokenInfo( + @Json(name = "contractAddress") + val contractAddress: String, + + @Json(name = "network") + val network: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/PairsRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/PairsRequestBody.kt new file mode 100644 index 0000000000..c22747d9cf --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/PairsRequestBody.kt @@ -0,0 +1,11 @@ +package com.tangem.datasource.api.express.models.request + +import com.squareup.moshi.Json + +data class PairsRequestBody( + @Json(name = "from") + val from: List, + + @Json(name = "to") + val to: List, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/Asset.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/Asset.kt new file mode 100644 index 0000000000..dc2fd581fe --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/Asset.kt @@ -0,0 +1,29 @@ +package com.tangem.datasource.api.express.models.response + +import com.squareup.moshi.Json + +data class Asset( + @Json(name = "contractAddress") + val contractAddress: String, + + @Json(name = "network") + val network: String, + + @Json(name = "token") + val token: String, + + @Json(name = "name") + val name: String, + + @Json(name = "symbol") + val symbol: String, + + @Json(name = "decimals") + val decimals: Int, + + @Json(name = "isActive") + val isActive: Boolean, + + @Json(name = "exchangeAvailable") + val exchangeAvailable: Boolean, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt new file mode 100644 index 0000000000..3083bdfd0b --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt @@ -0,0 +1,41 @@ +package com.tangem.datasource.api.express.models.response + +import com.squareup.moshi.Json +import java.math.BigDecimal + +data class ExchangeDataResponse( + @Json(name = "toAmount") + val toAmount: BigDecimal, + + @Json(name = "txType") + val txType: TxType, + + @Json(name = "txId") + val txId: String, // inner tangem-express transaction id + + @Json(name = "txFrom") + val txFrom: String?, // account for debiting tokens (same as toAddress) if DEX, null if CEX + + @Json(name = "txTo") + val txTo: String, // swap smart-contract address if DEX, address for sending transaction if CEX + + @Json(name = "txData") + val txData: String?, // transaction data if DEX, null if CEX + + @Json(name = "txValue") + val txValue: BigDecimal, // amount (same as fromAmount) + + @Json(name = "externalTxId") + val externalTxId: String?, // null if DEX, provider transaction id if CEX + + @Json(name = "externalTxUrl") + val externalTxUrl: String?, // null if DEX, url of provider exchange status page if CEX +) + +enum class TxType { + @Json(name = "send") + SEND, + + @Json(name = "swap") + SWAP, +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt new file mode 100644 index 0000000000..934d1a8497 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt @@ -0,0 +1,28 @@ +package com.tangem.datasource.api.express.models.response + +import com.squareup.moshi.Json + +data class ExchangeProvider( + @Json(name = "id") + val id: Int, + + @Json(name = "name") + val name: String, + + @Json(name = "id") + val type: ExchangeProviderType, + + @Json(name = "imageLarge") + val imageLargeUrl: Int, + + @Json(name = "imageSmall") + val imageSmallUrl: Int, +) + +enum class ExchangeProviderType { + @Json(name = "dex") + DEX, + + @Json(name = "cex") + CEX, +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt new file mode 100644 index 0000000000..214b4ac2d5 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt @@ -0,0 +1,12 @@ +package com.tangem.datasource.api.express.models.response + +import com.squareup.moshi.Json +import java.math.BigDecimal + +data class ExchangeQuoteResponse( + @Json(name = "toAmount") + val toAmount: BigDecimal, + + @Json(name = "allowanceContract") + val allowanceContract: String?, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeResultsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeResultsResponse.kt new file mode 100644 index 0000000000..c9b10f881e --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeResultsResponse.kt @@ -0,0 +1,42 @@ +package com.tangem.datasource.api.express.models.response + +import com.squareup.moshi.Json + +data class ExchangeResultsResponse( + @Json(name = "status") + val status: ExchangeResultsStatus, + + @Json(name = "externalStatus") + val externalStatus: String, + + @Json(name = "externalTxUrl") + val externalTxUrl: String, + + @Json(name = "error") + val error: ExchangeResultsError?, +) + +enum class ExchangeResultsStatus { + @Json(name = "processing") + PROCESSING, + + @Json(name = "done") + DONE, + + @Json(name = "failed") + FAILED, + + @Json(name = "refunded") + REFUNDED, + + @Json(name = "verificationRequired") + VERIFICATION_REQUIRED, +} + +data class ExchangeResultsError( + @Json(name = "code") + val code: Int, + + @Json(name = "description") + val description: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt new file mode 100644 index 0000000000..ba04207f85 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt @@ -0,0 +1,32 @@ +package com.tangem.datasource.api.express.models.response + +import com.squareup.moshi.Json +import com.tangem.datasource.api.express.models.request.LeastTokenInfo + +data class SwapPair( + @Json(name = "from") + val from: LeastTokenInfo, + + @Json(name = "to") + val to: LeastTokenInfo, + + @Json(name = "providers") + val providers: List, + +) + +data class SwapPairProvider( + @Json(name = "providerId") + val providerId: Int, + + @Json(name = "rateType") + val rateType: RateType, +) + +enum class RateType { + @Json(name = "float") + FLOAT, + + @Json(name = "fixed") + FIXED, +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/OneInchApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/OneInchApi.kt index 651e726ad3..cf950a0f66 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/OneInchApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/OneInchApi.kt @@ -117,8 +117,8 @@ interface OneInchApi { */ @GET("quote") suspend fun quote( - @Query("fromTokenAddress") fromTokenAddress: String, - @Query("toTokenAddress") toTokenAddress: String, + @Query("src") fromTokenAddress: String, + @Query("dst") toTokenAddress: String, @Query("amount") amount: String, @Query("protocols") protocols: String? = null, @Query("fee") fee: String? = null, @@ -128,6 +128,7 @@ interface OneInchApi { @Query("mainRouteParts") mainRouteParts: String? = null, @Query("parts") parts: String? = null, @Query("gasPrice") gasPrice: String? = null, + @Query("includeTokensInfo") includeTokensInfo: Boolean = true, ): Response /** @@ -178,18 +179,18 @@ interface OneInchApi { */ @GET("swap") suspend fun swap( - @Query("fromTokenAddress") fromTokenAddress: String, - @Query("toTokenAddress") toTokenAddress: String, + @Query("src") fromTokenAddress: String, + @Query("dst") toTokenAddress: String, @Query("amount") amount: String, - @Query("fromAddress") fromAddress: String, + @Query("from") fromAddress: String, @Query("slippage") slippage: Int, @Query("protocols") protocols: String? = null, - @Query("destReceiver") destinationAddress: String? = null, - @Query("referrerAddress") referrerAddress: String? = null, + @Query("receiver") destinationAddress: String? = null, + @Query("referrer") referrerAddress: String? = null, @Query("fee") fee: String? = null, @Query("disableEstimate") disableEstimate: Boolean? = null, @Query("permit") permit: String? = null, - @Query("compatibilityMode") compatibilityMode: Boolean? = null, + @Query("compatibility") compatibilityMode: Boolean? = null, @Query("burnChi") burnChi: Boolean? = null, @Query("allowPartialFill") allowPartialFill: Boolean? = null, @Query("parts") parts: String? = null, @@ -198,6 +199,7 @@ interface OneInchApi { @Query("complexityLevel") complexityLevel: String? = null, @Query("gasLimit") gasLimit: String? = null, @Query("gasPrice") gasPrice: String? = null, + @Query("includeTokensInfo") includeTokensInfo: Boolean = true, ): Response //endregion Swap } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/QuoteResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/QuoteResponse.kt index ecea3f4bc5..692f036f24 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/QuoteResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/QuoteResponse.kt @@ -5,16 +5,10 @@ import com.squareup.moshi.Json /** * Quote response * - * @property fromToken Source token info * @property toToken Destination token info * @property toTokenAmount Expected amount of destination token - * @property fromTokenAmount Amount of source token - * @property estimatedGas gas fee */ data class QuoteResponse( - @Json(name = "fromToken") val fromToken: TokenOneInchDto, @Json(name = "toToken") val toToken: TokenOneInchDto, - @Json(name = "toTokenAmount") val toTokenAmount: String, - @Json(name = "fromTokenAmount") val fromTokenAmount: String, - @Json(name = "estimatedGas") val estimatedGas: Int, + @Json(name = "toAmount") val toTokenAmount: String, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/SwapResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/SwapResponse.kt index 4a72fcccc5..bc1f668360 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/SwapResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/SwapResponse.kt @@ -5,7 +5,6 @@ import com.squareup.moshi.Json data class SwapResponse( @Json(name = "fromToken") val fromToken: TokenOneInchDto, @Json(name = "toToken") val toToken: TokenOneInchDto, - @Json(name = "toTokenAmount") val toTokenAmount: String, - @Json(name = "fromTokenAmount") val fromTokenAmount: String, + @Json(name = "toAmount") val toTokenAmount: String, @Json(name = "tx") val transaction: TransactionDto, ) \ 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/AppPreferencesStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/AppPreferencesStoreModule.kt new file mode 100644 index 0000000000..e36b36096b --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/AppPreferencesStoreModule.kt @@ -0,0 +1,31 @@ +package com.tangem.datasource.di + +import android.content.Context +import com.squareup.moshi.Moshi +import com.tangem.datasource.local.* +import com.tangem.datasource.local.preferences.* +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object AppPreferencesStoreModule { + + @Provides + @Singleton + fun provideAppPreferencesStore( + @ApplicationContext appContext: Context, + dispatchers: CoroutineDispatcherProvider, + @NetworkMoshi moshi: Moshi, + ): AppPreferencesStore { + return AppPreferencesStore( + preferencesDataStore = PreferencesDataStore.getInstance(context = appContext, dispatcher = dispatchers.io), + moshi = moshi, + ) + } +} \ No newline at end of file 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..341fd1a752 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() @@ -51,7 +61,7 @@ class OneInchApisModule { } companion object { - private const val ONE_INCH_BASE_URL = "https://api-tangem.1inch.io/v5.0/" + private const val ONE_INCH_BASE_URL = "https://api-tangem.1inch.io/v5.2/" private const val ONE_INCH_ETH_PATH = "1/" private const val ONE_INCH_BSC_PATH = "56/" private const val ONE_INCH_POLYGON_PATH = "137/" 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/local/preferences/AppPreferencesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/AppPreferencesStore.kt new file mode 100644 index 0000000000..7e401b9333 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/AppPreferencesStore.kt @@ -0,0 +1,45 @@ +package com.tangem.datasource.local.preferences + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.MutablePreferences +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import com.squareup.moshi.Moshi + +/** + * Application preferences store. + * AppPreferencesStore is wrapper around DataStore that supports json serialization and deserialization. + * + * @property moshi Moshi instance. Property has 'public' modifier because it is used + * by Public-API inline function. Don't use it directly. + * @property preferencesDataStore DataStore instance + * +[REDACTED_AUTHOR] + */ +class AppPreferencesStore( + val moshi: Moshi, + private val preferencesDataStore: DataStore, +) : DataStore by preferencesDataStore { + + /** + * Edit data according with transaction [transform]. + * + * @param transform transaction. It has receiver [AppPreferencesStore] that allows to use [getObject], [setObject] + * functions when creating transaction. + */ + suspend fun editData(transform: suspend AppPreferencesStore.(MutablePreferences) -> Unit): Preferences { + return edit { transform(it) } + } + + /** Get nullable data [T] by string [key] from [MutablePreferences] */ + inline fun MutablePreferences.getObject(key: Preferences.Key): T? { + val adapter = moshi.adapter(T::class.java) + return this[key]?.let(adapter::fromJson) + } + + /** Set data [T] by string [key] to [MutablePreferences] */ + inline fun MutablePreferences.setObject(key: Preferences.Key, value: T) { + val adapter = moshi.adapter(T::class.java) + this[key] = adapter.toJson(value) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesDataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesDataStore.kt new file mode 100644 index 0000000000..7d0d6ba94a --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesDataStore.kt @@ -0,0 +1,54 @@ +package com.tangem.datasource.local.preferences + +import android.content.Context +import androidx.datastore.core.DataMigration +import androidx.datastore.core.DataStore +import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.emptyPreferences +import androidx.datastore.preferences.preferencesDataStoreFile +import com.tangem.datasource.local.preferences.PreferencesDataStore.INSTANCE +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import timber.log.Timber +import kotlin.coroutines.CoroutineContext + +/** + * Application preferences data store 'DataStore'. + * Implements the singleton pattern [INSTANCE] under the hood. + * +[REDACTED_AUTHOR] + */ +internal object PreferencesDataStore { + + private const val PREFERENCES_FILE_NAME = "TAP_PREFS" + + private var INSTANCE: DataStore? = null + + fun getInstance(context: Context, dispatcher: CoroutineContext): DataStore { + return INSTANCE ?: create(context, dispatcher).also { INSTANCE = it } + } + + private fun create(context: Context, dispatcher: CoroutineContext): DataStore { + return PreferenceDataStoreFactory.create( + corruptionHandler = createCorruptionHandler(), + migrations = createMigrations(), + scope = CoroutineScope(context = dispatcher + SupervisorJob()), + produceFile = { context.preferencesDataStoreFile(name = PREFERENCES_FILE_NAME) }, + ) + } + + private fun createCorruptionHandler(): ReplaceFileCorruptionHandler { + return ReplaceFileCorruptionHandler( + produceNewData = { + Timber.w(it) + emptyPreferences() + }, + ) + } + + private fun createMigrations(): List> { + return listOf() + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt new file mode 100644 index 0000000000..90f2b61270 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -0,0 +1,13 @@ +package com.tangem.datasource.local.preferences + +/** + * All preferences keys that DataStore is stored. + * +[REDACTED_AUTHOR] + */ +object PreferencesKeys + +/** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore */ +internal fun getTapPrefKeysToMigrate(): Set { + return setOf() +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt new file mode 100644 index 0000000000..1ca1355871 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt @@ -0,0 +1,46 @@ +package com.tangem.datasource.local.preferences.utils + +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import com.tangem.datasource.local.preferences.AppPreferencesStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.map + +/** Get flow of nullable data [T] by string [key] */ +inline fun AppPreferencesStore.getObject(key: Preferences.Key): Flow { + val adapter = moshi.adapter(T::class.java) + return data.map { it[key]?.let(adapter::fromJson) } +} + +/** Get flow of data [T] by string [key]. If data is not found, it returns [default] */ +inline fun AppPreferencesStore.getObject(key: Preferences.Key, default: T): Flow { + val adapter = moshi.adapter(T::class.java) + return data.map { it[key]?.let(adapter::fromJson) ?: default } +} + +/** Get nullable data [T] by string [key] */ +suspend inline fun AppPreferencesStore.getObjectSyncOrNull(key: Preferences.Key): T? { + val adapter = moshi.adapter(T::class.java) + return data.firstOrNull() + ?.get(key) + ?.let(adapter::fromJson) +} + +/** Get data [T] by string [key]. If data is not found, it returns [default] */ +suspend inline fun AppPreferencesStore.getObjectSyncOrDefault( + key: Preferences.Key, + default: T, +): T { + val adapter = moshi.adapter(T::class.java) + return data.firstOrNull() + ?.get(key) + ?.let(adapter::fromJson) + ?: default +} + +/** Store data [value] by string [key] */ +suspend inline fun AppPreferencesStore.storeObject(key: Preferences.Key, value: T) { + val adapter = moshi.adapter(T::class.java) + edit { it[key] = adapter.toJson(value) } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/PreferencesDataStoreExt.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/PreferencesDataStoreExt.kt new file mode 100644 index 0000000000..e9177bad53 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/PreferencesDataStoreExt.kt @@ -0,0 +1,33 @@ +package com.tangem.datasource.local.preferences.utils + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.map + +/** Get flow of nullable data [T] by [key] */ +fun DataStore.get(key: Preferences.Key): Flow { + return data.map { it[key] } +} + +/** Get flow of data [T] by [key]. If data is not found, it returns [default] */ +fun DataStore.get(key: Preferences.Key, default: T): Flow { + return data.map { it[key] ?: default } +} + +/** Get nullable data [T] by [key] */ +suspend fun DataStore.getSyncOrNull(key: Preferences.Key): T? { + return data.firstOrNull()?.get(key) +} + +/** Get data [T] by [key]. If data is not found, it returns [default] */ +suspend fun DataStore.getSyncOrDefault(key: Preferences.Key, default: T): T { + return data.firstOrNull()?.get(key) ?: default +} + +/** Store data [value] by [key] */ +suspend fun DataStore.store(key: Preferences.Key, value: T) { + edit { it[key] = value } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/SharedPreferencesKeyMigration.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/SharedPreferencesKeyMigration.kt new file mode 100644 index 0000000000..f9c11e6471 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/SharedPreferencesKeyMigration.kt @@ -0,0 +1,104 @@ +package com.tangem.datasource.local.preferences.utils + +import android.content.Context +import android.os.Build +import androidx.annotation.DoNotInline +import androidx.annotation.RequiresApi +import androidx.datastore.core.DataMigration +import androidx.datastore.preferences.core.* +import java.io.File +import java.io.IOException + +/** + * Migration of a specified key with name changing. + * Example, migrate the "key1" from "pref1" to the "key2" from "pref2". + * + * @property context context + * @property legacyPrefsName legacy SharedPreferences name + * @property legacyKeyName legacy SharedPreferences key name + * @property keyName new SharedPreferences key name + */ +internal class SharedPreferencesKeyMigration( + private val context: Context, + private val legacyPrefsName: String, + private val legacyKeyName: String, + private val keyName: String, +) : DataMigration { + + private val legacyPrefs = context.getSharedPreferences(legacyPrefsName, Context.MODE_PRIVATE) + + override suspend fun cleanUp() { + val sharedPrefsEditor = legacyPrefs.edit() + + sharedPrefsEditor.remove(legacyKeyName) + + if (!sharedPrefsEditor.commit()) { + throw IOException("Unable to delete migrated keys from SharedPreferences.") + } + + if (legacyPrefs.all.isEmpty()) { + deleteSharedPreferences(context = context, name = legacyPrefsName) + } + } + + override suspend fun shouldMigrate(currentData: Preferences): Boolean = true + + override suspend fun migrate(currentData: Preferences): Preferences { + val currentKeys = currentData.asMap().keys.map(Preferences.Key<*>::name) + + // If migration is already happened, return + if (currentKeys.contains(keyName)) return currentData + + val value = legacyPrefs.all[legacyKeyName] + if (value != null) { + val mutablePreferences = currentData.toMutablePreferences() + + when (value) { + is Boolean -> mutablePreferences[booleanPreferencesKey(keyName)] = value + is Float -> mutablePreferences[floatPreferencesKey(keyName)] = value + is Int -> mutablePreferences[intPreferencesKey(keyName)] = value + is Long -> mutablePreferences[longPreferencesKey(keyName)] = value + is String -> mutablePreferences[stringPreferencesKey(keyName)] = value + is Set<*> -> { + @Suppress("UNCHECKED_CAST") + mutablePreferences[stringSetPreferencesKey(keyName)] = value as Set + } + } + + return mutablePreferences.toPreferences() + } + + return currentData + } + + private fun deleteSharedPreferences(context: Context, name: String) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (!Api24Impl.deleteSharedPreferences(context, name)) { + throw IOException("Unable to delete SharedPreferences: $name") + } + } else { + val prefsFile = getSharedPrefsFile(context, name) + val prefsBackup = getSharedPrefsBackup(prefsFile) + + prefsFile.delete() + prefsBackup.delete() + } + } + + @RequiresApi(Build.VERSION_CODES.N) + private object Api24Impl { + + @JvmStatic + @DoNotInline + fun deleteSharedPreferences(context: Context, name: String): Boolean { + return context.deleteSharedPreferences(name) + } + } + + private fun getSharedPrefsFile(context: Context, name: String): File { + val prefsDir = File(context.applicationInfo.dataDir, "shared_prefs") + return File(prefsDir, "$name.xml") + } + + private fun getSharedPrefsBackup(prefsFile: File) = File(prefsFile.path + ".bak") +} \ 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..2af2ae2c68 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 секунд или отсканируйте карту Слишком много попыток @@ -85,6 +89,12 @@ Обменять Посмотреть историю транзакций Обозреватель + Скорость и комиссия + Сетевые комиссии за транзакции используются для поддержки безопасности сети, поощрения валидаторов, выделения ресурсов и определения приоритета транзакции. + Медленно + По рынку + Быстро + Свое Сгенерировать адреса Импортировать Нравится @@ -96,6 +106,7 @@ Нет данных OK Основная карта + Вставить Получить Отклонить Перезагрузить @@ -210,6 +221,24 @@ Токены Добавить Изменить + Основная сеть + Не основной или основной блокчейн, на котором размещен токен + Не основные сети + Сети + Выберите сети + Кошелек + Не удалось найти этот токен, вы можете добавить его вручную. + + %1$d из %2$d кошелька + %1$d из %2$d кошельков + %1$d из %2$d кошельков + %1$d из %2$d кошельков + + например Bitcoin + Рыночная капитализация + Выбранный токен не доступен в кошельке на данный момент. Но не переживайте, вы можете выразить свой интерес проголосовав за его добавление. + Голосовать + Выберите кошелек Вам необходимо установить единый код доступа для защиты всех ваших карт Защита Позже вы сможете установить индивидуальный код доступа для каждой карты @@ -362,19 +391,52 @@ Отсканируйте карту, чтобы изменить ее настройки. Изменения затронут только ту карту, которую вы отсканировали, и не повлияют на другие карты, привязанные к вашему кошельку. Приготовьте свою карту Сумма + Вычесть из суммы отправки + Сумма к получению %1$s Адрес + Код назначения + %1$s в %2$s + Введите адрес Адрес совпадает с адресом кошелька Недопустимый Tag. Он не будет добавлен в транзакцию. Недопустимый Memo. Он не будет добавлен в транзакцию. Tag Memo + Последние + Убедитесь, что вы отправляете средства на адрес кошелька %1$s. Ошибки могут привести к потере ваших токенов. + Мемо/Код назначения - это код, разделяющий транзакции к общему получателю в сети криптовалют. Внимание: отсутствие мемо может привести к потере средств. Включая комиссию Комиссия Низкая Нормальная Приоритетная + Цена газа + Цена газа влияет на скорость транзакции. При сильно низкой, транзакция может быть не обработана. + Лимит газа Максимальная сумма + Всё + Максимальная cумма комиссии + Комиссия не превысит Сетевая комиссия + Покрытие сетевой комиссии + Сумма отправки будет уменьшена для покрытия выбранного уровня комиссии + Обратите внимание, что при определенных параметрах комиссии возможны задержки по вашей транзакции + Недостаточно средств + Недостаточно средств для перевода, так как сумма комиссии и сумма перевода в совокупности больше имеющегося баланса. + Недопустимая сумма + Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению. + Минимальная сумма отправки - %1$s. Пожалуйста, убедитесь, что остаток после отправки также не будет меньше %1$s. + Сумма резерва не может быть менее %1$s. + Пожалуйста, пополните свой баланс, чтобы продолжить. + Увеличение комиссии + Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить 0.01. + Комиссия превышает баланс + Размер комиссии превышает баланс сети. Для продолжения необходимо пополнить баланс сети. + Возможны задержки по транзакции + Необязательное + QR код содержит информацию о сумме отправки равной %s + Получатель + Мои кошельки Отправка %s Всего %1$s и %2$s будет отправлено @@ -434,6 +496,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..020371fd62 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 @@ -82,7 +86,14 @@ Error Exchange Explore transaction history + Explore Explorer + Speed and fee + Network transaction fees are used to support network security, incentivize validators, allocate resources, and determine transaction priority + Slow + Market + Fast + Custom Generate addresses Import Learn & Earn @@ -95,6 +106,7 @@ No data OK Primary Card + Paste Receive Reject Reload @@ -222,10 +234,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 @@ -380,19 +391,64 @@ Scan the card to change its settings. The changes will impact only the card you\'ve scanned and will not affect other cards tied to your wallet. Get your card ready! Amount + Subtract from send amount + The recipient will receive %1$s Address + Destination Tag + %1$s at %2$s + Enter address Address is the same as wallet address Invalid Tag. It won\'t be added to the transaction. Invalid Memo. It won\'t be added to the transaction. Tag Memo + Insufficient funds for transfer Include fee Fee Low Normal Priority + Network fee info unreachable + Check your network connection + Gas limit + Gas Limit is auto-calculated; raise it during network congestion + Gas price + Gas Price affects transaction speed. If it\'s too low, the transaction might not be processed. Maximum amount + Max + Maximum fee amount + Max fee + Numbers only for Destination Tag Network fee + Network fee coverage + Sending amount will be reduced to cover the selected fee level + Kindly be aware that your transaction may experience delays under specific fee settings + Total exceeds balance + Insufficient funds for the transfer, as the total of the fee and transfer amount exceeds the existing balance. + Invalid amount + The included commission exceeds the transfer amount, leading to a negative value. + The minimum sending amount is %1$s. Please ensure that the remaining balance after sending will not be less than %1$s. + The balance amount must be at least %1$s. + Please top up your balance to continue. + Fee is increased + The fee for transferring the entire balance is higher. To reduce the commission, you can leave 0.01. + Fee exceeds balance + The commission fee exceeds the network balance. To continue, it is necessary to replenish the network balance. + Transaction delays are possible + Optional + Please align your QR code with the square to scan it. Ensure you scan %s network address. + Recipient’s address scanned + Sending amount was changed + QR code contains information about the sending amount equal to %s + Change the entered amount? + Change + Decline + Recent + Recipient + Not a valid address + My wallets + Ensure that you are sending funds to an %1$s wallet address. Errors may result in the loss of your tokens. + A Memo/Destination Tag is a unique ID for differentiating transactions sent to the same recipient on the same network. Caution: Omitting a memo may lead to misplaced funds. Sending %s Total %1$s and %2$s will be sent @@ -452,6 +508,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..8ce113a33f 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,24 @@ 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(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..b169176106 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/DefaultCurrencyIcon.kt @@ -0,0 +1,62 @@ +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.draw.clip +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, + ) + .clip(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 + + init { + val seed = seedFromAddress(address) + // IMPORTANT! Color generation is based on the seed, thus ORDER is important. + primaryColor = colorFromSeed(seed) + backgroundColor = colorFromSeed(seed) + spotColor = colorFromSeed(seed) + data = dataFromSeed(seed) + } + + private fun seedFromAddress(address: String): MutableList { + val seed = MutableList(SIZE) { DEFAULT_VALUE_L } + address.indices.forEach { index -> + seed[index % HALF_SIZE] = + (seed[index % HALF_SIZE] shl 5) - seed[index % HALF_SIZE] + Character.codePointAt(address, index) + } + + // Important cast to preserve JavaScript type behavior + seed.indices.map { seed[it] = seed[it].toInt().toLong() } + return seed + } + + private fun colorFromSeed(seed: MutableList): Int { + val h = floor(nextSeed(seed) * DEGREE_CIRCLE_F) + val s = nextSeed(seed) * SATURATION_MAX + SATURATION_MIN + val l = (nextSeed(seed) + nextSeed(seed) + nextSeed(seed) + nextSeed(seed)) * PROBABILITY_LIGHTNESS + return toRgb(h, s, l) + } + + private fun nextSeed(seed: MutableList): Float { + val t = (seed[0] xor (seed[0] shl 11)).toInt() + seed[0] = seed[1] + seed[1] = seed[2] + seed[2] = seed[3] + seed[3] = seed[3] xor (seed[3] shr 19) xor t.toLong() xor (t shr 8).toLong() + + return abs(seed[3]).toFloat() / Integer.MAX_VALUE + } + + private fun dataFromSeed(seed: MutableList) = MutableList(SIZE * SIZE) { DEFAULT_VALUE_F }.apply { + (0 until SIZE).forEach { row -> + (0 until HALF_SIZE).forEach { column -> + val value = floor(nextSeed(seed) * PROBABILITY_COLOR) + this[row * SIZE + column] = value + this[(row + 1) * SIZE - column - 1] = value + } + } + } + + private fun toRgb(paramH: Float, paramS: Float, paramL: Float): Int { + val h = paramH % DEGREE_CIRCLE_F / DEGREE_CIRCLE_F + val s = paramS / PERCENT_MAX + val l = paramL / PERCENT_MAX + + val q = if (l < 0.5) l * (1 + s) else l + s - s * l + val p = 2 * l - q + + val r = hueToRGB(p, q, h + 1f / 3f).coerceIn(0f, 1f) + val g = hueToRGB(p, q, h).coerceIn(0f, 1f) + val b = hueToRGB(p, q, h - 1f / 3f).coerceIn(0f, 1f) + + val red = (r * HEX_COLOR_MAX).toInt() + val green = (g * HEX_COLOR_MAX).toInt() + val blue = (b * HEX_COLOR_MAX).toInt() + return RGB_ALPHA_PART shl RGB_R_PART or (red shl RGB_G_PART) or (green shl RGB_B_PART) or blue + } + + private fun hueToRGB(p: Float, q: Float, h: Float): Float { + var hue = h + if (hue < 0) hue += 1f + if (hue > 1) hue -= 1f + if (6 * hue < 1) return p + (q - p) * 6f * hue + if (2 * hue < 1) return q + + return if (3 * hue < 2) p + (q - p) * 6f * (2f / 3f - hue) else p + } + + companion object { + internal const val SIZE = 8 + private const val HALF_SIZE = SIZE / 2 + private const val DEFAULT_VALUE_L = 0L + private const val DEFAULT_VALUE_F = 0f + private const val DEGREE_CIRCLE_F = 360.0f + private const val HEX_COLOR_MAX = 255 + private const val PERCENT_MAX = 100f + private const val SATURATION_MIN = 40f + private const val SATURATION_MAX = 60f + private const val PROBABILITY_LIGHTNESS = 25f + private const val PROBABILITY_COLOR = 2.3f + private const val RGB_ALPHA_PART = 0xFF + private const val RGB_R_PART = 24 + private const val RGB_G_PART = 16 + private const val RGB_B_PART = 8 + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/icons/identicon/IdentIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/icons/identicon/IdentIcon.kt new file mode 100644 index 0000000000..23f26292ee --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/icons/identicon/IdentIcon.kt @@ -0,0 +1,88 @@ +package com.tangem.core.ui.components.icons.identicon + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.intl.Locale +import androidx.compose.ui.text.toLowerCase +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.res.LocalIsInDarkTheme +import com.tangem.core.ui.res.TangemTheme + +private const val GAP_WIDTH = 1f +private const val ALPHA = 0.9f + +/** + * Ident icon + * + * @param address to generate ident icon from + * @param modifier to apply to ident icon + */ +@Suppress("MagicNumber") +@Composable +fun IdentIcon(address: String, modifier: Modifier = Modifier) { + if (address.isBlank()) { + Box(modifier = modifier) + } else { + val blockies = Blockies(address.toLowerCase(Locale.current)) + Canvas( + modifier = modifier + .then( + if (LocalIsInDarkTheme.current) { + Modifier.alpha(ALPHA) + } else { + Modifier + }, + ), + ) { + val cellWidth = size.width / Blockies.SIZE + blockies.data.forEachIndexed { index, item -> + val y = index / Blockies.SIZE + val x = index % Blockies.SIZE + val colorInt = when (item) { + 1f -> blockies.primaryColor + 2f -> blockies.spotColor + else -> blockies.backgroundColor + } + drawRect( + color = Color(colorInt), + topLeft = Offset(x * cellWidth, y * cellWidth), + size = Size(cellWidth + GAP_WIDTH, cellWidth + GAP_WIDTH), + ) + } + } + } +} + +//region preview +@Preview +@Composable +private fun IdentIconPreview_Light() { + TangemTheme { + IdentIcon( + address = "0xfb6916095ca1df60bb79ce92ce3ea74c37c5d359", + modifier = Modifier + .size(TangemTheme.dimens.size40), + ) + } +} + +@Preview +@Composable +private fun IdentIconPreview_Dark() { + TangemTheme(isDark = true) { + IdentIcon( + address = "0xfb6916095ca1df60bb79ce92ce3ea74c37c5d359", + modifier = Modifier + .size(TangemTheme.dimens.size40), + ) + } +} + +//endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt index c5b35a183e..1992043a9e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt @@ -28,6 +28,7 @@ data class NotificationConfig( data class PrimaryButtonConfig( val text: TextReference, + val additionalText: TextReference? = null, @DrawableRes val iconResId: Int? = null, val onClick: () -> Unit, ) : ButtonsState() diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt index cd00ab76f2..5920e09e81 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt @@ -45,11 +45,14 @@ fun LazyListScope.txHistoryItems( ) } is TxHistoryState.Empty -> { - nonContentItem(state = EmptyTransactionsBlockState.Empty, modifier = modifier) + nonContentItem(state = EmptyTransactionsBlockState.Empty(state.onExploreClick), modifier = modifier) } is TxHistoryState.Error -> { nonContentItem( - state = EmptyTransactionsBlockState.FailedToLoad(onClick = state.onReloadClick), + state = EmptyTransactionsBlockState.FailedToLoad( + onReload = state.onReloadClick, + onExplore = state.onExploreClick, + ), modifier = modifier, ) } @@ -61,7 +64,7 @@ fun LazyListScope.txHistoryItems( } nonContentItem( - state = EmptyTransactionsBlockState.NotImplemented(onClick = state.onExploreClick), + state = EmptyTransactionsBlockState.NotImplemented(onExplore = state.onExploreClick), modifier = modifier, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryContentItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryContentItem.kt index b532c2d533..a69b3c0030 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryContentItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryContentItem.kt @@ -15,7 +15,7 @@ internal fun TxHistoryListItem( TxHistoryGroupTitle(config = state, modifier = modifier) } is TxHistoryState.TxHistoryItemState.Title -> { - TxHistoryTitle(config = state, modifier = modifier) + TxHistoryTitle(onExploreClick = state.onExploreClick, modifier = modifier) } is TxHistoryState.TxHistoryItemState.Transaction -> { Transaction( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryTitle.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryTitle.kt index fd59b88c81..89a804b588 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryTitle.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryTitle.kt @@ -11,17 +11,16 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R -import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.res.TangemTheme /** * Transactions block title * - * @param config config + * @param onExploreClick lambda be invoke when explore button was clicked * @param modifier modifier */ @Composable -internal fun TxHistoryTitle(config: TxHistoryState.TxHistoryItemState.Title, modifier: Modifier = Modifier) { +internal fun TxHistoryTitle(onExploreClick: () -> Unit, modifier: Modifier = Modifier) { Row( modifier = modifier .background(TangemTheme.colors.background.primary) @@ -37,7 +36,7 @@ internal fun TxHistoryTitle(config: TxHistoryState.TxHistoryItemState.Title, mod ) Row( - modifier = Modifier.clickable(onClick = config.onExploreClick), + modifier = Modifier.clickable(onClick = onExploreClick), horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing4), ) { Icon( @@ -59,7 +58,7 @@ internal fun TxHistoryTitle(config: TxHistoryState.TxHistoryItemState.Title, mod @Composable private fun Preview_TransactionsBlockTitle_Light() { TangemTheme(isDark = false) { - TxHistoryTitle(config = TxHistoryState.TxHistoryItemState.Title(onExploreClick = {})) + TxHistoryTitle(onExploreClick = {}) } } @@ -67,6 +66,6 @@ private fun Preview_TransactionsBlockTitle_Light() { @Composable private fun Preview_TransactionsBlockTitle_Dark() { TangemTheme(isDark = true) { - TxHistoryTitle(config = TxHistoryState.TxHistoryItemState.Title(onExploreClick = {})) + TxHistoryTitle(onExploreClick = {}) } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionBlock.kt index 7e5efdd446..7f85f98d19 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionBlock.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionBlock.kt @@ -47,9 +47,40 @@ fun EmptyTransactionBlock(state: EmptyTransactionsBlockState, modifier: Modifier color = TangemTheme.colors.text.secondary, ) - state.actionButtonConfig?.let { - ActionButton(config = it) - } + Buttons( + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing18), + state = state.buttonsState, + ) + } +} + +@Composable +private fun Buttons(state: EmptyTransactionsBlockState.ButtonsState, modifier: Modifier = Modifier) { + when (state) { + is EmptyTransactionsBlockState.ButtonsState.SingleButton -> SingleButton(state = state, modifier = modifier) + is EmptyTransactionsBlockState.ButtonsState.PairButtons -> PairButtons(state = state, modifier = modifier) + } +} + +@Composable +private fun SingleButton(state: EmptyTransactionsBlockState.ButtonsState.SingleButton, modifier: Modifier = Modifier) { + ActionButton(modifier = modifier, config = state.actionButtonConfig) +} + +@Composable +private fun PairButtons(state: EmptyTransactionsBlockState.ButtonsState.PairButtons, modifier: Modifier = Modifier) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + ) { + ActionButton( + modifier = Modifier.weight(1F), + config = state.firstButtonConfig, + ) + ActionButton( + modifier = Modifier.weight(1F), + config = state.secondButtonConfig, + ) } } @@ -75,8 +106,8 @@ private fun EmptyTransactionBlock_Dark( private class EmptyTransactionBlockStateProvider : CollectionPreviewParameterProvider( collection = listOf( - EmptyTransactionsBlockState.Empty, - EmptyTransactionsBlockState.FailedToLoad(onClick = {}), - EmptyTransactionsBlockState.NotImplemented(onClick = {}), + EmptyTransactionsBlockState.Empty {}, + EmptyTransactionsBlockState.FailedToLoad(onReload = {}, onExplore = {}), + EmptyTransactionsBlockState.NotImplemented(onExplore = {}), ), ) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionsBlockState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionsBlockState.kt index 6aa8c653c2..8254baa582 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionsBlockState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionsBlockState.kt @@ -5,33 +5,62 @@ import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig import com.tangem.core.ui.extensions.TextReference sealed class EmptyTransactionsBlockState( - val actionButtonConfig: ActionButtonConfig? = null, + val buttonsState: ButtonsState, val iconRes: Int, val text: TextReference, ) { - data class FailedToLoad(val onClick: () -> Unit) : EmptyTransactionsBlockState( - actionButtonConfig = ActionButtonConfig( - text = TextReference.Res(R.string.common_reload), - iconResId = R.drawable.ic_refresh_24, - onClick = onClick, - enabled = true, + sealed class ButtonsState { + data class SingleButton(val actionButtonConfig: ActionButtonConfig) : ButtonsState() + data class PairButtons( + val firstButtonConfig: ActionButtonConfig, + val secondButtonConfig: ActionButtonConfig, + ) : ButtonsState() + } + + data class FailedToLoad( + val onReload: () -> Unit, + val onExplore: () -> Unit, + ) : EmptyTransactionsBlockState( + buttonsState = ButtonsState.PairButtons( + firstButtonConfig = ActionButtonConfig( + text = TextReference.Res(R.string.common_reload), + iconResId = R.drawable.ic_refresh_24, + onClick = onReload, + enabled = true, + ), + secondButtonConfig = ActionButtonConfig( + text = TextReference.Res(R.string.common_explore), + iconResId = R.drawable.ic_arrow_top_right_24, + onClick = onExplore, + enabled = true, + ), ), iconRes = R.drawable.ic_alert_history_64, text = TextReference.Res(R.string.transaction_history_error_failed_to_load), ) - object Empty : EmptyTransactionsBlockState( + data class Empty(val onExplore: (() -> Unit)) : EmptyTransactionsBlockState( + buttonsState = ButtonsState.SingleButton( + actionButtonConfig = ActionButtonConfig( + text = TextReference.Res(R.string.common_explore), + iconResId = R.drawable.ic_arrow_top_right_24, + onClick = onExplore, + enabled = true, + ), + ), iconRes = R.drawable.ic_empty_token_64, text = TextReference.Res(R.string.transaction_history_empty_transactions), ) - data class NotImplemented(val onClick: () -> Unit) : EmptyTransactionsBlockState( - actionButtonConfig = ActionButtonConfig( - text = TextReference.Res(R.string.common_explore_transaction_history), - iconResId = R.drawable.ic_arrow_top_right_24, - onClick = onClick, - enabled = true, + data class NotImplemented(val onExplore: () -> Unit) : EmptyTransactionsBlockState( + buttonsState = ButtonsState.SingleButton( + actionButtonConfig = ActionButtonConfig( + text = TextReference.Res(R.string.common_explore_transaction_history), + iconResId = R.drawable.ic_arrow_top_right_24, + onClick = onExplore, + enabled = true, + ), ), iconRes = R.drawable.ic_compass_64, text = TextReference.Res(R.string.transaction_history_not_supported_description), diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TxHistoryState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TxHistoryState.kt index c5cac65a07..6e297a4196 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TxHistoryState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TxHistoryState.kt @@ -15,7 +15,7 @@ sealed interface TxHistoryState { data class Content(val contentItems: MutableStateFlow>) : TxHistoryState /** Empty state */ - object Empty : TxHistoryState + data class Empty(val onExploreClick: () -> Unit) : TxHistoryState /** * Not supported tx history state @@ -33,7 +33,7 @@ sealed interface TxHistoryState { * * @property onReloadClick lambda be invoke when reload button was clicked */ - data class Error(val onReloadClick: () -> Unit) : TxHistoryState + data class Error(val onReloadClick: () -> Unit, val onExploreClick: () -> Unit) : TxHistoryState /** Transactions history item state */ sealed interface TxHistoryItemState { 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..cb2f374b0c 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 @@ -31,8 +31,11 @@ data class TangemDimens internal constructor( val radius14: Dp = 14.dp, val radius16: Dp = 16.dp, val radius18: Dp = 18.dp, + val radius20: Dp = 20.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 +74,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 +86,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..df437d1241 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" } } @@ -219,6 +224,4 @@ private const val NODL_AMOUNT_TO_CREATE_ACCOUNT = 1.5 private val excludedBlockchains = listOf( Blockchain.Unknown, Blockchain.Ducatus, - Blockchain.Telos, - Blockchain.TelosTestnet, ) \ No newline at end of file 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/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenPriceChange.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenPriceChange.kt similarity index 57% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenPriceChange.kt rename to features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenPriceChange.kt index d40de53a30..1b2d04c7a9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenPriceChange.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenPriceChange.kt @@ -1,40 +1,27 @@ -package com.tangem.feature.wallet.presentation.common.component.token +package com.tangem.managetokens.presentation.managetokens.ui.components import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.foundation.layout.* +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.composed import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow -import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SpacerW4 -import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.res.TangemTheme -import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.common.state.TokenItemState -import com.tangem.feature.wallet.presentation.common.state.TokenItemState.PriceChangeState as TokenPriceChangeState +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: TokenPriceChangeState?, modifier: Modifier = Modifier) { +internal fun TokenPriceChange(state: QuotesState, modifier: Modifier = Modifier) { when (state) { - is TokenPriceChangeState.Content -> { - PriceChangeBlock(modifier = modifier, type = state.type, text = state.valueInPercent) - } - is TokenPriceChangeState.Unknown -> { - PriceChangeBlock(modifier = modifier) - } - is TokenPriceChangeState.Loading -> { - RectangleShimmer(modifier = modifier.placeholderSize(), radius = TangemTheme.dimens.radius4) - } - is TokenPriceChangeState.Locked -> { - LockedRectangle(modifier = modifier.placeholderSize()) - } - null -> Unit + is QuotesState.Content -> + PriceChangeBlock(modifier = modifier, type = state.changeType, text = state.priceChange) + QuotesState.Unknown -> PriceChangeBlock(modifier = modifier) } } @@ -51,7 +38,6 @@ private fun PriceChangeBlock(modifier: Modifier = Modifier, type: PriceChangeTyp } } -@OptIn(ExperimentalAnimationApi::class) @Composable private fun PriceChangeIcon(type: PriceChangeType?) { AnimatedContent(targetState = type, label = "Update the price change's arrow") { animatedType -> @@ -73,12 +59,13 @@ private fun PriceChangeIcon(type: PriceChangeType?) { } } -@OptIn(ExperimentalAnimationApi::class) @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 ?: TokenItemState.UNKNOWN_AMOUNT_SIGN, + text = animatedText, color = when (type) { PriceChangeType.UP -> TangemTheme.colors.text.accent PriceChangeType.DOWN -> TangemTheme.colors.text.warning @@ -89,10 +76,4 @@ private fun PriceChangeText(type: PriceChangeType?, text: String?) { style = TangemTheme.typography.body2, ) } -} - -private fun Modifier.placeholderSize(): Modifier = composed { - return@composed this - .padding(vertical = TangemTheme.dimens.spacing3) - .size(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12) } \ 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..61136054f5 --- /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.state.SendUiState +import com.tangem.features.send.impl.presentation.ui.SendScreen +import com.tangem.features.send.impl.presentation.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/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt new file mode 100644 index 0000000000..d60ad8e28b --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt @@ -0,0 +1,59 @@ +package com.tangem.features.send.impl.presentation.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.state.amount.SendAmountStateConverter +import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldChangeConverter +import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter +import com.tangem.features.send.impl.presentation.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/state/SendUiState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt new file mode 100644 index 0000000000..f4c9014b85 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt @@ -0,0 +1,71 @@ +package com.tangem.features.send.impl.presentation.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.state.amount.SendAmountSegmentedButtonsConfig +import com.tangem.features.send.impl.presentation.state.fields.SendTextField +import com.tangem.features.send.impl.presentation.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/state/amount/SendAmountSegmentedButtonsConfig.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountSegmentedButtonsConfig.kt new file mode 100644 index 0000000000..eca219a09d --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountSegmentedButtonsConfig.kt @@ -0,0 +1,21 @@ +package com.tangem.features.send.impl.presentation.state.amount + +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/state/amount/SendAmountStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt new file mode 100644 index 0000000000..3545aeccc6 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt @@ -0,0 +1,64 @@ +package com.tangem.features.send.impl.presentation.state.amount + +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.state.SendUiState +import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter +import com.tangem.features.send.impl.presentation.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/state/fields/SendAmountFieldChangeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt new file mode 100644 index 0000000000..5c91a7489e --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt @@ -0,0 +1,97 @@ +package com.tangem.features.send.impl.presentation.state.fields + +import com.tangem.common.Provider +import com.tangem.features.send.impl.presentation.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/state/fields/SendAmountFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt new file mode 100644 index 0000000000..d6577f14c2 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt @@ -0,0 +1,35 @@ +package com.tangem.features.send.impl.presentation.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.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/state/fields/SendTextField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt new file mode 100644 index 0000000000..f62f981b67 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt @@ -0,0 +1,35 @@ +package com.tangem.features.send.impl.presentation.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/ui/SendAmountContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendAmountContent.kt new file mode 100644 index 0000000000..0cc832db0c --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendAmountContent.kt @@ -0,0 +1,109 @@ +package com.tangem.features.send.impl.presentation.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.state.amount.SendAmountSegmentedButtonsConfig +import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.features.send.impl.presentation.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/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt new file mode 100644 index 0000000000..599b46babc --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt @@ -0,0 +1,94 @@ +package com.tangem.features.send.impl.presentation.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.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/ui/SendScreen.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt new file mode 100644 index 0000000000..34df63ae5a --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt @@ -0,0 +1,53 @@ +package com.tangem.features.send.impl.presentation.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.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/ui/amount/AmountField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt new file mode 100644 index 0000000000..5cd1c54f34 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt @@ -0,0 +1,168 @@ +package com.tangem.features.send.impl.presentation.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.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/ui/amount/AmountFieldContainer.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt new file mode 100644 index 0000000000..0bb6d13de0 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt @@ -0,0 +1,61 @@ +package com.tangem.features.send.impl.presentation.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.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/ui/recipient/ListItemWithIcon.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt new file mode 100644 index 0000000000..2c3bfff4b4 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt @@ -0,0 +1,154 @@ +package com.tangem.features.send.impl.presentation.ui.recipient + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +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.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.MiddleEllipsisText +import com.tangem.core.ui.components.icons.identicon.IdentIcon +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.impl.R + +/** + * Row item with title and subtitle + * + * @param title title + * @param subtitle subtitle + * @param onClick click listener + * @param modifier modifier + * @param subtitleIconRes icon + */ +@Composable +fun ListItemWithIcon( + title: String, + subtitle: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + @DrawableRes subtitleIconRes: Int? = null, +) { + Row( + modifier = modifier + .fillMaxWidth() + .background(TangemTheme.colors.background.action) + .clickable { onClick() } + .padding( + vertical = TangemTheme.dimens.spacing8, + horizontal = TangemTheme.dimens.spacing12, + ), + ) { + IdentIcon( + address = title, + modifier = Modifier + .size(TangemTheme.dimens.size40) + .clip(RoundedCornerShape(TangemTheme.dimens.radius20)), + ) + Column( + modifier = Modifier + .fillMaxWidth() + .padding( + start = TangemTheme.dimens.spacing12, + top = TangemTheme.dimens.spacing2, + bottom = TangemTheme.dimens.spacing2, + ), + ) { + MiddleEllipsisText( + text = title, + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Justify, + modifier = Modifier.fillMaxSize(), + ) + Row { + subtitleIconRes?.let { iconRes -> + Icon( + painter = painterResource(id = iconRes), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + modifier = Modifier + .size(TangemTheme.dimens.size16) + .background(TangemTheme.colors.background.tertiary, CircleShape) + .padding(TangemTheme.dimens.spacing3), + ) + } + Text( + text = subtitle, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + modifier = Modifier + .then( + if (subtitleIconRes != null) { + Modifier.padding(start = TangemTheme.dimens.spacing4) + } else { + Modifier + }, + ), + ) + } + } + } +} + +// region preview +@Preview +@Composable +private fun ListItemWithIconPreview_Light( + @PreviewParameter(ListItemWithIconPreviewProvider::class) config: ListItemWithIconPreviewConfig, +) { + TangemTheme { + ListItemWithIcon( + title = config.title, + subtitle = config.subtitle, + subtitleIconRes = config.iconRes, + onClick = {}, + ) + } +} + +@Preview +@Composable +private fun ListItemWithIconPreview_Dark( + @PreviewParameter(ListItemWithIconPreviewProvider::class) config: ListItemWithIconPreviewConfig, +) { + TangemTheme(isDark = true) { + ListItemWithIcon( + title = config.title, + subtitle = config.subtitle, + subtitleIconRes = config.iconRes, + onClick = {}, + ) + } +} + +private data class ListItemWithIconPreviewConfig( + val title: String, + val subtitle: String, + val iconRes: Int? = null, +) + +private class ListItemWithIconPreviewProvider : CollectionPreviewParameterProvider( + collection = listOf( + ListItemWithIconPreviewConfig( + title = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE", + subtitle = "Wallet", + iconRes = R.drawable.ic_arrow_down_24, + ), + ListItemWithIconPreviewConfig( + title = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE", + subtitle = "Wallet", + ), + ), +) +//endregion \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFields.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFields.kt new file mode 100644 index 0000000000..1b9118851a --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFields.kt @@ -0,0 +1,380 @@ +package com.tangem.features.send.impl.presentation.ui.recipient + +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.foundation.text.BasicTextField +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.Icon +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.CenterVertically +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import com.tangem.core.ui.components.SpacerH8 +import com.tangem.core.ui.components.icons.identicon.IdentIcon +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.R + +@Composable +internal fun TextFieldWithPasteAndIcon( + value: String, + placeholder: TextReference, + label: TextReference, + onValueChange: (String) -> Unit, + onPasteClick: (String) -> Unit, + modifier: Modifier = Modifier, + footer: String? = null, + singleLine: Boolean = false, +) { + FooterContainer(modifier, footer) { + Column( + modifier = Modifier + .fillMaxWidth() + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ), + ) { + Text( + text = label.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + top = TangemTheme.dimens.spacing12, + ), + ) + Row { + IdentIcon( + address = value, + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing16, + top = TangemTheme.dimens.spacing8, + bottom = TangemTheme.dimens.spacing10, + ) + .clip(RoundedCornerShape(TangemTheme.dimens.radius20)) + .size(TangemTheme.dimens.size40) + .background(TangemTheme.colors.background.tertiary), + ) + SimpleTextField( + value = value, + placeholder = placeholder, + onValueChange = onValueChange, + singleLine = singleLine, + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing12, + top = TangemTheme.dimens.spacing8, + bottom = TangemTheme.dimens.spacing10, + ) + .weight(1f) + .align(CenterVertically), + ) + PasteButton( + isPasteButtonVisible = value.isBlank(), + onClick = onPasteClick, + modifier = Modifier + .align(CenterVertically) + .padding( + start = TangemTheme.dimens.spacing4, + end = TangemTheme.dimens.spacing16, + ), + ) + } + } + } +} + +@Composable +internal fun TextFieldWithPaste( + value: String, + placeholder: TextReference, + label: TextReference, + onValueChange: (String) -> Unit, + onPasteClick: (String) -> Unit, + modifier: Modifier = Modifier, + footer: String? = null, +) { + FooterContainer(modifier, footer) { + Row( + modifier = Modifier + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ), + ) { + Column( + modifier = Modifier + .weight(1f) + .padding(TangemTheme.dimens.spacing12), + ) { + Text( + text = label.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + SimpleTextField( + value = value, + placeholder = placeholder, + onValueChange = onValueChange, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing6), + ) + } + PasteButton( + isPasteButtonVisible = value.isBlank(), + onClick = onPasteClick, + modifier = Modifier + .align(CenterVertically) + .padding(end = TangemTheme.dimens.spacing16), + ) + } + } +} + +@Composable +internal fun TextFieldWithInfo( + value: String, + label: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + info: TextReference? = null, + footer: String? = null, +) { + FooterContainer( + footer = footer, + footerTopPadding = TangemTheme.dimens.spacing6, + modifier = modifier, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ) + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + top = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing14, + ), + ) { + Text( + text = label, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + Row { + SimpleTextField( + value = value, + onValueChange = onValueChange, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing6) + .weight(1f), + ) + info?.let { + Text( + text = it.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing8) + .align(Alignment.Bottom), + ) + } + } + } + } +} + +@Composable +private fun FooterContainer( + modifier: Modifier = Modifier, + footer: String? = null, + footerTopPadding: Dp = TangemTheme.dimens.spacing8, + content: @Composable () -> Unit, +) { + Column(modifier = modifier) { + content() + footer?.let { + Text( + text = it, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier + .padding(top = footerTopPadding), + ) + } + } +} + +@Composable +private fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifier: Modifier = Modifier) { + val clipboardManager = LocalClipboardManager.current + val hapticFeedback = LocalHapticFeedback.current + + if (isPasteButtonVisible) { + Box(modifier = modifier) { + Text( + text = "Paste", + style = TangemTheme.typography.button, + color = TangemTheme.colors.text.primary2, + modifier = Modifier + .background( + color = TangemTheme.colors.button.primary, + shape = TangemTheme.shapes.roundedCornersXMedium, + ) + .padding( + horizontal = TangemTheme.dimens.spacing10, + vertical = TangemTheme.dimens.spacing2, + ) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(radius = TangemTheme.dimens.radius8), + onClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + onClick( + clipboardManager + .getText() + ?.toString() + .orEmpty(), + ) + }, + ), + ) + } + } else { + Icon( + painter = painterResource(id = R.drawable.ic_close_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = stringResource(R.string.common_close), + modifier = modifier + .size(TangemTheme.dimens.size20) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(radius = TangemTheme.dimens.radius10), + onClick = { onClick("") }, + ), + ) + } +} + +@Composable +private fun SimpleTextField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + placeholder: TextReference? = null, + singleLine: Boolean = false, +) { + val focusRequester = remember { FocusRequester() } + BasicTextField( + value = value, + onValueChange = onValueChange, + textStyle = TangemTheme.typography.body2, + cursorBrush = SolidColor(TangemTheme.colors.text.primary1), + singleLine = singleLine, + decorationBox = { textValue -> + Box { + if (value.isBlank() && placeholder != null) { + Text( + text = placeholder.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.disabled, + modifier = Modifier, + ) + } + textValue() + } + }, + modifier = modifier + .focusRequester(focusRequester), + ) +} + +//region preview +@Preview +@Composable +private fun TextFieldPreview_Light() { + TangemTheme { + Column { + TextFieldWithPaste( + value = "", + label = TextReference.Res(R.string.send_recipient), + placeholder = TextReference.Res(R.string.send_enter_address_field), + onValueChange = {}, + onPasteClick = {}, + ) + SpacerH8() + TextFieldWithPasteAndIcon( + value = "", + label = TextReference.Res(R.string.send_extras_hint_memo), + placeholder = TextReference.Res(R.string.send_optional_field), + onValueChange = {}, + onPasteClick = {}, + ) + SpacerH8() + TextFieldWithInfo( + value = "Text", + label = stringResource(R.string.send_extras_hint_memo), + info = TextReference.Res(R.string.send_optional_field), + footer = stringResource(R.string.send_max_fee), + onValueChange = {}, + ) + } + } +} + +@Preview +@Composable +private fun TextFieldPreview_Dark() { + TangemTheme(isDark = true) { + Column { + TextFieldWithPaste( + value = "", + label = TextReference.Res(R.string.send_recipient), + placeholder = TextReference.Res(R.string.send_enter_address_field), + onValueChange = {}, + onPasteClick = {}, + ) + SpacerH8() + TextFieldWithPasteAndIcon( + value = "", + label = TextReference.Res(R.string.send_extras_hint_memo), + placeholder = TextReference.Res(R.string.send_optional_field), + onValueChange = {}, + onPasteClick = {}, + ) + SpacerH8() + TextFieldWithInfo( + value = "Text", + label = stringResource(R.string.send_extras_hint_memo), + info = TextReference.Res(R.string.send_optional_field), + footer = stringResource(R.string.send_max_fee), + onValueChange = {}, + ) + } + } +} +//endregion \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt new file mode 100644 index 0000000000..118d166c69 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt @@ -0,0 +1,14 @@ +package com.tangem.features.send.impl.presentation.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/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt new file mode 100644 index 0000000000..274feef98e --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -0,0 +1,154 @@ +package com.tangem.features.send.impl.presentation.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.state.SendStateFactory +import com.tangem.features.send.impl.presentation.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/data/build.gradle.kts b/features/swap/data/build.gradle.kts index f91ab4c7c0..67d016525d 100644 --- a/features/swap/data/build.gradle.kts +++ b/features/swap/data/build.gradle.kts @@ -15,6 +15,8 @@ dependencies { /** Network */ implementation(deps.retrofit) + implementation(deps.moshi) + implementation(deps.moshi.kotlin) /** Domain */ implementation(projects.domain.tokens.models) diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/QuotesConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/QuotesConverter.kt index f0f9160171..4352341538 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/QuotesConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/QuotesConverter.kt @@ -9,11 +9,7 @@ class QuotesConverter : Converter { override fun convert(value: QuoteResponse): QuoteModel { return QuoteModel( - fromTokenAmount = createFromAmountWithOffset(value.fromTokenAmount, value.fromToken.decimals), toTokenAmount = createFromAmountWithOffset(value.toTokenAmount, value.toToken.decimals), - fromTokenAddress = value.fromToken.address, - toTokenAddress = value.toToken.address, - estimatedGas = value.estimatedGas, ) } } \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapConverter.kt index c441455fb7..3c8c1003ec 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapConverter.kt @@ -11,9 +11,6 @@ class SwapConverter : Converter { override fun convert(value: SwapResponse): SwapDataModel { return SwapDataModel( - fromTokenAddress = value.fromToken.address, - toTokenAddress = value.toToken.address, - fromTokenAmount = createFromAmountWithOffset(value.fromTokenAmount, value.fromToken.decimals), toTokenAmount = createFromAmountWithOffset(value.toTokenAmount, value.toToken.decimals), transaction = convertTransaction(value.transaction), ) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index 26d332d53b..56c1766da8 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -99,6 +99,8 @@ interface SwapInteractor { fun isAvailableToSwap(networkId: String): Boolean + fun getSwapAmountForToken(amount: String, token: Currency): SwapAmount + suspend fun checkFeeIsEnough( fee: BigDecimal?, spendAmount: SwapAmount, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 7801531e8d..d76a2d777e 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -215,11 +215,12 @@ internal class SwapInteractorImpl @Inject constructor( amountToSwap: String, fee: TxFee, ): TxState { - val amount = requireNotNull(toBigDecimalOrNull(amountToSwap)) { "wrong amount format, use only digits" } + val amountDecimal = requireNotNull(toBigDecimalOrNull(amountToSwap)) { "wrong amount format" } + val amount = SwapAmount(amountDecimal, getTokenDecimals(currencyToSend)) val result = transactionManager.sendTransaction( txData = SwapTxData( networkId = networkId, - amountToSend = amount, + amountToSend = amountDecimal, currencyToSend = swapCurrencyConverter.convert(currencyToSend), feeAmount = fee.feeValue, gasLimit = fee.gasLimit, @@ -242,7 +243,7 @@ internal class SwapInteractorImpl @Inject constructor( } TxState.TxSent( fromAmount = amountFormatter.formatSwapAmountToUI( - swapStateData.swapModel.fromTokenAmount, + amount, currencyToSend.symbol, ), toAmount = amountFormatter.formatSwapAmountToUI( @@ -272,6 +273,11 @@ internal class SwapInteractorImpl @Inject constructor( return ONE_INCH_SUPPORTED_NETWORKS.contains(networkId) } + override fun getSwapAmountForToken(amount: String, token: Currency): SwapAmount { + val amountDecimal = requireNotNull(toBigDecimalOrNull(amount)) { "wrong amount format" } + return SwapAmount(amountDecimal, getTokenDecimals(token)) + } + private suspend fun onSuccessLegacyFlow(currency: Currency) { userWalletManager.addToken(swapCurrencyConverter.convert(currency), derivationPath) userWalletManager.refreshWallet() @@ -412,7 +418,7 @@ internal class SwapInteractorImpl @Inject constructor( networkId = networkId, fromToken = fromToken, toToken = toToken, - fromTokenAmount = quoteDataModel.fromTokenAmount, + fromTokenAmount = amount, toTokenAmount = quoteDataModel.toTokenAmount, swapStateData = null, ) @@ -494,7 +500,7 @@ internal class SwapInteractorImpl @Inject constructor( networkId = networkId, fromToken = fromToken, toToken = toToken, - fromTokenAmount = swapData.fromTokenAmount, + fromTokenAmount = amount, toTokenAmount = swapData.toTokenAmount, swapStateData = SwapStateData( fee = txFeeState, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/QuoteModel.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/QuoteModel.kt index 871b9d77ea..5e5d782666 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/QuoteModel.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/QuoteModel.kt @@ -5,16 +5,8 @@ import com.tangem.feature.swap.domain.models.SwapAmount /** * Quote model holds data about current amounts of exchange and fees * - * @property fromTokenAmount amount of token you want to exchange * @property toTokenAmount amount of token you want to receive - * @property fromTokenAddress address token you want to exchange - * @property toTokenAddress address token you want to receive - * @property estimatedGas fee */ data class QuoteModel( - val fromTokenAmount: SwapAmount, val toTokenAmount: SwapAmount, - val fromTokenAddress: String, - val toTokenAddress: String, - val estimatedGas: Int, ) \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapDataModel.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapDataModel.kt index e2b5356100..7c15701329 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapDataModel.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapDataModel.kt @@ -5,15 +5,10 @@ import com.tangem.feature.swap.domain.models.SwapAmount /** * Swap transaction model * - * @property fromTokenAddress token from which want to convert - * @property toTokenAddress token to want to convert * @property toTokenAmount amount "target" token - * @property fromTokenAmount amount "initial" token + * @property transaction info about transaction */ data class SwapDataModel( - val fromTokenAddress: String, - val toTokenAddress: String, val toTokenAmount: SwapAmount, - val fromTokenAmount: SwapAmount, val transaction: TransactionModel, ) \ 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/ui/SwapScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt index bfd13ad8c8..850d4434df 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt @@ -34,6 +34,12 @@ internal fun SwapScreen(stateHolder: SwapStateHolder) { }, ) + LaunchedEffect(bottomSheetState.targetValue) { + if (bottomSheetState.targetValue == ModalBottomSheetValue.Hidden) { + stateHolder.onCancelPermissionBottomSheet.invoke() + } + } + ModalBottomSheetLayout( modifier = Modifier.systemBarsPadding(), sheetContent = { 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..4740549f50 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() { @@ -480,9 +471,11 @@ internal class SwapViewModel @Inject constructor( onBackClicked = { onSearchEntered("") }, onMaxAmountSelected = { onMaxAmountClicked() }, openPermissionBottomSheet = { + singleTaskScheduler.cancelTask() analyticsEventHandler.send(SwapEvents.ButtonGivePermissionClicked) }, hidePermissionBottomSheet = { + startLoadingQuotesFromLastState() analyticsEventHandler.send(SwapEvents.ButtonPermissionCancelClicked) }, onAmountSelected = { onAmountSelected(it) }, @@ -491,8 +484,10 @@ internal class SwapViewModel @Inject constructor( }, onSelectItemFee = { feeItem -> dataState = dataState.copy(selectedFee = feeItem.data) - val spendAmount = dataState.swapDataModel?.swapModel?.fromTokenAmount - ?: dataState.approveDataModel?.fromTokenAmount + val spendAmount = dataState.amount?.let { amount -> + val fromToken = dataState.fromCurrency ?: return@let null + swapInteractor.getSwapAmountForToken(amount, fromToken) + } ?: dataState.approveDataModel?.fromTokenAmount spendAmount ?: return@UiActions val fromToken = dataState.fromCurrency ?: return@UiActions viewModelScope.launch(dispatchers.io) { 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/state/factory/txhistory/TokenDetailsLoadedTxHistoryConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadedTxHistoryConverter.kt index b5675ea9f0..b4ba123b6f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadedTxHistoryConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadedTxHistoryConverter.kt @@ -34,7 +34,10 @@ internal class TokenDetailsLoadedTxHistoryConverter( private fun convertError(error: TxHistoryListError): TxHistoryState { return when (error) { is TxHistoryListError.DataError -> { - TxHistoryState.Error(onReloadClick = clickIntents::onReloadClick) + TxHistoryState.Error( + onReloadClick = clickIntents::onReloadClick, + onExploreClick = clickIntents::onExploreClick, + ) } } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadingTxHistoryConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadingTxHistoryConverter.kt index 6aeef3d745..6ba1f4cf21 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadingTxHistoryConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadingTxHistoryConverter.kt @@ -32,8 +32,11 @@ internal class TokenDetailsLoadingTxHistoryConverter( ): TokenDetailsState { return currentStateProvider().copy( txHistoryState = when (error) { - is TxHistoryStateError.EmptyTxHistories -> TxHistoryState.Empty - is TxHistoryStateError.DataError -> TxHistoryState.Error(onReloadClick = clickIntents::onReloadClick) + is TxHistoryStateError.EmptyTxHistories -> TxHistoryState.Empty(clickIntents::onExploreClick) + is TxHistoryStateError.DataError -> TxHistoryState.Error( + onReloadClick = clickIntents::onReloadClick, + onExploreClick = clickIntents::onExploreClick, + ) is TxHistoryStateError.TxHistoryNotImplemented -> { TxHistoryState.NotSupported( pendingTransactions = pendingTransactions.toImmutableList(), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt index 7ccdf694b5..8b48d54da4 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorMatrix @@ -48,7 +49,9 @@ internal fun TokenInfoBlock(state: TokenInfoBlockState, modifier: Modifier = Mod } } CurrencyIcon( - modifier = Modifier.size(TangemTheme.dimens.size48), + modifier = Modifier + .size(TangemTheme.dimens.size48) + .clip(TangemTheme.shapes.roundedCorners8), icon = state.iconState, alpha = alpha, colorFilter = colorFilter, 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..e4f7243ad9 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 @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.common import androidx.paging.PagingData import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.currency.tokenicon.TokenIconState import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.marketprice.PriceChangeState import com.tangem.core.ui.components.marketprice.PriceChangeType @@ -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, @@ -120,7 +121,7 @@ internal object WalletPreviewData { titleState = TokenItemState.TitleState.Content(text = "Polygon", hasPending = true), fiatAmountState = TokenItemState.FiatAmountState.Content(text = "321 $"), cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "5,412 MATIC"), - priceChangeState = TokenItemState.PriceChangeState.Unknown, + cryptoPriceState = TokenItemState.CryptoPriceState.Unknown, onItemClick = {}, onItemLongClick = {}, ) @@ -140,8 +141,9 @@ internal object WalletPreviewData { titleState = TokenItemState.TitleState.Content(text = "Polygon"), fiatAmountState = TokenItemState.FiatAmountState.Content(text = "321 $"), cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "5,412 MATIC"), - priceChangeState = TokenItemState.PriceChangeState.Content( - valueInPercent = "2.0%", + cryptoPriceState = TokenItemState.CryptoPriceState.Content( + price = "312 USD", + priceChangePercent = "2.0%", type = PriceChangeType.UP, ), onItemClick = {}, 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..ac0fac75db 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 @@ -13,21 +13,21 @@ 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.Constraints +import com.tangem.core.ui.components.currency.tokenicon.TokenIcon import com.tangem.core.ui.components.marketprice.PriceChangeType 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 -private const val TITLE_MIN_WIDTH_COEFFICIENT = 0.22 -private const val PRICE_CHANGE_MIN_WIDTH_COEFFICIENT = 0.16 +private const val TITLE_MIN_WIDTH_COEFFICIENT = 0.3 +private const val PRICE_MIN_WIDTH_COEFFICIENT = 0.32 private enum class LayoutId { - ICON, TITLE, FIAT_AMOUNT, CRYPTO_AMOUNT, PRICE_CHANGE, NON_FIAT_CONTENT + ICON, TITLE, FIAT_AMOUNT, CRYPTO_AMOUNT, CRYPTO_PRICE, NON_FIAT_CONTENT } @Composable @@ -37,6 +37,8 @@ internal fun TokenItem( modifier: Modifier = Modifier, reorderableTokenListState: ReorderableLazyListState? = null, ) { + val betweenRowsMargin = TangemTheme.dimens.spacing2 + CustomContainer( state = state, modifier = modifier @@ -50,7 +52,7 @@ internal fun TokenItem( modifier = Modifier .layoutId(layoutId = LayoutId.TITLE) .padding(horizontal = TangemTheme.dimens.spacing8) - .padding(bottom = TangemTheme.dimens.spacing2), + .padding(bottom = betweenRowsMargin), ) TokenFiatAmount( @@ -58,20 +60,20 @@ internal fun TokenItem( isBalanceHidden = isBalanceHidden, modifier = Modifier .layoutId(layoutId = LayoutId.FIAT_AMOUNT) - .padding(bottom = TangemTheme.dimens.spacing2), + .padding(bottom = betweenRowsMargin), + ) + + TokenPrice( + state = state.cryptoPriceState, + modifier = Modifier + .layoutId(layoutId = LayoutId.CRYPTO_PRICE) + .padding(horizontal = TangemTheme.dimens.spacing8), ) TokenCryptoAmount( state = state.cryptoAmountState, isBalanceHidden = isBalanceHidden, - modifier = Modifier - .layoutId(layoutId = LayoutId.CRYPTO_AMOUNT) - .padding(horizontal = TangemTheme.dimens.spacing8), - ) - - TokenPriceChange( - state = state.priceChangeState, - modifier = Modifier.layoutId(layoutId = LayoutId.PRICE_CHANGE), + modifier = Modifier.layoutId(layoutId = LayoutId.CRYPTO_AMOUNT), ) NonFiatContentBlock( @@ -117,10 +119,10 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier val layoutWidth = constraints.maxWidth val layoutPadding = with(density) { dimens.size14.roundToPx() } - val layoutWidthWithPaddings = layoutWidth - 2 * layoutPadding + val layoutWidthWithoutPaddings = layoutWidth - 2 * layoutPadding val titleMinWidth = (layoutWidth * TITLE_MIN_WIDTH_COEFFICIENT).toInt() - val priceChangeMinWidth = (layoutWidth * PRICE_CHANGE_MIN_WIDTH_COEFFICIENT).toInt() + val priceChangeMinWidth = (layoutWidth * PRICE_MIN_WIDTH_COEFFICIENT).toInt() val icon = measurables.measure(layoutId = LayoutId.ICON, constraints = constraints) @@ -154,32 +156,32 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier -> { fiatAmount = measurables.measureFiatAmount( state = state, - maxWidth = layoutWidthWithPaddings - icon.width - titleMinWidth, + maxWidth = layoutWidthWithoutPaddings - icon.width - titleMinWidth, defaultConstraints = constraints, ) cryptoAmount = measurables.measureCryptoAmount( state = state, - maxWidth = layoutWidthWithPaddings - icon.width - priceChangeMinWidth, + maxWidth = layoutWidthWithoutPaddings - icon.width - priceChangeMinWidth, defaultConstraints = constraints, ) - firstRowRemainingFreeSpace = layoutWidthWithPaddings - icon.width - fiatAmount.width - secondRowRemainingFreeSpace = layoutWidthWithPaddings - icon.width - cryptoAmount.width + firstRowRemainingFreeSpace = layoutWidthWithoutPaddings - icon.width - fiatAmount.width + secondRowRemainingFreeSpace = layoutWidthWithoutPaddings - icon.width - cryptoAmount.width } is TokenItemState.Draggable -> { cryptoAmount = measurables.measureCryptoAmount( state = state, - maxWidth = layoutWidthWithPaddings - icon.width - nonFiatContent.width, + maxWidth = layoutWidthWithoutPaddings - icon.width - nonFiatContent.width, defaultConstraints = constraints, ) - firstRowRemainingFreeSpace = layoutWidthWithPaddings - icon.width - nonFiatContent.width + firstRowRemainingFreeSpace = layoutWidthWithoutPaddings - icon.width - nonFiatContent.width } is TokenItemState.NoAddress, is TokenItemState.Unreachable, -> { - firstRowRemainingFreeSpace = layoutWidthWithPaddings - icon.width - nonFiatContent.width + firstRowRemainingFreeSpace = layoutWidthWithoutPaddings - icon.width - nonFiatContent.width } } @@ -223,18 +225,18 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier }, ) - cryptoAmount?.placeRelative( - x = layoutPadding + icon.width, - y = layoutHeight - cryptoAmount.height - layoutPadding, - ) - fiatAmount?.placeRelative(x = layoutWidth - fiatAmount.width - layoutPadding, y = layoutPadding) priceChange?.placeRelative( - x = layoutWidth - priceChange.width - layoutPadding, + x = layoutPadding + icon.width, y = layoutHeight - priceChange.height - layoutPadding, ) + cryptoAmount?.placeRelative( + x = layoutWidth - cryptoAmount.width - layoutPadding, + y = layoutHeight - cryptoAmount.height - layoutPadding, + ) + nonFiatContent.placeRelative( x = layoutWidth - nonFiatContent.width - layoutPadding, y = (layoutHeight - nonFiatContent.height).div(other = 2), @@ -301,7 +303,7 @@ private fun List.measurePriceChange( defaultConstraints: Constraints, ): Placeable { return measure( - layoutId = LayoutId.PRICE_CHANGE, + layoutId = LayoutId.CRYPTO_PRICE, constraints = when (state) { is TokenItemState.Content, -> createDynamicConstrains(minWidth = minWidth, remainingFreeSpace = remainingFreeSpace) @@ -377,7 +379,7 @@ private fun calculateLayoutHeight( @Preview(widthDp = 360) @Composable -private fun Preview_CustomTokenItem_InLight(@PreviewParameter(TokenItemStateProvider::class) state: TokenItemState) { +private fun Preview_TokenItem_InLight(@PreviewParameter(TokenItemStateProvider::class) state: TokenItemState) { TangemTheme(isDark = false) { TokenItem(state = state, isBalanceHidden = false) } @@ -393,8 +395,9 @@ private class TokenItemStateProvider : CollectionPreviewParameterProvider { + PriceBlock( + modifier = modifier, + price = state.price, + type = state.type, + priceChangePercent = state.priceChangePercent, + ) + } + is TokenPriceChangeState.Unknown -> { + PriceText(text = TokenItemState.UNKNOWN_AMOUNT_SIGN, modifier = modifier) + } + is TokenPriceChangeState.Loading -> { + RectangleShimmer(modifier = modifier.placeholderSize(), radius = TangemTheme.dimens.radius4) + } + is TokenPriceChangeState.Locked -> { + LockedRectangle(modifier = modifier.placeholderSize()) + } + null -> Unit + } +} + +@Composable +private fun PriceBlock( + price: String, + modifier: Modifier = Modifier, + type: PriceChangeType? = null, + priceChangePercent: String? = null, +) { + Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { + PriceText(text = price, modifier = Modifier.weight(weight = 1f, fill = false)) + + SpacerW6() + + if (type != null) { + PriceChangeIcon(type = type) + SpacerW4() + } + + PriceChangeText(type = type, text = priceChangePercent) + } +} + +@Composable +private fun PriceText(text: String, modifier: Modifier = Modifier) { + AnimatedContent(targetState = text, label = "Update the price text", modifier = modifier) { animatedText -> + Text( + text = animatedText, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = TangemTheme.typography.caption2, + ) + } +} + +@Composable +private fun PriceChangeIcon(type: PriceChangeType) { + AnimatedContent(targetState = type, label = "Update the price change's arrow") { animatedType -> + 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?, modifier: Modifier = Modifier) { + AnimatedContent(targetState = text, modifier = modifier, label = "Update the price change's text") { animatedText -> + Text( + text = animatedText ?: TokenItemState.UNKNOWN_AMOUNT_SIGN, + color = when (type) { + PriceChangeType.UP -> TangemTheme.colors.text.accent + PriceChangeType.DOWN -> TangemTheme.colors.text.warning + null -> TangemTheme.colors.text.tertiary + }, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = TangemTheme.typography.caption2, + ) + } +} + +private fun Modifier.placeholderSize(): Modifier = composed { + return@composed this + .padding(vertical = TangemTheme.dimens.spacing2) + .size(width = TangemTheme.dimens.size52, height = TangemTheme.dimens.size12) +} \ No newline at end of file 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..80fdc10cdc 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,8 +1,7 @@ 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.currency.tokenicon.TokenIconState import com.tangem.core.ui.components.marketprice.PriceChangeType /** Token item state */ @@ -11,7 +10,7 @@ internal sealed class TokenItemState { abstract val id: String - abstract val iconState: IconState + abstract val iconState: TokenIconState abstract val titleState: TitleState @@ -19,26 +18,26 @@ internal sealed class TokenItemState { abstract val cryptoAmountState: CryptoAmountState? - abstract val priceChangeState: PriceChangeState? + abstract val cryptoPriceState: CryptoPriceState? /** 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 override val cryptoAmountState: CryptoAmountState = CryptoAmountState.Loading - override val priceChangeState: PriceChangeState = PriceChangeState.Loading + override val cryptoPriceState: CryptoPriceState = CryptoPriceState.Loading } /** 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 - override val priceChangeState: PriceChangeState = PriceChangeState.Locked + override val cryptoPriceState: CryptoPriceState = CryptoPriceState.Locked } /** @@ -52,11 +51,11 @@ 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, - override val priceChangeState: PriceChangeState?, + override val cryptoPriceState: CryptoPriceState, val onItemClick: () -> Unit, val onItemLongClick: () -> Unit, ) : TokenItemState() @@ -70,12 +69,12 @@ 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() { override val fiatAmountState: FiatAmountState? = null - override val priceChangeState: PriceChangeState? = null + override val cryptoPriceState: CryptoPriceState? = null } /** @@ -89,14 +88,14 @@ 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, ) : TokenItemState() { override val fiatAmountState: FiatAmountState? = null override val cryptoAmountState: CryptoAmountState? = null - override val priceChangeState: PriceChangeState? = null + override val cryptoPriceState: CryptoPriceState? = null } /** @@ -109,91 +108,13 @@ 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() { override val fiatAmountState: FiatAmountState? = null override val cryptoAmountState: CryptoAmountState? = null - 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 - } + override val cryptoPriceState: CryptoPriceState? = null } @Immutable @@ -226,15 +147,19 @@ internal sealed class TokenItemState { object Locked : CryptoAmountState() } - sealed class PriceChangeState { + sealed class CryptoPriceState { - data class Content(val valueInPercent: String, val type: PriceChangeType) : PriceChangeState() + data class Content( + val price: String, + val priceChangePercent: String?, + val type: PriceChangeType?, + ) : CryptoPriceState() - object Unknown : PriceChangeState() + object Unknown : CryptoPriceState() - object Loading : PriceChangeState() + object Loading : CryptoPriceState() - object Locked : PriceChangeState() + object Locked : CryptoPriceState() } companion object { 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/state/factory/WalletUpdateCardCountConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletUpdateCardCountConverter.kt index 59a45b0f28..62adad9aec 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletUpdateCardCountConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletUpdateCardCountConverter.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.factory import com.tangem.common.Provider import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory +import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount import com.tangem.feature.wallet.presentation.wallet.state.WalletState import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState @@ -27,16 +28,16 @@ internal class WalletUpdateCardCountConverter( } private fun WalletsListConfig.refreshCardCount(): WalletsListConfig { + val selectedWallet = currentWalletProvider() return copy( wallets = wallets .mapIndexed { index, walletCard -> if (index == selectedWalletIndex) { when (walletCard) { is WalletCardState.Content -> walletCard.copy( - additionalInfo = WalletAdditionalInfoFactory.resolve( - wallet = currentWalletProvider(), - ), - cardCount = currentWalletProvider().getCardsCount(), + additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = selectedWallet), + imageResId = WalletImageResolver.resolve(userWallet = selectedWallet), + cardCount = selectedWallet.getCardsCount(), ) else -> walletCard } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt index 202fcc3ae5..d9154f422e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt @@ -47,7 +47,10 @@ internal class WalletLoadedTxHistoryConverter( state.copy( txHistoryState = when (error) { is TxHistoryListError.DataError -> { - TxHistoryState.Error(onReloadClick = clickIntents::onReloadClick) + TxHistoryState.Error( + onReloadClick = clickIntents::onReloadClick, + onExploreClick = clickIntents::onExploreClick, + ) } }, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadingTxHistoryConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadingTxHistoryConverter.kt index 6bbd84b5d3..adb05dd819 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadingTxHistoryConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadingTxHistoryConverter.kt @@ -52,8 +52,11 @@ internal class WalletLoadingTxHistoryConverter( return if (state is WalletSingleCurrencyState.Content) { state.copy( txHistoryState = when (error) { - is TxHistoryStateError.EmptyTxHistories -> Empty - is TxHistoryStateError.DataError -> Error(onReloadClick = clickIntents::onReloadClick) + is TxHistoryStateError.EmptyTxHistories -> Empty(onExploreClick = clickIntents::onExploreClick) + is TxHistoryStateError.DataError -> Error( + onReloadClick = clickIntents::onReloadClick, + onExploreClick = clickIntents::onExploreClick, + ) is TxHistoryStateError.TxHistoryNotImplemented -> { NotSupported( pendingTransactions = txHistoryItemConverter.convertList(pendingTransactions) 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..9ae6cfcdfb 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 @@ -1,12 +1,12 @@ package com.tangem.feature.wallet.presentation.wallet.utils import com.tangem.common.Provider +import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.marketprice.PriceChangeType 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.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import com.tangem.utils.converter.Converter import java.math.BigDecimal @@ -53,7 +53,7 @@ internal class CryptoCurrencyStatusToTokenItemConverter( text = getFormattedFiatAmount(), ), cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = getFormattedAmount()), - priceChangeState = getPriceChangeConfig(), + cryptoPriceState = getCryptoPriceState(), onItemClick = { clickIntents.onTokenItemClick(currency) }, onItemLongClick = { clickIntents.onTokenItemLongClick(cryptoCurrencyStatus = this) }, ) @@ -87,12 +87,14 @@ internal class CryptoCurrencyStatusToTokenItemConverter( onItemLongClick = { clickIntents.onTokenItemLongClick(cryptoCurrencyStatus = this) }, ) - private fun CryptoCurrencyStatus.getPriceChangeConfig(): TokenItemState.PriceChangeState { + private fun CryptoCurrencyStatus.getCryptoPriceState(): TokenItemState.CryptoPriceState { + val fiatRate = value.fiatRate val priceChange = value.priceChange - return if (priceChange != null) { - TokenItemState.PriceChangeState.Content( - valueInPercent = BigDecimalFormatter.formatPercent( + return if (fiatRate != null && priceChange != null) { + TokenItemState.CryptoPriceState.Content( + price = fiatRate.getFormattedCryptoPrice(), + priceChangePercent = BigDecimalFormatter.formatPercent( percent = priceChange, useAbsoluteValue = true, maxFractionDigits = 1, @@ -101,10 +103,19 @@ internal class CryptoCurrencyStatusToTokenItemConverter( type = priceChange.getPriceChangeType(), ) } else { - TokenItemState.PriceChangeState.Unknown + TokenItemState.CryptoPriceState.Unknown } } + private fun BigDecimal.getFormattedCryptoPrice(): String { + val appCurrency = appCurrencyProvider() + return BigDecimalFormatter.formatFiatAmount( + fiatAmount = this, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } + private fun BigDecimal.getPriceChangeType(): PriceChangeType { return if (this > BigDecimal.ZERO) PriceChangeType.UP else PriceChangeType.DOWN } 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..7befcd6495 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -20,6 +20,7 @@ androidxFragment = "1.5.3" androidxLifecycle = "2.5.1" androidx-paging = "3.1.1" androidx-palette = "1.0.0" +androidx-datastore = "1.0.0" # endregion AndroidX # region Compose @@ -67,7 +68,8 @@ spongycastleCryptoCore = "1.58.0.0" timber = "4.7.1" viewBindingDelegate = "1.5.9" xmlShimmer = "1.1.3" -zxingQrBarcodeScanner = "1.9.8" +zendeskChat = "3.3.5" +zendeskMessaging = "5.2.4" zxingQrCode = "3.5.1" mviCore = "1.3.1" kotlinSerialization = "1.4.1" @@ -77,13 +79,18 @@ walletConnectCore = "1.18.0" walletConnectWeb3 = "1.11.0" prettyLogger = "2.2.0" okHttp-prettyLogging = "3.1.0" +chucker = "4.0.0" +mlKit-barcodeScanning = "17.2.0" +androidXCamera = "1.3.0" +listenableFuture = "1.0" +swipeRefreshLayout = "1.1.0" spr-client = "3.6.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.0-379" +tangemBlockchainSdk = "release-app_5.2-385" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.0-309" +tangemCardSdk = "release-app_5.2-311" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds # endregion Tangem @@ -130,11 +137,13 @@ androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx androidx-core-splashScreen = { module = "androidx.core:core-splashscreen", version.ref = "androidxSplashScreen" } androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "androidxFragment" } androidx-paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "androidx-paging" } +androidx-swipeRefreshLayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "swipeRefreshLayout" } +androidx-palette = { module = "androidx.palette:palette", version.ref = "androidx-palette" } lifecycle-common-java8 = { module = "androidx.lifecycle:lifecycle-common-java8", version.ref = "androidxLifecycle" } lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidxLifecycle" } lifecycle-viewModel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidxLifecycle" } lifecycle-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "compoese-lifecycle-runtime" } -androidx-palette = { module = "androidx.palette:palette", version.ref = "androidx-palette" } +androidx-datastore = { module = "androidx.datastore:datastore-preferences", version.ref = "androidx-datastore" } # region AndroidX # region Compose @@ -217,7 +226,8 @@ retrofit-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.re timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } viewBindingDelegate = { module = "com.github.kirich1409:viewbindingpropertydelegate-noreflection", version.ref = "viewBindingDelegate" } xmlShimmer = { module = "com.github.skydoves:androidveil", version.ref = "xmlShimmer" } -zxing-qrBarcodeScanner = { module = "me.dm7.barcodescanner:zxing", version.ref = "zxingQrBarcodeScanner" } +zendesk-chat = { module = "com.zendesk:chat", version.ref = "zendeskChat" } +zendesk-messaging = { module = "com.zendesk:messaging", version.ref = "zendeskMessaging" } zxing-qrCore = { module = "com.google.zxing:core", version.ref = "zxingQrCode" } mviCore-watcher = { module = "com.github.badoo.mvicore:mvicore-diff", version.ref = "mviCore" } kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinSerialization" } @@ -228,4 +238,12 @@ 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" } +mlKit-barcodeScanning = { module = "com.google.mlkit:barcode-scanning", version.ref = "mlKit-barcodeScanning" } +listenableFuture = { module = "com.google.guava:listenablefuture", version.ref = "listenableFuture" } +camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "androidXCamera" } +camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "androidXCamera" } +camera-view = { module = "androidx.camera:camera-view", version.ref = "androidXCamera" } + # endregion Other diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt index 5a915b109e..3aa47352d5 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt @@ -6,7 +6,6 @@ import java.math.BigDecimal interface TransactionManager { - @Suppress("LongParameterList") @Throws(IllegalStateException::class) suspend fun sendApproveTransaction( txData: ApproveTxData, diff --git a/settings.gradle.kts b/settings.gradle.kts index e2478e48b5..2bda17e30f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -33,7 +33,14 @@ dependencyResolutionManagement { // setting any repository from tangem project allows maven search all packages in the project url = uri("https://maven.pkg.github.com/tangem/blockchain-sdk-kotlin") credentials { - println(System.getenv("GITHUB_ACTOR")) + username = properties.getProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR") + password = properties.getProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN") + } + } + maven { + // setting any repository from tangem project allows maven search all packages in the project + url = uri("https://maven.pkg.github.com/tangem/wallet-core") + credentials { username = properties.getProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR") password = properties.getProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN") } @@ -93,6 +100,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