Updated on 2026-08-14

This commit is contained in:
Tangem 2023-11-10 15:50:56 +03:00
commit 005847a3aa
262 changed files with 6411 additions and 1089 deletions

View file

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

View file

@ -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"
}
]
}
]
}

View file

@ -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<AppThemeMode?>
// 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<Snackbar>() {
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)
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<ProcessCameraProvider>? = 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<out String>, 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() {

View file

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

View file

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

View file

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

View file

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

View file

@ -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<ProcessCameraProvider>? = 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<out String>, 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() {

View file

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

View file

@ -94,7 +94,7 @@ class HomeFragment : ComposeFragment(), StoreSubscriber<HomeState> {
},
onSearchTokensClick = {
Analytics.send(IntroductionProcess.ButtonTokensList())
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens))
store.dispatch(NavigationAction.NavigateTo(AppScreen.ManageTokens))
store.dispatch(TokensAction.SetArgs.ReadAccess)
},
)

View file

@ -0,0 +1,12 @@
package com.tangem.tap.features.main
internal interface MainIntents {
fun onHiddenBalanceToastAction()
fun onShownBalanceToastAction()
fun onHiddenBalanceNotificationAction(isPermanent: Boolean)
fun onDismissBottomSheet()
}

View file

@ -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<MainScreenState> = MutableStateFlow(getInitialState())
val stateFlow: StateFlow<MainScreenState> = 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,
)
}

View file

@ -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<BalanceHidingSettings> = getBalanceHidingSettingsUseCase()
.shareIn(viewModelScope, SharingStarted.WhileSubscribed())
val state: StateFlow<MainScreenState> = 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()
}
}

View file

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

View file

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

View file

@ -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<Toast>,
val modalNotification: TangemBottomSheetConfig?,
)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -243,7 +243,7 @@ internal class SendStateSubscriber(
}
val filter = DecimalDigitsInputFilter(
digitsBeforeDecimal = 12,
digitsBeforeDecimal = 40,
digitsAfterDecimal = state.maxLengthOfAmount,
decimalSeparator = state.decimalSeparator,
)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -18,7 +18,9 @@ object DaggerGraphReducer {
walletRouter = action.walletRouter,
walletConnectInteractor = action.walletConnectInteractor,
tokenDetailsRouter = action.tokenDetailsRouter,
manageTokensRouter = action.manageTokensRouter,
cardSdkConfigRepository = action.cardSdkConfigRepository,
sendRouter = action.sendRouter,
)
}
}

View file

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

View file

@ -1,12 +0,0 @@
<vector android:autoMirrored="true" android:height="22dp"
android:viewportHeight="22" android:viewportWidth="22"
android:width="22dp" xmlns:android="http://schemas.android.com/apk/res/android">
<group>
<clip-path android:pathData="M0,0h22v22h-22z"/>
<path android:fillColor="#A1A1A4" android:pathData="M16.5486,12.9469L13.6283,8.469L12.5575,10.2212L15.3805,14.7964L16.646,14.2124L16.5486,12.9469Z"/>
<path android:fillColor="#A1A1A4" android:pathData="M15.186,15.3805L12.2656,10.9026L11.0001,12.8495L13.3364,16.5486L14.7966,15.9645L15.186,15.3805Z"/>
<path android:fillColor="#A1A1A4" android:fillType="evenOdd" android:pathData="M5.6461,7.8851L11.0001,4.7701L16.3541,7.8851V14.1152L11.0001,17.2303L7.0089,14.9913L7,16L10.9028,18.3984C11.0001,18.4958 11.0974,18.4958 11.1948,18.3984L17.3275,14.894C17.4249,14.7966 17.5222,14.6993 17.5222,14.602V7.5931C17.5222,7.4958 17.4249,7.3984 17.3275,7.3011L11.1948,3.7966C11.0974,3.6993 11.0001,3.6993 10.9028,3.7966L4.77,7.2037C4.6727,7.3011 4.5753,7.3984 4.5753,7.4958V14.5046C4.5753,14.602 4.6727,14.6993 4.77,14.7966L6,15.5L6.5,14.5L5.7435,14.1152V7.8851H5.6461Z"/>
<path android:fillColor="#A1A1A4" android:pathData="M13.3364,7.96H11.2921L6.5222,15.7476L8.1771,16.721L13.3364,7.96Z"/>
<path android:fillColor="#A1A1A4" android:pathData="M8.1771,8.6636C8.6638,7.8849 10.0266,7.8849 10.6107,7.9822L6,15.5C5.8053,15.4027 5.3541,15.1857 5.062,14.9911C4.6727,14.7964 4.5753,14.7964 4.5753,14.5043V14.1149C5.6461,12.5574 7.6904,9.4424 8.1771,8.6636Z"/>
</group>
</vector>

View file

@ -1,5 +0,0 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#A1A1A4" android:pathData="M10.6797,5.3445C10.272,4.4259 9.3351,4.2295 8.1882,4.4198L7.8196,3L6.9507,3.223L7.3131,4.6387C7.0845,4.696 6.8498,4.7451 6.6171,4.8106L6.2547,3.403L5.3858,3.626L5.7543,5.0458C5.567,5.099 4,5.498 4,5.498L4.2389,6.4227C4.2389,6.4227 4.8772,6.2447 4.871,6.259C5.2252,6.1669 5.3919,6.3429 5.4702,6.5086L6.4833,10.3998C6.4956,10.5123 6.475,10.7046 6.232,10.77C6.2465,10.7782 5.5999,10.9317 5.5999,10.9317L5.6946,12.0098C5.6946,12.0098 7.2472,11.615 7.451,11.5638L7.8237,13L8.6927,12.777L8.32,11.3306C8.5588,11.2754 8.7915,11.2181 9.018,11.1588L9.3886,12.597L10.2576,12.374L9.8849,10.9399C11.2233,10.6166 12.1684,9.7778 11.9748,8.4951C11.8513,7.7218 11.0009,7.0876 10.2946,7.016C10.7291,6.6334 10.9494,6.0749 10.6797,5.3445ZM10.2617,8.7304C10.4346,10.0008 8.6577,10.1563 8.0708,10.3097L7.5602,8.4092C8.1491,8.2557 9.9693,7.6113 10.2617,8.7304ZM9.191,6.1424C9.3742,7.2717 7.8546,7.4026 7.3646,7.5274L6.8992,5.8028C7.3913,5.6821 8.8183,5.097 9.191,6.1424Z"/>
</vector>

View file

@ -1,8 +0,0 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#A1A1A4" android:pathData="M4.3968,8.0001L4.4025,10.1154L6.2,11.1731V12.4116L3.3506,10.7404V7.3815L4.3968,8.0001ZM4.3968,5.8847V7.1174L3.35,6.4981V5.2655L4.3968,4.6462L5.4487,5.2655L4.3968,5.8847ZM6.9506,5.2655L7.9974,4.6462L9.0493,5.2655L7.9974,5.8847L6.9506,5.2655Z"/>
<path android:fillColor="#A1A1A4" android:pathData="M5.1532,9.677V8.4385L6.2,9.0577V10.2904L5.1532,9.677ZM6.9506,11.6167L7.9974,12.2359L9.0493,11.6167V12.8494L7.9974,13.4686L6.9506,12.8494V11.6167ZM10.5505,5.2655L11.5973,4.6462L12.6492,5.2655V6.4981L11.5973,7.1174V5.8847L10.5505,5.2655ZM11.5973,10.1154L11.6031,8.0001L12.6499,7.3808V10.7398L9.8006,12.4109V11.1725L11.5973,10.1154Z"/>
<path android:fillColor="#A1A1A4" android:pathData="M10.8468,9.6769L9.8,10.2903V9.0577L10.8468,8.4385V9.6769Z"/>
<path android:fillColor="#A1A1A4" android:pathData="M10.8468,6.3232L10.8526,7.5616L9.05,8.6193V10.7398L8.0032,11.3533L6.9564,10.7398V8.6193L5.1538,7.5616V6.3232L6.2051,5.7039L7.9968,6.7667L9.7993,5.7039L10.8513,6.3232H10.8468ZM5.1532,4.2084L7.9974,2.5315L10.8468,4.2084L9.8,4.8276L7.9974,3.7648L6.2,4.8276L5.1532,4.2084Z"/>
</vector>

View file

@ -1,5 +0,0 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#A1A1A4" android:pathData="M7.8738,3.2662C8.098,3.1493 8.3717,3.4204 8.2526,3.6444C8.1834,3.827 7.9211,3.8755 7.7871,3.7364C7.6452,3.6028 7.6915,3.3388 7.8738,3.2662ZM5.4626,3.5261C5.5988,3.47 5.7712,3.5948 5.7573,3.7432C5.7718,3.9043 5.5747,4.0229 5.4359,3.9464C5.252,3.8751 5.273,3.5766 5.4626,3.5261ZM10.4011,3.9509C10.1883,3.9252 10.1546,3.5906 10.3595,3.5232C10.5184,3.4457 10.6642,3.591 10.6875,3.7417C10.6541,3.8703 10.5465,3.9881 10.4011,3.9509ZM6.0113,4.5523C6.2389,4.4083 6.5638,4.6228 6.5288,4.8872C6.517,5.1543 6.1623,5.309 5.9622,5.1284C5.7687,4.9867 5.798,4.6614 6.0113,4.5523ZM9.4869,4.7051C9.5884,4.4434 10,4.4354 10.1156,4.6903C10.2299,4.8886 10.0894,5.1342 9.8857,5.2016C9.616,5.2519 9.3599,4.9604 9.4869,4.7051ZM7.5835,5.1867C7.5813,4.9617 7.7862,4.7941 8.0001,4.7787C8.1419,4.8072 8.2965,4.8676 8.3573,5.0107C8.4707,5.2082 8.37,5.48 8.1651,5.5706C8.0714,5.6231 7.9607,5.6034 7.8586,5.5973C7.7027,5.5208 7.5713,5.3706 7.5835,5.1867ZM3.9976,5.5146C4.2056,5.3867 4.4862,5.62 4.4075,5.846C4.3641,6.0414 4.1014,6.1209 3.9512,5.9962C3.7909,5.8793 3.8163,5.5982 3.9976,5.5146ZM11.6978,5.5142C11.8742,5.3772 12.1654,5.5343 12.1444,5.7571C12.1562,5.9603 11.9045,6.1135 11.7293,6.0089C11.5364,5.9222 11.5167,5.6253 11.6978,5.5142ZM8.5919,5.9187C8.8831,5.814 9.2289,5.9392 9.3949,6.1988C9.6138,6.5162 9.4974,6.9995 9.1576,7.1816C8.8056,7.397 8.2916,7.2144 8.1615,6.8213C8.014,6.4694 8.229,6.029 8.5919,5.9187ZM6.8834,5.9533C7.2062,5.8009 7.6405,5.9287 7.7964,6.2589C7.9798,6.5802 7.838,7.0276 7.51,7.1935C7.1816,7.38 6.7183,7.2386 6.5594,6.8936C6.3812,6.56 6.5395,6.1069 6.8834,5.9533ZM5.1728,6.5381C5.1973,6.3302 5.3934,6.1949 5.5913,6.1809C5.8046,6.2066 5.9749,6.3722 5.9981,6.588C5.9841,6.8016 5.819,7.0104 5.5926,7.0091C5.3436,7.0306 5.1252,6.7828 5.1728,6.5381ZM10.2255,6.2261C10.4812,6.0773 10.8363,6.2939 10.8249,6.5877C10.8425,6.8989 10.4495,7.1183 10.1949,6.9357C9.9304,6.7837 9.951,6.3555 10.2255,6.2261ZM6.1882,7.3231C6.4872,7.2408 6.8292,7.3818 6.9759,7.6566C7.1365,7.9333 7.0708,8.3177 6.8213,8.5206C6.5013,8.8183 5.911,8.6988 5.7407,8.2926C5.545,7.9201 5.7827,7.4211 6.1882,7.3231ZM9.4286,7.3215C9.7194,7.2384 10.0609,7.3504 10.2172,7.6153C10.4238,7.9182 10.3252,8.37 10.0176,8.5653C9.6953,8.792 9.1961,8.673 9.0188,8.3184C8.8038,7.9517 9.0202,7.4334 9.4286,7.3215ZM4.2647,7.6671C4.523,7.5694 4.8094,7.8545 4.7047,8.1106C4.6408,8.3439 4.312,8.4302 4.1421,8.2581C3.9433,8.0966 4.0169,7.7372 4.2647,7.6671ZM11.2667,8.0388C11.2593,7.8247 11.4159,7.6352 11.6354,7.6234C11.8123,7.6514 11.9843,7.7941 11.9703,7.9867C11.9865,8.2411 11.68,8.414 11.4633,8.2944C11.3617,8.2428 11.3096,8.1386 11.2667,8.0388ZM2.9945,7.7928C3.1499,7.7249 3.3422,7.8596 3.3067,8.0322C3.2983,8.2276 3.0098,8.3028 2.9038,8.1395C2.8102,8.0261 2.8662,7.8512 2.9945,7.7928ZM12.7939,7.7889C12.9033,7.7109 13.0789,7.767 13.1223,7.8948C13.1958,8.0344 13.061,8.2257 12.9038,8.2021C12.6832,8.2222 12.6118,7.886 12.7939,7.7889ZM7.0323,8.727C7.4413,8.6206 7.8809,8.9616 7.8835,9.3827C7.9102,9.8083 7.4772,10.1856 7.0585,10.0873C6.735,10.0352 6.4811,9.7235 6.4915,9.3965C6.4907,9.0838 6.7267,8.7931 7.0323,8.727ZM8.6519,8.7261C9.067,8.6101 9.5197,8.9572 9.5131,9.388C9.5351,9.8057 9.1156,10.1703 8.7057,10.0875C8.32,10.0376 8.0354,9.6138 8.1431,9.2395C8.1974,8.9901 8.4058,8.7872 8.6519,8.7261ZM5.5292,8.9853C5.8165,8.9363 6.0857,9.2567 5.9692,9.5295C5.8843,9.8201 5.4613,9.8989 5.2743,9.6615C5.055,9.4335 5.2187,9.0168 5.5292,8.9853ZM10.2789,8.9981C10.535,8.8908 10.8494,9.1124 10.8297,9.3904C10.84,9.6964 10.4519,9.9074 10.2014,9.7288C9.9177,9.5684 9.9694,9.0965 10.2789,8.9981ZM11.6065,10.3297C11.5062,10.1427 11.6884,9.895 11.8959,9.9296C11.9993,9.9278 12.0715,10.0092 12.1377,10.0775C12.1512,10.1883 12.174,10.3205 12.0821,10.4059C11.9572,10.5639 11.6743,10.5188 11.6065,10.3297ZM3.9968,9.9816C4.1907,9.8525 4.4699,10.0451 4.4263,10.2719C4.4031,10.4725 4.1504,10.5871 3.984,10.4733C3.8054,10.37 3.812,10.0758 3.9968,9.9816ZM7.8292,10.4015C8.0832,10.2894 8.4067,10.5105 8.3826,10.7902C8.4015,11.0993 8.0013,11.3134 7.753,11.1278C7.4772,10.9684 7.5232,10.4995 7.8292,10.4015ZM6.0542,10.8156C6.2705,10.7018 6.5607,10.8983 6.541,11.1404C6.5476,11.3392 6.3536,11.5086 6.1575,11.4792C5.995,11.4757 5.8891,11.3301 5.837,11.1916C5.8396,11.0406 5.9018,10.8725 6.0542,10.8156ZM9.6497,10.8191C9.8722,10.6816 10.1883,10.8791 10.1673,11.1379C10.173,11.4085 9.8157,11.5818 9.6086,11.4041C9.4081,11.2648 9.4326,10.9299 9.6497,10.8191ZM10.275,12.3649C10.1761,12.2152 10.2925,12.0348 10.4532,12.0007C10.5824,12.0269 10.719,12.1241 10.6918,12.2739C10.673,12.4765 10.3766,12.5387 10.275,12.3649ZM5.3116,12.2349C5.351,12.1162 5.4464,11.9983 5.5861,12.0292C5.7897,12.0436 5.8501,12.3505 5.6719,12.4451C5.5117,12.5531 5.3258,12.4051 5.3116,12.2349ZM7.7062,12.371C7.7451,12.1955 7.9723,12.1101 8.1221,12.2033C8.2211,12.2458 8.25,12.3565 8.2754,12.4507C8.2618,12.4997 8.2496,12.5487 8.2377,12.5978C8.1764,12.6704 8.0976,12.7431 7.9956,12.7405C7.806,12.7627 7.6349,12.5483 7.7062,12.371Z"/>
</vector>

View file

@ -1,5 +0,0 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#A1A1A4" android:fillType="evenOdd" android:pathData="M5.3545,12.185H8.1164H8.1165C8.1165,12.185 11.9999,12.5157 11.9999,8.0674C11.9999,3.7917 8.4536,3.8068 7.7682,3.8098C7.7521,3.8098 7.7376,3.8099 7.7248,3.8099H5.3544V7.5406H4.3809V8.4545H5.3545V12.185ZM6.9101,5.3579H7.9998C8.4074,5.3579 10.4578,5.5245 10.4611,8.0975C10.4643,10.6399 8.397,10.6373 8.0645,10.6368C8.0604,10.6368 8.0567,10.6368 8.0532,10.6368H6.9101V8.4544H8.6245V7.5405H6.9101V5.3579Z"/>
</vector>

View file

@ -1,13 +0,0 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<group>
<clip-path android:pathData="M8.0002,2.8001L8.0002,2.8001A3.2001,3.2001 0,0 1,11.2003 6.0001L11.2003,10.0003A3.2001,3.2001 0,0 1,8.0002 13.2003L8.0002,13.2003A3.2001,3.2001 0,0 1,4.8002 10.0003L4.8002,6.0001A3.2001,3.2001 0,0 1,8.0002 2.8001z"/>
<path android:fillColor="#A1A1A4" android:pathData="M7.9994,2.8001L7.9296,3.037V9.9123L7.9994,9.9819L11.1986,8.0955L7.9994,2.8001Z"/>
<path android:fillColor="#A1A1A4" android:pathData="M7.9993,2.8001L4.8002,8.0955L7.9993,9.9819V6.6449V2.8001Z"/>
<path android:fillColor="#A1A1A4" android:pathData="M7.9992,10.5862L7.9598,10.6341V13.0832L7.9992,13.198L11.2003,8.7007L7.9992,10.5862Z"/>
<path android:fillColor="#A1A1A4" android:pathData="M7.9993,13.1982V10.5864L4.8002,8.7009L7.9993,13.1982Z"/>
<path android:fillColor="#A1A1A4" android:pathData="M7.9991,9.9821L11.1982,8.0956L7.9991,6.645V9.9821Z"/>
<path android:fillColor="#A1A1A4" android:pathData="M4.8002,8.0956L7.9992,9.9821V6.645L4.8002,8.0956Z"/>
</group>
</vector>

View file

@ -1,5 +0,0 @@
<vector android:autoMirrored="true" android:height="22dp"
android:viewportHeight="22" android:viewportWidth="22"
android:width="22dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#A1A1A4" android:fillType="evenOdd" android:pathData="M11.825,8.8687L14.3,7.425V10.3125L11.825,8.8687ZM14.3,15.0562L11,16.9813L7.7,15.0562V11.6875L11,13.6125L14.3,11.6875V15.0562ZM7.7,7.425L10.175,8.8687L7.7,10.3125V7.425ZM11.4125,9.5562L13.8875,11L11.4125,12.4438V9.5562ZM10.5875,12.4438L8.1125,11L10.5875,9.5562V12.4438ZM13.8875,6.7375L11,8.3875L8.1125,6.7375L11,5.0188L13.8875,6.7375ZM6.875,6.4625V15.4688L11,17.8062L15.125,15.4688V6.4625L11,4.125L6.875,6.4625Z"/>
</vector>

View file

@ -1,5 +0,0 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#A1A1A4" android:pathData="M12.4155,5.3206C12.265,5.2041 12.0856,5.0449 11.7587,5.0051C11.452,4.9653 11.1395,5.1671 10.9282,5.3007C10.717,5.4343 10.3177,5.8266 10.1528,5.946C9.9879,6.0654 9.5654,6.1762 8.8854,6.577C8.2054,6.9778 5.5376,8.6606 5.5376,8.6606L6.2321,8.6691L3.136,10.2354H3.4456L3,10.5679C3,10.5679 3.3935,10.6703 3.7234,10.4656V10.5594C3.7234,10.5594 7.4097,9.1325 8.1215,9.502L7.6875,9.6271C7.7251,9.6271 8.4254,9.6725 8.4254,9.6725C8.4423,9.815 8.4911,9.952 8.5681,10.0738C8.6451,10.1957 8.7485,10.2994 8.8709,10.3775C9.2934,10.6504 9.3021,10.801 9.3021,10.801C9.3021,10.801 9.0822,10.8891 9.0822,11C9.0822,11 9.4063,10.9034 9.7072,10.9119C9.8982,10.919 10.0876,10.9486 10.2714,11C10.2714,11 10.2483,10.8806 9.956,10.801C9.6638,10.7214 9.3744,10.4088 9.2326,10.2382C9.1458,10.1296 9.0892,10.0008 9.0684,9.8642C9.0476,9.7276 9.0633,9.5881 9.114,9.4593C9.2153,9.2007 9.5683,9.0585 10.2975,8.689C11.1568,8.2513 11.3536,7.9272 11.4751,7.6742C11.5966,7.4212 11.776,6.9181 11.8773,6.6822C12.0046,6.378 12.1609,6.216 12.2911,6.1194C12.4213,6.0227 13,5.8095 13,5.8095C13,5.8095 12.5573,5.4315 12.4155,5.3206Z"/>
</vector>

View file

@ -1,10 +0,0 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#A1A1A4" android:pathData="M7.9994,4.5169C9.2703,4.5169 10.3006,3.9535 10.3006,3.2585C10.3006,2.5634 9.2703,2 7.9994,2C6.7285,2 5.6982,2.5634 5.6982,3.2585C5.6982,3.9535 6.7285,4.5169 7.9994,4.5169Z"/>
<path android:fillColor="#A1A1A4" android:pathData="M7.9994,13.9991C9.2703,13.9991 10.3006,13.4357 10.3006,12.7406C10.3006,12.0456 9.2703,11.4822 7.9994,11.4822C6.7285,11.4822 5.6982,12.0456 5.6982,12.7406C5.6982,13.4357 6.7285,13.9991 7.9994,13.9991Z"/>
<path android:fillColor="#A1A1A4" android:pathData="M4.7927,6.259C5.4282,5.2241 5.424,4.1032 4.7835,3.7555C4.1429,3.4077 3.1086,3.9648 2.4731,4.9997C1.8376,6.0346 1.8418,7.1554 2.4823,7.5032C3.1228,7.8509 4.1572,7.2939 4.7927,6.259Z"/>
<path android:fillColor="#A1A1A4" android:pathData="M13.5251,10.9994C14.1605,9.9645 14.1567,8.8438 13.5166,8.4963C12.8765,8.1488 11.8425,8.706 11.207,9.7409C10.5715,10.7758 10.5753,11.8965 11.2154,12.244C11.8555,12.5915 12.8896,12.0343 13.5251,10.9994Z"/>
<path android:fillColor="#A1A1A4" android:pathData="M4.7837,12.2438C5.4243,11.896 5.4284,10.7752 4.793,9.7403C4.1575,8.7054 3.1231,8.1483 2.4826,8.4961C1.842,8.8438 1.8379,9.9647 2.4733,10.9996C3.1088,12.0345 4.1432,12.5915 4.7837,12.2438Z"/>
<path android:fillColor="#A1A1A4" android:pathData="M13.517,7.5035C14.1571,7.156 14.1609,6.0353 13.5255,5.0004C12.89,3.9655 11.856,3.4083 11.2159,3.7558C10.5758,4.1033 10.572,5.224 11.2075,6.2589C11.8429,7.2938 12.8769,7.851 13.517,7.5035Z"/>
</vector>

View file

@ -1,5 +0,0 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#A1A1A4" android:pathData="M10.6283,6.3937C10.4378,6.2889 10.1902,6.2889 9.9808,6.3937L8.4952,7.1968L7.4858,7.7205L6.0003,8.5237C5.8098,8.6284 5.5622,8.6284 5.3527,8.5237L4.1719,7.8951C3.9815,7.7904 3.8482,7.5983 3.8482,7.3888V6.1492C3.8482,5.9397 3.9624,5.7477 4.1719,5.6429L5.3337,5.0319C5.5241,4.9271 5.7717,4.9271 5.9812,5.0319L7.143,5.6429C7.3335,5.7477 7.4668,5.9397 7.4668,6.1492V6.9524L8.4762,6.4111V5.608C8.4762,5.3985 8.3619,5.2064 8.1524,5.1017L6.0003,3.9494C5.8098,3.8447 5.5622,3.8447 5.3527,3.9494L3.1625,5.1017C2.953,5.2064 2.8387,5.3985 2.8387,5.608V7.93C2.8387,8.1395 2.953,8.3316 3.1625,8.4363L5.3527,9.5886C5.5432,9.6934 5.7908,9.6934 6.0003,9.5886L7.4858,8.803L8.4952,8.2618L9.9808,7.4761C10.1712,7.3714 10.4188,7.3714 10.6283,7.4761L11.7901,8.0872C11.9805,8.1919 12.1138,8.384 12.1138,8.5935V9.8331C12.1138,10.0426 11.9996,10.2346 11.7901,10.3394L10.6283,10.9679C10.4378,11.0727 10.1902,11.0727 9.9808,10.9679L8.819,10.3568C8.6285,10.2521 8.4952,10.06 8.4952,9.8505V9.0474L7.4858,9.5886V10.3917C7.4858,10.6013 7.6001,10.7933 7.8096,10.8981L9.9998,12.0504C10.1902,12.1551 10.4378,12.1551 10.6473,12.0504L12.8376,10.8981C13.028,10.7933 13.1613,10.6013 13.1613,10.3917V8.0697C13.1613,7.8602 13.0471,7.6682 12.8376,7.5634L10.6283,6.3937Z"/>
</vector>

File diff suppressed because one or more lines are too long

View file

@ -1,5 +0,0 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#A1A1A4" android:pathData="M12.3419,10.4366L10.8935,12.0202C10.862,12.0546 10.8239,12.082 10.7816,12.1008C10.7392,12.1195 10.6936,12.1292 10.6474,12.1292H3.7811C3.7483,12.1292 3.7163,12.1194 3.6889,12.1011C3.6615,12.0828 3.6399,12.0567 3.6268,12.0261C3.6138,11.9954 3.6098,11.9616 3.6154,11.9287C3.6209,11.8957 3.6358,11.8652 3.6581,11.8408L5.1077,10.2572C5.1391,10.2228 5.177,10.1955 5.2192,10.1767C5.2614,10.158 5.307,10.1483 5.353,10.1482H12.219C12.2517,10.1482 12.2838,10.158 12.3112,10.1763C12.3385,10.1946 12.3601,10.2207 12.3732,10.2513C12.3862,10.2819 12.3902,10.3158 12.3847,10.3487C12.3791,10.3816 12.3643,10.4122 12.3419,10.4366ZM10.8935,7.2476C10.862,7.2132 10.8239,7.1858 10.7816,7.167C10.7392,7.1483 10.6936,7.1386 10.6474,7.1387H3.7811C3.7483,7.1387 3.7163,7.1484 3.6889,7.1667C3.6615,7.1851 3.6399,7.2111 3.6268,7.2418C3.6138,7.2724 3.6098,7.3063 3.6154,7.3392C3.6209,7.3721 3.6358,7.4026 3.6581,7.4271L5.1077,9.0107C5.1391,9.045 5.177,9.0723 5.2192,9.0911C5.2614,9.1098 5.307,9.1195 5.353,9.1196H12.219C12.2517,9.1196 12.2838,9.1099 12.3112,9.0915C12.3385,9.0732 12.3601,9.0472 12.3732,9.0165C12.3862,8.9859 12.3902,8.952 12.3847,8.9191C12.3791,8.8862 12.3643,8.8557 12.3419,8.8312L10.8935,7.2476ZM3.7811,6.1101H10.6474C10.6936,6.1101 10.7392,6.1005 10.7816,6.0817C10.8239,6.063 10.862,6.0356 10.8935,6.0012L12.3419,4.4175C12.3643,4.3931 12.3791,4.3626 12.3847,4.3296C12.3902,4.2967 12.3862,4.2629 12.3732,4.2323C12.3601,4.2016 12.3385,4.1755 12.3112,4.1572C12.2838,4.1389 12.2517,4.1291 12.219,4.1291H5.353C5.307,4.1292 5.2614,4.1389 5.2192,4.1577C5.177,4.1764 5.1391,4.2038 5.1077,4.2381L3.6585,5.8217C3.6362,5.8461 3.6213,5.8766 3.6157,5.9095C3.6102,5.9424 3.6141,5.9762 3.6272,6.0068C3.6402,6.0374 3.6617,6.0635 3.689,6.0819C3.7163,6.1002 3.7483,6.11 3.7811,6.1101Z"/>
</vector>

View file

@ -1,11 +0,0 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<group>
<clip-path android:pathData="M0,0h16v16h-16z"/>
<group>
<clip-path android:pathData="M7.871,2.8386L7.871,2.8386A5.0322,5.0322 0,0 1,12.9032 7.8709L12.9032,7.8709A5.0322,5.0322 0,0 1,7.871 12.9031L7.871,12.9031A5.0322,5.0322 0,0 1,2.8387 7.8709L2.8387,7.8709A5.0322,5.0322 0,0 1,7.871 2.8386z"/>
<path android:fillColor="#A1A1A4" android:pathData="M7.9897,3.6149C7.4207,3.5991 6.8543,3.6974 6.324,3.9042C5.7937,4.111 5.3102,4.4221 4.9022,4.8189C4.4941,5.2158 4.1698,5.6904 3.9483,6.2148C3.7269,6.7392 3.6128,7.3026 3.6129,7.8718C3.6129,7.9804 3.6171,8.0882 3.6254,8.196C3.637,8.3484 3.6031,8.5007 3.5282,8.6339C3.4533,8.7671 3.3406,8.8752 3.2044,8.9445L2.8387,9.1311V10.0009L3.9156,9.4519L4.2646,9.2741L4.6084,9.0993L10.7771,5.9558L11.4703,5.6027L12.9033,4.8726V4.0028L11.4829,4.7275L10.271,5.3452L4.4189,8.326C4.3992,8.175 4.3894,8.0228 4.3895,7.8706C4.3903,7.2632 4.5498,6.6667 4.8521,6.1399C5.1544,5.6132 5.589,5.1746 6.113,4.8675C6.637,4.5605 7.2321,4.3956 7.8393,4.3893C8.4466,4.3829 9.045,4.5354 9.5753,4.8315L10.2689,4.478L10.3725,4.4251C9.678,3.9211 8.8474,3.6388 7.9897,3.6149ZM12.9033,5.7427L4.9586,9.7878L4.2654,10.1418L2.8387,10.8694V11.7383L4.2553,11.0161L5.4673,10.3984L11.3256,7.4139C11.3453,7.5657 11.3551,7.7187 11.355,7.8718C11.3546,8.4798 11.1952,9.0771 10.8926,9.6045C10.59,10.1319 10.1548,10.5709 9.6301,10.8781C9.1054,11.1853 8.5094,11.3498 7.9015,11.3555C7.2935,11.3612 6.6946,11.2078 6.1642,10.9105L6.1219,10.9331L5.37,11.3164C6.0053,11.7776 6.7557,12.0541 7.5383,12.1155C8.3209,12.177 9.1053,12.0208 9.8048,11.6644C10.5042,11.308 11.0915,10.7651 11.5018,10.0958C11.912,9.4265 12.1293,8.6568 12.1296,7.8718C12.1296,7.7628 12.1254,7.6529 12.117,7.5447C12.1055,7.3924 12.1393,7.24 12.2141,7.1069C12.289,6.9737 12.4015,6.8656 12.5376,6.7962L12.9033,6.6095V5.7427Z"/>
</group>
</group>
</vector>

View file

@ -1,5 +0,0 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#A1A1A4" android:pathData="M8.5216,13C7.8414,13 7.345,12.8281 7.0322,12.4844C6.7425,12.1934 6.5744,11.7939 6.5646,11.3732C6.5611,11.2455 6.5893,11.1189 6.6465,11.006C6.6999,10.9085 6.777,10.8275 6.8699,10.7715C6.9779,10.715 7.097,10.6856 7.2177,10.6856C7.3384,10.6856 7.4574,10.715 7.5654,10.7715C7.6582,10.8273 7.7349,10.9083 7.7875,11.006C7.8464,11.1184 7.8757,11.2451 7.8724,11.3732C7.8784,11.5263 7.8366,11.6772 7.7532,11.8029C7.6838,11.9056 7.5844,11.9818 7.4702,12.0203C7.5819,12.1695 7.74,12.2729 7.917,12.3125C8.1149,12.3728 8.3199,12.4038 8.5261,12.4046C8.799,12.4071 9.0663,12.3233 9.2931,12.164C9.5284,11.9896 9.7038,11.7403 9.7935,11.4531C9.9071,11.1112 9.9626,10.7511 9.9573,10.3891C9.9665,10.0117 9.9058,9.636 9.7786,9.2828C9.6829,9.0003 9.5013,8.7587 9.2618,8.5953C9.0392,8.4475 8.7807,8.3699 8.5173,8.3719C8.2877,8.3923 8.0663,8.4711 7.8724,8.6016L7.3942,8.8531V8.6016L9.5433,5.5769H6.5646V8.7156C6.5562,8.9421 6.6133,9.166 6.7284,9.3578C6.7842,9.4414 6.8595,9.5085 6.9472,9.5525C7.0348,9.5966 7.1319,9.6162 7.2288,9.6093C7.4081,9.6055 7.5814,9.5415 7.7233,9.4265C7.8841,9.3029 8.0241,9.1518 8.1373,8.9796C8.1475,8.9495 8.1663,8.9233 8.1912,8.9047C8.2111,8.8866 8.2366,8.8766 8.263,8.8766C8.3147,8.8815 8.3637,8.9034 8.403,8.939C8.4528,8.9974 8.4798,9.0732 8.4789,9.1515C8.4732,9.2042 8.4642,9.2563 8.4521,9.3078C8.3463,9.5765 8.1689,9.8075 7.9412,9.9732C7.7298,10.1233 7.48,10.2029 7.2247,10.2016C6.5798,10.2016 6.133,10.0682 5.8844,9.8016C5.7566,9.6542 5.6581,9.4815 5.5947,9.2937C5.5312,9.1059 5.5042,8.9067 5.515,8.7078V5.5769H4V4.9937H5.5221V3.664L5.1736,3.2982V3H6.1848L6.5646,3.2063V4.9937L10.4993,4.9812L10.8911,5.3937L8.4784,7.9372C8.6241,7.8757 8.7776,7.8368 8.9341,7.8216C9.2437,7.8358 9.5458,7.9263 9.8159,8.0857C10.1287,8.2417 10.3923,8.4885 10.5755,8.7969C10.7396,9.0579 10.8558,9.3488 10.9181,9.6547C10.9692,9.8959 10.9967,10.142 11,10.3891C11.0016,10.8607 10.8997,11.3264 10.7021,11.75C10.5148,12.1602 10.1995,12.491 9.8085,12.6876C9.4082,12.8945 8.9678,13.0014 8.5216,13Z"/>
</vector>

View file

@ -1,8 +0,0 @@
<vector android:autoMirrored="true" android:height="22dp"
android:viewportHeight="22" android:viewportWidth="22"
android:width="22dp" xmlns:android="http://schemas.android.com/apk/res/android">
<group>
<clip-path android:pathData="M5,6h12v12h-12z"/>
<path android:fillColor="#A1A1A4" android:pathData="M16.5405,9.6149C15.978,9.0955 15.1999,8.3024 14.5662,7.7399L14.5287,7.7137C14.4663,7.6636 14.3959,7.6243 14.3205,7.5974C12.7924,7.3124 5.6805,5.983 5.5418,5.9999C5.5029,6.0054 5.4657,6.0195 5.433,6.0412L5.3974,6.0693C5.3535,6.1138 5.3202,6.1677 5.2999,6.2268L5.2905,6.2512V6.3843V6.4049C6.0911,8.6343 9.2524,15.9374 9.8749,17.6512C9.9124,17.7674 9.9837,17.9887 10.1168,17.9999H10.1468C10.218,17.9999 10.5218,17.5987 10.5218,17.5987C10.5218,17.5987 15.9518,11.0137 16.5012,10.3124C16.5723,10.226 16.635,10.1331 16.6887,10.0349C16.7023,9.9581 16.6959,9.879 16.6699,9.8054C16.644,9.7318 16.5994,9.6662 16.5405,9.6149ZM11.9149,10.3818L14.2324,8.4599L15.5918,9.7124L11.9149,10.3818ZM11.0149,10.2562L7.0249,6.9862L13.4805,8.1768L11.0149,10.2562ZM11.3749,11.113L15.4587,10.4549L10.7899,16.0799L11.3749,11.113ZM6.483,7.3124L10.6812,10.8749L10.0737,16.0837L6.483,7.3124Z"/>
</group>
</vector>

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="false"
tools:background="@android:color/white">
<androidx.camera.view.PreviewView
android:id="@+id/cameraPreview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.tangem.tap.common.qrCodeScan.ViewFinderOverlay
android:id="@+id/overlay"
android:layerType="software"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>

View file

@ -7,63 +7,34 @@
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/tilAddress">
<FrameLayout
android:id="@+id/flAmountToSend"
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilAmountToSend"
style="@style/SecondaryTextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:errorIconDrawable="@null"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<FrameLayout
android:id="@+id/flShadow"
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etAmountToSend"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:focusable="false"
android:visibility="invisible">
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" />
<TextView
android:id="@+id/tvAmountToSendShadow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="false"
android:fontFamily="sans-serif-light"
android:maxLines="1"
android:paddingStart="0dp"
android:paddingEnd="96dp"
android:textSize="@dimen/text_size_amount_to_send"
app:autoSizeTextType="uniform" />
</FrameLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilAmountToSend"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
style="@style/SecondaryTextInputLayout"
app:errorIconDrawable="@null">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etAmountToSend"
android:background="@color/background_secondary"
android:layout_width="match_parent"
android:layout_height="82dp"
android:fontFamily="sans-serif-light"
android:imeOptions="actionDone"
android:inputType="numberDecimal"
android:paddingStart="0dp"
android:paddingEnd="96dp"
android:textSize="@dimen/text_size_amount_to_send"
android:textColor="@color/text_primary_1"
tools:text="139" />
</com.google.android.material.textfield.TextInputLayout>
</FrameLayout>
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/tvAmountCurrency"
@ -84,14 +55,17 @@
<TextView
android:id="@+id/tvBalance"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:gravity="end"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:textColor="@color/text_tertiary"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/flAmountToSend" />
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tilAmountToSend" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -42,4 +42,7 @@
<dimen name="layout_address_info_margin">16dp</dimen>
<dimen name="qr_border_stroke_width">4dp</dimen>
<dimen name="qr_border_corner_radius">8dp</dimen>
</resources>

View file

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

View file

@ -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<List<Asset>>
@POST("pairs")
suspend fun getPairs(@Body body: PairsRequestBody): ApiResponse<List<SwapPair>>
@GET("providers")
suspend fun getProviders(): ApiResponse<List<ExchangeProvider>>
@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<ExchangeQuoteResponse>
@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<ExchangeDataResponse>
@GET("exchange-results")
suspend fun getExchangeResults(@Query("txId") txId: String): ApiResponse<ExchangeResultsResponse>
}

View file

@ -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<LeastTokenInfo>?,
)

View file

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

View file

@ -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<LeastTokenInfo>,
@Json(name = "to")
val to: List<LeastTokenInfo>,
)

View file

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

View file

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

View file

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

View file

@ -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?,
)

View file

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

View file

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

View file

@ -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<QuoteResponse>
/**
@ -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<SwapResponse>
//endregion Swap
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -13,6 +13,7 @@ internal class BalanceStateHidingSettingsStore(
return getSyncOrNull() ?: BalanceHidingSettings(
isHidingEnabledInSettings = false,
isBalanceHidden = false,
isBalanceHidingNotificationEnabled = true,
)
}
}

View file

@ -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<Preferences> 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<Preferences> instance
*
[REDACTED_AUTHOR]
*/
class AppPreferencesStore(
val moshi: Moshi,
private val preferencesDataStore: DataStore<Preferences>,
) : DataStore<Preferences> 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 <reified T> MutablePreferences.getObject(key: Preferences.Key<String>): T? {
val adapter = moshi.adapter(T::class.java)
return this[key]?.let(adapter::fromJson)
}
/** Set data [T] by string [key] to [MutablePreferences] */
inline fun <reified T> MutablePreferences.setObject(key: Preferences.Key<String>, value: T) {
val adapter = moshi.adapter(T::class.java)
this[key] = adapter.toJson(value)
}
}

View file

@ -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<Preferences>'.
* 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<Preferences>? = null
fun getInstance(context: Context, dispatcher: CoroutineContext): DataStore<Preferences> {
return INSTANCE ?: create(context, dispatcher).also { INSTANCE = it }
}
private fun create(context: Context, dispatcher: CoroutineContext): DataStore<Preferences> {
return PreferenceDataStoreFactory.create(
corruptionHandler = createCorruptionHandler(),
migrations = createMigrations(),
scope = CoroutineScope(context = dispatcher + SupervisorJob()),
produceFile = { context.preferencesDataStoreFile(name = PREFERENCES_FILE_NAME) },
)
}
private fun createCorruptionHandler(): ReplaceFileCorruptionHandler<Preferences> {
return ReplaceFileCorruptionHandler(
produceNewData = {
Timber.w(it)
emptyPreferences()
},
)
}
private fun createMigrations(): List<DataMigration<Preferences>> {
return listOf()
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.datasource.local.preferences
/**
* All preferences keys that DataStore<Preferences> is stored.
*
[REDACTED_AUTHOR]
*/
object PreferencesKeys
/** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore<Preferences> */
internal fun getTapPrefKeysToMigrate(): Set<String> {
return setOf()
}

View file

@ -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 <reified T> AppPreferencesStore.getObject(key: Preferences.Key<String>): Flow<T?> {
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 <reified T> AppPreferencesStore.getObject(key: Preferences.Key<String>, default: T): Flow<T> {
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 <reified T> AppPreferencesStore.getObjectSyncOrNull(key: Preferences.Key<String>): 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 <reified T> AppPreferencesStore.getObjectSyncOrDefault(
key: Preferences.Key<String>,
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 <reified T> AppPreferencesStore.storeObject(key: Preferences.Key<String>, value: T) {
val adapter = moshi.adapter(T::class.java)
edit { it[key] = adapter.toJson(value) }
}

View file

@ -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 <T> DataStore<Preferences>.get(key: Preferences.Key<T>): Flow<T?> {
return data.map { it[key] }
}
/** Get flow of data [T] by [key]. If data is not found, it returns [default] */
fun <T> DataStore<Preferences>.get(key: Preferences.Key<T>, default: T): Flow<T> {
return data.map { it[key] ?: default }
}
/** Get nullable data [T] by [key] */
suspend fun <T> DataStore<Preferences>.getSyncOrNull(key: Preferences.Key<T>): T? {
return data.firstOrNull()?.get(key)
}
/** Get data [T] by [key]. If data is not found, it returns [default] */
suspend fun <T> DataStore<Preferences>.getSyncOrDefault(key: Preferences.Key<T>, default: T): T {
return data.firstOrNull()?.get(key) ?: default
}
/** Store data [value] by [key] */
suspend fun <T> DataStore<Preferences>.store(key: Preferences.Key<T>, value: T) {
edit { it[key] = value }
}

View file

@ -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<Preferences> {
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<String>
}
}
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")
}

View file

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

View file

@ -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"
}
]

View file

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

View file

@ -28,6 +28,10 @@
<string name="app_settings_theme_mode_system">Как в системе</string>
<string name="app_settings_theme_selector_title">Тема</string>
<string name="app_settings_title">Настройки приложения</string>
<string name="balance_hidden_description">Чтобы скрыть или показать баланс, просто поверните ваше устройство вниз или отключите опцию его в разделе \"Настройки\"</string>
<string name="balance_hidden_do_not_show_button">Больше не показывать</string>
<string name="balance_hidden_got_it_button">Понятно</string>
<string name="balance_hidden_title">Балансы скрыты</string>
<string name="biometric_lockout_permanent_warning_description">Пожалуйста, отсканируйте карту</string>
<string name="biometric_lockout_warning_description">Пожалуйста, попробуйте снова через 30 секунд или отсканируйте карту</string>
<string name="biometric_lockout_warning_title">Слишком много попыток</string>
@ -85,6 +89,12 @@
<string name="common_exchange">Обменять</string>
<string name="common_explore_transaction_history">Посмотреть историю транзакций</string>
<string name="common_explorer">Обозреватель</string>
<string name="common_fee_selector_title">Скорость и комиссия</string>
<string name="common_fee_selector_footer">Сетевые комиссии за транзакции используются для поддержки безопасности сети, поощрения валидаторов, выделения ресурсов и определения приоритета транзакции.</string>
<string name="common_fee_selector_option_slow">Медленно</string>
<string name="common_fee_selector_option_market">По рынку</string>
<string name="common_fee_selector_option_fast">Быстро</string>
<string name="common_fee_selector_option_custom">Свое</string>
<string name="common_generate_addresses">Сгенерировать адреса</string>
<string name="common_import">Импортировать</string>
<string name="common_like">Нравится</string>
@ -96,6 +106,7 @@
<string name="common_no_data">Нет данных</string>
<string name="common_ok">OK</string>
<string name="common_origin_card">Основная карта</string>
<string name="common_paste">Вставить</string>
<string name="common_receive">Получить</string>
<string name="common_reject">Отклонить</string>
<string name="common_reload">Перезагрузить</string>
@ -210,6 +221,24 @@
<string name="main_tokens">Токены</string>
<string name="manage_tokens_add">Добавить</string>
<string name="manage_tokens_edit">Изменить</string>
<string name="manage_tokens_network_selector_native_title">Основная сеть</string>
<string name="manage_tokens_network_selector_non_native_subtitle">Не основной или основной блокчейн, на котором размещен токен</string>
<string name="manage_tokens_network_selector_non_native_title">Не основные сети</string>
<string name="manage_tokens_network_selector_other_title">Сети</string>
<string name="manage_tokens_network_selector_title">Выберите сети</string>
<string name="manage_tokens_network_selector_wallet">Кошелек</string>
<string name="manage_tokens_nothing_found">Не удалось найти этот токен, вы можете добавить его вручную.</string>
<plurals name="manage_tokens_number_of_wallets_android">
<item quantity="one">%1$d из %2$d кошелька</item>
<item quantity="few">%1$d из %2$d кошельков</item>
<item quantity="many">%1$d из %2$d кошельков</item>
<item quantity="other">%1$d из %2$d кошельков</item>
</plurals>
<string name="manage_tokens_search_placeholder">например Bitcoin</string>
<string name="manage_tokens_title">Рыночная капитализация</string>
<string name="manage_tokens_unavailable_description">Выбранный токен не доступен в кошельке на данный момент. Но не переживайте, вы можете выразить свой интерес проголосовав за его добавление.</string>
<string name="manage_tokens_unavailable_vote">Голосовать</string>
<string name="manage_tokens_wallet_selector_title">Выберите кошелек</string>
<string name="onboarding_access_code_feature_1_description">Вам необходимо установить единый код доступа для защиты всех ваших карт</string>
<string name="onboarding_access_code_feature_1_title">Защита</string>
<string name="onboarding_access_code_feature_2_description">Позже вы сможете установить индивидуальный код доступа для каждой карты</string>
@ -362,19 +391,52 @@
<string name="scan_card_settings_message">Отсканируйте карту, чтобы изменить ее настройки. Изменения затронут только ту карту, которую вы отсканировали, и не повлияют на другие карты, привязанные к вашему кошельку.</string>
<string name="scan_card_settings_title">Приготовьте свою карту</string>
<string name="send_amount_label">Сумма</string>
<string name="send_amount_substract">Вычесть из суммы отправки</string>
<string name="send_amount_substract_footer">Сумма к получению %1$s</string>
<string name="send_destination_hint_address">Адрес</string>
<string name="send_destination_tag_field">Код назначения</string>
<string name="send_datetime_formatter">%1$s в %2$s</string>
<string name="send_enter_address_field">Введите адрес</string>
<string name="send_error_address_same_as_wallet">Адрес совпадает с адресом кошелька</string>
<string name="send_extras_error_invalid_destination_tag">Недопустимый Tag. Он не будет добавлен в транзакцию.</string>
<string name="send_extras_error_invalid_memo">Недопустимый Memo. Он не будет добавлен в транзакцию.</string>
<string name="send_extras_hint_destination_tag">Tag</string>
<string name="send_extras_hint_memo">Memo</string>
<string name="send_recent_transactions">Последние</string>
<string name="send_recipient_address_footer">Убедитесь, что вы отправляете средства на адрес кошелька %1$s. Ошибки могут привести к потере ваших токенов.</string>
<string name="send_recipient_memo_footer">Мемо/Код назначения - это код, разделяющий транзакции к общему получателю в сети криптовалют. Внимание: отсутствие мемо может привести к потере средств.</string>
<string name="send_fee_include_description">Включая комиссию</string>
<string name="send_fee_label">Комиссия</string>
<string name="send_fee_picker_low">Низкая</string>
<string name="send_fee_picker_normal">Нормальная</string>
<string name="send_fee_picker_priority">Приоритетная</string>
<string name="send_gas_price">Цена газа</string>
<string name="send_gas_price_footer">Цена газа влияет на скорость транзакции. При сильно низкой, транзакция может быть не обработана.</string>
<string name="send_gas_limit">Лимит газа</string>
<string name="send_max_amount_label">Максимальная сумма</string>
<string name="send_max_amount">Всё</string>
<string name="send_max_fee_footer">Максимальная cумма комиссии</string>
<string name="send_max_fee">Комиссия не превысит</string>
<string name="send_network_fee_title">Сетевая комиссия</string>
<string name="send_network_fee_warning_title">Покрытие сетевой комиссии</string>
<string name="send_network_fee_warning_content">Сумма отправки будет уменьшена для покрытия выбранного уровня комиссии</string>
<string name="send_notification_transaction_delay_text">Обратите внимание, что при определенных параметрах комиссии возможны задержки по вашей транзакции</string>
<string name="send_notification_exceed_balance_title">Недостаточно средств</string>
<string name="send_notification_exceed_balance_text">Недостаточно средств для перевода, так как сумма комиссии и сумма перевода в совокупности больше имеющегося баланса.</string>
<string name="send_notification_invalid_amount_title">Недопустимая сумма</string>
<string name="send_notification_invalid_amount_text">Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению.</string>
<string name="send_notification_invalid_minimum_amount_text">Минимальная сумма отправки - %1$s. Пожалуйста, убедитесь, что остаток после отправки также не будет меньше %1$s.</string>
<string name="send_notification_invalid_reserve_amount_title">Сумма резерва не может быть менее %1$s.</string>
<string name="send_notification_top_up_text">Пожалуйста, пополните свой баланс, чтобы продолжить.</string>
<string name="send_notification_high_fee_title">Увеличение комиссии</string>
<string name="send_notification_high_fee_text">Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить 0.01.</string>
<string name="send_notification_exceed_fee_title">Комиссия превышает баланс</string>
<string name="send_notification_exceed_fee_text">Размер комиссии превышает баланс сети. Для продолжения необходимо пополнить баланс сети.</string>
<string name="send_notification_transaction_delay_title">Возможны задержки по транзакции</string>
<string name="send_optional_field">Необязательное</string>
<string name="send_qrcode_scan_amount_alert_title">QR код содержит информацию о сумме отправки равной %s</string>
<string name="send_recipient">Получатель</string>
<string name="send_recipient_wallets_title">Мои кошельки</string>
<string name="send_title_currency_format">Отправка %s</string>
<string name="send_total_label">Всего</string>
<string name="send_total_subtitle_asset_format">%1$s и %2$s будет отправлено</string>
@ -434,6 +496,9 @@
<string name="swapping_token_list_title">Выберите токен</string>
<string name="swapping_token_list_your_tokens">Ваши токены</string>
<string name="swapping_token_not_available">не доступен</string>
<string name="toast_balances_hidden">Балансы скрыты</string>
<string name="toast_balances_shown">Балансы показаны</string>
<string name="toast_undo">Отменить</string>
<plurals name="token_count">
<item quantity="one">%d токен</item>
<item quantity="few">%d токена</item>

View file

@ -28,6 +28,10 @@
<string name="app_settings_theme_mode_system">System default</string>
<string name="app_settings_theme_selector_title">Theme</string>
<string name="app_settings_title">App Settings</string>
<string name="balance_hidden_description">To hide or show your balances, simply flip your device screen down, or switch it off in Settings</string>
<string name="balance_hidden_do_not_show_button">Don\'t show again</string>
<string name="balance_hidden_got_it_button">Got it</string>
<string name="balance_hidden_title">Balances are hidden</string>
<string name="biometric_lockout_permanent_warning_description">Please scan the card</string>
<string name="biometric_lockout_warning_description">Please try again in 30 seconds or scan the card</string>
<string name="biometric_lockout_warning_title">Too many attempts</string>
@ -82,7 +86,14 @@
<string name="common_error">Error</string>
<string name="common_exchange">Exchange</string>
<string name="common_explore_transaction_history">Explore transaction history</string>
<string name="common_explore">Explore</string>
<string name="common_explorer">Explorer</string>
<string name="common_fee_selector_title">Speed and fee</string>
<string name="common_fee_selector_footer">Network transaction fees are used to support network security, incentivize validators, allocate resources, and determine transaction priority</string>
<string name="common_fee_selector_option_slow">Slow</string>
<string name="common_fee_selector_option_market">Market</string>
<string name="common_fee_selector_option_fast">Fast</string>
<string name="common_fee_selector_option_custom">Custom</string>
<string name="common_generate_addresses">Generate addresses</string>
<string name="common_import">Import</string>
<string name="common_learn_and_earn">Learn &amp; Earn</string>
@ -95,6 +106,7 @@
<string name="common_no_data">No data</string>
<string name="common_ok">OK</string>
<string name="common_origin_card">Primary Card</string>
<string name="common_paste">Paste</string>
<string name="common_receive">Receive</string>
<string name="common_reject">Reject</string>
<string name="common_reload">Reload</string>
@ -222,10 +234,9 @@
<string name="manage_tokens_network_selector_title">Choose networks</string>
<string name="manage_tokens_network_selector_wallet">Wallet</string>
<string name="manage_tokens_nothing_found">Couldnt find this token, you can add it manually</string>
<string name="manage_tokens_number_of_wallets">%d of %#@total_wallets@</string>
<plurals name="manage_tokens_number_of_walletstotal_wallets">
<item quantity="one">%d wallet</item>
<item quantity="other">%d wallets</item>
<plurals name="manage_tokens_number_of_wallets_android">
<item quantity="one">%1$d of %2$d wallet</item>
<item quantity="other">%1$d of %2$d wallets</item>
</plurals>
<string name="manage_tokens_search_placeholder">e.g. BTC I trust, hodl I must</string>
<string name="manage_tokens_title">Coin market cap</string>
@ -380,19 +391,64 @@
<string name="scan_card_settings_message">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.</string>
<string name="scan_card_settings_title">Get your card ready!</string>
<string name="send_amount_label">Amount</string>
<string name="send_amount_substract">Subtract from send amount</string>
<string name="send_amount_substract_footer">The recipient will receive %1$s</string>
<string name="send_destination_hint_address">Address</string>
<string name="send_destination_tag_field">Destination Tag</string>
<string name="send_datetime_formatter">%1$s at %2$s</string>
<string name="send_enter_address_field">Enter address</string>
<string name="send_error_address_same_as_wallet">Address is the same as wallet address</string>
<string name="send_extras_error_invalid_destination_tag">Invalid Tag. It won\'t be added to the transaction.</string>
<string name="send_extras_error_invalid_memo">Invalid Memo. It won\'t be added to the transaction.</string>
<string name="send_extras_hint_destination_tag">Tag</string>
<string name="send_extras_hint_memo">Memo</string>
<string name="send_insufficient_funds">Insufficient funds for transfer</string>
<string name="send_fee_include_description">Include fee</string>
<string name="send_fee_label">Fee</string>
<string name="send_fee_picker_low">Low</string>
<string name="send_fee_picker_normal">Normal</string>
<string name="send_fee_picker_priority">Priority</string>
<string name="send_fee_unreachable_error_title">Network fee info unreachable</string>
<string name="send_fee_unreachable_error_text">Check your network connection</string>
<string name="send_gas_limit">Gas limit</string>
<string name="send_gas_limit_footer">Gas Limit is auto-calculated; raise it during network congestion</string>
<string name="send_gas_price">Gas price</string>
<string name="send_gas_price_footer">Gas Price affects transaction speed. If it\'s too low, the transaction might not be processed.</string>
<string name="send_max_amount_label">Maximum amount</string>
<string name="send_max_amount">Max</string>
<string name="send_max_fee_footer">Maximum fee amount</string>
<string name="send_max_fee">Max fee</string>
<string name="send_memo_destination_tag_error">Numbers only for Destination Tag</string>
<string name="send_network_fee_title">Network fee</string>
<string name="send_network_fee_warning_title">Network fee coverage</string>
<string name="send_network_fee_warning_content">Sending amount will be reduced to cover the selected fee level</string>
<string name="send_notification_transaction_delay_text">Kindly be aware that your transaction may experience delays under specific fee settings</string>
<string name="send_notification_exceed_balance_title">Total exceeds balance</string>
<string name="send_notification_exceed_balance_text">Insufficient funds for the transfer, as the total of the fee and transfer amount exceeds the existing balance.</string>
<string name="send_notification_invalid_amount_title">Invalid amount</string>
<string name="send_notification_invalid_amount_text">The included commission exceeds the transfer amount, leading to a negative value.</string>
<string name="send_notification_invalid_minimum_amount_text">The minimum sending amount is %1$s. Please ensure that the remaining balance after sending will not be less than %1$s.</string>
<string name="send_notification_invalid_reserve_amount_title">The balance amount must be at least %1$s.</string>
<string name="send_notification_invalid_reserve_amount_text">Please top up your balance to continue.</string>
<string name="send_notification_high_fee_title">Fee is increased</string>
<string name="send_notification_high_fee_text">The fee for transferring the entire balance is higher. To reduce the commission, you can leave 0.01.</string>
<string name="send_notification_exceed_fee_title">Fee exceeds balance</string>
<string name="send_notification_exceed_fee_text">The commission fee exceeds the network balance. To continue, it is necessary to replenish the network balance.</string>
<string name="send_notification_transaction_delay_title">Transaction delays are possible</string>
<string name="send_optional_field">Optional</string>
<string name="send_qrcode_scan_info">Please align your QR code with the square to scan it. Ensure you scan %s network address.</string>
<string name="send_qrcode_scan_address_success">Recipients address scanned</string>
<string name="send_qrcode_scan_memo_success">Sending amount was changed</string>
<string name="send_qrcode_scan_amount_alert_title">QR code contains information about the sending amount equal to %s</string>
<string name="send_qrcode_scan_amount_alert_text">Change the entered amount?</string>
<string name="send_qrcode_alert_change">Change</string>
<string name="send_qrcode_alert_decline">Decline</string>
<string name="send_recent_transactions">Recent</string>
<string name="send_recipient">Recipient</string>
<string name="send_recipient_address_error">Not a valid address</string>
<string name="send_recipient_wallets_title">My wallets</string>
<string name="send_recipient_address_footer">Ensure that you are sending funds to an %1$s wallet address. Errors may result in the loss of your tokens.</string>
<string name="send_recipient_memo_footer">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.</string>
<string name="send_title_currency_format">Sending %s</string>
<string name="send_total_label">Total</string>
<string name="send_total_subtitle_asset_format">%1$s and %2$s will be sent</string>
@ -452,6 +508,9 @@
<string name="swapping_token_list_title">Choose token</string>
<string name="swapping_token_list_your_tokens">Your tokens</string>
<string name="swapping_token_not_available">not available</string>
<string name="toast_balances_hidden">Balances hidden</string>
<string name="toast_balances_shown">Balances shown</string>
<string name="toast_undo">Undo</string>
<plurals name="token_count">
<item quantity="one">%d token</item>
<item quantity="other">%d tokens</item>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 <reified T> SegmentedButtons(
config: PersistentList<T>,
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<SegmentedButtonsConfigPreview>,
) {
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<SegmentedButtonsConfigPreview>,
) {
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<PersistentList<SegmentedButtonsConfigPreview>>(
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

View file

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

View file

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

View file

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

View file

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

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