Updated on 2026-08-14
This commit is contained in:
commit
005847a3aa
262 changed files with 6411 additions and 1089 deletions
|
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.tap.features.main
|
||||
|
||||
internal interface MainIntents {
|
||||
|
||||
fun onHiddenBalanceToastAction()
|
||||
|
||||
fun onShownBalanceToastAction()
|
||||
|
||||
fun onHiddenBalanceNotificationAction(isPermanent: Boolean)
|
||||
|
||||
fun onDismissBottomSheet()
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
127
app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt
Normal file
127
app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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) },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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?,
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -243,7 +243,7 @@ internal class SendStateSubscriber(
|
|||
}
|
||||
|
||||
val filter = DecimalDigitsInputFilter(
|
||||
digitsBeforeDecimal = 12,
|
||||
digitsBeforeDecimal = 40,
|
||||
digitsAfterDecimal = state.maxLengthOfAmount,
|
||||
decimalSeparator = state.decimalSeparator,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -18,7 +18,9 @@ object DaggerGraphReducer {
|
|||
walletRouter = action.walletRouter,
|
||||
walletConnectInteractor = action.walletConnectInteractor,
|
||||
tokenDetailsRouter = action.tokenDetailsRouter,
|
||||
manageTokensRouter = action.manageTokensRouter,
|
||||
cardSdkConfigRepository = action.cardSdkConfigRepository,
|
||||
sendRouter = action.sendRouter,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
20
app/src/main/res/layout/layout_qr_scanning.xml
Normal file
20
app/src/main/res/layout/layout_qr_scanning.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>?,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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>,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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?,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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/"
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ internal class BalanceStateHidingSettingsStore(
|
|||
return getSyncOrNull() ?: BalanceHidingSettings(
|
||||
isHidingEnabledInSettings = false,
|
||||
isBalanceHidden = false,
|
||||
isBalanceHidingNotificationEnabled = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 & 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">Couldn’t 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">Recipient’s 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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")))
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue