Updated on 2026-08-14
This commit is contained in:
commit
31da96588b
473 changed files with 11011 additions and 9716 deletions
|
|
@ -20,6 +20,9 @@ configurations.all {
|
|||
|
||||
substitute(module("com.facebook.react:hermes-engine"))
|
||||
.using(module("com.facebook.react:hermes-android:0.72.4"))
|
||||
|
||||
substitute(module("org.bouncycastle:bcprov-jdk15on"))
|
||||
.using(module("org.bouncycastle:bcprov-jdk18on:1.73"))
|
||||
}
|
||||
|
||||
force(
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@
|
|||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="success.tangem.com"
|
||||
android:host="redirect"
|
||||
android:scheme="tangem" />
|
||||
</intent-filter>
|
||||
|
||||
|
|
|
|||
|
|
@ -434,6 +434,16 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id" : "algorand",
|
||||
"symbol" : "ALGO",
|
||||
"name" : "Algorand",
|
||||
"networks" : [
|
||||
{
|
||||
"networkId" : "algorand/test"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id" : "arbitrum-one",
|
||||
"symbol" : "ETH",
|
||||
|
|
@ -569,6 +579,16 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "shibarium",
|
||||
"name": "Shibarium",
|
||||
"symbol": "BONE",
|
||||
"networks": [
|
||||
{
|
||||
"networkId": "shibarium/test"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "aptos",
|
||||
"name": "Aptos",
|
||||
|
|
@ -578,6 +598,16 @@
|
|||
"networkId": "aptos/test"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "hedera-hashgraph",
|
||||
"name": "Hedera",
|
||||
"symbol": "HBAR",
|
||||
"networks": [
|
||||
{
|
||||
"networkId": "hedera-hashgraph/test"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ import com.tangem.domain.card.ScanCardUseCase
|
|||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.feature.qrscanning.QrScanningRouter
|
||||
import com.tangem.features.managetokens.navigation.ManageTokensRouter
|
||||
import com.tangem.features.managetokens.navigation.ManageTokensUi
|
||||
import com.tangem.features.send.api.navigation.SendRouter
|
||||
import com.tangem.features.tester.api.TesterRouter
|
||||
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
|
||||
|
|
@ -126,7 +126,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
lateinit var tokenDetailsRouter: TokenDetailsRouter
|
||||
|
||||
@Inject
|
||||
lateinit var manageTokensRouter: ManageTokensRouter
|
||||
lateinit var manageTokensUi: ManageTokensUi
|
||||
|
||||
@Inject
|
||||
lateinit var walletConnectInteractor: WalletConnectInteractor
|
||||
|
|
@ -213,7 +213,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
walletRouter = walletRouter,
|
||||
walletConnectInteractor = walletConnectInteractor,
|
||||
tokenDetailsRouter = tokenDetailsRouter,
|
||||
manageTokensRouter = manageTokensRouter,
|
||||
manageTokensUi = manageTokensUi,
|
||||
cardSdkConfigRepository = cardSdkConfigRepository,
|
||||
sendRouter = sendRouter,
|
||||
qrScanningRouter = qrScanningRouter,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import com.orhanobut.logger.AndroidLogAdapter
|
|||
import com.orhanobut.logger.Logger
|
||||
import com.tangem.Log
|
||||
import com.tangem.LogFormat
|
||||
import com.tangem.blockchain.common.AccountCreator
|
||||
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
|
||||
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.filter.OneTimeEventFilter
|
||||
|
|
@ -28,7 +30,6 @@ import com.tangem.domain.apptheme.GetAppThemeModeUseCase
|
|||
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
|
||||
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
|
||||
import com.tangem.domain.card.ScanCardProcessor
|
||||
import com.tangem.domain.card.repository.DerivationsRepository
|
||||
import com.tangem.domain.common.LogConfig
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.tokens.repository.NetworksRepository
|
||||
|
|
@ -37,10 +38,8 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
|||
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.tester.api.TesterFeatureToggles
|
||||
import com.tangem.tap.common.analytics.AnalyticsFactory
|
||||
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
|
||||
import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler
|
||||
import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler
|
||||
import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerAnalyticsHandler
|
||||
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler
|
||||
|
|
@ -118,9 +117,6 @@ internal class TapApplication : Application(), ImageLoaderFactory {
|
|||
@Inject
|
||||
lateinit var scanCardProcessor: ScanCardProcessor
|
||||
|
||||
@Inject
|
||||
lateinit var blockchainExceptionHandler: BlockchainExceptionHandler
|
||||
|
||||
@Inject
|
||||
lateinit var appCurrencyRepository: AppCurrencyRepository
|
||||
|
||||
|
|
@ -155,10 +151,10 @@ internal class TapApplication : Application(), ImageLoaderFactory {
|
|||
lateinit var oneTimeEventFilter: OneTimeEventFilter
|
||||
|
||||
@Inject
|
||||
lateinit var derivationsRepository: DerivationsRepository
|
||||
lateinit var blockchainDataStorage: BlockchainDataStorage
|
||||
|
||||
@Inject
|
||||
lateinit var testerFeatureToggles: TesterFeatureToggles
|
||||
lateinit var accountCreator: AccountCreator
|
||||
// endregion Injected
|
||||
|
||||
override fun onCreate() {
|
||||
|
|
@ -234,8 +230,8 @@ internal class TapApplication : Application(), ImageLoaderFactory {
|
|||
balanceHidingRepository = balanceHidingRepository,
|
||||
walletsRepository = walletsRepository,
|
||||
sendFeatureToggles = sendFeatureToggles,
|
||||
derivationsRepository = derivationsRepository,
|
||||
testerFeatureToggles = testerFeatureToggles,
|
||||
blockchainDataStorage = blockchainDataStorage,
|
||||
accountCreator = accountCreator,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.tap.common
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
import android.content.Intent.FLAG_ACTIVITY_NO_HISTORY
|
||||
import android.net.Uri
|
||||
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
|
|
@ -22,6 +24,10 @@ class CustomTabsManager {
|
|||
if (MutableAppThemeModeHolder.isDarkThemeActive) COLOR_SCHEME_DARK else COLOR_SCHEME_LIGHT,
|
||||
)
|
||||
.build()
|
||||
|
||||
// Open CustomTabsActivity as new task without saving into the stack
|
||||
customTabsIntent.intent.setFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_NO_HISTORY)
|
||||
|
||||
customTabsIntent.launchUrl(context, Uri.parse(url))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.tap.common.analytics.events
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.tap.common.extensions.filterNotNull
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -42,17 +41,22 @@ sealed class WalletConnect(
|
|||
val methodName: String,
|
||||
val blockchain: String,
|
||||
val errorCode: String? = null,
|
||||
val errorDescription: String? = null,
|
||||
) {
|
||||
fun toParamsMap(): Map<String, String> {
|
||||
val validation = if (errorCode == null) Validation.SUCCESS.param else Validation.FAIL.param
|
||||
return mapOf(
|
||||
AnalyticsParam.DAPP_NAME to dAppName,
|
||||
AnalyticsParam.DAPP_URL to dAppUrl,
|
||||
AnalyticsParam.METHOD_NAME to methodName,
|
||||
AnalyticsParam.BLOCKCHAIN to blockchain,
|
||||
AnalyticsParam.VALIDATION to validation,
|
||||
if (errorCode != null) AnalyticsParam.ERROR_CODE to errorCode else null to null,
|
||||
).filterNotNull()
|
||||
val code = errorCode ?: SUCCESS_CODE
|
||||
return buildMap {
|
||||
put(AnalyticsParam.DAPP_NAME, dAppName)
|
||||
put(AnalyticsParam.DAPP_URL, dAppUrl)
|
||||
put(AnalyticsParam.METHOD_NAME, methodName)
|
||||
put(AnalyticsParam.BLOCKCHAIN, blockchain)
|
||||
put(AnalyticsParam.VALIDATION, validation)
|
||||
put(AnalyticsParam.ERROR_CODE, code)
|
||||
if (errorDescription != null) {
|
||||
put(AnalyticsParam.ERROR_DESCRIPTION, errorDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -60,4 +64,8 @@ sealed class WalletConnect(
|
|||
SUCCESS("Success"),
|
||||
FAIL("Fail"),
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SUCCESS_CODE = "0"
|
||||
}
|
||||
}
|
||||
|
|
@ -151,18 +151,7 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
|
|||
AppScreen.ResetToFactory -> ResetCardFragment()
|
||||
AppScreen.AccessCodeRecovery -> AccessCodeRecoveryFragment()
|
||||
AppScreen.Disclaimer -> DisclaimerFragment()
|
||||
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.ManageTokens -> TokensListFragment()
|
||||
AppScreen.AddCustomToken -> AddCustomTokenFragment()
|
||||
AppScreen.WalletDetails -> {
|
||||
store.state.daggerGraphState
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ import com.tangem.tap.features.shop.redux.ShopMiddleware
|
|||
import com.tangem.tap.features.shop.redux.ShopState
|
||||
import com.tangem.tap.features.signin.redux.SignInMiddleware
|
||||
import com.tangem.tap.features.signin.redux.SignInState
|
||||
import com.tangem.tap.features.tokens.legacy.redux.TokensMiddleware
|
||||
import com.tangem.tap.features.tokens.legacy.redux.TokensState
|
||||
import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware
|
||||
import com.tangem.tap.features.welcome.redux.WelcomeMiddleware
|
||||
|
|
@ -91,7 +90,6 @@ data class AppState(
|
|||
SendMiddleware().sendMiddleware,
|
||||
DetailsMiddleware().detailsMiddleware,
|
||||
DisclaimerMiddleware().disclaimerMiddleware,
|
||||
TokensMiddleware.tokensMiddleware,
|
||||
WalletConnectMiddleware().walletConnectMiddleware,
|
||||
BackupMiddleware().backupMiddleware,
|
||||
ShopMiddleware().shopMiddleware,
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ class TruncateStart : BaseTruncate() {
|
|||
class TruncateMiddle : BaseTruncate() {
|
||||
|
||||
override fun roughTruncate(text: String, residualLength: Int): String {
|
||||
if (text.length <= residualLength) return text
|
||||
if (text.length <= residualLength || residualLength < 0) return text
|
||||
|
||||
hasBeenTruncated = true
|
||||
val halfOfResidualLength = residualLength / 2
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ import com.tangem.domain.demo.DemoConfig
|
|||
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||
import com.tangem.domain.wallets.legacy.WalletsStateHolder
|
||||
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
|
||||
import com.tangem.tap.domain.TangemSdkManager
|
||||
import com.tangem.tap.domain.card.DefaultDerivePublicKeysUseCase
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -62,14 +60,8 @@ internal object CardDomainModule {
|
|||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideDerivePublicKeysUseCase(
|
||||
tangemSdkManager: TangemSdkManager,
|
||||
derivationsRepository: DerivationsRepository,
|
||||
): DerivePublicKeysUseCase {
|
||||
return DefaultDerivePublicKeysUseCase(
|
||||
tangemSdkManager = tangemSdkManager,
|
||||
derivationsRepository = derivationsRepository,
|
||||
)
|
||||
fun provideDerivePublicKeysUseCase(derivationsRepository: DerivationsRepository): DerivePublicKeysUseCase {
|
||||
return DerivePublicKeysUseCase(derivationsRepository = derivationsRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ internal object TokensDomainModule {
|
|||
networksRepository: NetworksRepository,
|
||||
marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository,
|
||||
swapRepository: SwapRepository,
|
||||
currencyChecksRepository: CurrencyChecksRepository,
|
||||
showSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase,
|
||||
promoRepository: PromoRepository,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
|
|
@ -109,6 +110,7 @@ internal object TokensDomainModule {
|
|||
quotesRepository = quotesRepository,
|
||||
networksRepository = networksRepository,
|
||||
marketCryptoCurrencyRepository = marketCryptoCurrencyRepository,
|
||||
currencyChecksRepository = currencyChecksRepository,
|
||||
swapRepository = swapRepository,
|
||||
showSwapPromoTokenUseCase = showSwapPromoTokenUseCase,
|
||||
promoRepository = promoRepository,
|
||||
|
|
|
|||
|
|
@ -1,18 +1,34 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.visa.GetVisaCurrencyUseCase
|
||||
import com.tangem.domain.visa.GetVisaTxDetailsUseCase
|
||||
import com.tangem.domain.visa.GetVisaTxHistoryUseCase
|
||||
import com.tangem.domain.visa.repository.VisaRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ViewModelComponent
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
|
||||
@Module
|
||||
@InstallIn(ViewModelComponent::class)
|
||||
internal object VisaDomainModule {
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideVisaCurrencyUseCase(visaRepository: VisaRepository): GetVisaCurrencyUseCase {
|
||||
return GetVisaCurrencyUseCase(visaRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideGetVisaTxHistoryUseCase(visaRepository: VisaRepository): GetVisaTxHistoryUseCase {
|
||||
return GetVisaTxHistoryUseCase(visaRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideGetVisaTxDetailsUseCase(visaRepository: VisaRepository): GetVisaTxDetailsUseCase {
|
||||
return GetVisaTxDetailsUseCase(visaRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.blockchain.common.AccountCreator
|
||||
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
|
||||
import com.tangem.datasource.asset.AssetReader
|
||||
import com.tangem.datasource.config.ConfigManager
|
||||
import com.tangem.datasource.di.SdkMoshi
|
||||
|
|
@ -25,6 +27,8 @@ internal object WalletManagersFacadeModule {
|
|||
walletManagersStore: WalletManagersStore,
|
||||
userWalletsStore: UserWalletsStore,
|
||||
configManager: ConfigManager,
|
||||
blockchainDataStorage: BlockchainDataStorage,
|
||||
accountCreator: AccountCreator,
|
||||
mnemonicRepository: MnemonicRepository,
|
||||
assetReader: AssetReader,
|
||||
@SdkMoshi moshi: Moshi,
|
||||
|
|
@ -33,9 +37,11 @@ internal object WalletManagersFacadeModule {
|
|||
walletManagersStore = walletManagersStore,
|
||||
userWalletsStore = userWalletsStore,
|
||||
configManager = configManager,
|
||||
blockchainDataStorage = blockchainDataStorage,
|
||||
assetReader = assetReader,
|
||||
moshi = moshi,
|
||||
mnemonic = mnemonicRepository.generateDefaultMnemonic(),
|
||||
accountCreator = accountCreator,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,19 +5,20 @@ import com.tangem.blockchain.common.Token
|
|||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.blockchain.common.WalletManagerFactory
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.datasource.config.ConfigManager
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.operations.attestation.Attestation
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.tap.common.extensions.setContext
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
|
||||
import com.tangem.tap.features.disclaimer.createDisclaimer
|
||||
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
|
||||
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
|
||||
|
|
@ -40,8 +41,13 @@ class TapWalletManager(
|
|||
field = value
|
||||
}
|
||||
|
||||
val walletManagerFactory: WalletManagerFactory
|
||||
by lazy { WalletManagerFactory(blockchainSdkConfig) }
|
||||
val walletManagerFactory: WalletManagerFactory by lazy {
|
||||
WalletManagerFactory(
|
||||
config = blockchainSdkConfig,
|
||||
accountCreator = store.state.daggerGraphState.get(DaggerGraphState::accountCreator),
|
||||
blockchainDataStorage = store.state.daggerGraphState.get(DaggerGraphState::blockchainDataStorage),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun onWalletSelected(userWallet: UserWallet, sendAnalyticsEvent: Boolean) {
|
||||
// If a previous job was running, it gets cancelled before the new one starts,
|
||||
|
|
|
|||
|
|
@ -1,41 +0,0 @@
|
|||
package com.tangem.tap.domain.card
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.tangem.common.doOnFailure
|
||||
import com.tangem.common.doOnSuccess
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.domain.card.DerivePublicKeysUseCase
|
||||
import com.tangem.domain.card.repository.DerivationsRepository
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.operations.derivation.DerivationTaskResponse
|
||||
import com.tangem.tap.domain.TangemSdkManager
|
||||
|
||||
internal class DefaultDerivePublicKeysUseCase(
|
||||
private val tangemSdkManager: TangemSdkManager,
|
||||
private val derivationsRepository: DerivationsRepository,
|
||||
) : DerivePublicKeysUseCase {
|
||||
|
||||
override suspend fun invoke(
|
||||
cardId: String?,
|
||||
derivations: Map<ByteArrayKey, List<DerivationPath>>,
|
||||
): Either<Unit, DerivationTaskResponse> {
|
||||
tangemSdkManager.derivePublicKeys(cardId = cardId, derivations = derivations)
|
||||
.doOnSuccess { return it.right() }
|
||||
.doOnFailure { return Unit.left() }
|
||||
|
||||
return Unit.left()
|
||||
}
|
||||
|
||||
override suspend fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
currencies: List<CryptoCurrency>,
|
||||
): Either<Throwable, Unit> {
|
||||
return Either.catch {
|
||||
derivationsRepository.derivePublicKeys(userWalletId, currencies)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -352,7 +352,8 @@ class WalletConnectRepositoryImpl @Inject constructor(
|
|||
dAppUrl = session?.url ?: "",
|
||||
methodName = requestData.method,
|
||||
blockchain = requestData.blockchain,
|
||||
errorCode = WalletConnectError.ValidationError.toString(),
|
||||
errorCode = WalletConnectError.ValidationError.error,
|
||||
errorDescription = it.throwable.message,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,24 @@
|
|||
package com.tangem.tap.domain.walletconnect2.domain.models
|
||||
|
||||
sealed class WalletConnectError : Exception() {
|
||||
data class ApprovalErrorMissingNetworks(val missingChains: List<String>) : WalletConnectError()
|
||||
data class ApprovalErrorAddNetwork(val networks: List<String>) : WalletConnectError()
|
||||
data class ApprovalErrorUnsupportedNetwork(val unsupportedNetworks: List<String>) : WalletConnectError()
|
||||
data class ExternalApprovalError(override val message: String?) : WalletConnectError()
|
||||
object WrongUserWallet : WalletConnectError()
|
||||
object UnsupportedMethod : WalletConnectError()
|
||||
object SigningError : WalletConnectError()
|
||||
object ValidationError : WalletConnectError()
|
||||
sealed class WalletConnectError(val error: String) : Exception() {
|
||||
data class ApprovalErrorMissingNetworks(
|
||||
val missingChains: List<String>,
|
||||
) : WalletConnectError("ApprovalErrorMissingNetworks")
|
||||
|
||||
data class ApprovalErrorAddNetwork(
|
||||
val networks: List<String>,
|
||||
) : WalletConnectError("ApprovalErrorAddNetwork")
|
||||
|
||||
data class ApprovalErrorUnsupportedNetwork(
|
||||
val unsupportedNetworks: List<String>,
|
||||
) : WalletConnectError("ApprovalErrorUnsupportedNetwork")
|
||||
|
||||
data class ExternalApprovalError(
|
||||
override val message: String?,
|
||||
) : WalletConnectError("ExternalApprovalError")
|
||||
|
||||
object WrongUserWallet : WalletConnectError("WrongUserWallet")
|
||||
object UnsupportedMethod : WalletConnectError("UnsupportedMethod")
|
||||
object SigningError : WalletConnectError("SigningError")
|
||||
object ValidationError : WalletConnectError("ValidationError")
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ package com.tangem.tap.features.customtoken.impl.di
|
|||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.domain.card.DerivePublicKeysUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.features.tester.api.TesterFeatureToggles
|
||||
import com.tangem.tap.features.customtoken.impl.data.DefaultCustomTokenRepository
|
||||
import com.tangem.tap.features.customtoken.impl.domain.CustomTokenInteractor
|
||||
import com.tangem.tap.features.customtoken.impl.domain.DefaultCustomTokenInteractor
|
||||
|
|
@ -30,7 +29,6 @@ internal object CustomTokenInteractorModule {
|
|||
reduxStateHolder: AppStateHolder,
|
||||
getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
derivePublicKeysUseCase: DerivePublicKeysUseCase,
|
||||
testerFeatureToggles: TesterFeatureToggles,
|
||||
): CustomTokenInteractor {
|
||||
return DefaultCustomTokenInteractor(
|
||||
featureRepository = DefaultCustomTokenRepository(
|
||||
|
|
@ -40,7 +38,6 @@ internal object CustomTokenInteractorModule {
|
|||
),
|
||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||
derivePublicKeysUseCase = derivePublicKeysUseCase,
|
||||
testerFeatureToggles = testerFeatureToggles,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,40 +1,19 @@
|
|||
package com.tangem.tap.features.customtoken.impl.domain
|
||||
|
||||
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.core.TangemError
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.common.extensions.toMapKey
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
|
||||
import com.tangem.domain.card.DerivePublicKeysUseCase
|
||||
import com.tangem.domain.common.configs.CardConfig
|
||||
import com.tangem.domain.common.extensions.toNetworkId
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.common.util.hasDerivation
|
||||
import com.tangem.domain.features.addCustomToken.CustomCurrency
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.features.tester.api.TesterFeatureToggles
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.model.Currency
|
||||
import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken
|
||||
import com.tangem.tap.features.tokens.legacy.redux.TokensMiddleware
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
import com.tangem.tap.userWalletsListManager
|
||||
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
|
||||
import kotlinx.coroutines.delay
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
|
|
@ -48,7 +27,6 @@ class DefaultCustomTokenInteractor(
|
|||
private val featureRepository: CustomTokenRepository,
|
||||
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
private val derivePublicKeysUseCase: DerivePublicKeysUseCase,
|
||||
private val testerFeatureToggles: TesterFeatureToggles,
|
||||
) : CustomTokenInteractor {
|
||||
|
||||
// TODO: Move to DI
|
||||
|
|
@ -70,136 +48,12 @@ class DefaultCustomTokenInteractor(
|
|||
val userWallet = getSelectedWalletSyncUseCase().fold(ifLeft = { return }, ifRight = { it })
|
||||
val currency = Currency.fromCustomCurrency(customCurrency)
|
||||
|
||||
if (testerFeatureToggles.isDerivePublicKeysRefactoringEnabled) {
|
||||
val currencies = listOfNotNull(element = currency.toCryptoCurrency(userWallet.scanResponse))
|
||||
derivePublicKeysUseCase(userWalletId = userWallet.walletId, currencies = currencies)
|
||||
.onRight {
|
||||
addCryptoCurrenciesUseCase(userWalletId = userWallet.walletId, currencies = currencies)
|
||||
}
|
||||
.onLeft { Timber.e("Failed to derive public keys: $it") }
|
||||
} else {
|
||||
// TODO: delete [REDACTED_JIRA]
|
||||
val isNeedToDerive = isNeedToDerive(userWallet, currency)
|
||||
if (isNeedToDerive) {
|
||||
deriveMissingBlockchains(
|
||||
userWallet = userWallet,
|
||||
currencyList = listOf(currency),
|
||||
onSuccess = { submitAdd(userWallet = userWallet.copy(scanResponse = it), currency = currency) },
|
||||
) {
|
||||
throw it
|
||||
}
|
||||
} else {
|
||||
submitAdd(userWallet, currency)
|
||||
val currencies = listOfNotNull(element = currency.toCryptoCurrency(userWallet.scanResponse))
|
||||
derivePublicKeysUseCase(userWalletId = userWallet.walletId, currencies = currencies)
|
||||
.onRight {
|
||||
addCryptoCurrenciesUseCase(userWalletId = userWallet.walletId, currencies = currencies)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isNeedToDerive(userWallet: UserWallet, currency: Currency): Boolean {
|
||||
val scanResponse = userWallet.scanResponse
|
||||
return currency.derivationPath?.let { !scanResponse.hasDerivation(currency.blockchain, it) } ?: false
|
||||
}
|
||||
|
||||
private suspend fun deriveMissingBlockchains(
|
||||
userWallet: UserWallet,
|
||||
currencyList: List<Currency>,
|
||||
onSuccess: suspend (ScanResponse) -> Unit,
|
||||
onFailure: suspend (TangemError) -> Unit,
|
||||
) {
|
||||
val scanResponse = userWallet.scanResponse
|
||||
val config = CardConfig.createConfig(scanResponse.card)
|
||||
val derivationDataList = currencyList.mapNotNull { currency ->
|
||||
val curve = config.primaryCurve(currency.blockchain)
|
||||
curve?.let { getDerivations(curve, scanResponse, currency) }
|
||||
}
|
||||
|
||||
val derivations = buildMap<ByteArrayKey, MutableList<DerivationPath>> {
|
||||
derivationDataList.forEach {
|
||||
val current = this[it.derivations.first]
|
||||
if (current != null) {
|
||||
current.addAll(it.derivations.second)
|
||||
current.distinct()
|
||||
} else {
|
||||
this[it.derivations.first] = it.derivations.second.toMutableList()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (derivations.isEmpty()) {
|
||||
onSuccess(scanResponse)
|
||||
return
|
||||
}
|
||||
|
||||
when (val result = tangemSdkManager.derivePublicKeys(cardId = null, derivations = derivations)) {
|
||||
is CompletionResult.Success -> {
|
||||
val newDerivedKeys = result.data.entries
|
||||
val oldDerivedKeys = scanResponse.derivedKeys
|
||||
|
||||
val walletKeys = (newDerivedKeys.keys + oldDerivedKeys.keys).toSet()
|
||||
|
||||
val updatedDerivedKeys = walletKeys.associateWith { walletKey ->
|
||||
val oldDerivations = ExtendedPublicKeysMap(oldDerivedKeys[walletKey] ?: emptyMap())
|
||||
val newDerivations = newDerivedKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap())
|
||||
ExtendedPublicKeysMap(oldDerivations + newDerivations)
|
||||
}
|
||||
|
||||
val updatedScanResponse = scanResponse.copy(derivedKeys = updatedDerivedKeys)
|
||||
store.dispatchOnMain(GlobalAction.SaveScanResponse(updatedScanResponse))
|
||||
delay(DELAY_SDK_DIALOG_CLOSE)
|
||||
|
||||
onSuccess(updatedScanResponse)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
onFailure.invoke(result.error)
|
||||
store.dispatchDebugErrorNotification(TapError.CustomError("Error adding tokens"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDerivations(
|
||||
curve: EllipticCurve,
|
||||
scanResponse: ScanResponse,
|
||||
currency: Currency,
|
||||
): TokensMiddleware.DerivationData? {
|
||||
val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null
|
||||
|
||||
val supportedCurves = currency.blockchain.getSupportedCurves()
|
||||
val path = currency.blockchain.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle())
|
||||
.takeIf { supportedCurves.contains(curve) }
|
||||
|
||||
val customPath = currency.derivationPath?.let {
|
||||
DerivationPath(it)
|
||||
}.takeIf { supportedCurves.contains(curve) }
|
||||
|
||||
val bothCandidates = listOfNotNull(path, customPath).distinct().toMutableList()
|
||||
if (bothCandidates.isEmpty()) return null
|
||||
|
||||
if (currency is Currency.Blockchain && currency.blockchain == Blockchain.Cardano) {
|
||||
currency.derivationPath?.let {
|
||||
bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it)))
|
||||
}
|
||||
}
|
||||
|
||||
val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey()
|
||||
val alreadyDerivedKeys: ExtendedPublicKeysMap =
|
||||
scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap())
|
||||
val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList()
|
||||
|
||||
val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) }
|
||||
if (toDerive.isEmpty()) return null
|
||||
|
||||
return TokensMiddleware.DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive)
|
||||
}
|
||||
|
||||
private suspend fun submitAdd(userWallet: UserWallet, currency: Currency) {
|
||||
val scanResponse = userWallet.scanResponse
|
||||
val userWalletId = userWallet.walletId
|
||||
|
||||
val currencyList = listOfNotNull(element = currency.toCryptoCurrency(scanResponse))
|
||||
|
||||
userWalletsListManager.update(userWalletId) {
|
||||
it.copy(scanResponse = scanResponse)
|
||||
}
|
||||
|
||||
addCryptoCurrenciesUseCase(userWalletId, currencyList)
|
||||
.onLeft { Timber.e("Failed to derive public keys: $it") }
|
||||
}
|
||||
|
||||
private fun Currency.toCryptoCurrency(scanResponse: ScanResponse): CryptoCurrency? {
|
||||
|
|
|
|||
|
|
@ -3,21 +3,21 @@ package com.tangem.tap.features.customtoken.impl.presentation.validators
|
|||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.address.AddressService
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.domain.AddCustomTokenError
|
||||
import com.tangem.domain.tokens.error.AddCustomTokenError
|
||||
|
||||
/**
|
||||
* Validator of contract address
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
object ContactAddressValidator {
|
||||
object ContractAddressValidator {
|
||||
|
||||
/** Validate a [address] using [blockchain] */
|
||||
fun validate(address: String, blockchain: Blockchain): ContractAddressValidatorResult {
|
||||
return when {
|
||||
address.isEmpty() -> ContractAddressValidatorResult.Error(type = AddCustomTokenError.FieldIsEmpty)
|
||||
address.isEmpty() -> ContractAddressValidatorResult.Error(type = AddCustomTokenError.FIELD_IS_EMPTY)
|
||||
validateAddress(blockchain, address) -> ContractAddressValidatorResult.Success
|
||||
else -> ContractAddressValidatorResult.Error(type = AddCustomTokenError.InvalidContractAddress)
|
||||
else -> ContractAddressValidatorResult.Error(type = AddCustomTokenError.INVALID_CONTRACT_ADDRESS)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.tap.features.customtoken.impl.presentation.validators
|
||||
|
||||
import com.tangem.domain.AddCustomTokenError
|
||||
import com.tangem.domain.tokens.error.AddCustomTokenError
|
||||
|
||||
/**
|
||||
* Result of validation contract address
|
||||
|
|
|
|||
|
|
@ -16,13 +16,13 @@ import com.tangem.blockchain.common.derivation.DerivationStyle
|
|||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.crypto.hdWallet.HDWalletError
|
||||
import com.tangem.domain.AddCustomTokenError
|
||||
import com.tangem.domain.common.DerivationStyleProvider
|
||||
import com.tangem.domain.common.extensions.*
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.features.addCustomToken.CustomCurrency
|
||||
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.tokens.error.AddCustomTokenError
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.tap.features.customtoken.impl.domain.CustomTokenInteractor
|
||||
|
|
@ -33,7 +33,7 @@ import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTok
|
|||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenSelectorField.SelectorItem
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.routers.CustomTokenRouter
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTokenStateHolder
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.validators.ContactAddressValidator
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.validators.ContractAddressValidator
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.validators.ContractAddressValidatorResult
|
||||
import com.tangem.tap.features.details.ui.cardsettings.TextReference
|
||||
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
|
||||
|
|
@ -461,11 +461,11 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
private fun getTokenWarningSet(): Set<AddCustomTokenWarning> {
|
||||
val networkSelectorValue = uiState.form.networkSelectorField.selectedItem.blockchain
|
||||
|
||||
val isContractAddressFieldEmpty = ContactAddressValidator.validate(
|
||||
val isContractAddressFieldEmpty = ContractAddressValidator.validate(
|
||||
address = uiState.form.contractAddressInputField.value,
|
||||
blockchain = networkSelectorValue,
|
||||
).let {
|
||||
it is ContractAddressValidatorResult.Error && it.type == AddCustomTokenError.FieldIsEmpty
|
||||
it is ContractAddressValidatorResult.Error && it.type == AddCustomTokenError.FIELD_IS_EMPTY
|
||||
}
|
||||
|
||||
val isSupportedToken = if (!isNetworkSelected()) {
|
||||
|
|
@ -513,7 +513,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
val state = when {
|
||||
isAllTokenFieldsFilled() && isNetworkSelected() -> {
|
||||
val networkSelectorValue = uiState.form.networkSelectorField.selectedItem.blockchain
|
||||
val error = ContactAddressValidator.validate(
|
||||
val error = ContractAddressValidator.validate(
|
||||
address = uiState.form.contractAddressInputField.value,
|
||||
blockchain = networkSelectorValue,
|
||||
)
|
||||
|
|
@ -621,7 +621,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
|
||||
private fun handleContractAddressErrorValidation(type: AddCustomTokenError) {
|
||||
when {
|
||||
isNetworkSelected() && type == AddCustomTokenError.InvalidContractAddress -> {
|
||||
isNetworkSelected() && type == AddCustomTokenError.INVALID_CONTRACT_ADDRESS -> {
|
||||
val isAnotherTokenFieldsFilled = isAnyTokenFieldsFilled()
|
||||
uiState = uiState.copySealed(
|
||||
form = uiState.form.copy(
|
||||
|
|
@ -644,7 +644,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
!isNetworkSelected() || type == AddCustomTokenError.FieldIsEmpty -> {
|
||||
!isNetworkSelected() || type == AddCustomTokenError.FIELD_IS_EMPTY -> {
|
||||
uiState = uiState.copySealed(
|
||||
form = uiState.form.copy(
|
||||
contractAddressInputField = uiState.form.contractAddressInputField.copy(isError = false),
|
||||
|
|
@ -727,7 +727,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
)
|
||||
|
||||
val selectedNetwork = uiState.form.networkSelectorField.selectedItem.blockchain
|
||||
val validatorResult = ContactAddressValidator.validate(
|
||||
val validatorResult = ContractAddressValidator.validate(
|
||||
address = enteredValue,
|
||||
blockchain = selectedNetwork,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ private fun StoriesScreenContent(config: StoriesScreenContentConfig, modifier: M
|
|||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = config.currentStory == Stories.Currencies,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Arrangement
|
|||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.ButtonColors
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
|
@ -13,15 +12,11 @@ import androidx.compose.ui.res.stringResource
|
|||
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.SpacerW8
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButton
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonColors
|
||||
import com.tangem.core.ui.components.SpacerW12
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.wallet.R
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
internal fun HomeButtons(
|
||||
btnScanStateInProgress: Boolean,
|
||||
|
|
@ -38,7 +33,7 @@ internal fun HomeButtons(
|
|||
showProgress = btnScanStateInProgress,
|
||||
onClick = onScanButtonClick,
|
||||
)
|
||||
SpacerW8()
|
||||
SpacerW12()
|
||||
OrderCardButton(
|
||||
modifier = Modifier.weight(weight = 1f),
|
||||
onClick = onShopButtonClick,
|
||||
|
|
@ -48,44 +43,26 @@ internal fun HomeButtons(
|
|||
|
||||
@Composable
|
||||
private fun ScanCardButton(showProgress: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
TangemButton(
|
||||
StoriesButton(
|
||||
modifier = modifier,
|
||||
text = stringResource(id = R.string.home_button_scan),
|
||||
useDarkerColors = false,
|
||||
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_tangem_24),
|
||||
colors = LightBgScanCardButtonColors,
|
||||
showProgress = showProgress,
|
||||
enabled = true,
|
||||
onClick = onClick,
|
||||
showProgress = showProgress,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OrderCardButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
TangemButton(
|
||||
StoriesButton(
|
||||
modifier = modifier,
|
||||
text = stringResource(id = R.string.home_button_order),
|
||||
icon = TangemButtonIconPosition.None,
|
||||
colors = LightBgOrderCardButtonColors,
|
||||
showProgress = false,
|
||||
enabled = true,
|
||||
useDarkerColors = true,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
||||
private val LightBgScanCardButtonColors: ButtonColors = TangemButtonColors(
|
||||
backgroundColor = TangemColorPalette.Light4,
|
||||
contentColor = TangemColorPalette.Dark6,
|
||||
disabledBackgroundColor = TangemColorPalette.Dark5,
|
||||
disabledContentColor = TangemColorPalette.Dark6,
|
||||
)
|
||||
|
||||
private val LightBgOrderCardButtonColors: ButtonColors = TangemButtonColors(
|
||||
backgroundColor = TangemColorPalette.Dark6,
|
||||
contentColor = TangemColorPalette.White,
|
||||
disabledBackgroundColor = TangemColorPalette.Dark6,
|
||||
disabledContentColor = TangemColorPalette.White,
|
||||
)
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
|
|
@ -95,10 +72,10 @@ private fun HomeButtonsPreview(@PreviewParameter(HomeButtonsParameterProvider::c
|
|||
modifier = Modifier.background(Color.Black),
|
||||
) {
|
||||
HomeButtons(
|
||||
modifier = Modifier.padding(all = TangemTheme.dimens.spacing16),
|
||||
btnScanStateInProgress = state.btnScanStateInProgress,
|
||||
onScanButtonClick = {},
|
||||
onShopButtonClick = {},
|
||||
modifier = Modifier.padding(all = TangemTheme.dimens.spacing16),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,31 +1,42 @@
|
|||
package com.tangem.tap.features.home.compose.views
|
||||
|
||||
import androidx.compose.material.ButtonColors
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButton
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonColors
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.wallet.R
|
||||
|
||||
@Composable
|
||||
internal fun SearchCurrenciesButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
TangemButton(
|
||||
StoriesButton(
|
||||
modifier = modifier,
|
||||
text = stringResource(id = R.string.common_search_tokens),
|
||||
icon = TangemButtonIconPosition.Start(R.drawable.ic_search_24),
|
||||
onClick = onClick,
|
||||
colors = SearchCurrenciesButtonColors,
|
||||
showProgress = false,
|
||||
enabled = true,
|
||||
useDarkerColors = true,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
||||
private val SearchCurrenciesButtonColors: ButtonColors = TangemButtonColors(
|
||||
backgroundColor = TangemColorPalette.Dark5,
|
||||
contentColor = TangemColorPalette.White,
|
||||
disabledBackgroundColor = TangemColorPalette.Dark5,
|
||||
disabledContentColor = TangemColorPalette.White,
|
||||
)
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun SearchCurrenciesButtonPreview() {
|
||||
TangemTheme {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(color = Color.Black)
|
||||
.padding(all = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
SearchCurrenciesButton(modifier = Modifier.fillMaxWidth(), onClick = {})
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion Preview
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package com.tangem.tap.features.home.compose.views
|
||||
|
||||
import androidx.compose.material.ButtonColors
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButton
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonColors
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
@Composable
|
||||
internal fun StoriesButton(
|
||||
text: String,
|
||||
useDarkerColors: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
icon: TangemButtonIconPosition = TangemButtonIconPosition.None,
|
||||
showProgress: Boolean = false,
|
||||
) {
|
||||
TangemButton(
|
||||
modifier = modifier,
|
||||
text = text,
|
||||
icon = icon,
|
||||
colors = if (useDarkerColors) DarkerButtonColors else LighterButtonColors,
|
||||
showProgress = showProgress,
|
||||
enabled = true,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
iconPadding = when (icon) {
|
||||
is TangemButtonIconPosition.Start -> TangemTheme.dimens.spacing4
|
||||
is TangemButtonIconPosition.End,
|
||||
is TangemButtonIconPosition.None,
|
||||
-> TangemTheme.dimens.spacing8
|
||||
},
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
||||
private val LighterButtonColors: ButtonColors = TangemButtonColors(
|
||||
backgroundColor = TangemColorPalette.Light4,
|
||||
contentColor = TangemColorPalette.Dark6,
|
||||
disabledBackgroundColor = TangemColorPalette.Dark5,
|
||||
disabledContentColor = TangemColorPalette.Dark6,
|
||||
)
|
||||
|
||||
private val DarkerButtonColors: ButtonColors = TangemButtonColors(
|
||||
backgroundColor = TangemColorPalette.Dark4,
|
||||
contentColor = TangemColorPalette.White,
|
||||
disabledBackgroundColor = TangemColorPalette.Dark4,
|
||||
disabledContentColor = TangemColorPalette.White,
|
||||
)
|
||||
|
|
@ -13,10 +13,10 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.SpacerW4
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
|
|
@ -30,7 +30,7 @@ fun StoriesProgressBar(
|
|||
stepDuration: Int = 8_000,
|
||||
onStepFinish: () -> Unit = {},
|
||||
) {
|
||||
val progress = remember(currentStep) { Animatable(0f) }
|
||||
val progress = remember(currentStep) { Animatable(initialValue = 0f) }
|
||||
|
||||
val context = LocalContext.current
|
||||
val animatorSpeed = Settings.Global.getFloat(
|
||||
|
|
@ -75,20 +75,21 @@ fun StoriesProgressBar(
|
|||
.height(TangemTheme.dimens.size2)
|
||||
.weight(1f)
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius2))
|
||||
.background(Color.White.copy(alpha = 0.4f)),
|
||||
.background(TangemColorPalette.White.copy(alpha = .2f)),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius2))
|
||||
.background(Color.White)
|
||||
.fillMaxHeight().let {
|
||||
.background(TangemColorPalette.White)
|
||||
.fillMaxHeight()
|
||||
.let {
|
||||
when (index) {
|
||||
currentStep -> it.fillMaxWidth(progress.value)
|
||||
in 0..currentStep -> it.fillMaxWidth(1f)
|
||||
in 0..currentStep -> it.fillMaxWidth(fraction = 1f)
|
||||
else -> it
|
||||
}
|
||||
},
|
||||
) {}
|
||||
)
|
||||
}
|
||||
if (index != steps) {
|
||||
SpacerW4()
|
||||
|
|
@ -100,5 +101,12 @@ fun StoriesProgressBar(
|
|||
@Preview
|
||||
@Composable
|
||||
private fun StoriesProgressBarPreview() {
|
||||
StoriesProgressBar(steps = 3, currentStep = 2, paused = false) { }
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.wrapContentSize()
|
||||
.background(TangemColorPalette.Black)
|
||||
.padding(vertical = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
StoriesProgressBar(steps = 5, currentStep = 3, paused = false)
|
||||
}
|
||||
}
|
||||
|
|
@ -59,26 +59,41 @@ sealed class TransactionExtrasAction : SendScreenActionUi {
|
|||
|
||||
object Release : TransactionExtrasAction()
|
||||
|
||||
@Deprecated("Only in legacy send screen")
|
||||
sealed class XlmMemo : TransactionExtrasAction() {
|
||||
// data class ChangeSelectedMemo(val memoType: XlmMemoType) : XlmMemo()
|
||||
data class HandleUserInput(val data: String) : XlmMemo()
|
||||
}
|
||||
|
||||
@Deprecated("Only in legacy send screen")
|
||||
sealed class BinanceMemo : TransactionExtrasAction() {
|
||||
data class HandleUserInput(val data: String) : BinanceMemo()
|
||||
}
|
||||
|
||||
@Deprecated("Only in legacy send screen")
|
||||
sealed class XrpDestinationTag : TransactionExtrasAction() {
|
||||
data class HandleUserInput(val data: String) : XrpDestinationTag()
|
||||
}
|
||||
|
||||
@Deprecated("Only in legacy send screen")
|
||||
sealed class TonMemo : TransactionExtrasAction() {
|
||||
data class HandleUserInput(val data: String) : TonMemo()
|
||||
}
|
||||
|
||||
@Deprecated("Only in legacy send screen")
|
||||
sealed class CosmosMemo : TransactionExtrasAction() {
|
||||
data class HandleUserInput(val data: String) : CosmosMemo()
|
||||
}
|
||||
|
||||
@Deprecated("Only in legacy send screen")
|
||||
sealed class HederaMemo : TransactionExtrasAction() {
|
||||
data class HandleUserInput(val data: String) : HederaMemo()
|
||||
}
|
||||
|
||||
@Deprecated("Only in legacy send screen")
|
||||
sealed class AlgorandMemo : TransactionExtrasAction() {
|
||||
data class HandleUserInput(val data: String) : AlgorandMemo()
|
||||
}
|
||||
}
|
||||
|
||||
sealed class AddressVerifyAction : SendScreenAction {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
package com.tangem.tap.features.send.redux.middlewares
|
||||
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import com.tangem.blockchain.blockchains.algorand.AlgorandTransactionExtras
|
||||
import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras
|
||||
import com.tangem.blockchain.blockchains.cosmos.CosmosTransactionExtras
|
||||
import com.tangem.blockchain.blockchains.hedera.HederaTransactionBuilder
|
||||
import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider
|
||||
import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras
|
||||
import com.tangem.blockchain.blockchains.ton.TonTransactionExtras
|
||||
|
|
@ -171,6 +173,12 @@ private fun sendTransaction(
|
|||
transactionExtras.xrpDestinationTag?.tag?.let { txData = txData.copy(extras = XrpTransactionExtras(it)) }
|
||||
transactionExtras.cosmosMemoState?.memo?.let { txData = txData.copy(extras = CosmosTransactionExtras(it)) }
|
||||
transactionExtras.tonMemoState?.memo?.let { txData = txData.copy(extras = TonTransactionExtras(it)) }
|
||||
transactionExtras.hederaMemoState?.memo?.let {
|
||||
txData = txData.copy(
|
||||
extras = HederaTransactionBuilder.HederaTransactionExtras(it),
|
||||
)
|
||||
}
|
||||
transactionExtras.algorandMemoState?.memo?.let { txData = txData.copy(extras = AlgorandTransactionExtras(it)) }
|
||||
|
||||
scope.launch {
|
||||
// TODO: Risky commented this part, unknown logic, need to test if removed
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ class TransactionExtrasReducer : SendInternalReducer {
|
|||
is XrpDestinationTag -> handleXrpTag(action, sendState, sendState.transactionExtrasState)
|
||||
is TonMemo -> handleTonMemo(action, sendState, sendState.transactionExtrasState)
|
||||
is CosmosMemo -> handleCosmosMemo(action, sendState, sendState.transactionExtrasState)
|
||||
is HederaMemo -> handleHederaMemo(action, sendState, sendState.transactionExtrasState)
|
||||
is AlgorandMemo -> handleAlgorandMemo(action, sendState, sendState.transactionExtrasState)
|
||||
else -> sendState
|
||||
}
|
||||
}
|
||||
|
|
@ -52,6 +54,10 @@ class TransactionExtrasReducer : SendInternalReducer {
|
|||
Blockchain.TerraV1,
|
||||
Blockchain.TerraV2,
|
||||
-> TransactionExtrasState(cosmosMemoState = CosmosMemoState())
|
||||
Blockchain.Hedera, Blockchain.HederaTestnet -> TransactionExtrasState(hederaMemoState = HederaMemoState())
|
||||
Blockchain.Algorand, Blockchain.AlgorandTestnet -> TransactionExtrasState(
|
||||
algorandMemoState = AlgorandMemoState(),
|
||||
)
|
||||
else -> emptyResult
|
||||
}
|
||||
return updateLastState(sendState.copy(transactionExtrasState = result), result)
|
||||
|
|
@ -162,4 +168,34 @@ class TransactionExtrasReducer : SendInternalReducer {
|
|||
}
|
||||
return updateLastState(sendState.copy(transactionExtrasState = result), result)
|
||||
}
|
||||
|
||||
private fun handleHederaMemo(
|
||||
action: HederaMemo,
|
||||
sendState: SendState,
|
||||
infoState: TransactionExtrasState,
|
||||
): SendState {
|
||||
val result = when (action) {
|
||||
is HederaMemo.HandleUserInput -> {
|
||||
val memo = action.data
|
||||
val input = InputViewValue(memo, true)
|
||||
infoState.copy(hederaMemoState = HederaMemoState(input, memo))
|
||||
}
|
||||
}
|
||||
return updateLastState(sendState.copy(transactionExtrasState = result), result)
|
||||
}
|
||||
|
||||
private fun handleAlgorandMemo(
|
||||
action: AlgorandMemo,
|
||||
sendState: SendState,
|
||||
infoState: TransactionExtrasState,
|
||||
): SendState {
|
||||
val result = when (action) {
|
||||
is AlgorandMemo.HandleUserInput -> {
|
||||
val memo = action.data
|
||||
val input = InputViewValue(memo, true)
|
||||
infoState.copy(algorandMemoState = AlgorandMemoState(input, memo))
|
||||
}
|
||||
}
|
||||
return updateLastState(sendState.copy(transactionExtrasState = result), result)
|
||||
}
|
||||
}
|
||||
|
|
@ -23,18 +23,21 @@ data class AddressState(
|
|||
fun isReady(): Boolean = error == null && destinationWalletAddress?.isNotEmpty() ?: false
|
||||
}
|
||||
|
||||
@Deprecated("Legacy")
|
||||
data class TransactionExtrasState(
|
||||
val xlmMemo: XlmMemoState? = null,
|
||||
val binanceMemo: BinanceMemoState? = null,
|
||||
val xrpDestinationTag: XrpDestinationTagState? = null,
|
||||
val tonMemoState: TonMemoState? = null,
|
||||
val cosmosMemoState: CosmosMemoState? = null,
|
||||
val hederaMemoState: HederaMemoState? = null,
|
||||
val algorandMemoState: AlgorandMemoState? = null,
|
||||
) : IdStateHolder {
|
||||
override val stateId: StateId = StateId.TRANSACTION_EXTRAS
|
||||
|
||||
fun isNull(): Boolean {
|
||||
return xlmMemo == null && binanceMemo == null && xrpDestinationTag == null && tonMemoState == null &&
|
||||
cosmosMemoState == null
|
||||
cosmosMemoState == null && hederaMemoState == null && algorandMemoState == null
|
||||
}
|
||||
|
||||
fun isEmpty(): Boolean {
|
||||
|
|
@ -43,8 +46,16 @@ data class TransactionExtrasState(
|
|||
val isXrpEmpty = xrpDestinationTag?.viewFieldValue?.value?.isEmpty() ?: false
|
||||
val isTonEmpty = tonMemoState?.viewFieldValue?.value?.isEmpty() ?: false
|
||||
val isCosmosEmpty = cosmosMemoState?.viewFieldValue?.value?.isEmpty() ?: false
|
||||
val isHederaEmpty = hederaMemoState?.viewFieldValue?.value?.isEmpty() ?: false
|
||||
val isAlgorandEmpty = algorandMemoState?.viewFieldValue?.value?.isEmpty() ?: false
|
||||
|
||||
return isXlmEmpty || isBinanceEmpty || isXrpEmpty || isTonEmpty || isCosmosEmpty
|
||||
return isXlmEmpty ||
|
||||
isBinanceEmpty ||
|
||||
isXrpEmpty ||
|
||||
isTonEmpty ||
|
||||
isCosmosEmpty ||
|
||||
isHederaEmpty ||
|
||||
isAlgorandEmpty
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -120,6 +131,18 @@ data class CosmosMemoState(
|
|||
val error: TransactionExtraError? = null,
|
||||
)
|
||||
|
||||
data class HederaMemoState(
|
||||
val viewFieldValue: InputViewValue = InputViewValue(""),
|
||||
val memo: String? = null,
|
||||
val error: TransactionExtraError? = null,
|
||||
)
|
||||
|
||||
data class AlgorandMemoState(
|
||||
val viewFieldValue: InputViewValue = InputViewValue(""),
|
||||
val memo: String? = null,
|
||||
val error: TransactionExtraError? = null,
|
||||
)
|
||||
|
||||
enum class TransactionExtraError {
|
||||
INVALID_DESTINATION_TAG,
|
||||
INVALID_XLM_MEMO,
|
||||
|
|
|
|||
|
|
@ -234,6 +234,24 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
|
|||
}
|
||||
.onEach { store.dispatch(TransactionExtrasAction.CosmosMemo.HandleUserInput(it)) }
|
||||
.launchIn(mainScope)
|
||||
|
||||
etHederaMemo.inputtedTextAsFlow()
|
||||
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
|
||||
.filter {
|
||||
val info = store.state.sendState.transactionExtrasState
|
||||
info.hederaMemoState?.viewFieldValue?.value != it
|
||||
}
|
||||
.onEach { store.dispatch(TransactionExtrasAction.HederaMemo.HandleUserInput(it)) }
|
||||
.launchIn(mainScope)
|
||||
|
||||
etAlgorandMemo.inputtedTextAsFlow()
|
||||
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
|
||||
.filter {
|
||||
val info = store.state.sendState.transactionExtrasState
|
||||
info.algorandMemoState?.viewFieldValue?.value != it
|
||||
}
|
||||
.onEach { store.dispatch(TransactionExtrasAction.AlgorandMemo.HandleUserInput(it)) }
|
||||
.launchIn(mainScope)
|
||||
}
|
||||
|
||||
private fun onCodeScanned(scannedCode: String) {
|
||||
|
|
|
|||
|
|
@ -62,7 +62,8 @@ internal class SendStateSubscriber(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("ComplexMethod")
|
||||
@Deprecated("Legacy")
|
||||
@Suppress("ComplexMethod", "LongMethod")
|
||||
private fun handleTransactionExtrasState(fg: SendFragment, infoState: TransactionExtrasState) =
|
||||
with(fg.binding.lSendAddress) {
|
||||
fun showView(view: View, info: Any?) {
|
||||
|
|
@ -73,6 +74,8 @@ internal class SendStateSubscriber(
|
|||
showView(binanceMemoContainer, infoState.binanceMemo)
|
||||
showView(tonMemoContainer, infoState.tonMemoState)
|
||||
showView(cosmosMemoContainer, infoState.cosmosMemoState)
|
||||
showView(hederaMemoContainer, infoState.hederaMemoState)
|
||||
showView(algorandMemoContainer, infoState.algorandMemoState)
|
||||
|
||||
infoState.xlmMemo?.let {
|
||||
if (!it.viewFieldValue.isFromUserInput) etXlmMemo.setText(it.viewFieldValue.value)
|
||||
|
|
@ -129,6 +132,26 @@ internal class SendStateSubscriber(
|
|||
etCosmosMemo.setText(it.viewFieldValue.value)
|
||||
}
|
||||
}
|
||||
infoState.hederaMemoState?.let {
|
||||
if (infoState.hederaMemoState.error != null) {
|
||||
tilHederaMemo.error = fg.getText(R.string.send_extras_error_invalid_memo)
|
||||
} else {
|
||||
tilHederaMemo.error = null
|
||||
}
|
||||
if (!it.viewFieldValue.isFromUserInput) {
|
||||
etHederaMemo.setText(it.viewFieldValue.value)
|
||||
}
|
||||
}
|
||||
infoState.algorandMemoState?.let {
|
||||
if (infoState.algorandMemoState.error != null) {
|
||||
tilAlgorandMemo.error = fg.getText(R.string.send_extras_error_invalid_memo)
|
||||
} else {
|
||||
tilAlgorandMemo.error = null
|
||||
}
|
||||
if (!it.viewFieldValue.isFromUserInput) {
|
||||
etAlgorandMemo.setText(it.viewFieldValue.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("ComplexMethod")
|
||||
|
|
|
|||
|
|
@ -83,7 +83,9 @@ internal fun BriefNetworkItem(model: NetworkItemState, modifier: Modifier = Modi
|
|||
Icon(
|
||||
painter = painterResource(id = model.iconResId.value),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(size = TangemTheme.dimens.size20),
|
||||
modifier = Modifier
|
||||
.size(size = TangemTheme.dimens.size20)
|
||||
.clip(CircleShape),
|
||||
tint = if (isAdded) Color.Unspecified else TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -80,6 +80,13 @@ object TokenListPreviewData {
|
|||
iconResId = mutableStateOf(R.drawable.ic_bsc_16),
|
||||
isMainNetwork = false,
|
||||
),
|
||||
// check icon clipping
|
||||
NetworkItemState.ReadContent(
|
||||
name = "SHIBARIUM",
|
||||
protocolName = "BEP20",
|
||||
iconResId = mutableStateOf(R.drawable.ic_shibarium_22),
|
||||
isMainNetwork = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,14 +3,20 @@ package com.tangem.tap.features.tokens.impl.presentation.viewmodels
|
|||
import arrow.core.Either
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
|
||||
import com.tangem.domain.card.DerivePublicKeysUseCase
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.tokens.TokenWithBlockchain
|
||||
import com.tangem.domain.tokens.TokensAction
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.store
|
||||
import timber.log.Timber
|
||||
import kotlin.properties.Delegates
|
||||
|
|
@ -24,6 +30,8 @@ import kotlin.properties.Delegates
|
|||
internal class TokensListMigration(
|
||||
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
private val getCurrenciesUseCase: GetCryptoCurrenciesUseCase,
|
||||
private val derivePublicKeysUseCase: DerivePublicKeysUseCase,
|
||||
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
|
||||
) {
|
||||
|
||||
private var currentNewCoins: List<CryptoCurrency.Coin> by Delegates.notNull()
|
||||
|
|
@ -76,31 +84,72 @@ internal class TokensListMigration(
|
|||
}
|
||||
}
|
||||
|
||||
fun onSaveButtonClick(
|
||||
suspend fun onSaveButtonClick(
|
||||
changedTokensList: MutableList<TokenWithBlockchain>,
|
||||
changedBlockchainList: List<Blockchain>,
|
||||
) {
|
||||
store.dispatch(
|
||||
action = TokensAction.SaveChanges(
|
||||
currentTokens = currentNewTokens,
|
||||
currentCoins = currentNewCoins,
|
||||
changedTokens = changedTokensList.mapNotNull {
|
||||
cryptoCurrencyFactory.createToken(
|
||||
sdkToken = it.token,
|
||||
blockchain = it.blockchain,
|
||||
extraDerivationPath = null,
|
||||
derivationStyleProvider = currentUserWallet.scanResponse.derivationStyleProvider,
|
||||
)
|
||||
},
|
||||
changedCoins = changedBlockchainList.mapNotNull {
|
||||
cryptoCurrencyFactory.createCoin(
|
||||
blockchain = it,
|
||||
extraDerivationPath = null,
|
||||
derivationStyleProvider = currentUserWallet.scanResponse.derivationStyleProvider,
|
||||
)
|
||||
},
|
||||
userWallet = currentUserWallet,
|
||||
),
|
||||
val changedTokens = changedTokensList.mapNotNull {
|
||||
cryptoCurrencyFactory.createToken(
|
||||
sdkToken = it.token,
|
||||
blockchain = it.blockchain,
|
||||
extraDerivationPath = null,
|
||||
derivationStyleProvider = currentUserWallet.scanResponse.derivationStyleProvider,
|
||||
)
|
||||
}
|
||||
|
||||
val changedCoins = changedBlockchainList.mapNotNull {
|
||||
cryptoCurrencyFactory.createCoin(
|
||||
blockchain = it,
|
||||
extraDerivationPath = null,
|
||||
derivationStyleProvider = currentUserWallet.scanResponse.derivationStyleProvider,
|
||||
)
|
||||
}
|
||||
|
||||
val blockchainsToAdd = changedCoins.filterNot(currentNewCoins::contains)
|
||||
val blockchainsToRemove = currentNewCoins.filterNot(changedCoins::contains)
|
||||
|
||||
val tokensToAdd = changedTokens.filterNot(currentNewTokens::contains)
|
||||
val tokensToRemove = currentNewTokens.filterNot { token -> changedTokens.any { it == token } }
|
||||
|
||||
removeCurrenciesIfNeeded(
|
||||
userWalletId = currentUserWallet.walletId,
|
||||
currencies = blockchainsToRemove + tokensToRemove,
|
||||
)
|
||||
|
||||
val isNothingToDoWithTokens = tokensToAdd.isEmpty() && tokensToRemove.isEmpty()
|
||||
val isNothingToDoWithBlockchain = blockchainsToAdd.isEmpty() && blockchainsToRemove.isEmpty()
|
||||
if (isNothingToDoWithTokens && isNothingToDoWithBlockchain) {
|
||||
store.dispatchDebugErrorNotification(message = "Nothing to save")
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo())
|
||||
return
|
||||
}
|
||||
|
||||
val currencyList = blockchainsToAdd + tokensToAdd
|
||||
|
||||
derivePublicKeysUseCase(userWalletId = currentUserWallet.walletId, currencies = currencyList)
|
||||
.onRight {
|
||||
addCryptoCurrenciesUseCase(userWalletId = currentUserWallet.walletId, currencies = currencyList)
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo())
|
||||
}
|
||||
.onLeft { Timber.e("Failed to derive public keys: $it") }
|
||||
}
|
||||
|
||||
private suspend fun removeCurrenciesIfNeeded(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
|
||||
if (currencies.isEmpty()) return
|
||||
val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository)
|
||||
val walletManagersFacade = store.state.daggerGraphState.get(DaggerGraphState::walletManagersFacade)
|
||||
|
||||
currenciesRepository.removeCurrencies(userWalletId = userWalletId, currencies = currencies)
|
||||
|
||||
walletManagersFacade.remove(
|
||||
userWalletId = userWalletId,
|
||||
networks = currencies
|
||||
.filterIsInstance<CryptoCurrency.Coin>()
|
||||
.mapTo(hashSetOf(), CryptoCurrency::network),
|
||||
)
|
||||
walletManagersFacade.removeTokens(
|
||||
userWalletId = userWalletId,
|
||||
tokens = currencies.filterIsInstance<CryptoCurrency.Token>().toSet(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -13,12 +13,14 @@ 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.card.DerivePublicKeysUseCase
|
||||
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
|
||||
import com.tangem.domain.common.extensions.canHandleBlockchain
|
||||
import com.tangem.domain.common.extensions.canHandleToken
|
||||
import com.tangem.domain.common.extensions.fromNetworkId
|
||||
import com.tangem.domain.common.extensions.supportedTokens
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.tokens.TokenWithBlockchain
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
|
|
@ -67,6 +69,8 @@ internal class TokensListViewModel @Inject constructor(
|
|||
private val router: TokensListRouter,
|
||||
private val dispatchers: AppCoroutineDispatcherProvider,
|
||||
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
derivePublicKeysUseCase: DerivePublicKeysUseCase,
|
||||
addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
|
||||
analyticsEventHandler: AnalyticsEventHandler,
|
||||
getCurrenciesUseCase: GetCryptoCurrenciesUseCase,
|
||||
) : ViewModel(), DefaultLifecycleObserver {
|
||||
|
|
@ -88,6 +92,8 @@ internal class TokensListViewModel @Inject constructor(
|
|||
private val tokensListMigration = TokensListMigration(
|
||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||
getCurrenciesUseCase = getCurrenciesUseCase,
|
||||
derivePublicKeysUseCase = derivePublicKeysUseCase,
|
||||
addCryptoCurrenciesUseCase = addCryptoCurrenciesUseCase,
|
||||
)
|
||||
|
||||
init {
|
||||
|
|
@ -303,10 +309,13 @@ internal class TokensListViewModel @Inject constructor(
|
|||
|
||||
fun onSaveButtonClick() {
|
||||
analyticsSender.sendWhenSaveButtonClicked()
|
||||
tokensListMigration.onSaveButtonClick(
|
||||
changedTokensList = changedTokensList,
|
||||
changedBlockchainList = changedBlockchainList,
|
||||
)
|
||||
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
tokensListMigration.onSaveButtonClick(
|
||||
changedTokensList = changedTokensList,
|
||||
changedBlockchainList = changedBlockchainList,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSearchValueChange(newValue: String) {
|
||||
|
|
|
|||
|
|
@ -1,250 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.legacy.redux
|
||||
|
||||
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.doOnSuccess
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.common.extensions.toMapKey
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.domain.common.configs.CardConfig
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.common.util.supportsHdWallet
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.tokens.TokensAction
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.card.DefaultDerivePublicKeysUseCase
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
import com.tangem.tap.userWalletsListManager
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.Middleware
|
||||
import timber.log.Timber
|
||||
|
||||
@Suppress("LargeClass")
|
||||
object TokensMiddleware {
|
||||
|
||||
// TODO: Move to DI
|
||||
private val addCryptoCurrenciesUseCase by lazy(LazyThreadSafetyMode.NONE) {
|
||||
val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository)
|
||||
val networksRepository = store.state.daggerGraphState.get(DaggerGraphState::networksRepository)
|
||||
|
||||
AddCryptoCurrenciesUseCase(currenciesRepository, networksRepository)
|
||||
}
|
||||
|
||||
val tokensMiddleware: Middleware<AppState> = { _, _ ->
|
||||
{ next ->
|
||||
{ action ->
|
||||
when (action) {
|
||||
is TokensAction.SaveChanges -> handleSaveChanges(action)
|
||||
}
|
||||
next(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSaveChanges(action: TokensAction.SaveChanges) {
|
||||
scope.launch {
|
||||
val scanResponse = action.userWallet.scanResponse
|
||||
|
||||
val currentTokens = action.currentTokens
|
||||
val currentBlockchains = action.currentCoins
|
||||
|
||||
val blockchainsToAdd = action.changedCoins.filterNot(currentBlockchains::contains)
|
||||
val blockchainsToRemove = currentBlockchains.filterNot(action.changedCoins::contains)
|
||||
|
||||
val tokensToAdd = action.changedTokens.filterNot(currentTokens::contains)
|
||||
val tokensToRemove = currentTokens.filterNot { token -> action.changedTokens.any { it == token } }
|
||||
|
||||
removeCurrenciesIfNeeded(
|
||||
userWalletId = action.userWallet.walletId,
|
||||
currencies = blockchainsToRemove + tokensToRemove,
|
||||
)
|
||||
|
||||
val isNothingToDoWithTokens = tokensToAdd.isEmpty() && tokensToRemove.isEmpty()
|
||||
val isNothingToDoWithBlockchain = blockchainsToAdd.isEmpty() && blockchainsToRemove.isEmpty()
|
||||
if (isNothingToDoWithTokens && isNothingToDoWithBlockchain) {
|
||||
store.dispatchDebugErrorNotification(message = "Nothing to save")
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo())
|
||||
return@launch
|
||||
}
|
||||
|
||||
val currencyList = blockchainsToAdd + tokensToAdd
|
||||
|
||||
val featureToggles = store.state.daggerGraphState.get(DaggerGraphState::testerFeatureToggles)
|
||||
if (featureToggles.isDerivePublicKeysRefactoringEnabled) {
|
||||
val derivePublicKeys = DefaultDerivePublicKeysUseCase(
|
||||
tangemSdkManager = tangemSdkManager,
|
||||
derivationsRepository = store.state.daggerGraphState.get(DaggerGraphState::derivationsRepository),
|
||||
)
|
||||
|
||||
derivePublicKeys(userWalletId = action.userWallet.walletId, currencies = currencyList)
|
||||
.onRight {
|
||||
addCryptoCurrenciesUseCase(
|
||||
userWalletId = action.userWallet.walletId,
|
||||
currencies = currencyList,
|
||||
)
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo())
|
||||
}
|
||||
.onLeft { Timber.e("Failed to derive public keys: $it") }
|
||||
} else {
|
||||
// TODO: delete [REDACTED_JIRA]
|
||||
if (scanResponse.supportsHdWallet()) {
|
||||
deriveMissingCoins(scanResponse = scanResponse, currencyList = currencyList) {
|
||||
submitAdd(
|
||||
userWallet = action.userWallet,
|
||||
updatedScanResponse = it,
|
||||
currencyList = currencyList,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
submitAdd(action.userWallet, scanResponse, currencyList)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated(message = "Use DerivePublicKeysUseCase instead")
|
||||
private fun deriveMissingCoins(
|
||||
scanResponse: ScanResponse,
|
||||
currencyList: List<CryptoCurrency>,
|
||||
onSuccess: (ScanResponse) -> Unit,
|
||||
) {
|
||||
val config = CardConfig.createConfig(scanResponse.card)
|
||||
val derivationDataList = currencyList.mapNotNull { currency ->
|
||||
val curve = config.primaryCurve(blockchain = Blockchain.fromId(currency.network.id.value))
|
||||
curve?.let { getDerivations(curve, scanResponse, currency) }
|
||||
}
|
||||
val derivations = buildMap<ByteArrayKey, MutableList<DerivationPath>> {
|
||||
derivationDataList.forEach {
|
||||
val current = this[it.derivations.first]
|
||||
if (current != null) {
|
||||
current.addAll(it.derivations.second)
|
||||
current.distinct()
|
||||
} else {
|
||||
this[it.derivations.first] = it.derivations.second.toMutableList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (derivations.isEmpty()) {
|
||||
onSuccess(scanResponse)
|
||||
return
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
val result = tangemSdkManager.derivePublicKeys(
|
||||
cardId = null,
|
||||
derivations = derivations,
|
||||
)
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
val newDerivedKeys = result.data.entries
|
||||
val oldDerivedKeys = scanResponse.derivedKeys
|
||||
|
||||
val walletKeys = (newDerivedKeys.keys + oldDerivedKeys.keys).toSet()
|
||||
|
||||
val updatedDerivedKeys = walletKeys.associateWith { walletKey ->
|
||||
val oldDerivations = ExtendedPublicKeysMap(oldDerivedKeys[walletKey] ?: emptyMap())
|
||||
val newDerivations = newDerivedKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap())
|
||||
ExtendedPublicKeysMap(oldDerivations + newDerivations)
|
||||
}
|
||||
val updatedScanResponse = scanResponse.copy(derivedKeys = updatedDerivedKeys)
|
||||
|
||||
store.dispatchOnMain(GlobalAction.SaveScanResponse(updatedScanResponse))
|
||||
|
||||
onSuccess(updatedScanResponse)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
store.dispatchDebugErrorNotification(TapError.CustomError("Error adding tokens"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDerivations(
|
||||
curve: EllipticCurve,
|
||||
scanResponse: ScanResponse,
|
||||
currency: CryptoCurrency,
|
||||
): DerivationData? {
|
||||
val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null
|
||||
|
||||
val blockchain = Blockchain.fromId(currency.network.id.value)
|
||||
val supportedCurves = blockchain.getSupportedCurves()
|
||||
val path = blockchain.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle())
|
||||
.takeIf { supportedCurves.contains(curve) }
|
||||
|
||||
val customPath = currency.network.derivationPath.value?.let {
|
||||
DerivationPath(it)
|
||||
}.takeIf { supportedCurves.contains(curve) }
|
||||
|
||||
val bothCandidates = listOfNotNull(path, customPath).distinct().toMutableList()
|
||||
if (bothCandidates.isEmpty()) return null
|
||||
|
||||
if (currency is CryptoCurrency.Coin && blockchain == Blockchain.Cardano) {
|
||||
currency.network.derivationPath.value?.let {
|
||||
bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it)))
|
||||
}
|
||||
}
|
||||
|
||||
val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey()
|
||||
val alreadyDerivedKeys: ExtendedPublicKeysMap =
|
||||
scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap())
|
||||
val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList()
|
||||
|
||||
val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) }
|
||||
if (toDerive.isEmpty()) return null
|
||||
|
||||
return DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive)
|
||||
}
|
||||
|
||||
class DerivationData(val derivations: Pair<ByteArrayKey, List<DerivationPath>>)
|
||||
|
||||
private fun submitAdd(
|
||||
userWallet: UserWallet,
|
||||
updatedScanResponse: ScanResponse,
|
||||
currencyList: List<CryptoCurrency>,
|
||||
) {
|
||||
scope.launch {
|
||||
userWalletsListManager.update(
|
||||
userWalletId = userWallet.walletId,
|
||||
update = { it.copy(scanResponse = updatedScanResponse) },
|
||||
).doOnSuccess {
|
||||
addCryptoCurrenciesUseCase(userWallet.walletId, currencyList)
|
||||
}
|
||||
}
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo())
|
||||
}
|
||||
|
||||
private suspend fun removeCurrenciesIfNeeded(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
|
||||
if (currencies.isEmpty()) return
|
||||
val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository)
|
||||
val walletManagersFacade = store.state.daggerGraphState.get(DaggerGraphState::walletManagersFacade)
|
||||
|
||||
currenciesRepository.removeCurrencies(userWalletId = userWalletId, currencies = currencies)
|
||||
|
||||
walletManagersFacade.remove(
|
||||
userWalletId = userWalletId,
|
||||
networks = currencies
|
||||
.filterIsInstance<CryptoCurrency.Coin>()
|
||||
.mapTo(hashSetOf(), CryptoCurrency::network),
|
||||
)
|
||||
walletManagersFacade.removeTokens(
|
||||
userWalletId = userWalletId,
|
||||
tokens = currencies.filterIsInstance<CryptoCurrency.Token>().toSet(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -46,6 +46,11 @@ object TradeCryptoMiddleware {
|
|||
}
|
||||
}
|
||||
|
||||
private val isSendRedesignedEnabled: Boolean
|
||||
get() = store.state.daggerGraphState.get(
|
||||
getDependency = DaggerGraphState::sendFeatureToggles,
|
||||
).isRedesignedSendEnabled
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
private fun handle(state: () -> AppState?, action: TradeCryptoAction) {
|
||||
if (DemoHelper.tryHandle(state, action)) return
|
||||
|
|
@ -54,11 +59,21 @@ object TradeCryptoMiddleware {
|
|||
is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId)
|
||||
is TradeCryptoAction.Buy -> proceedBuyAction(state, action)
|
||||
is TradeCryptoAction.Sell -> proceedSellAction(action)
|
||||
is TradeCryptoAction.Swap -> openSwap(
|
||||
currency = action.cryptoCurrency,
|
||||
)
|
||||
is TradeCryptoAction.SendToken -> handleSendToken(action = action)
|
||||
is TradeCryptoAction.SendCoin -> handleSendCoin(action = action)
|
||||
is TradeCryptoAction.Swap -> openSwap(currency = action.cryptoCurrency)
|
||||
is TradeCryptoAction.SendToken -> {
|
||||
if (isSendRedesignedEnabled) {
|
||||
handleNewSendToken(action = action)
|
||||
} else {
|
||||
handleSendToken(action = action)
|
||||
}
|
||||
}
|
||||
is TradeCryptoAction.SendCoin -> {
|
||||
if (isSendRedesignedEnabled) {
|
||||
handleNewSendCoin(action = action)
|
||||
} else {
|
||||
handleSendCoin(action = action)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -280,4 +295,35 @@ object TradeCryptoMiddleware {
|
|||
store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleNewSendToken(action: TradeCryptoAction.SendToken) {
|
||||
handleNewSend(
|
||||
userWalletId = action.userWallet.walletId.stringValue,
|
||||
txInfo = action.transactionInfo,
|
||||
currency = action.tokenCurrency,
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleNewSendCoin(action: TradeCryptoAction.SendCoin) {
|
||||
handleNewSend(
|
||||
userWalletId = action.userWallet.walletId.stringValue,
|
||||
txInfo = action.transactionInfo,
|
||||
currency = action.coinStatus.currency,
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleNewSend(
|
||||
userWalletId: String,
|
||||
txInfo: TradeCryptoAction.TransactionInfo?,
|
||||
currency: CryptoCurrency,
|
||||
) {
|
||||
val bundle = bundleOf(
|
||||
SendRouter.CRYPTO_CURRENCY_KEY to currency,
|
||||
SendRouter.USER_WALLET_ID_KEY to userWalletId,
|
||||
SendRouter.TRANSACTION_ID_KEY to txInfo?.transactionId,
|
||||
SendRouter.DESTINATION_ADDRESS_KEY to txInfo?.destinationAddress,
|
||||
SendRouter.AMOUNT_KEY to txInfo?.amount,
|
||||
)
|
||||
store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle))
|
||||
}
|
||||
}
|
||||
|
|
@ -67,6 +67,6 @@ interface ExchangeUrlBuilder {
|
|||
|
||||
companion object {
|
||||
const val SCHEME = "https"
|
||||
const val SUCCESS_URL = "tangem://success.tangem.com"
|
||||
const val SUCCESS_URL = "https://tangem.com/success"
|
||||
}
|
||||
}
|
||||
|
|
@ -41,4 +41,11 @@ data class MoonPayCurrencies(
|
|||
val isSupportedInUS: Boolean = false,
|
||||
val isSellSupported: Boolean = false,
|
||||
val notAllowedUSStates: List<String> = emptyList(),
|
||||
val metadata: MoonPayCurrenciesMetadata? = null,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MoonPayCurrenciesMetadata(
|
||||
val contractAddress: String?,
|
||||
val networkCode: String?,
|
||||
)
|
||||
|
|
@ -81,6 +81,7 @@ class MoonPayService(
|
|||
|
||||
override fun availableForSell(currency: Currency): Boolean {
|
||||
val availableForSell = status?.availableForSell ?: return false
|
||||
val metadata = status?.responseCurrencies?.map { it.metadata }
|
||||
if (!isSellAllowed()) return false
|
||||
|
||||
return when (currency) {
|
||||
|
|
@ -92,7 +93,9 @@ class MoonPayService(
|
|||
else -> availableForSell.contains(currency.currencySymbol)
|
||||
}
|
||||
}
|
||||
is Currency.Token -> false
|
||||
is Currency.Token -> {
|
||||
metadata?.any { it?.contractAddress.equals(currency.token.contractAddress, ignoreCase = true) } ?: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,298 +0,0 @@
|
|||
package com.tangem.tap.proxy
|
||||
|
||||
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.core.TangemError
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.common.extensions.toMapKey
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
|
||||
import com.tangem.domain.common.BlockchainNetwork
|
||||
import com.tangem.domain.common.DerivationStyleProvider
|
||||
import com.tangem.domain.common.configs.CardConfig
|
||||
import com.tangem.domain.common.extensions.fromNetworkId
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.common.util.hasDerivation
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.tokens.repository.NetworksRepository
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.lib.crypto.DerivationManager
|
||||
import com.tangem.lib.crypto.models.Currency
|
||||
import com.tangem.lib.crypto.models.Currency.NonNativeToken
|
||||
import com.tangem.lib.crypto.models.errors.UserCancelledException
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.features.tokens.legacy.redux.TokensMiddleware
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.userWalletsListManager
|
||||
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import com.tangem.tap.domain.model.Currency as WalletModelCurrency
|
||||
|
||||
class DerivationManagerImpl(
|
||||
private val appStateHolder: AppStateHolder,
|
||||
private val currenciesRepository: CurrenciesRepository,
|
||||
private val networksRepository: NetworksRepository,
|
||||
) : DerivationManager {
|
||||
|
||||
// TODO: Move to DI
|
||||
private val addCryptoCurrenciesUseCase by lazy(LazyThreadSafetyMode.NONE) {
|
||||
AddCryptoCurrenciesUseCase(currenciesRepository, networksRepository)
|
||||
}
|
||||
|
||||
override suspend fun deriveAndAddTokens(currency: Currency) = suspendCoroutine { continuation ->
|
||||
val selectedUserWallet = requireNotNull(
|
||||
userWalletsListManager.selectedUserWalletSync,
|
||||
) { "selectedUserWallet shouldn't be null" }
|
||||
val scanResponse = selectedUserWallet.scanResponse
|
||||
val blockchain = requireNotNull(
|
||||
Blockchain.fromNetworkId(currency.networkId),
|
||||
) { "unsupported blockchain" }
|
||||
val derivationStyleProvider = scanResponse.derivationStyleProvider
|
||||
val derivationPath = requireNotNull(
|
||||
blockchain.derivationPath(derivationStyleProvider.getDerivationStyle())?.rawPath,
|
||||
) { "derivationPath shouldn't be null" }
|
||||
val hasDerivation = scanResponse.hasDerivation(
|
||||
blockchain,
|
||||
derivationPath,
|
||||
)
|
||||
if (hasDerivation) {
|
||||
scope.launch {
|
||||
addToken(
|
||||
userWalletId = selectedUserWallet.walletId,
|
||||
blockchain = blockchain,
|
||||
currency = currency,
|
||||
derivationPath = derivationPath,
|
||||
derivationStyleProvider = derivationStyleProvider,
|
||||
)
|
||||
continuation.resumeWith(Result.success(derivationPath))
|
||||
}
|
||||
} else {
|
||||
val blockchainNetwork = BlockchainNetwork(blockchain, scanResponse.derivationStyleProvider)
|
||||
val appCurrency = com.tangem.tap.domain.model.Currency.fromBlockchainNetwork(
|
||||
blockchainNetwork,
|
||||
getAppToken(currency),
|
||||
)
|
||||
deriveMissingBlockchains(
|
||||
scanResponse = scanResponse,
|
||||
currencyList = listOf(appCurrency),
|
||||
onSuccess = { updatedScanResponse ->
|
||||
scope.launch {
|
||||
userWalletsListManager.update(
|
||||
userWalletId = selectedUserWallet.walletId,
|
||||
update = { it.copy(scanResponse = updatedScanResponse) },
|
||||
)
|
||||
addToken(
|
||||
userWalletId = selectedUserWallet.walletId,
|
||||
blockchain = blockchain,
|
||||
currency = currency,
|
||||
derivationPath = derivationPath,
|
||||
derivationStyleProvider = derivationStyleProvider,
|
||||
)
|
||||
continuation.resumeWith(Result.success(derivationPath))
|
||||
}
|
||||
},
|
||||
onFailure = {
|
||||
continuation.resumeWith(Result.failure(it))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun addToken(
|
||||
userWalletId: UserWalletId,
|
||||
blockchain: Blockchain,
|
||||
currency: Currency,
|
||||
derivationPath: String,
|
||||
derivationStyleProvider: DerivationStyleProvider,
|
||||
) {
|
||||
val cryptoCurrency = convertCurrency(
|
||||
blockchain = blockchain,
|
||||
currency = currency,
|
||||
derivationPath = derivationPath,
|
||||
derivationStyleProvider = derivationStyleProvider,
|
||||
)
|
||||
|
||||
addCryptoCurrenciesUseCase(userWalletId, cryptoCurrency)
|
||||
}
|
||||
|
||||
private fun convertCurrency(
|
||||
blockchain: Blockchain,
|
||||
currency: Currency,
|
||||
derivationPath: String,
|
||||
derivationStyleProvider: DerivationStyleProvider,
|
||||
): CryptoCurrency {
|
||||
val cryptoCurrencyFactory = CryptoCurrencyFactory()
|
||||
return when (currency) {
|
||||
is Currency.NativeToken -> {
|
||||
cryptoCurrencyFactory.createCoin(
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = derivationPath,
|
||||
derivationStyleProvider = derivationStyleProvider,
|
||||
)
|
||||
}
|
||||
is NonNativeToken -> {
|
||||
val sdkToken = Token(
|
||||
name = currency.name,
|
||||
symbol = currency.symbol,
|
||||
contractAddress = currency.contractAddress,
|
||||
decimals = currency.decimalCount,
|
||||
id = currency.id,
|
||||
)
|
||||
cryptoCurrencyFactory.createToken(
|
||||
sdkToken = sdkToken,
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = derivationPath,
|
||||
derivationStyleProvider = derivationStyleProvider,
|
||||
)
|
||||
}
|
||||
} as CryptoCurrency
|
||||
}
|
||||
|
||||
private fun deriveMissingBlockchains(
|
||||
scanResponse: ScanResponse,
|
||||
currencyList: List<WalletModelCurrency>,
|
||||
onSuccess: (ScanResponse) -> Unit,
|
||||
onFailure: (Exception) -> Unit,
|
||||
) {
|
||||
val config = CardConfig.createConfig(scanResponse.card)
|
||||
val derivationDataList = currencyList.mapNotNull { currency ->
|
||||
val curve = config.primaryCurve(currency.blockchain)
|
||||
curve?.let { getDerivations(curve, scanResponse, currency) }
|
||||
}
|
||||
val derivations = buildMap<ByteArrayKey, MutableList<DerivationPath>> {
|
||||
derivationDataList.forEach {
|
||||
val current = this[it.derivations.first]
|
||||
if (current != null) {
|
||||
current.addAll(it.derivations.second)
|
||||
current.distinct()
|
||||
} else {
|
||||
this[it.derivations.first] = it.derivations.second.toMutableList()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (derivations.isEmpty()) {
|
||||
onSuccess(scanResponse)
|
||||
return
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
|
||||
|
||||
val result = appStateHolder.tangemSdkManager?.derivePublicKeys(
|
||||
cardId = null, // always ignore cardId in derive task
|
||||
derivations = derivations,
|
||||
)
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
val newDerivedKeys = result.data.entries
|
||||
val oldDerivedKeys = scanResponse.derivedKeys
|
||||
|
||||
val walletKeys = (newDerivedKeys.keys + oldDerivedKeys.keys).toSet()
|
||||
|
||||
val updatedDerivedKeys = walletKeys.associateWith { walletKey ->
|
||||
val oldDerivations = ExtendedPublicKeysMap(oldDerivedKeys[walletKey] ?: emptyMap())
|
||||
val newDerivations = newDerivedKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap())
|
||||
ExtendedPublicKeysMap(oldDerivations + newDerivations)
|
||||
}
|
||||
val updatedScanResponse = scanResponse.copy(
|
||||
derivedKeys = updatedDerivedKeys,
|
||||
)
|
||||
if (selectedUserWallet != null) {
|
||||
userWalletsListManager.update(
|
||||
userWalletId = selectedUserWallet.walletId,
|
||||
update = { it.copy(scanResponse = updatedScanResponse) },
|
||||
)
|
||||
}
|
||||
appStateHolder.mainStore?.dispatchOnMain(GlobalAction.SaveScanResponse(updatedScanResponse))
|
||||
delay(DELAY_SDK_DIALOG_CLOSE)
|
||||
onSuccess(updatedScanResponse)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
appStateHolder.mainStore?.dispatchDebugErrorNotification(
|
||||
TapError.CustomError(
|
||||
"Error derivation",
|
||||
),
|
||||
)
|
||||
onFailure.invoke(handleTangemError(result.error))
|
||||
}
|
||||
else -> {
|
||||
error("result result is null")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDerivations(
|
||||
curve: EllipticCurve,
|
||||
scanResponse: ScanResponse,
|
||||
currency: com.tangem.tap.domain.model.Currency,
|
||||
): TokensMiddleware.DerivationData? {
|
||||
val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null
|
||||
|
||||
val supportedCurves = currency.blockchain.getSupportedCurves()
|
||||
val path = currency.blockchain.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle())
|
||||
.takeIf { supportedCurves.contains(curve) }
|
||||
|
||||
val customPath = currency.derivationPath?.let {
|
||||
DerivationPath(it)
|
||||
}.takeIf { supportedCurves.contains(curve) }
|
||||
|
||||
val bothCandidates = listOfNotNull(path, customPath).distinct().toMutableList()
|
||||
if (bothCandidates.isEmpty()) return null
|
||||
|
||||
if (currency is WalletModelCurrency.Blockchain &&
|
||||
currency.blockchain == Blockchain.Cardano
|
||||
) {
|
||||
currency.derivationPath?.let {
|
||||
bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it)))
|
||||
}
|
||||
}
|
||||
|
||||
val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey()
|
||||
val alreadyDerivedKeys: ExtendedPublicKeysMap =
|
||||
scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap())
|
||||
val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList()
|
||||
|
||||
val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) }
|
||||
if (toDerive.isEmpty()) return null
|
||||
|
||||
return TokensMiddleware.DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive)
|
||||
}
|
||||
|
||||
private fun getAppToken(currency: Currency): Token? {
|
||||
return if (currency is NonNativeToken) {
|
||||
Token(
|
||||
symbol = currency.symbol,
|
||||
contractAddress = currency.contractAddress,
|
||||
decimals = currency.decimalCount,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Simple error handler
|
||||
* for now specifically handle only UserCancelled
|
||||
*
|
||||
* @param error [TangemError]
|
||||
*/
|
||||
private fun handleTangemError(error: TangemError): Exception {
|
||||
if (error is TangemSdkError.UserCancelled) {
|
||||
return UserCancelledException()
|
||||
}
|
||||
return IllegalStateException(error.customMessage)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,9 +4,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
|
|||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.tokens.repository.NetworksRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.lib.crypto.DerivationManager
|
||||
import com.tangem.lib.crypto.TransactionManager
|
||||
import com.tangem.lib.crypto.UserWalletManager
|
||||
import com.tangem.tap.proxy.*
|
||||
|
|
@ -56,18 +54,4 @@ internal object ProxyModule {
|
|||
walletManagersFacade = walletManagersFacade,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDerivationManager(
|
||||
appStateHolder: AppStateHolder,
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
networksRepository: NetworksRepository,
|
||||
): DerivationManager {
|
||||
return DerivationManagerImpl(
|
||||
appStateHolder = appStateHolder,
|
||||
currenciesRepository = currenciesRepository,
|
||||
networksRepository = networksRepository,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ package com.tangem.tap.proxy.redux
|
|||
import com.tangem.feature.qrscanning.QrScanningRouter
|
||||
import com.tangem.domain.card.ScanCardUseCase
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.features.managetokens.navigation.ManageTokensRouter
|
||||
import com.tangem.features.managetokens.navigation.ManageTokensUi
|
||||
import com.tangem.features.send.api.navigation.SendRouter
|
||||
import com.tangem.features.tester.api.TesterRouter
|
||||
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
|
||||
|
|
@ -19,7 +19,7 @@ sealed interface DaggerGraphAction : Action {
|
|||
val walletRouter: WalletRouter,
|
||||
val walletConnectInteractor: WalletConnectInteractor,
|
||||
val tokenDetailsRouter: TokenDetailsRouter,
|
||||
val manageTokensRouter: ManageTokensRouter,
|
||||
val manageTokensUi: ManageTokensUi,
|
||||
val cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
val sendRouter: SendRouter,
|
||||
val qrScanningRouter: QrScanningRouter,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ object DaggerGraphReducer {
|
|||
walletRouter = action.walletRouter,
|
||||
walletConnectInteractor = action.walletConnectInteractor,
|
||||
tokenDetailsRouter = action.tokenDetailsRouter,
|
||||
manageTokensRouter = action.manageTokensRouter,
|
||||
manageTokensUi = action.manageTokensUi,
|
||||
cardSdkConfigRepository = action.cardSdkConfigRepository,
|
||||
sendRouter = action.sendRouter,
|
||||
qrScanningRouter = action.qrScanningRouter,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.tap.proxy.redux
|
||||
|
||||
import com.tangem.blockchain.common.AccountCreator
|
||||
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
|
||||
import com.tangem.datasource.connection.NetworkConnectionManager
|
||||
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
||||
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
|
||||
|
|
@ -7,17 +9,15 @@ import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
|
|||
import com.tangem.domain.card.ScanCardProcessor
|
||||
import com.tangem.domain.card.ScanCardUseCase
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.card.repository.DerivationsRepository
|
||||
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.feature.qrscanning.QrScanningRouter
|
||||
import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles
|
||||
import com.tangem.features.managetokens.navigation.ManageTokensRouter
|
||||
import com.tangem.features.managetokens.navigation.ManageTokensUi
|
||||
import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
|
||||
import com.tangem.features.send.api.navigation.SendRouter
|
||||
import com.tangem.features.tester.api.TesterFeatureToggles
|
||||
import com.tangem.features.tester.api.TesterRouter
|
||||
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
|
||||
import com.tangem.features.wallet.navigation.WalletRouter
|
||||
|
|
@ -39,7 +39,7 @@ data class DaggerGraphState(
|
|||
val walletConnectInteractor: WalletConnectInteractor? = null,
|
||||
val tokenDetailsRouter: TokenDetailsRouter? = null,
|
||||
val manageTokensFeatureToggles: ManageTokensFeatureToggles? = null,
|
||||
val manageTokensRouter: ManageTokensRouter? = null,
|
||||
val manageTokensUi: ManageTokensUi? = null,
|
||||
val scanCardProcessor: ScanCardProcessor? = null,
|
||||
val cardSdkConfigRepository: CardSdkConfigRepository? = null,
|
||||
val appCurrencyRepository: AppCurrencyRepository? = null,
|
||||
|
|
@ -52,11 +52,9 @@ data class DaggerGraphState(
|
|||
val sendFeatureToggles: SendFeatureToggles? = null,
|
||||
val sendRouter: SendRouter? = null,
|
||||
val qrScanningRouter: QrScanningRouter? = null,
|
||||
|
||||
// FIXME: It is used only for TokensList screen. Remove after refactoring of TokensList
|
||||
val currenciesRepository: CurrenciesRepository? = null,
|
||||
val derivationsRepository: DerivationsRepository? = null,
|
||||
val testerFeatureToggles: TesterFeatureToggles? = null,
|
||||
val blockchainDataStorage: BlockchainDataStorage? = null,
|
||||
val accountCreator: AccountCreator? = null,
|
||||
) : StateType {
|
||||
|
||||
inline fun <reified T> get(getDependency: DaggerGraphState.() -> T?): T {
|
||||
|
|
|
|||
|
|
@ -260,6 +260,70 @@
|
|||
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/hederaMemoContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/tilHederaMemo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/send_extras_hint_memo"
|
||||
style="@style/SecondaryTextInputLayout"
|
||||
app:errorIconDrawable="@null">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etHederaMemo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/background_secondary"
|
||||
android:textColor="@color/text_primary_1"
|
||||
android:ellipsize="end"
|
||||
android:paddingStart="0dp"
|
||||
android:paddingEnd="0dp"
|
||||
android:singleLine="true"
|
||||
android:textSize="16sp"
|
||||
tools:text="Hedera memo" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/algorandMemoContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/tilAlgorandMemo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/send_extras_hint_memo"
|
||||
style="@style/SecondaryTextInputLayout"
|
||||
app:errorIconDrawable="@null">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etAlgorandMemo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/background_secondary"
|
||||
android:textColor="@color/text_primary_1"
|
||||
android:ellipsize="end"
|
||||
android:paddingStart="0dp"
|
||||
android:paddingEnd="0dp"
|
||||
android:singleLine="true"
|
||||
android:textSize="16sp"
|
||||
tools:text="Algorand memo" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
|
|
|||
|
|
@ -125,6 +125,7 @@ sealed class AnalyticsParam {
|
|||
const val SOURCE = "Source"
|
||||
const val BALANCE = "Balance"
|
||||
const val BATCH = "Batch"
|
||||
const val TYPE = "Type"
|
||||
const val FEE_TYPE = "Fee Type"
|
||||
const val PERMISSION_TYPE = "Permission Type"
|
||||
const val PRODUCT_TYPE = "Product Type"
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ dependencies {
|
|||
implementation(deps.timber)
|
||||
|
||||
/** Network */
|
||||
implementation(deps.krateSharedPref)
|
||||
implementation(deps.moshi)
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.okHttp)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import java.math.BigDecimal
|
|||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class BigDecimalAdapter {
|
||||
internal class BigDecimalAdapter {
|
||||
@FromJson
|
||||
fun fromJson(value: String) = BigDecimal(value)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.datasource.api.common
|
||||
|
||||
import com.squareup.moshi.*
|
||||
import org.joda.time.DateTime
|
||||
|
||||
internal class DateTimeAdapter : JsonAdapter<DateTime>() {
|
||||
|
||||
@FromJson
|
||||
override fun fromJson(reader: JsonReader): DateTime? {
|
||||
val dateString = reader.nextString() ?: return null
|
||||
|
||||
return DateTime.parse(dateString)
|
||||
}
|
||||
|
||||
@ToJson
|
||||
override fun toJson(writer: JsonWriter, value: DateTime?) {
|
||||
if (value != null) {
|
||||
writer.value(value.toString())
|
||||
} else {
|
||||
writer.nullValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,10 @@
|
|||
package com.tangem.datasource.api.common
|
||||
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.JsonWriter
|
||||
import com.squareup.moshi.ToJson
|
||||
import com.squareup.moshi.*
|
||||
import org.joda.time.LocalDate
|
||||
import org.joda.time.format.DateTimeFormat
|
||||
|
||||
class LocalDateAdapter : JsonAdapter<LocalDate>() {
|
||||
internal class LocalDateAdapter : JsonAdapter<LocalDate>() {
|
||||
|
||||
private val formatter = DateTimeFormat.forPattern("yyyy-MM-dd")
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import retrofit2.Retrofit
|
|||
import java.lang.reflect.ParameterizedType
|
||||
import java.lang.reflect.Type
|
||||
|
||||
internal class ApiResponseCallAdapterFactory private constructor() : CallAdapter.Factory() {
|
||||
class ApiResponseCallAdapterFactory private constructor() : CallAdapter.Factory() {
|
||||
|
||||
override fun get(returnType: Type, annotations: Array<out Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {
|
||||
if (getRawType(returnType) != Call::class.java) {
|
||||
|
|
|
|||
|
|
@ -75,4 +75,11 @@ interface TangemTechApi {
|
|||
|
||||
@GET("promotion")
|
||||
suspend fun getPromotionInfo(@Query("programName") name: String): ApiResponse<PromotionInfoResponse>
|
||||
|
||||
@POST("user-network-account")
|
||||
suspend fun createUserNetworkAccount(
|
||||
@Header("card_public_key") cardPublicKey: String,
|
||||
@Header("card_id") cardId: String,
|
||||
@Body body: CreateUserNetworkAccountBody,
|
||||
): ApiResponse<CreateUserNetworkAccountResponse>
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.datasource.api.tangemTech.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CreateUserNetworkAccountBody(
|
||||
@Json(name = "networkId") val networkId: String,
|
||||
@Json(name = "walletPublicKey") val publicWalletKey: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.datasource.api.tangemTech.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CreateUserNetworkAccountResponse(
|
||||
@Json(name = "status") val status: Boolean,
|
||||
@Json(name = "data") val data: AccountCreated,
|
||||
) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class AccountCreated(
|
||||
@Json(name = "accountId") val accountId: String,
|
||||
@Json(name = "publicWalletKey") val publicWalletKey: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.datasource.config
|
||||
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.datasource.BuildConfig
|
||||
import com.tangem.datasource.config.ConfigManager.Companion.IS_CREATING_TWIN_CARDS_ALLOWED
|
||||
import com.tangem.datasource.config.ConfigManager.Companion.IS_TOP_UP_ENABLED
|
||||
import com.tangem.datasource.config.models.Config
|
||||
|
|
@ -105,7 +106,7 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
|
|||
sprinklr = configValues.sprinklr,
|
||||
walletConnectProjectId = configValues.walletConnectProjectId,
|
||||
tangemComAuthorization = configValues.tangemComAuthorization,
|
||||
express = configValues.express,
|
||||
express = if (BuildConfig.ENVIRONMENT == "dev") configValues.devExpress else configValues.express,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -145,6 +146,7 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
|
|||
jsonRpc = accessTokens.bitcoin?.jsonRPC,
|
||||
blockBookRest = accessTokens.bitcoin?.blockBookRest,
|
||||
),
|
||||
algorand = GetBlockAccessToken(rest = accessTokens.algorand?.rest),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ class ConfigValueModel(
|
|||
val tangemComAuthorization: String?,
|
||||
val chiaFireAcademyApiKey: String?,
|
||||
val chiaTangemApiKey: String?,
|
||||
val devExpress: ExpressModel?,
|
||||
val express: ExpressModel?,
|
||||
)
|
||||
|
||||
|
|
@ -66,6 +67,7 @@ data class GetBlockAccessTokens(
|
|||
@Json(name = "litecoin") val litecoin: GetBlockToken?,
|
||||
@Json(name = "dash") val dash: GetBlockToken?,
|
||||
@Json(name = "bitcoin") val bitcoin: GetBlockToken?,
|
||||
@Json(name = "algorand") val algorand: GetBlockToken?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import com.tangem.blockchain.common.AccountCreator
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.blockchain.DefaultAccountCreator
|
||||
import com.tangem.lib.auth.AuthProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object AccountCreatorModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAccountCreator(authProvider: AuthProvider, @DevTangemApi tangemTechApi: TangemTechApi): AccountCreator {
|
||||
return DefaultAccountCreator(authProvider, tangemTechApi)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import com.tangem.datasource.local.AppPreferenceStorage
|
||||
import com.tangem.datasource.local.AppPreferenceStorageImpl
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface AppPreferenceStorageModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindAppPreferenceStorage(appPreferenceStorageImpl: AppPreferenceStorageImpl): AppPreferenceStorage
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
|
||||
import com.tangem.datasource.local.blockchain.DefaultBlockchainDataStorage
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object BlockchainDataStorageModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideBlockchainDataStorage(appPreferencesStore: AppPreferencesStore): BlockchainDataStorage {
|
||||
return DefaultBlockchainDataStorage(appPreferencesStore)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import com.squareup.moshi.Moshi
|
|||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import com.tangem.common.json.MoshiJsonConverter
|
||||
import com.tangem.datasource.api.common.BigDecimalAdapter
|
||||
import com.tangem.datasource.api.common.DateTimeAdapter
|
||||
import com.tangem.datasource.api.common.LocalDateAdapter
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -22,6 +23,7 @@ class MoshiModule {
|
|||
return Moshi.Builder()
|
||||
.add(BigDecimalAdapter())
|
||||
.add(LocalDateAdapter())
|
||||
.add(DateTimeAdapter())
|
||||
.add(KotlinJsonAdapterFactory())
|
||||
.build()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
package com.tangem.datasource.local
|
||||
|
||||
/**
|
||||
* Application local storage
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Deprecated(message = "Use AppPreferencesStore", level = DeprecationLevel.WARNING)
|
||||
interface AppPreferenceStorage {
|
||||
|
||||
/** Json config with feature toggles 'ToggleName: String - Availability: Boolean' */
|
||||
var featureToggles: String
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
package com.tangem.datasource.local
|
||||
|
||||
import android.content.Context
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import hu.autsoft.krate.SimpleKrate
|
||||
import hu.autsoft.krate.default.withDefault
|
||||
import hu.autsoft.krate.stringPref
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Implementation of application local storage
|
||||
*
|
||||
* @param context application context
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Singleton
|
||||
internal class AppPreferenceStorageImpl @Inject constructor(
|
||||
@ApplicationContext context: Context,
|
||||
) : SimpleKrate(context = context), AppPreferenceStorage {
|
||||
|
||||
override var featureToggles: String by stringPref().withDefault("")
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.datasource.local.blockchain
|
||||
|
||||
import com.tangem.blockchain.common.AccountCreator
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.BlockchainSdkError
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.CreateUserNetworkAccountBody
|
||||
import com.tangem.lib.auth.AuthProvider
|
||||
|
||||
internal class DefaultAccountCreator(
|
||||
private val authProvider: AuthProvider,
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
) : AccountCreator {
|
||||
|
||||
override suspend fun createAccount(blockchain: Blockchain, walletPublicKey: ByteArray): Result<String> {
|
||||
val request = CreateUserNetworkAccountBody(blockchain.id.removeSuffix("/test"), walletPublicKey.toHexString())
|
||||
return try {
|
||||
val response = tangemTechApi.createUserNetworkAccount(
|
||||
cardPublicKey = authProvider.getCardPublicKey(),
|
||||
cardId = authProvider.getCardId(),
|
||||
body = request,
|
||||
).getOrThrow()
|
||||
Result.Success(response.data.accountId)
|
||||
} catch (e: Exception) {
|
||||
Result.Failure(BlockchainSdkError.FailedToCreateAccount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.datasource.local.blockchain
|
||||
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
|
||||
|
||||
/**
|
||||
* [BlockchainDataStorage] implementation
|
||||
*
|
||||
* @property appPreferencesStore app preferences store
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultBlockchainDataStorage(
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
) : BlockchainDataStorage {
|
||||
|
||||
override suspend fun getOrNull(key: String): String? {
|
||||
return appPreferencesStore.getSyncOrNull(key = stringPreferencesKey(name = key))
|
||||
}
|
||||
|
||||
override suspend fun store(key: String, value: String) {
|
||||
appPreferencesStore.edit {
|
||||
it[stringPreferencesKey(key)] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -52,6 +52,8 @@ object PreferencesKeys {
|
|||
val IS_TOKEN_SWAP_PROMO_CHANGELLY_SHOW_KEY by lazy {
|
||||
booleanPreferencesKey(name = "isTokenSwapPromoChangellyShown")
|
||||
}
|
||||
|
||||
val FEATURE_TOGGLES_KEY by lazy { stringPreferencesKey(name = "featureToggles") }
|
||||
}
|
||||
|
||||
/** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore<Preferences> */
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.datasource.local.txhistory
|
|||
|
||||
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
|
||||
import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator
|
||||
import com.tangem.domain.txhistory.models.Page
|
||||
import com.tangem.domain.txhistory.models.PaginationWrapper
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.utils.extensions.addOrReplace
|
||||
|
|
@ -13,28 +14,16 @@ internal class DefaultTxHistoryItemsStore(
|
|||
|
||||
override fun provideStringKey(key: TxHistoryItemsStore.Key): String = key.toString()
|
||||
|
||||
override suspend fun getNextPageSyncOrNull(key: TxHistoryItemsStore.Key): Int? {
|
||||
val storedValue = getSyncOrNull(key) ?: return null
|
||||
val lastWrappedItems = storedValue.maxBy(PaginationWrapper<*>::page)
|
||||
val lastPage = lastWrappedItems.page
|
||||
|
||||
return if (lastPage <= lastWrappedItems.totalPages) {
|
||||
lastPage
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getSyncOrNull(key: TxHistoryItemsStore.Key, page: Int): PaginationWrapper<TxHistoryItem>? {
|
||||
override suspend fun getSyncOrNull(key: TxHistoryItemsStore.Key, page: Page): PaginationWrapper<TxHistoryItem>? {
|
||||
val storedValue = getSyncOrNull(key)
|
||||
|
||||
return storedValue?.firstOrNull { it.page == page }
|
||||
return storedValue?.firstOrNull { it.currentPage == page }
|
||||
}
|
||||
|
||||
override suspend fun store(key: TxHistoryItemsStore.Key, value: PaginationWrapper<TxHistoryItem>) {
|
||||
val oldValue = getSyncOrNull(key).orEmpty()
|
||||
val newValue = oldValue.addOrReplace(value) {
|
||||
it.page == value.page
|
||||
it.currentPage == value.currentPage
|
||||
}
|
||||
|
||||
store(key, newValue)
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
package com.tangem.datasource.local.txhistory
|
||||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.txhistory.models.Page
|
||||
import com.tangem.domain.txhistory.models.PaginationWrapper
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
interface TxHistoryItemsStore {
|
||||
|
||||
suspend fun getNextPageSyncOrNull(key: Key): Int?
|
||||
|
||||
suspend fun getSyncOrNull(key: Key, page: Int): PaginationWrapper<TxHistoryItem>?
|
||||
suspend fun getSyncOrNull(key: Key, page: Page): PaginationWrapper<TxHistoryItem>?
|
||||
|
||||
suspend fun remove(key: Key)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import com.tangem.core.deeplink.DeepLink
|
|||
|
||||
class BuyCurrencyDeepLink(val onReceive: () -> Unit) : DeepLink {
|
||||
|
||||
override val uri: String = "tangem://success.tangem.com"
|
||||
override val uri: String = "tangem://redirect?action=dismissBrowser"
|
||||
|
||||
override fun onReceive(params: Map<String, String>) {
|
||||
onReceive()
|
||||
|
|
|
|||
|
|
@ -7,14 +7,22 @@ plugins {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":core:datasource"))
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Local storages */
|
||||
implementation(deps.androidx.datastore)
|
||||
|
||||
/** Other libraries */
|
||||
implementation(deps.moshi)
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.timber)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.core.datasource)
|
||||
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.test.junit)
|
||||
testImplementation(deps.test.mockk)
|
||||
|
|
|
|||
|
|
@ -14,13 +14,5 @@
|
|||
{
|
||||
"name": "REDESIGNED_SEND_SCREEN_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "WALLETS_SCROLLING_PREVIEW_ENABLED",
|
||||
"version": "5.5.0"
|
||||
},
|
||||
{
|
||||
"name": "DERIVE_PUBLIC_KEYS_REFACTORING_ENABLED",
|
||||
"version": "5.5.0"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import com.tangem.core.featuretoggle.storage.LocalFeatureTogglesStorage
|
|||
import com.tangem.core.featuretoggle.version.DefaultVersionProvider
|
||||
import com.tangem.core.featuretoggles.BuildConfig
|
||||
import com.tangem.datasource.asset.AssetReader
|
||||
import com.tangem.datasource.local.AppPreferenceStorage
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -29,7 +29,7 @@ internal object FeatureTogglesManagerModule {
|
|||
fun provideFeatureTogglesManager(
|
||||
@ApplicationContext context: Context,
|
||||
assetReader: AssetReader,
|
||||
appPreferenceStorage: AppPreferenceStorage,
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
): FeatureTogglesManager {
|
||||
val moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build()
|
||||
val localFeatureTogglesStorage = LocalFeatureTogglesStorage(
|
||||
|
|
@ -41,8 +41,7 @@ internal object FeatureTogglesManagerModule {
|
|||
return if (BuildConfig.TESTER_MENU_ENABLED) {
|
||||
DevFeatureTogglesManager(
|
||||
localFeatureTogglesStorage = localFeatureTogglesStorage,
|
||||
appPreferenceStorage = appPreferenceStorage,
|
||||
jsonAdapter = moshi.adapter(),
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
versionProvider = versionProvider,
|
||||
)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,25 +1,25 @@
|
|||
package com.tangem.core.featuretoggle.manager
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.tangem.core.featuretoggle.storage.FeatureTogglesStorage
|
||||
import com.tangem.core.featuretoggle.utils.associateToggles
|
||||
import com.tangem.core.featuretoggle.version.VersionProvider
|
||||
import com.tangem.datasource.local.AppPreferenceStorage
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
|
||||
import com.tangem.datasource.local.preferences.utils.storeObject
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
/**
|
||||
* Feature toggles manager implementation in DEV build
|
||||
*
|
||||
* @property localFeatureTogglesStorage local feature toggles storage
|
||||
* @property appPreferenceStorage application local storage
|
||||
* @property jsonAdapter adapter for parsing json
|
||||
* @property appPreferencesStore application local store
|
||||
* @property versionProvider application version provider
|
||||
*/
|
||||
internal class DevFeatureTogglesManager(
|
||||
private val localFeatureTogglesStorage: FeatureTogglesStorage,
|
||||
private val appPreferenceStorage: AppPreferenceStorage,
|
||||
private val jsonAdapter: JsonAdapter<Map<String, Boolean>>,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val versionProvider: VersionProvider,
|
||||
) : MutableFeatureTogglesManager {
|
||||
|
||||
|
|
@ -28,11 +28,9 @@ internal class DevFeatureTogglesManager(
|
|||
override suspend fun init() {
|
||||
localFeatureTogglesStorage.init()
|
||||
|
||||
val savedFeatureToggles = if (appPreferenceStorage.featureToggles.isNotEmpty()) {
|
||||
jsonAdapter.fromJson(appPreferenceStorage.featureToggles).orEmpty()
|
||||
} else {
|
||||
emptyMap()
|
||||
}
|
||||
val savedFeatureToggles = appPreferencesStore.getObjectSyncOrNull<Map<String, Boolean>>(
|
||||
key = PreferencesKeys.FEATURE_TOGGLES_KEY,
|
||||
) ?: emptyMap()
|
||||
|
||||
featureTogglesMap = localFeatureTogglesStorage.featureToggles
|
||||
.associateToggles(currentVersion = versionProvider.get().orEmpty())
|
||||
|
|
@ -46,10 +44,10 @@ internal class DevFeatureTogglesManager(
|
|||
|
||||
override fun getFeatureToggles(): Map<String, Boolean> = featureTogglesMap
|
||||
|
||||
override fun changeToggle(name: String, isEnabled: Boolean) {
|
||||
override suspend fun changeToggle(name: String, isEnabled: Boolean) {
|
||||
featureTogglesMap[name] ?: return
|
||||
featureTogglesMap[name] = isEnabled
|
||||
appPreferenceStorage.featureToggles = jsonAdapter.toJson(featureTogglesMap)
|
||||
appPreferencesStore.storeObject(PreferencesKeys.FEATURE_TOGGLES_KEY, featureTogglesMap)
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
|
|
|
|||
|
|
@ -11,5 +11,5 @@ interface MutableFeatureTogglesManager : FeatureTogglesManager {
|
|||
fun getFeatureToggles(): Map<String, Boolean>
|
||||
|
||||
/** Change availability [isEnabled] of toggle with name [name] */
|
||||
fun changeToggle(name: String, isEnabled: Boolean)
|
||||
suspend fun changeToggle(name: String, isEnabled: Boolean)
|
||||
}
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
package com.tangem.core.featuretoggle.storage
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Data model with information about feature toggle
|
||||
*
|
||||
|
|
@ -10,4 +13,8 @@ package com.tangem.core.featuretoggle.storage
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal data class FeatureToggle(val name: String, val version: String)
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class FeatureToggle(
|
||||
@Json(name = "name") val name: String,
|
||||
@Json(name = "version") val version: String,
|
||||
)
|
||||
|
|
@ -2,48 +2,42 @@ package com.tangem.core.featuretoggle.manager
|
|||
|
||||
import android.annotation.SuppressLint
|
||||
import com.google.common.truth.Truth
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.tangem.core.featuretoggle.storage.FeatureToggle
|
||||
import com.tangem.core.featuretoggle.storage.FeatureTogglesStorage
|
||||
import com.tangem.core.featuretoggle.utils.associateToggles
|
||||
import com.tangem.core.featuretoggle.version.VersionProvider
|
||||
import com.tangem.datasource.local.AppPreferenceStorage
|
||||
import io.mockk.Runs
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerifyOrder
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verifyAll
|
||||
import io.mockk.verifyOrder
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
|
||||
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import kotlin.collections.set
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@SuppressLint("CheckResult")
|
||||
internal class DevFeatureTogglesManagerTest {
|
||||
|
||||
private val localFeatureTogglesStorage = mockk<FeatureTogglesStorage>()
|
||||
private val appPreferenceStorage = mockk<AppPreferenceStorage>(relaxed = true)
|
||||
private val jsonAdapter = mockk<JsonAdapter<Map<String, Boolean>>>()
|
||||
private val appPreferenceStore = mockk<AppPreferencesStore>(relaxed = true)
|
||||
private val versionProvider = mockk<VersionProvider>()
|
||||
private val manager = DevFeatureTogglesManager(
|
||||
localFeatureTogglesStorage = localFeatureTogglesStorage,
|
||||
appPreferenceStorage = appPreferenceStorage,
|
||||
jsonAdapter = jsonAdapter,
|
||||
appPreferencesStore = appPreferenceStore,
|
||||
versionProvider = versionProvider,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage if shared prefs kept feature toggles`() = runTest {
|
||||
val currentVersion = "1.0.0"
|
||||
val currentVersion = "0.1.0"
|
||||
|
||||
coEvery { localFeatureTogglesStorage.init() } just Runs
|
||||
coEvery { appPreferenceStorage.featureToggles } returns savedFeatureToggles
|
||||
coEvery { jsonAdapter.fromJson(savedFeatureToggles) } returns savedFeatureTogglesMap
|
||||
coEvery {
|
||||
appPreferenceStore.getObjectSyncOrNull<Map<String, Boolean>>(PreferencesKeys.FEATURE_TOGGLES_KEY)
|
||||
} returns savedFeatureTogglesMap
|
||||
coEvery { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles
|
||||
coEvery { versionProvider.get() } returns currentVersion
|
||||
|
||||
|
|
@ -51,7 +45,6 @@ internal class DevFeatureTogglesManagerTest {
|
|||
|
||||
coVerifyOrder {
|
||||
localFeatureTogglesStorage.init()
|
||||
jsonAdapter.fromJson(savedFeatureToggles)
|
||||
versionProvider.get()
|
||||
}
|
||||
|
||||
|
|
@ -66,12 +59,12 @@ internal class DevFeatureTogglesManagerTest {
|
|||
|
||||
@Test
|
||||
fun `successfully initialize storage if shared prefs kept empty list`() = runTest {
|
||||
val currentVersion = "1.0.0"
|
||||
val sharedPrefFeatureToggles = "[]"
|
||||
val currentVersion = "0.1.0"
|
||||
|
||||
coEvery { localFeatureTogglesStorage.init() } just Runs
|
||||
coEvery { appPreferenceStorage.featureToggles } returns sharedPrefFeatureToggles
|
||||
coEvery { jsonAdapter.fromJson(sharedPrefFeatureToggles) } returns savedFeatureTogglesMap
|
||||
coEvery {
|
||||
appPreferenceStore.getObjectSyncOrNull<Map<String, Boolean>>(PreferencesKeys.FEATURE_TOGGLES_KEY)
|
||||
} returns emptyMap()
|
||||
coEvery { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles
|
||||
coEvery { versionProvider.get() } returns currentVersion
|
||||
|
||||
|
|
@ -79,7 +72,6 @@ internal class DevFeatureTogglesManagerTest {
|
|||
|
||||
coVerifyOrder {
|
||||
localFeatureTogglesStorage.init()
|
||||
jsonAdapter.fromJson(sharedPrefFeatureToggles)
|
||||
versionProvider.get()
|
||||
}
|
||||
|
||||
|
|
@ -94,10 +86,12 @@ internal class DevFeatureTogglesManagerTest {
|
|||
|
||||
@Test
|
||||
fun `successfully initialize storage if shared prefs didn't keep feature toggles`() = runTest {
|
||||
val currentVersion = "1.0.0"
|
||||
val currentVersion = "0.1.0"
|
||||
|
||||
coEvery { localFeatureTogglesStorage.init() } just Runs
|
||||
coEvery { appPreferenceStorage.featureToggles } returns ""
|
||||
coEvery {
|
||||
appPreferenceStore.getObjectSyncOrNull<Map<String, Boolean>>(PreferencesKeys.FEATURE_TOGGLES_KEY)
|
||||
} returns null
|
||||
coEvery { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles
|
||||
coEvery { versionProvider.get() } returns currentVersion
|
||||
|
||||
|
|
@ -107,7 +101,6 @@ internal class DevFeatureTogglesManagerTest {
|
|||
localFeatureTogglesStorage.init()
|
||||
versionProvider.get()
|
||||
}
|
||||
verifyAll(inverse = true) { jsonAdapter.fromJson(any<String>()) }
|
||||
|
||||
val expected = localFeatureToggles
|
||||
.associateToggles(currentVersion)
|
||||
|
|
@ -119,8 +112,7 @@ internal class DevFeatureTogglesManagerTest {
|
|||
@Test
|
||||
fun `successfully initialize storage if versionProvider returns null`() = runTest {
|
||||
coEvery { localFeatureTogglesStorage.init() } just Runs
|
||||
coEvery { appPreferenceStorage.featureToggles } returns savedFeatureToggles
|
||||
coEvery { jsonAdapter.fromJson(savedFeatureToggles) } returns savedFeatureTogglesMap
|
||||
coEvery { appPreferenceStore.getSyncOrNull(PreferencesKeys.FEATURE_TOGGLES_KEY) } returns savedFeatureToggles
|
||||
coEvery { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles
|
||||
coEvery { versionProvider.get() } returns null
|
||||
|
||||
|
|
@ -128,7 +120,6 @@ internal class DevFeatureTogglesManagerTest {
|
|||
|
||||
coVerifyOrder {
|
||||
localFeatureTogglesStorage.init()
|
||||
jsonAdapter.fromJson(savedFeatureToggles)
|
||||
versionProvider.get()
|
||||
}
|
||||
|
||||
|
|
@ -180,7 +171,7 @@ internal class DevFeatureTogglesManagerTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `change toggle that contains in map`() {
|
||||
fun `change toggle that contains in map`() = runTest {
|
||||
val changeableToggleName = "INACTIVE_TEST_FEATURE_ENABLED"
|
||||
val resultMap = mutableMapOf(
|
||||
changeableToggleName to false,
|
||||
|
|
@ -188,18 +179,16 @@ internal class DevFeatureTogglesManagerTest {
|
|||
)
|
||||
|
||||
manager.setFeatureToggles(resultMap)
|
||||
coEvery { jsonAdapter.toJson(resultMap) } returns ""
|
||||
|
||||
manager.changeToggle(changeableToggleName, true)
|
||||
|
||||
resultMap[changeableToggleName] = true
|
||||
verifyOrder { jsonAdapter.toJson(resultMap) }
|
||||
|
||||
Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(resultMap)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `change toggle that doesn't contains in map`() {
|
||||
fun `change toggle that doesn't contains in map`() = runTest {
|
||||
val resultMap = mutableMapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to false,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
|
||||
|
|
@ -209,8 +198,6 @@ internal class DevFeatureTogglesManagerTest {
|
|||
|
||||
manager.changeToggle("FEATURE_TOGGLE", true)
|
||||
|
||||
verifyAll(inverse = true) { jsonAdapter.toJson(any()) }
|
||||
|
||||
Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(resultMap)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -91,7 +91,8 @@
|
|||
<string name="common_explore_transaction_history">Посмотреть историю транзакций</string>
|
||||
<string name="common_explorer">Обозреватель</string>
|
||||
<string name="common_fee_label">Комиссия</string>
|
||||
<string name="common_fee_selector_footer">Сетевые комиссии за транзакции используются для поддержки безопасности сети, поощрения валидаторов, выделения ресурсов и определения приоритета транзакции.</string>
|
||||
<string name="common_fee_selector_footer">Сетевые комиссии – это плата пользователя за обработку и подтверждение транзакций. Размер комиссии зависит от нагрузка на сеть, объема транзакции и приоритета исполнения. %s</string>
|
||||
<string name="common_fee_selector_link_description">Подробнее</string>
|
||||
<string name="common_fee_selector_option_custom">Свое</string>
|
||||
<string name="common_fee_selector_option_fast">Быстро</string>
|
||||
<string name="common_fee_selector_option_market">По рынку</string>
|
||||
|
|
@ -449,6 +450,7 @@
|
|||
<string name="scan_card_settings_button">Сканировать</string>
|
||||
<string name="scan_card_settings_message">Отсканируйте карту, чтобы изменить ее настройки. Изменения затронут только ту карту, которую вы отсканировали, и не повлияют на другие карты, привязанные к вашему кошельку.</string>
|
||||
<string name="scan_card_settings_title">Приготовьте свою карту</string>
|
||||
<string name="send_additional_field_already_included">Уже содержится в введенном адресе</string>
|
||||
<string name="send_amount_label">Сумма</string>
|
||||
<string name="send_amount_substract">Вычесть из суммы отправки</string>
|
||||
<string name="send_amount_substract_footer">Сумма к получению %s</string>
|
||||
|
|
@ -535,6 +537,9 @@
|
|||
<string name="swapping_give_permission">Дать разрешение</string>
|
||||
<string name="swapping_high_price_impact_description">Обмен этой суммы выбранных токенов может вызвать значительные колебания цены и уменьшить получаемую сумму.</string>
|
||||
<string name="swapping_insufficient_funds">Недостаточно средств</string>
|
||||
<string name="swapping_alert_title">Комиссии</string>
|
||||
<string name="swapping_alert_cex_description">В сумму включено: \n• комиссия провайдера сервиса\n• комиссия сети за отправку %s от биржи обратно на адрес пользователя</string>
|
||||
<string name="swapping_alert_dex_description">В сумму включена комиссия провайдера сервиса.</string>
|
||||
<string name="swapping_not_enough_funds_for_fee">Недостаточно средств для оплаты комиссии на вашем %1$s кошельке для создания транзакции. Сначала пополните свой %2$s кошелек.</string>
|
||||
<string name="swapping_pending_transaction_subtitle">Транзакция в процессе…</string>
|
||||
<string name="swapping_pending_transaction_title">Подождите</string>
|
||||
|
|
|
|||
|
|
@ -89,7 +89,8 @@
|
|||
<string name="common_explore_transaction_history">Explore transaction history</string>
|
||||
<string name="common_explorer">Explorer</string>
|
||||
<string name="common_fee_label">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_footer">Network fees are charges users pay to process and confirm transactions. The fee amount can be affected by network congestion, transaction size, and execution priority. %s</string>
|
||||
<string name="common_fee_selector_link_description">Read more</string>
|
||||
<string name="common_fee_selector_option_custom">Custom</string>
|
||||
<string name="common_fee_selector_option_fast">Fast</string>
|
||||
<string name="common_fee_selector_option_market">Market</string>
|
||||
|
|
@ -285,7 +286,6 @@
|
|||
<string name="manage_tokens_network_selector_non_native_info">Using non-native networks for tokens enables cross-blockchain interoperability, allowing assets to be utilized in diverse decentralized applications and smart contracts across platforms. However, this often involves a custodian or smart contract to hold the original asset securely, introducing centralization and counterparty risk.</string>
|
||||
<string name="manage_tokens_network_selector_non_native_subtitle">Not original or primary blockchain the token is hosted</string>
|
||||
<string name="manage_tokens_network_selector_non_native_title">Non-native networks</string>
|
||||
<string name="manage_tokens_network_selector_other_subtitle">Blockchain the cryptocurrency was initially created</string>
|
||||
<string name="manage_tokens_network_selector_other_title">Networks</string>
|
||||
<string name="manage_tokens_network_selector_title">Choose networks</string>
|
||||
<string name="manage_tokens_network_selector_wallet">Wallet</string>
|
||||
|
|
@ -448,6 +448,7 @@
|
|||
<string name="scan_card_settings_button">Scan Card</string>
|
||||
<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_additional_field_already_included">Already included in the entered address</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 %s</string>
|
||||
|
|
@ -549,6 +550,9 @@
|
|||
<string name="swapping_give_permission">Give Permission</string>
|
||||
<string name="swapping_high_price_impact_description">Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome.</string>
|
||||
<string name="swapping_insufficient_funds">Insufficient funds</string>
|
||||
<string name="swapping_alert_title">Fees</string>
|
||||
<string name="swapping_alert_cex_description">The amount includes:\n• service provider\'s fee\n• network fee for sending %s from the exchange back to the user\'s address.</string>
|
||||
<string name="swapping_alert_dex_description">The amount includes the service provider\'s fee.</string>
|
||||
<string name="swapping_not_enough_funds_for_fee">Insufficient funds in your %1$s wallet to cover fees. Top up your %2$s wallet first.</string>
|
||||
<string name="swapping_pending_transaction_subtitle">Transaction in progress...</string>
|
||||
<string name="swapping_pending_transaction_title">Waiting</string>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ dependencies {
|
|||
implementation(deps.compose.ui.tooling)
|
||||
implementation(deps.compose.ui.utils)
|
||||
implementation(deps.compose.coil)
|
||||
implementation(deps.compose.navigation)
|
||||
implementation(deps.compose.navigation.hilt)
|
||||
|
||||
/** Other libraries */
|
||||
implementation(deps.compose.accompanist.systemUiController)
|
||||
|
|
@ -38,4 +40,5 @@ dependencies {
|
|||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.zxing.qrCore)
|
||||
implementation(deps.jodatime)
|
||||
implementation(deps.timber)
|
||||
}
|
||||
|
|
@ -39,6 +39,11 @@ fun SpacerH16(modifier: Modifier = Modifier) {
|
|||
SpacerH(16.dp, modifier)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SpacerH18(modifier: Modifier = Modifier) {
|
||||
SpacerH(18.dp, modifier)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SpacerH24(modifier: Modifier = Modifier) {
|
||||
SpacerH(24.dp, modifier)
|
||||
|
|
|
|||
|
|
@ -1,17 +1,13 @@
|
|||
package com.tangem.core.ui.components.atoms
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
|
|
@ -21,7 +17,7 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
fun Hand(modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.padding(vertical = TangemTheme.dimens.spacing8)
|
||||
.height(handComposableComponentHeight)
|
||||
.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
|
|
@ -37,6 +33,11 @@ fun Hand(modifier: Modifier = Modifier) {
|
|||
}
|
||||
}
|
||||
|
||||
val handComposableComponentHeight: Dp
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
get() = TangemTheme.dimens.size4 + TangemTheme.dimens.spacing16
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
private fun HandSample(modifier: Modifier = Modifier) {
|
||||
|
|
|
|||
|
|
@ -6,4 +6,6 @@ import androidx.compose.runtime.Immutable
|
|||
* General interface for bottom sheet config model
|
||||
*/
|
||||
@Immutable
|
||||
interface TangemBottomSheetConfigContent
|
||||
interface TangemBottomSheetConfigContent {
|
||||
object Empty : TangemBottomSheetConfigContent
|
||||
}
|
||||
|
|
@ -39,47 +39,57 @@ fun HorizontalActionChips(
|
|||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
// region Preview
|
||||
@Preview(widthDp = 360)
|
||||
@Composable
|
||||
private fun Preview_HorizontalActionChips_Light(
|
||||
@PreviewParameter(ActionButtonConfigProvider::class) buttons: ImmutableList<ActionButtonConfig>,
|
||||
@PreviewParameter(ActionButtonConfigProvider::class) buttons: HorizontalActionChips,
|
||||
) {
|
||||
TangemTheme(isDark = false) {
|
||||
HorizontalActionChips(buttons = buttons)
|
||||
HorizontalActionChips(buttons = buttons.buttons)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(widthDp = 360)
|
||||
@Composable
|
||||
private fun Preview_HorizontalActionChips_Dark(
|
||||
@PreviewParameter(ActionButtonConfigProvider::class) buttons: ImmutableList<ActionButtonConfig>,
|
||||
@PreviewParameter(ActionButtonConfigProvider::class) buttons: HorizontalActionChips,
|
||||
) {
|
||||
TangemTheme(isDark = true) {
|
||||
HorizontalActionChips(buttons = buttons)
|
||||
HorizontalActionChips(buttons = buttons.buttons)
|
||||
}
|
||||
}
|
||||
|
||||
private class ActionButtonConfigProvider : CollectionPreviewParameterProvider<ActionButtonConfig>(
|
||||
collection = persistentListOf(
|
||||
ActionButtonConfig(
|
||||
text = TextReference.Str(value = "Buy"),
|
||||
iconResId = R.drawable.ic_plus_24,
|
||||
onClick = {},
|
||||
),
|
||||
ActionButtonConfig(
|
||||
text = TextReference.Str(value = "Send"),
|
||||
iconResId = R.drawable.ic_arrow_up_24,
|
||||
onClick = {},
|
||||
),
|
||||
ActionButtonConfig(
|
||||
text = TextReference.Str(value = "Receive"),
|
||||
iconResId = R.drawable.ic_arrow_down_24,
|
||||
onClick = {},
|
||||
),
|
||||
ActionButtonConfig(
|
||||
text = TextReference.Str(value = "Exchange"),
|
||||
iconResId = R.drawable.ic_exchange_vertical_24,
|
||||
onClick = {},
|
||||
private class ActionButtonConfigProvider : CollectionPreviewParameterProvider<HorizontalActionChips>(
|
||||
collection = listOf(
|
||||
HorizontalActionChips(
|
||||
buttons = persistentListOf(
|
||||
ActionButtonConfig(
|
||||
text = TextReference.Str(value = "Buy"),
|
||||
iconResId = R.drawable.ic_plus_24,
|
||||
onClick = {},
|
||||
),
|
||||
ActionButtonConfig(
|
||||
text = TextReference.Str(value = "Send"),
|
||||
iconResId = R.drawable.ic_arrow_up_24,
|
||||
onClick = {},
|
||||
),
|
||||
ActionButtonConfig(
|
||||
text = TextReference.Str(value = "Receive"),
|
||||
iconResId = R.drawable.ic_arrow_down_24,
|
||||
onClick = {},
|
||||
),
|
||||
ActionButtonConfig(
|
||||
text = TextReference.Str(value = "Exchange"),
|
||||
iconResId = R.drawable.ic_exchange_vertical_24,
|
||||
onClick = {},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
private data class HorizontalActionChips(
|
||||
val buttons: ImmutableList<ActionButtonConfig>,
|
||||
)
|
||||
// endregion Preview
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
package com.tangem.core.ui.components.buttons
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
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.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
|
|
@ -51,12 +53,22 @@ fun SecondarySmallButton(config: SmallButtonConfig, modifier: Modifier = Modifie
|
|||
@Composable
|
||||
private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier: Modifier = Modifier) {
|
||||
val shape = RoundedCornerShape(size = TangemTheme.dimens.radius16)
|
||||
|
||||
val backgroundColor by animateColorAsState(
|
||||
targetValue = if (isPrimary) TangemTheme.colors.button.primary else TangemTheme.colors.button.secondary,
|
||||
label = "Update background color",
|
||||
)
|
||||
|
||||
val textColor by animateColorAsState(
|
||||
targetValue = if (isPrimary) TangemTheme.colors.text.primary2 else TangemTheme.colors.text.primary1,
|
||||
label = "Update text color",
|
||||
)
|
||||
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,
|
||||
color = backgroundColor,
|
||||
shape = shape,
|
||||
)
|
||||
.clickable(enabled = true, onClick = config.onClick)
|
||||
|
|
@ -66,8 +78,11 @@ private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier:
|
|||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(
|
||||
horizontal = TangemTheme.dimens.spacing10,
|
||||
),
|
||||
text = config.text.resolveReference(),
|
||||
color = if (isPrimary) TangemTheme.colors.text.primary2 else TangemTheme.colors.text.primary1,
|
||||
color = textColor,
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.button,
|
||||
)
|
||||
|
|
@ -97,7 +112,7 @@ private fun ButtonsSample() {
|
|||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
|
||||
) {
|
||||
val config = SmallButtonConfig(
|
||||
text = TextReference.Str(value = "Add"),
|
||||
text = TextReference.Str(value = "Adddddddd"),
|
||||
onClick = {},
|
||||
)
|
||||
PrimarySmallButton(config = config)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
|
|
@ -87,7 +86,6 @@ private fun Button(
|
|||
Row(
|
||||
modifier = modifier
|
||||
.heightIn(min = TangemTheme.dimens.size36)
|
||||
.clip(shape)
|
||||
.background(color = backgroundColor, shape = shape)
|
||||
.clickable(enabled = config.enabled, onClick = config.onClick)
|
||||
.padding(start = TangemTheme.dimens.spacing16, end = TangemTheme.dimens.spacing24)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ fun TangemButton(
|
|||
elevation: ButtonElevation = TangemButtonsDefaults.elevation,
|
||||
textStyle: TextStyle = TangemTheme.typography.button,
|
||||
shape: Shape = size.toShape(),
|
||||
iconPadding: Dp = size.toIconPadding(),
|
||||
) {
|
||||
val multipleClickPreventer = remember { MultipleClickPreventer.get() }
|
||||
|
||||
|
|
@ -53,7 +54,7 @@ fun TangemButton(
|
|||
|
||||
ButtonContentContainer(
|
||||
buttonIcon = icon,
|
||||
iconPadding = size.toIconPadding(),
|
||||
iconPadding = iconPadding,
|
||||
showProgress = showProgress,
|
||||
progressIndicator = {
|
||||
CircularProgressIndicator(
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
package com.tangem.core.ui.components.buttons.segmentedbutton
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.spring
|
||||
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.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
|
|
@ -58,11 +64,16 @@ inline fun <reified T> SegmentedButtons(
|
|||
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
|
||||
|
||||
val animateColor by animateColorAsState(
|
||||
targetValue = if (index == selected) selectedColor else color,
|
||||
label = "Segmented Button Selected Color Animation",
|
||||
animationSpec = spring(stiffness = Spring.StiffnessMedium),
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.background(
|
||||
color = if (index == selected) selectedColor else color,
|
||||
color = animateColor,
|
||||
shape = RoundedCornerShape(
|
||||
topStart = leftRadius,
|
||||
topEnd = rightRadius,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.core.ui.components.fields
|
|||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
|
|
@ -54,6 +55,7 @@ fun AmountTextField(
|
|||
keyboardOptions: KeyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
) {
|
||||
val decimalFormat = rememberDecimalFormat()
|
||||
|
||||
|
|
@ -75,6 +77,7 @@ fun AmountTextField(
|
|||
textStyle = textStyle,
|
||||
color = color,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
singleLine = true,
|
||||
visualTransformation = AmountVisualTransformation(decimals, symbol, decimalFormat),
|
||||
decorationBox = { innerTextField ->
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
package com.tangem.core.ui.components.fields
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
|
||||
import androidx.compose.foundation.text.selection.TextSelectionColors
|
||||
|
|
@ -33,6 +35,7 @@ fun SimpleTextField(
|
|||
singleLine: Boolean = false,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
color: Color = TangemTheme.colors.text.primary1,
|
||||
textStyle: TextStyle = TangemTheme.typography.body2.copy(color = color),
|
||||
readOnly: Boolean = false,
|
||||
|
|
@ -42,26 +45,31 @@ fun SimpleTextField(
|
|||
mutableStateOf(
|
||||
TextFieldValue(
|
||||
text = value,
|
||||
selection = when {
|
||||
value.isEmpty() -> TextRange.Zero
|
||||
else -> TextRange(value.length, value.length)
|
||||
},
|
||||
selection = getValueRange(value),
|
||||
),
|
||||
)
|
||||
}
|
||||
val focusRequester = remember { FocusRequester.Default }
|
||||
val customTextSelectionColors = TextSelectionColors(
|
||||
handleColor = TangemTheme.colors.text.secondary,
|
||||
backgroundColor = TangemTheme.colors.text.secondary.copy(alpha = 0.4f),
|
||||
handleColor = TangemTheme.colors.text.accent,
|
||||
backgroundColor = TangemTheme.colors.text.accent.copy(alpha = 0.3f),
|
||||
)
|
||||
|
||||
val textFieldValue = textFieldValueState.copy(text = value)
|
||||
|
||||
SideEffect {
|
||||
if (textFieldValue.selection != textFieldValueState.selection ||
|
||||
textFieldValue.composition != textFieldValueState.composition
|
||||
) {
|
||||
textFieldValueState = textFieldValue
|
||||
val isSelectionChanged by remember {
|
||||
derivedStateOf {
|
||||
textFieldValue.selection != textFieldValueState.selection ||
|
||||
textFieldValue.composition != textFieldValueState.composition ||
|
||||
textFieldValue.text != textFieldValueState.text
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = isSelectionChanged) {
|
||||
if (isSelectionChanged) {
|
||||
textFieldValueState = textFieldValue.copy(
|
||||
selection = getValueRange(value),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -86,20 +94,46 @@ fun SimpleTextField(
|
|||
readOnly = readOnly,
|
||||
visualTransformation = visualTransformation,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
decorationBox = decorationBox ?: { textValue ->
|
||||
Box {
|
||||
if (value.isBlank() && placeholder != null) {
|
||||
Text(
|
||||
text = placeholder.resolveReference(),
|
||||
style = textStyle,
|
||||
color = TangemTheme.colors.text.disabled,
|
||||
)
|
||||
}
|
||||
textValue()
|
||||
}
|
||||
SimpleTextPlaceholder(
|
||||
placeholder = placeholder,
|
||||
value = value,
|
||||
textStyle = textStyle,
|
||||
textValue = textValue,
|
||||
)
|
||||
},
|
||||
modifier = modifier
|
||||
.focusRequester(focusRequester),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getValueRange(value: String) = when {
|
||||
value.isEmpty() -> TextRange.Zero
|
||||
else -> TextRange(value.length, value.length)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SimpleTextPlaceholder(
|
||||
placeholder: TextReference?,
|
||||
value: String,
|
||||
textStyle: TextStyle,
|
||||
textValue: @Composable () -> Unit,
|
||||
) {
|
||||
Box {
|
||||
if (value.isBlank() && placeholder != null) {
|
||||
AnimatedContent(
|
||||
targetState = placeholder,
|
||||
label = "Placeholder Change Animation",
|
||||
) {
|
||||
Text(
|
||||
text = it.resolveReference(),
|
||||
style = textStyle,
|
||||
color = TangemTheme.colors.text.disabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
textValue()
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Column
|
|||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.Icon
|
||||
|
|
@ -51,6 +52,7 @@ fun InputRowEnterAmount(
|
|||
titleColor: Color = TangemTheme.colors.text.secondary,
|
||||
textColor: Color = TangemTheme.colors.text.primary1,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
iconRes: Int? = null,
|
||||
iconTint: Color = TangemTheme.colors.icon.informative,
|
||||
onIconClick: (() -> Unit)? = null,
|
||||
|
|
@ -79,6 +81,7 @@ fun InputRowEnterAmount(
|
|||
color = textColor,
|
||||
textStyle = TangemTheme.typography.body2,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing8),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Column
|
|||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -49,6 +50,7 @@ fun InputRowEnterInfoAmount(
|
|||
textColor: Color = TangemTheme.colors.text.primary1,
|
||||
infoColor: Color = TangemTheme.colors.text.tertiary,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
showDivider: Boolean = false,
|
||||
) {
|
||||
DividerContainer(
|
||||
|
|
@ -74,6 +76,7 @@ fun InputRowEnterInfoAmount(
|
|||
color = textColor,
|
||||
textStyle = TangemTheme.typography.body2,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing8)
|
||||
.weight(1f),
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment.Companion.CenterEnd
|
||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
|
|
@ -16,6 +17,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
|||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.fields.SimpleTextField
|
||||
import com.tangem.core.ui.components.icons.identicon.IdentIcon
|
||||
import com.tangem.core.ui.components.inputrow.inner.CrossIcon
|
||||
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
|
||||
import com.tangem.core.ui.components.inputrow.inner.PasteButton
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
|
@ -73,44 +75,37 @@ fun InputRowRecipient(
|
|||
color = color,
|
||||
)
|
||||
}
|
||||
Row(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing8),
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = isLoading,
|
||||
label = "Indicator Show Change",
|
||||
modifier = Modifier
|
||||
.align(CenterVertically)
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius18))
|
||||
.size(TangemTheme.dimens.size36)
|
||||
.background(TangemTheme.colors.background.tertiary),
|
||||
) { showIndicator ->
|
||||
if (showIndicator) {
|
||||
CircularProgressIndicator(
|
||||
color = TangemTheme.colors.icon.informative,
|
||||
modifier = Modifier
|
||||
.padding(TangemTheme.dimens.spacing8),
|
||||
)
|
||||
} else {
|
||||
IdentIcon(address = value)
|
||||
}
|
||||
Row {
|
||||
InputIcon(
|
||||
isLoading = isLoading,
|
||||
value = value,
|
||||
)
|
||||
SimpleTextField(
|
||||
value = value,
|
||||
placeholder = placeholder,
|
||||
onValueChange = onValueChange,
|
||||
singleLine = singleLine,
|
||||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens.spacing12)
|
||||
.weight(1f)
|
||||
.align(CenterVertically),
|
||||
)
|
||||
CrossIcon(
|
||||
onClick = onPasteClick,
|
||||
modifier = Modifier
|
||||
.align(CenterVertically)
|
||||
.padding(start = TangemTheme.dimens.spacing8),
|
||||
)
|
||||
}
|
||||
SimpleTextField(
|
||||
value = value,
|
||||
placeholder = placeholder,
|
||||
onValueChange = onValueChange,
|
||||
singleLine = singleLine,
|
||||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens.spacing12)
|
||||
.weight(1f)
|
||||
.align(CenterVertically),
|
||||
)
|
||||
PasteButton(
|
||||
isPasteButtonVisible = value.isBlank(),
|
||||
onClick = onPasteClick,
|
||||
modifier = Modifier
|
||||
.align(CenterVertically)
|
||||
.align(CenterEnd)
|
||||
.padding(start = TangemTheme.dimens.spacing8),
|
||||
)
|
||||
}
|
||||
|
|
@ -118,6 +113,29 @@ fun InputRowRecipient(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.InputIcon(isLoading: Boolean, value: String) {
|
||||
AnimatedContent(
|
||||
targetState = isLoading,
|
||||
label = "Indicator Show Change",
|
||||
modifier = Modifier
|
||||
.align(CenterVertically)
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius18))
|
||||
.size(TangemTheme.dimens.size36)
|
||||
.background(TangemTheme.colors.background.tertiary),
|
||||
) { showIndicator ->
|
||||
if (showIndicator) {
|
||||
CircularProgressIndicator(
|
||||
color = TangemTheme.colors.icon.informative,
|
||||
modifier = Modifier
|
||||
.padding(TangemTheme.dimens.spacing8),
|
||||
)
|
||||
} else {
|
||||
IdentIcon(address = value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//region preview
|
||||
@Preview
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -1,24 +1,28 @@
|
|||
package com.tangem.core.ui.components.inputrow.inner
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.DEFAULT_ANIMATION_DURATION
|
||||
|
||||
/**
|
||||
* Paste button with cross icon. Retrieves text from clipboard.
|
||||
|
|
@ -33,48 +37,67 @@ fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifi
|
|||
val clipboardManager = LocalClipboardManager.current
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
|
||||
if (isPasteButtonVisible) {
|
||||
Box(modifier = modifier) {
|
||||
Text(
|
||||
text = "Paste",
|
||||
style = TangemTheme.typography.button,
|
||||
color = TangemTheme.colors.text.primary2,
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.button.primary,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
)
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing10,
|
||||
vertical = TangemTheme.dimens.spacing2,
|
||||
)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(radius = TangemTheme.dimens.radius8),
|
||||
onClick = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onClick(
|
||||
clipboardManager
|
||||
.getText()
|
||||
?.toString()
|
||||
.orEmpty(),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_close_24),
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
contentDescription = stringResource(R.string.common_close),
|
||||
modifier = modifier
|
||||
.size(TangemTheme.dimens.size20)
|
||||
AnimatedVisibility(
|
||||
visible = isPasteButtonVisible,
|
||||
label = "Paste Button Visibility Animation",
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(animationSpec = tween(DEFAULT_ANIMATION_DURATION)),
|
||||
modifier = modifier,
|
||||
) {
|
||||
Text(
|
||||
text = "Paste",
|
||||
style = TangemTheme.typography.button,
|
||||
color = TangemTheme.colors.text.primary2,
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.button.primary,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
)
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing10,
|
||||
vertical = TangemTheme.dimens.spacing2,
|
||||
)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(radius = TangemTheme.dimens.radius10),
|
||||
onClick = { onClick("") },
|
||||
indication = rememberRipple(radius = TangemTheme.dimens.radius8),
|
||||
onClick = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onClick(
|
||||
clipboardManager
|
||||
.getText()
|
||||
?.toString()
|
||||
.orEmpty(),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CrossIcon(onClick: (String) -> Unit, modifier: Modifier = Modifier) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_close_24),
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
contentDescription = stringResource(R.string.common_close),
|
||||
modifier = modifier
|
||||
.size(TangemTheme.dimens.size20)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(radius = TangemTheme.dimens.radius10),
|
||||
onClick = { onClick("") },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PasteButtonPreview() {
|
||||
var isVisible by remember { mutableStateOf(true) }
|
||||
TangemTheme {
|
||||
PasteButton(
|
||||
isPasteButtonVisible = isVisible,
|
||||
onClick = { isVisible = !isVisible },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ package com.tangem.core.ui.components.marketprice
|
|||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.*
|
||||
|
|
@ -35,39 +34,60 @@ import com.tangem.core.ui.utils.BigDecimalFormatter
|
|||
*/
|
||||
@Composable
|
||||
fun MarketPriceBlock(state: MarketPriceBlockState, modifier: Modifier = Modifier) {
|
||||
var rootWidth by remember { mutableIntStateOf(value = 0) }
|
||||
|
||||
Column(
|
||||
Row(
|
||||
modifier = modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.background.primary,
|
||||
shape = RoundedCornerShape(TangemTheme.dimens.radius14),
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
)
|
||||
.heightIn(min = TangemTheme.dimens.size70)
|
||||
.fillMaxWidth()
|
||||
.padding(all = TangemTheme.dimens.spacing14)
|
||||
.onSizeChanged { rootWidth = it.width },
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing6),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
.heightIn(min = TangemTheme.dimens.size72)
|
||||
.padding(all = TangemTheme.dimens.spacing12),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Title(currencyName = state.currencySymbol)
|
||||
var rootWidth by remember { mutableIntStateOf(value = 0) }
|
||||
|
||||
Content(state = state, rootWidth = rootWidth)
|
||||
Column(
|
||||
modifier = Modifier.onSizeChanged { rootWidth = it.width },
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
Title(currencyName = state.currencySymbol)
|
||||
Content(state = state, rootWidth = rootWidth)
|
||||
}
|
||||
|
||||
Icon(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size20),
|
||||
painter = painterResource(id = R.drawable.ic_chevron_right_24),
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Title(currencyName: String) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.wallet_marketplace_block_title, currencyName),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
)
|
||||
private fun Title(currencyName: String, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier.heightIn(min = TangemTheme.dimens.size24),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.wallet_marketplace_block_title, currencyName),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(state: MarketPriceBlockState, rootWidth: Int) {
|
||||
AnimatedContent(targetState = state, label = "Update the content") { marketPriceBlockState ->
|
||||
private fun Content(state: MarketPriceBlockState, rootWidth: Int, modifier: Modifier = Modifier) {
|
||||
AnimatedContent(
|
||||
modifier = modifier.heightIn(min = TangemTheme.dimens.size20),
|
||||
targetState = state,
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
label = "Update the content",
|
||||
) { marketPriceBlockState ->
|
||||
when (marketPriceBlockState) {
|
||||
is MarketPriceBlockState.Content,
|
||||
is MarketPriceBlockState.Error,
|
||||
|
|
@ -97,6 +117,7 @@ private fun PriceContent(state: MarketPriceBlockState, priceWidthDp: Dp) {
|
|||
@Composable
|
||||
private fun PriceBlock(state: MarketPriceBlockState, priceWidthDp: Dp) {
|
||||
val priceModifier = Modifier.widthIn(max = priceWidthDp)
|
||||
|
||||
AnimatedContent(targetState = state, label = "Update the price block") { marketPriceBlockState ->
|
||||
if (marketPriceBlockState is MarketPriceBlockState.Content) {
|
||||
Row(
|
||||
|
|
@ -127,12 +148,17 @@ private fun Price(price: String, modifier: Modifier = Modifier) {
|
|||
|
||||
@Composable
|
||||
private fun PriceChangeInPercent(config: PriceChangeState.Content) {
|
||||
AnimatedContent(targetState = config.type, label = "Update price change") { type ->
|
||||
AnimatedContent(
|
||||
targetState = config.type,
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
label = "Update price change",
|
||||
) { type ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing4),
|
||||
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing2),
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size8),
|
||||
painter = painterResource(
|
||||
id = when (type) {
|
||||
PriceChangeType.UP -> R.drawable.ic_arrow_up_8
|
||||
|
|
@ -184,7 +210,8 @@ private fun QuoteTimeStatus() {
|
|||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun Preview_MarketPriceBlock_Light(
|
||||
@PreviewParameter(WalletMarketPriceBlockStateProvider::class)
|
||||
|
|
@ -195,7 +222,7 @@ private fun Preview_MarketPriceBlock_Light(
|
|||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun Preview_MarketPriceBlock_Dark(
|
||||
@PreviewParameter(WalletMarketPriceBlockStateProvider::class)
|
||||
|
|
@ -227,4 +254,5 @@ private class WalletMarketPriceBlockStateProvider : CollectionPreviewParameterPr
|
|||
MarketPriceBlockState.Loading(currencySymbol = "BTC"),
|
||||
MarketPriceBlockState.Error(currencySymbol = "BTC"),
|
||||
),
|
||||
)
|
||||
)
|
||||
// endregion Preview
|
||||
|
|
@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
|
|
@ -30,6 +31,7 @@ import com.tangem.core.ui.components.transactions.state.TransactionState.Content
|
|||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import java.util.UUID
|
||||
|
||||
|
|
@ -46,16 +48,21 @@ import java.util.UUID
|
|||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
@Suppress("LongMethod")
|
||||
fun Transaction(state: TransactionState, isBalanceHidden: Boolean, modifier: Modifier = Modifier) {
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.defaultMinSize(minHeight = TangemTheme.dimens.size56)
|
||||
.clickable(
|
||||
enabled = state is TransactionState.Content,
|
||||
onClick = (state as? TransactionState.Content)?.onClick ?: {},
|
||||
)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing12, vertical = TangemTheme.dimens.spacing10),
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = TangemTheme.dimens.size56)
|
||||
.padding(
|
||||
vertical = TangemTheme.dimens.spacing8,
|
||||
horizontal = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
color = TangemTheme.colors.background.primary,
|
||||
) {
|
||||
@Suppress("DestructuringDeclarationWithTooManyEntries")
|
||||
|
|
@ -76,21 +83,30 @@ fun Transaction(state: TransactionState, isBalanceHidden: Boolean, modifier: Mod
|
|||
Title(
|
||||
state = state,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing12)
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing4,
|
||||
)
|
||||
.constrainAs(titleItem) {
|
||||
top.linkTo(parent.top)
|
||||
bottom.linkTo(subtitleItem.top)
|
||||
start.linkTo(iconItem.end)
|
||||
top.linkTo(iconItem.top)
|
||||
end.linkTo(amountItem.start)
|
||||
width = Dimension.fillToConstraints
|
||||
},
|
||||
)
|
||||
|
||||
Subtitle(
|
||||
state = state,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing6)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing12)
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing4,
|
||||
)
|
||||
.constrainAs(subtitleItem) {
|
||||
top.linkTo(titleItem.bottom)
|
||||
bottom.linkTo(parent.bottom)
|
||||
start.linkTo(iconItem.end)
|
||||
top.linkTo(amountItem.bottom)
|
||||
end.linkTo(timestampItem.start)
|
||||
width = Dimension.fillToConstraints
|
||||
},
|
||||
|
|
@ -100,8 +116,9 @@ fun Transaction(state: TransactionState, isBalanceHidden: Boolean, modifier: Mod
|
|||
state = state,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
modifier = Modifier.constrainAs(amountItem) {
|
||||
top.linkTo(parent.top)
|
||||
bottom.linkTo(timestampItem.top)
|
||||
start.linkTo(titleItem.end)
|
||||
top.linkTo(titleItem.top)
|
||||
end.linkTo(parent.end)
|
||||
width = Dimension.fillToConstraints
|
||||
},
|
||||
|
|
@ -109,12 +126,11 @@ fun Transaction(state: TransactionState, isBalanceHidden: Boolean, modifier: Mod
|
|||
|
||||
Timestamp(
|
||||
state = state,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing6)
|
||||
.constrainAs(timestampItem) {
|
||||
top.linkTo(amountItem.bottom)
|
||||
end.linkTo(parent.end)
|
||||
},
|
||||
modifier = Modifier.constrainAs(timestampItem) {
|
||||
top.linkTo(amountItem.bottom)
|
||||
bottom.linkTo(parent.bottom)
|
||||
end.linkTo(parent.end)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -173,11 +189,16 @@ private fun Icon(state: TransactionState, modifier: Modifier = Modifier) {
|
|||
private fun Title(state: TransactionState, modifier: Modifier = Modifier) {
|
||||
when (state) {
|
||||
is TransactionState.Content -> {
|
||||
Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing6)) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing6),
|
||||
) {
|
||||
Text(
|
||||
text = state.title.resolveReference(),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
|
||||
if (state.status is Status.Unconfirmed) {
|
||||
|
|
@ -266,7 +287,7 @@ private fun Timestamp(state: TransactionState, modifier: Modifier = Modifier) {
|
|||
when (state) {
|
||||
is TransactionState.Content -> {
|
||||
Text(
|
||||
text = state.timestamp,
|
||||
text = state.time,
|
||||
modifier = modifier,
|
||||
textAlign = TextAlign.End,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
|
|
@ -321,89 +342,109 @@ private class TransactionItemStateProvider : CollectionPreviewParameterProvider<
|
|||
TransactionState.Content(
|
||||
txHash = UUID.randomUUID().toString(),
|
||||
amount = "-0.500913 BTC",
|
||||
timestamp = "8:41",
|
||||
time = "8:41",
|
||||
status = Status.Confirmed,
|
||||
direction = Direction.OUTGOING,
|
||||
iconRes = R.drawable.ic_arrow_up_24,
|
||||
title = resourceReference(R.string.common_transfer),
|
||||
subtitle = TextReference.Str("33BddS...ga2B"),
|
||||
timestamp = 0L,
|
||||
onClick = {},
|
||||
),
|
||||
TransactionState.Content(
|
||||
txHash = UUID.randomUUID().toString(),
|
||||
amount = "+0.500913 BTC",
|
||||
timestamp = "8:41",
|
||||
time = "8:41",
|
||||
status = Status.Unconfirmed,
|
||||
direction = Direction.INCOMING,
|
||||
iconRes = R.drawable.ic_arrow_down_24,
|
||||
title = resourceReference(R.string.common_transfer),
|
||||
subtitle = TextReference.Str("33BddS...ga2B"),
|
||||
timestamp = 0L,
|
||||
onClick = {},
|
||||
),
|
||||
TransactionState.Content(
|
||||
txHash = UUID.randomUUID().toString(),
|
||||
amount = "+0.500913 BTC",
|
||||
timestamp = "8:41",
|
||||
time = "8:41",
|
||||
status = Status.Unconfirmed,
|
||||
direction = Direction.OUTGOING,
|
||||
iconRes = R.drawable.ic_doc_24,
|
||||
title = resourceReference(R.string.common_approval),
|
||||
subtitle = TextReference.Str("33BddS...ga2B"),
|
||||
timestamp = 0L,
|
||||
onClick = {},
|
||||
),
|
||||
TransactionState.Content(
|
||||
txHash = UUID.randomUUID().toString(),
|
||||
amount = "+0.500913 BTC",
|
||||
timestamp = "8:41",
|
||||
time = "8:41",
|
||||
status = Status.Failed,
|
||||
direction = Direction.OUTGOING,
|
||||
iconRes = R.drawable.ic_doc_24,
|
||||
title = resourceReference(R.string.common_approval),
|
||||
subtitle = TextReference.Str("33BddS...ga2B"),
|
||||
timestamp = 0L,
|
||||
onClick = {},
|
||||
),
|
||||
TransactionState.Content(
|
||||
txHash = UUID.randomUUID().toString(),
|
||||
amount = "+0.500913 BTC",
|
||||
timestamp = "8:41",
|
||||
time = "8:41",
|
||||
status = Status.Confirmed,
|
||||
direction = Direction.OUTGOING,
|
||||
iconRes = R.drawable.ic_doc_24,
|
||||
title = resourceReference(R.string.common_approval),
|
||||
subtitle = TextReference.Str("33BddS...ga2B"),
|
||||
timestamp = 0L,
|
||||
onClick = {},
|
||||
),
|
||||
TransactionState.Content(
|
||||
txHash = UUID.randomUUID().toString(),
|
||||
amount = "+0.500913 BTC",
|
||||
timestamp = "8:41",
|
||||
time = "8:41",
|
||||
status = Status.Unconfirmed,
|
||||
direction = Direction.INCOMING,
|
||||
iconRes = R.drawable.ic_arrow_down_24,
|
||||
title = resourceReference(R.string.common_swap),
|
||||
subtitle = TextReference.Str("33BddS...ga2B"),
|
||||
timestamp = 0L,
|
||||
onClick = {},
|
||||
),
|
||||
TransactionState.Content(
|
||||
txHash = UUID.randomUUID().toString(),
|
||||
amount = "+0.500913 BTC",
|
||||
timestamp = "8:41",
|
||||
time = "8:41",
|
||||
status = Status.Confirmed,
|
||||
direction = Direction.INCOMING,
|
||||
iconRes = R.drawable.ic_doc_24,
|
||||
title = TextReference.Str("Submit"),
|
||||
subtitle = TextReference.Str("33BddS...ga2B"),
|
||||
timestamp = 0L,
|
||||
onClick = {},
|
||||
),
|
||||
TransactionState.Content(
|
||||
txHash = UUID.randomUUID().toString(),
|
||||
amount = "+0.500913 BTC",
|
||||
timestamp = "8:41",
|
||||
time = "8:41",
|
||||
status = Status.Confirmed,
|
||||
direction = Direction.OUTGOING,
|
||||
iconRes = R.drawable.ic_arrow_up_24,
|
||||
title = TextReference.Str("Submit"),
|
||||
subtitle = TextReference.Str("33BddS...ga2B"),
|
||||
timestamp = 0L,
|
||||
onClick = {},
|
||||
),
|
||||
TransactionState.Content(
|
||||
txHash = UUID.randomUUID().toString(),
|
||||
amount = "0.625 USDT",
|
||||
time = "€0.50",
|
||||
status = Status.Confirmed,
|
||||
direction = Direction.OUTGOING,
|
||||
iconRes = R.drawable.ic_arrow_up_24,
|
||||
title = TextReference.Str("Unlimint Banking Cards Sandbox London"),
|
||||
subtitle = stringReference(value = "8:41 • authorized"),
|
||||
timestamp = 0L,
|
||||
onClick = {},
|
||||
),
|
||||
TransactionState.Loading(txHash = UUID.randomUUID().toString()),
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
package com.tangem.core.ui.components.transactions
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState.TxHistoryItemState
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
|
@ -20,19 +22,23 @@ import java.util.UUID
|
|||
*/
|
||||
@Composable
|
||||
internal fun TxHistoryGroupTitle(config: TxHistoryItemState.GroupTitle, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
text = config.title,
|
||||
Box(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
vertical = TangemTheme.dimens.spacing14,
|
||||
),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.Start,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
vertical = TangemTheme.dimens.spacing8,
|
||||
horizontal = TangemTheme.dimens.spacing12,
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = TangemTheme.dimens.size24),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
Text(
|
||||
text = config.title,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.*
|
|||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
|
|
@ -24,9 +25,11 @@ internal fun TxHistoryTitle(onExploreClick: () -> Unit, modifier: Modifier = Mod
|
|||
Row(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.fillMaxWidth()
|
||||
.padding(top = TangemTheme.dimens.spacing12)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
.padding(horizontal = TangemTheme.dimens.spacing12)
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = TangemTheme.dimens.size24),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(
|
||||
|
|
@ -37,13 +40,14 @@ internal fun TxHistoryTitle(onExploreClick: () -> Unit, modifier: Modifier = Mod
|
|||
|
||||
Row(
|
||||
modifier = Modifier.clickable(onClick = onExploreClick),
|
||||
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing4),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing2),
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(size = TangemTheme.dimens.size20),
|
||||
painter = painterResource(id = R.drawable.ic_compass_24),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(size = TangemTheme.dimens.size18),
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.common_explorer),
|
||||
|
|
|
|||
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