Updated on 2026-08-14

This commit is contained in:
Tangem 2023-02-17 14:57:55 +03:00
commit 2c32846da1
40 changed files with 1119 additions and 230 deletions

@ -1 +1 @@
Subproject commit 2b8aa19f58e17a78015629943d49df0da0b4fd4b
Subproject commit f5f6ac954894ef66331db8fe383b9773feb69ef4

View file

@ -57,6 +57,7 @@ import com.tangem.tap.persistence.PreferencesStorage
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.wallet.BuildConfig
import dagger.hilt.android.HiltAndroidApp
import okhttp3.logging.HttpLoggingInterceptor
import org.rekotlin.Store
import timber.log.Timber
import javax.inject.Inject
@ -146,7 +147,12 @@ class TapApplication : Application(), ImageLoaderFactory {
initConfigManager(configLoader, ::initWithConfigDependency)
initWarningMessagesManager()
BlockchainSdkRetrofitBuilder.enableNetworkLogging = LogConfig.network.blockchainSdkNetwork
if (LogConfig.network.blockchainSdkNetwork) {
BlockchainSdkRetrofitBuilder.interceptors = listOf(
HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY },
)
}
userTokensRepository = UserTokensRepository.init(
context = this,

View file

@ -0,0 +1,33 @@
package com.tangem.tap.common
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
/**
[REDACTED_AUTHOR]
*/
open class ShimmerRecyclerAdapter(
private val viewHolderViewFactory: (ViewGroup) -> ViewGroup,
) : ListAdapter<ShimmerData, ShimmerVH>(DiffUtilCallback) {
override fun getItemId(position: Int): Long {
return if (currentList.isEmpty()) 0 else currentList[position].hashCode().toLong()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ShimmerVH {
return ShimmerVH(viewHolderViewFactory.invoke(parent))
}
override fun onBindViewHolder(holder: ShimmerVH, position: Int) {}
object DiffUtilCallback : DiffUtil.ItemCallback<ShimmerData>() {
override fun areContentsTheSame(oldItem: ShimmerData, newItem: ShimmerData) = oldItem == newItem
override fun areItemsTheSame(oldItem: ShimmerData, newItem: ShimmerData) = oldItem == newItem
}
}
class ShimmerVH(viewGroup: ViewGroup) : RecyclerView.ViewHolder(viewGroup)
data class ShimmerData(private val any: String = "")

View file

@ -2,6 +2,7 @@ package com.tangem.tap.domain.configurable.config
import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.blockchain.common.BlockchairCredentials
import com.tangem.blockchain.common.BlockscoutCredentials
import com.tangem.blockchain.common.GetBlockCredentials
import com.tangem.blockchain.common.NowNodeCredentials
import com.tangem.blockchain.common.QuickNodeCredentials
@ -93,6 +94,10 @@ class ConfigManager {
authToken = values.blockchairAuthorizationToken,
),
blockcypherTokens = values.blockcypherTokens,
blockscoutCredentials = BlockscoutCredentials(
userName = values.saltPay.blockscoutCredentials.user,
password = values.saltPay.blockscoutCredentials.password,
),
quickNodeSolanaCredentials = QuickNodeCredentials(
apiKey = values.quiknodeApiKey,
subdomain = values.quiknodeSubdomain,

View file

@ -15,6 +15,7 @@ class FeatureModel(
val isCreatingTwinCardsAllowed: Boolean,
)
@Suppress("LongParameterList")
class ConfigValueModel(
val coinMarketCapKey: String,
val mercuryoWidgetId: String,
@ -37,6 +38,7 @@ class ConfigValueModel(
val saltPay: SaltPayConfig,
val tronGridApiKey: String,
val amplitudeApiKey: String,
val swapReferrerAccount: SwapReferrerAccount?,
)
data class AppsFlyer(
@ -44,6 +46,11 @@ data class AppsFlyer(
val appsFlyerAppID: String,
)
data class SwapReferrerAccount(
val address: String,
val fee: String,
)
class ConfigModel(
val features: FeatureModel?,
val configValues: ConfigValueModel?,

View file

@ -1,19 +1,15 @@
package com.tangem.tap.domain.extensions
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationParams
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.common.card.EllipticCurve
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toMapKey
import com.tangem.common.hdWallet.DerivationPath
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.SaltPayWorkaround
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
@ -137,23 +133,4 @@ private fun selectWallet(wallets: List<CardDTO.Wallet>): CardDTO.Wallet? {
1 -> wallets[0]
else -> wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: wallets[0]
}
}
fun WalletManagerFactory.makeSaltPayWalletManager(
scanResponse: ScanResponse,
): EthereumWalletManager {
val blockchain = scanResponse.getBlockchain()
if (blockchain != Blockchain.SaltPay)
throw IllegalArgumentException()
val token = SaltPayWorkaround.tokenFrom(blockchain)
val cardWallet = scanResponse.card.wallets.firstOrNull().guard {
throw NullPointerException("SaltPay card must have one wallet at least")
}
return makeWalletManager(
blockchain = blockchain,
publicKey = Wallet.PublicKey(cardWallet.publicKey, null, null),
tokens = listOf(token),
) as EthereumWalletManager
}

View file

@ -27,10 +27,10 @@ import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.onboarding.OnboardingSaltPayHelper
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivationManagerFactory
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayExceptionHandler
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
@ -187,7 +187,10 @@ object ScanCardProcessor {
if (scanResponse.isSaltPay()) {
if (scanResponse.isSaltPayVisa()) {
val (manager, config) = OnboardingSaltPayState.initDependency(scanResponse)
val manager = SaltPayActivationManagerFactory(
blockchain = scanResponse.getBlockchain(),
card = scanResponse.card,
).create()
val result = OnboardingSaltPayHelper.isOnboardingCase(scanResponse, manager)
delay(500)
withMainContext {
@ -197,7 +200,7 @@ object ScanCardProcessor {
if (isOnboardingCase) {
onWalletNotCreated()
store.dispatch(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = false))
store.dispatch(OnboardingSaltPayAction.SetDependencies(manager, config))
store.dispatch(OnboardingSaltPayAction.SetDependencies(manager))
store.dispatch(OnboardingSaltPayAction.Update)
navigateTo(AppScreen.OnboardingWallet) { onProgressStateChange(it) }
} else {

View file

@ -3,13 +3,13 @@ package com.tangem.tap.features.onboarding.products.wallet.redux
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.ifNotNull
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isSaltPay
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.operations.backup.BackupService
import com.tangem.tap.backupService
import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
@ -22,8 +22,8 @@ import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivationManagerFactory
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState
import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.preferencesStorage
@ -396,8 +396,11 @@ private fun initSaltPayOnBackupFinishedIfNeeded(
if (onboardingWalletState.isSaltPay && onboardingWalletState.onboardingSaltPayState == null) {
if (scanResponse == null) throw IllegalArgumentException()
val (manager, config) = OnboardingSaltPayState.initDependency(scanResponse)
store.dispatchOnMain(OnboardingSaltPayAction.SetDependencies(manager, config))
val manager = SaltPayActivationManagerFactory(
blockchain = scanResponse.getBlockchain(),
card = scanResponse.card,
).create()
store.dispatchOnMain(OnboardingSaltPayAction.SetDependencies(manager))
store.dispatchOnMain(OnboardingSaltPayAction.Update)
}
}

View file

@ -23,6 +23,8 @@ import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import java.math.BigDecimal
import java.math.BigInteger
import java.math.MathContext
import java.math.RoundingMode
import java.util.concurrent.atomic.AtomicLong
/**
@ -33,11 +35,12 @@ class GnosisRegistrator(
) {
private val walletAddress = walletManager.wallet.address
private val blockchain = walletManager.wallet.blockchain
private val token = walletManager.wallet.getFirstToken().guard {
throw NullPointerException("WalletManager for GnosisRegistrator must contain token")
}
private val otpProcessorContractAddress: String = when (walletManager.wallet.blockchain) {
private val otpProcessorContractAddress: String = when (blockchain) {
Blockchain.SaltPay -> "0xc659f4FEd7A84a188F54cBA4A7a49D77c1a20522"
else -> throw IllegalArgumentException("GnosisRegistrator supports only the SaltPay blockchain")
}
@ -154,6 +157,12 @@ class GnosisRegistrator(
return Result.Failure(BlockchainSdkError.WrappedThrowable(it.error))
}
}
// return transferFromStandardWay(amountToClaim, signer)
return transferFromHardcodeWay(amountToClaim, signer)
}
private suspend fun transferFromStandardWay(amountToClaim: BigDecimal, signer: TransactionSigner): Result<Unit> {
val amount = Amount(token, amountToClaim)
val feeAmount = walletManager.getFeeToTransferFrom(amount, addressTreasureSafe)
.extractFeeAmount().successOr { return it }
@ -164,6 +173,34 @@ class GnosisRegistrator(
return Result.Success(Unit)
}
private suspend fun transferFromHardcodeWay(amountToClaim: BigDecimal, signer: TransactionSigner): Result<Unit> {
val amount = Amount(token, amountToClaim)
val gasPrice = walletManager.getGasPrice().successOr { return it }
val hardcodeGasLimit = BigInteger("300000")
val hardcodeFeeValue = (hardcodeGasLimit * gasPrice).toBigDecimal(
scale = blockchain.decimals(),
mathContext = MathContext(blockchain.decimals(), RoundingMode.HALF_EVEN),
).movePointLeft(blockchain.decimals())
val hardcodeFeeAmount = Amount(hardcodeFeeValue, blockchain)
val transactionData = walletManager.createTransferFromTransaction(
amount = amount,
fee = hardcodeFeeAmount,
source = addressTreasureSafe,
)
val transactionToSign = walletManager.transactionBuilder.buildTransferFromToSign(
transactionData,
walletManager.txCount.toBigInteger(),
hardcodeGasLimit,
) ?: return Result.Failure(BlockchainSdkError.CustomError("Not enough data"))
return when (val result = walletManager.signAndSend(transactionToSign, signer)) {
SimpleResult.Success -> Result.Success(Unit)
is SimpleResult.Failure -> Result.Failure(result.error)
}
}
private fun Result<List<Amount>>.extractFeeAmount(): Result<Amount> {
return when (this) {
is Result.Success -> {

View file

@ -1,14 +1,20 @@
package com.tangem.tap.features.onboarding.products.wallet.saltPay
import android.net.Uri
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.blockchains.ethereum.SignedEthereumTransaction
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.TransactionSigner
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.blockchain.extensions.successOr
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.isZero
import com.tangem.common.services.Result
import com.tangem.datasource.api.paymentology.AttestationResponse
@ -18,12 +24,15 @@ import com.tangem.datasource.api.paymentology.RegisterWalletRequest
import com.tangem.datasource.api.paymentology.RegisterWalletResponse
import com.tangem.datasource.api.paymentology.RegistrationResponse
import com.tangem.datasource.api.paymentology.tryExtractError
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.SaltPayWorkaround
import com.tangem.domain.common.extensions.successOr
import com.tangem.operations.attestation.AttestWalletKeyResponse
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
import com.tangem.tap.store
import java.math.BigDecimal
/**
@ -200,4 +209,96 @@ class KYCUrlProvider(
.appendQueryParameter(kycProvider.externalIdParameterKey, kycRefId)
.build().toString()
}
}
class SaltPayActivationManagerFactory(
private val blockchain: Blockchain,
private val card: CardDTO,
) {
fun create(): SaltPayActivationManager {
val wallet = card.wallets.first()
val saltPayConfig = store.state.globalState.configManager?.config?.saltPayConfig.guard {
throw NullPointerException("SaltPayConfig is not initialized")
}
val wmFactory = store.state.globalState.tapWalletManager.walletManagerFactory
val paymentologyService = store.state.domainNetworks.paymentologyService
// return createDummyActivationManager(saltPayConfig, wmFactory, paymentologyService)
return createActivationPayManager(
cardId = card.cardId,
cardPublicKey = card.cardPublicKey,
kycProvider = saltPayConfig.kycProvider,
gnosisRegistrator = GnosisRegistrator(
walletManager = makeSaltPayWalletManager(
blockchain = blockchain,
walletPublicKey = wallet.publicKey,
wmFactory = wmFactory,
),
),
paymentologyService = paymentologyService,
)
}
private fun makeSaltPayWalletManager(
blockchain: Blockchain,
walletPublicKey: ByteArray,
wmFactory: WalletManagerFactory,
): EthereumWalletManager {
return wmFactory.makeWalletManager(
blockchain = blockchain,
publicKey = Wallet.PublicKey(walletPublicKey, null, null),
tokens = listOf(SaltPayWorkaround.tokenFrom(blockchain)),
) as EthereumWalletManager
}
private fun createActivationPayManager(
cardId: String,
cardPublicKey: ByteArray,
kycProvider: KYCProvider,
gnosisRegistrator: GnosisRegistrator,
paymentologyService: PaymentologyApiService,
): SaltPayActivationManager {
return SaltPayActivationManager(
cardId = cardId,
cardPublicKey = cardPublicKey,
kycProvider = kycProvider,
paymentologyService = paymentologyService,
gnosisRegistrator = gnosisRegistrator,
)
}
private fun createDummyActivationManager(
saltPayConfig: SaltPayConfig,
wmFactory: WalletManagerFactory,
paymentologyService: PaymentologyApiService,
): SaltPayActivationManager {
val fakeSource = DummySaltPayCardSource()
val walletManager = wmFactory.makeWalletManager(
blockchain = fakeSource.blockchain,
publicKey = fakeSource.blockchainPublicKey,
tokens = listOf(fakeSource.token),
) as EthereumWalletManager
return createActivationPayManager(
cardId = fakeSource.cardId,
cardPublicKey = fakeSource.cardPublicKey,
kycProvider = saltPayConfig.kycProvider,
gnosisRegistrator = GnosisRegistrator(walletManager),
paymentologyService = paymentologyService,
)
}
private data class DummySaltPayCardSource(
val blockchain: Blockchain = Blockchain.SaltPay,
val cardId: String = "FF03000001001057",
private val cardPublicKeyString: String = "039C2D2F9B68003766BFC29766761F55F4084E48B8B012BF439E115A79AB122D48",
private val walletPublicKeyString: String = "0399B2DF5B129FBF69097DAA7C44FB5849E578CCFC22228A409870C74B56C4C3D1",
) {
val cardPublicKey: ByteArray = cardPublicKeyString.hexToBytes()
val walletPublicKey: ByteArray = walletPublicKeyString.hexToBytes()
val blockchainPublicKey = Wallet.PublicKey(walletPublicKey, null, null)
val token: Token = SaltPayWorkaround.tokenFrom(blockchain)
}
}

View file

@ -9,6 +9,7 @@ data class SaltPayConfig(
val sprinklrAppID: String,
val kycProvider: KYCProvider,
val credentials: Credentials,
val blockscoutCredentials: Credentials,
) {
companion object {
fun stub(): SaltPayConfig {
@ -16,6 +17,7 @@ data class SaltPayConfig(
sprinklrAppID = "",
kycProvider = KYCProvider("", "", "", ""),
credentials = Credentials("", ""),
blockscoutCredentials = Credentials("", ""),
)
}
}

View file

@ -2,7 +2,6 @@ package com.tangem.tap.features.onboarding.products.wallet.saltPay.redux
import com.tangem.blockchain.common.Amount
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivationManager
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayConfig
import org.rekotlin.Action
import java.math.BigDecimal
@ -14,7 +13,6 @@ sealed class OnboardingSaltPayAction : Action {
data class SetDependencies(
val registrationManager: SaltPayActivationManager,
val saltPayConfig: SaltPayConfig,
) : OnboardingSaltPayAction()
data class SetInProgress(val isInProgress: Boolean) : OnboardingSaltPayAction()

View file

@ -91,6 +91,17 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App
handleInProgress = false
dispatchOnMain(OnboardingSaltPayAction.SetStep(updateStep))
if (updateStep == SaltPayActivationStep.Claim) {
handleClaimRefreshInProgress = true
val tokenAmountValue = state.saltPayManager.getTokenAmount().successOr {
SaltPayExceptionHandler.handle(it.error)
handleClaimRefreshInProgress = false
return@launch
}
dispatchOnMain(OnboardingSaltPayAction.SetTokenBalance(tokenAmountValue))
handleClaimRefreshInProgress = false
}
}
}
is OnboardingSaltPayAction.RegisterCard -> {
@ -220,35 +231,29 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App
val state = getState()
scope.launch {
when (val amountToClaimResult = state.saltPayManager.getAmountToClaim()) {
is Result.Success -> {
// need to re claim
handleClaimRefreshInProgress = false
dispatchOnMain(OnboardingSaltPayAction.SetAmountToClaim(amountToClaimResult.data))
dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.Claim))
}
is Result.Failure -> {
when (amountToClaimResult.error) {
is SaltPayActivationError.NoFundsToClaim -> {
val tokenAmountValue = state.saltPayManager.getTokenAmount().successOr {
SaltPayExceptionHandler.handle(it.error)
handleClaimRefreshInProgress = false
return@launch
}
val tokenAmountValue = state.saltPayManager.getTokenAmount().successOr {
SaltPayExceptionHandler.handle(it.error)
handleClaimRefreshInProgress = false
return@launch
}
dispatchOnMain(OnboardingSaltPayAction.SetTokenBalance(tokenAmountValue))
handleClaimRefreshInProgress = false
if (tokenAmountValue.isPositive()) {
dispatchOnMain(OnboardingSaltPayAction.SetTokenBalance(tokenAmountValue))
dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.ClaimSuccess))
}
}
else -> {
handleClaimRefreshInProgress = false
SaltPayExceptionHandler.handle(amountToClaimResult.error)
}
}
val amountToClaim = state.saltPayManager.getAmountToClaim().successOr {
if (it.error !is SaltPayActivationError.NoFundsToClaim) {
handleClaimRefreshInProgress = false
SaltPayExceptionHandler.handle(it.error)
return@launch
} else {
null
}
}
handleClaimRefreshInProgress = false
if (tokenAmountValue.isPositive() && amountToClaim == null) {
dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.ClaimSuccess))
} else {
// dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.ClaimSuccess))
}
}
}
is OnboardingSaltPayAction.SetStep -> {

View file

@ -21,7 +21,6 @@ private fun internalReduce(anyAction: Action, onboardingWalletState: OnboardingW
return onboardingWalletState.copy(
onboardingSaltPayState = OnboardingSaltPayState(
saltPayManager = action.registrationManager,
saltPayConfig = action.saltPayConfig,
),
)
}

View file

@ -2,19 +2,10 @@ package com.tangem.tap.features.onboarding.products.wallet.saltPay.redux
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.common.extensions.guard
import com.tangem.domain.common.SaltPayWorkaround
import com.tangem.domain.common.ScanResponse
import com.tangem.datasource.api.paymentology.PaymentologyApiService
import com.tangem.tap.common.toggleWidget.WidgetState
import com.tangem.tap.domain.extensions.makeSaltPayWalletManager
import com.tangem.tap.features.onboarding.products.wallet.saltPay.GnosisRegistrator
import com.tangem.tap.features.onboarding.products.wallet.saltPay.KYCProvider
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivationManager
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayConfig
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.store
import java.math.BigDecimal
/**
@ -23,7 +14,6 @@ import java.math.BigDecimal
data class OnboardingSaltPayState(
@Transient
val saltPayManager: SaltPayActivationManager = SaltPayActivationManager.stub(),
val saltPayConfig: SaltPayConfig = SaltPayConfig.stub(),
val pinCode: String? = null,
val accessCode: String? = null,
val amountToClaim: Amount? = null,
@ -37,57 +27,7 @@ data class OnboardingSaltPayState(
val mainButtonState: WidgetState
get() = if (inProgress) ProgressState.Loading else ProgressState.Done
fun readyToClaim(): Boolean = amountToClaim != null
val pinLength: Int = 4
companion object {
fun initDependency(scanResponse: ScanResponse): Pair<SaltPayActivationManager, SaltPayConfig> {
val globalState = store.state.globalState
val saltPayConfig = globalState.configManager?.config?.saltPayConfig.guard {
throw NullPointerException("SaltPayConfig is not initialized")
}
val gnosisRegistrator = makeGnosisRegistrator(
scanResponse = scanResponse,
wmFactory = globalState.tapWalletManager.walletManagerFactory,
)
val registrationManager = makeSaltPayRegistrationManager(
scanResponse = scanResponse,
gnosisRegistrator = gnosisRegistrator,
paymentologyService = store.state.domainNetworks.paymentologyService,
kycProvider = saltPayConfig.kycProvider,
)
return registrationManager to saltPayConfig
}
fun makeSaltPayRegistrationManager(
scanResponse: ScanResponse,
gnosisRegistrator: GnosisRegistrator,
paymentologyService: PaymentologyApiService,
kycProvider: KYCProvider,
): SaltPayActivationManager {
if (!scanResponse.isSaltPay()) {
throw IllegalArgumentException("Can't initialize the OnboardingSaltPayMiddleware if card is not SalPay")
}
return SaltPayActivationManager(
cardId = scanResponse.card.cardId,
cardPublicKey = scanResponse.card.cardPublicKey,
kycProvider = kycProvider,
paymentologyService = paymentologyService,
gnosisRegistrator = gnosisRegistrator,
)
}
fun makeGnosisRegistrator(
scanResponse: ScanResponse,
wmFactory: WalletManagerFactory,
): GnosisRegistrator {
return GnosisRegistrator(
walletManager = wmFactory.makeSaltPayWalletManager(scanResponse),
)
}
}
}
enum class SaltPayActivationStep {

View file

@ -84,7 +84,7 @@ data class WalletState(
get() = if (isMultiwalletAllowed || walletsStores.isEmpty() || walletsStores.size > 1) null
else walletsStores[0]
private val primaryWalletManager: WalletManager?
val primaryWalletManager: WalletManager?
get() = primaryWalletStore?.walletManager
val primaryWalletData: WalletData?

View file

@ -249,6 +249,8 @@ class WalletMiddleware {
}
is NetworkStateChanged -> {
store.dispatch(WalletAction.Warnings.CheckHashesCount.CheckHashesCountOnline)
if (!action.isOnline) return
val selectedUserWallet = userWalletsListManagerSafe?.selectedUserWalletSync
if (selectedUserWallet != null) {
scope.launch { globalState.tapWalletManager.loadData(selectedUserWallet) }

View file

@ -39,9 +39,9 @@ import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter
import com.tangem.tap.features.wallet.ui.wallet.MultiWalletView
import com.tangem.tap.features.wallet.ui.wallet.SaltPayWalletView
import com.tangem.tap.features.wallet.ui.wallet.SingleWalletView
import com.tangem.tap.features.wallet.ui.wallet.WalletView
import com.tangem.tap.features.wallet.ui.wallet.saltPay.SaltPayWalletView
import com.tangem.tap.store
import com.tangem.tap.userWalletsListManager
import com.tangem.wallet.BuildConfig
@ -150,15 +150,18 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
when {
isSaltPay && (walletView !is SaltPayWalletView) -> {
walletView.onViewDestroy()
walletView = SaltPayWalletView()
walletView.changeWalletView(this, binding)
}
state.isMultiwalletAllowed && state.primaryWalletData?.currencyData?.status != BalanceStatus.EmptyCard &&
walletView !is MultiWalletView -> {
walletView.onViewDestroy()
walletView = MultiWalletView()
walletView.changeWalletView(this, binding)
}
!state.isMultiwalletAllowed && !isSaltPay && walletView !is SingleWalletView -> {
walletView.onViewDestroy()
walletView = SingleWalletView()
walletView.changeWalletView(this, binding)
}
@ -174,7 +177,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
}
setupNoInternetHandling(state)
setupCardImage(state)
setupCardImage(state, isSaltPay)
if (!isSaltPay) showWarningsIfPresent(state.mainWarningsList)
@ -183,6 +186,8 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
if (state.state != ProgressState.Loading &&
state.state != ProgressState.Refreshing
) {
//TODO: blueprint
walletView.pullToRefreshListener?.invoke()
Analytics.send(Portfolio.Refreshed())
store.dispatch(WalletAction.LoadData.Refresh)
}
@ -211,9 +216,9 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
}
}
private fun setupCardImage(state: WalletState) {
private fun setupCardImage(state: WalletState, isSaltPay: Boolean) {
//TODO: SaltPay: remove hardCode
if (store.state.globalState.scanResponse?.isSaltPay() == true) {
if (isSaltPay) {
binding.ivCard.load(R.drawable.img_salt_pay_visa) {
scale(Scale.FIT)
crossfade(enable = true)

View file

@ -1,88 +0,0 @@
package com.tangem.tap.features.wallet.ui.wallet
import com.tangem.domain.common.extensions.debounce
import com.tangem.tap.common.extensions.animateVisibility
import com.tangem.tap.common.extensions.formatAmountAsSpannedString
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.ui.WalletFragment
import com.tangem.tap.mainScope
import com.tangem.tap.store
import com.tangem.wallet.databinding.FragmentWalletBinding
import com.tangem.wallet.databinding.LayoutSaltPayWalletBinding
import org.rekotlin.Action
class SaltPayWalletView : WalletView() {
private lateinit var saltPayBinding: LayoutSaltPayWalletBinding
private val actionDebouncer = debounce<Action>(500, mainScope) { store.dispatch(it) }
override fun changeWalletView(fragment: WalletFragment, binding: FragmentWalletBinding) {
saltPayBinding = binding.lSaltPayWallet
setFragment(fragment, binding)
onViewCreated()
showSaltPayView(binding)
}
private fun showSaltPayView(binding: FragmentWalletBinding) = with(binding) {
rvWarningMessages.hide()
rvPendingTransaction.hide()
rvMultiwallet.hide()
tvTwinCardNumber.hide()
lCardBalance.root.hide()
lSingleWalletBalance.root.hide()
lAddress.root.hide()
rowButtons.hide()
btnAddToken.hide()
pbLoadingUserTokens.hide()
lSaltPayWallet.root.show()
}
override fun onViewCreated() {
}
override fun onNewState(state: WalletState) {
setupBalanceWidget(state)
}
private fun setupBalanceWidget(state: WalletState) = with(saltPayBinding.lSaltPayBalance) {
val tokenData = state.primaryTokenData ?: return@with
val appCurrency = store.state.globalState.appCurrency
val mainProgressState = state.state
if (mainProgressState == ProgressState.Loading) {
veilBalance.veil()
veilBalanceCrypto.veil()
} else {
veilBalanceCrypto.unVeil()
}
tvProcessing.animateVisibility(show = mainProgressState == ProgressState.Error)
veilBalanceCrypto.animateVisibility(show = mainProgressState != ProgressState.Error)
if (tokenData.currencyData.fiatAmount == null) {
actionDebouncer(WalletAction.LoadFiatRate())
} else {
veilBalance.unVeil()
tvBalance.text = tokenData.currencyData.fiatAmount.formatAmountAsSpannedString(
currencySymbol = appCurrency.symbol,
)
}
tvBalanceCrypto.text = tokenData.currencyData.amountFormatted
tvCurrencyName.text = appCurrency.code
tvCurrencyName.setOnClickListener {
store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency)
}
btnBuy.show(tokenData.isAvailableToBuy)
btnBuy.setOnClickListener {
store.dispatch(WalletAction.TradeCryptoAction.Buy(false))
}
}
}

View file

@ -5,8 +5,13 @@ import com.tangem.tap.features.wallet.ui.WalletFragment
import com.tangem.wallet.databinding.FragmentWalletBinding
abstract class WalletView {
//TODO: blueprint
var pullToRefreshListener: (() -> Unit)? = null
protected var fragment: WalletFragment? = null
protected var binding: FragmentWalletBinding? = null
fun setFragment(fragment: WalletFragment, binding: FragmentWalletBinding) {
this.fragment = fragment
this.binding = binding
@ -17,6 +22,11 @@ abstract class WalletView {
binding = null
}
//TODO: blueprint
open fun onViewDestroy() {
pullToRefreshListener = null
}
open fun onDestroyFragment() {}
abstract fun changeWalletView(fragment: WalletFragment, binding: FragmentWalletBinding)

View file

@ -0,0 +1,233 @@
package com.tangem.tap.features.wallet.ui.wallet.saltPay
import android.view.LayoutInflater
import android.view.ViewGroup
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.extensions.Result
import com.tangem.domain.common.extensions.debounce
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.common.ShimmerData
import com.tangem.tap.common.ShimmerRecyclerAdapter
import com.tangem.tap.common.extensions.animateVisibility
import com.tangem.tap.common.extensions.beginDelayedTransition
import com.tangem.tap.common.extensions.formatAmountAsSpannedString
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.recyclerView.SpaceItemDecoration
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.ui.WalletFragment
import com.tangem.tap.features.wallet.ui.wallet.WalletView
import com.tangem.tap.features.wallet.ui.wallet.saltPay.rv.HistoryItemData
import com.tangem.tap.features.wallet.ui.wallet.saltPay.rv.HistoryTransactionData
import com.tangem.tap.features.wallet.ui.wallet.saltPay.rv.TxHistoryAdapter
import com.tangem.tap.mainScope
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.wallet.databinding.FragmentWalletBinding
import com.tangem.wallet.databinding.ItemSaltPayTxHistoryShimmerBinding
import com.tangem.wallet.databinding.LayoutSaltPayBalanceBinding
import com.tangem.wallet.databinding.LayoutSaltPayTxHistoryBinding
import com.tangem.wallet.databinding.LayoutSaltPayWalletBinding
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Action
import java.util.concurrent.atomic.AtomicBoolean
/**
[REDACTED_AUTHOR]
*/
class SaltPayWalletView : WalletView() {
private lateinit var saltPayBinding: LayoutSaltPayWalletBinding
private val actionDebouncer = debounce<Action>(500, mainScope) { store.dispatch(it) }
private val balanceWidget: LayoutSaltPayBalanceBinding
get() = saltPayBinding.lSaltPayBalance
private val txWidget: LayoutSaltPayTxHistoryBinding
get() = saltPayBinding.lSaltPayTxHistory
private var initialized = AtomicBoolean(false)
override fun changeWalletView(fragment: WalletFragment, binding: FragmentWalletBinding) {
saltPayBinding = binding.lSaltPayWallet
setFragment(fragment, binding)
onViewCreated()
showSaltPayView(binding)
}
override fun onViewCreated() {
this.pullToRefreshListener = this::handlePullToRefresh
prepareTxRecyclers()
}
private fun showSaltPayView(binding: FragmentWalletBinding) = with(binding) {
rvWarningMessages.hide()
rvPendingTransaction.hide()
rvMultiwallet.hide()
tvTwinCardNumber.hide()
lCardBalance.root.hide()
lSingleWalletBalance.root.hide()
lAddress.root.hide()
rowButtons.hide()
btnAddToken.hide()
pbLoadingUserTokens.hide()
lSaltPayWallet.root.show()
}
private fun prepareTxRecyclers() {
val vhViewFactory: (ViewGroup) -> ViewGroup = {
val inflater = LayoutInflater.from(it.context)
ItemSaltPayTxHistoryShimmerBinding.inflate(inflater, it, false).root
}
val adapter = ShimmerRecyclerAdapter(vhViewFactory)
txWidget.rvTxHistoryShimmer.adapter = adapter
txWidget.rvTxHistoryShimmer.addItemDecoration(SpaceItemDecoration.vertical(10F))
txWidget.rvTxHistory.adapter = TxHistoryAdapter()
txWidget.rvTxHistory.addItemDecoration(SpaceItemDecoration.vertical(10F))
handleTxInit()
}
private fun handlePullToRefresh() {
requestTxHistory(store.state.walletState)
}
override fun onNewState(state: WalletState) {
if (!initialized.getAndSet(true)) requestTxHistory(state)
setupBalanceWidget(state)
}
private fun setupBalanceWidget(state: WalletState) = with(balanceWidget) {
val tokenData = state.primaryTokenData ?: return@with
val appCurrency = store.state.globalState.appCurrency
val mainProgressState = state.state
if (mainProgressState == ProgressState.Loading) {
veilBalance.veil()
veilBalanceCrypto.veil()
} else {
veilBalanceCrypto.unVeil()
}
tvUnreachable.animateVisibility(show = mainProgressState == ProgressState.Error)
veilBalanceCrypto.animateVisibility(show = mainProgressState != ProgressState.Error)
if (tokenData.currencyData.fiatAmount == null) {
actionDebouncer(WalletAction.LoadFiatRate())
} else {
veilBalance.unVeil()
tvBalance.text = tokenData.currencyData.fiatAmount.formatAmountAsSpannedString(
currencySymbol = appCurrency.symbol,
)
}
tvBalanceCrypto.text = tokenData.currencyData.amountFormatted
tvCurrencyName.text = appCurrency.code
tvCurrencyName.setOnClickListener {
store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency)
}
btnBuy.show(tokenData.isAvailableToBuy)
btnBuy.setOnClickListener {
store.dispatch(WalletAction.TradeCryptoAction.Buy(false))
}
}
private fun requestTxHistory(state: WalletState) {
scope.launch {
val walletManager = state.primaryWalletManager as? EthereumWalletManager ?: return@launch
val wallet = walletManager.wallet
val token = wallet.getFirstToken() ?: return@launch
val walletAddress = wallet.address
// val walletAddress = "0xDA94Aae02a4Db0e09E1Cf240E3a0973ba89052cf"
when (val result = walletManager.getTransactionHistory(walletAddress, wallet.blockchain, setOf(token))) {
is Result.Success -> {
val dateAssociatedHistory = mutableMapOf<String, MutableList<HistoryTransactionData>>()
result.data
.filter { it.contractAddress == token.contractAddress }
.sortedByDescending { it.date?.timeInMillis ?: 0 }
.forEach {
val txData = HistoryTransactionData(it, walletAddress)
val list = dateAssociatedHistory[txData.date] ?: mutableListOf()
list.add(txData)
dateAssociatedHistory[txData.date] = list
}
val dataList = mutableListOf<HistoryItemData>()
dateAssociatedHistory.forEach { entry ->
dataList.add(HistoryItemData.Date(entry.key))
entry.value.forEach { dataList.add(HistoryItemData.TransactionData(it)) }
}
// .toMutableList().apply { clear() }
delay(300)
withMainContext {
if (dataList.isEmpty()) {
handleTxEmpty()
} else {
handleTxSuccess(dataList)
}
}
}
is Result.Failure -> {
delay(300)
withMainContext { handleTxError() }
}
}
}
}
private fun handleTxInit() = with(txWidget) {
(rvTxHistoryShimmer.adapter as? ShimmerRecyclerAdapter)?.submitList(
listOf(
ShimmerData(),
ShimmerData(),
ShimmerData(),
),
)
groupEmpty.hide()
groupError.hide()
groupSuccess.hide()
root.beginDelayedTransition()
txWidget.groupShimmer.show()
}
private fun handleTxSuccess(dataList: List<HistoryItemData>) = with(txWidget) {
groupShimmer.hide()
groupEmpty.hide()
groupError.hide()
root.beginDelayedTransition()
updateTxHistoryWidget(dataList)
groupSuccess.show()
}
private fun handleTxEmpty() = with(txWidget) {
groupShimmer.hide()
groupError.hide()
groupSuccess.hide()
root.beginDelayedTransition()
updateTxHistoryWidget(emptyList())
groupEmpty.show()
}
private fun handleTxError() = with(txWidget) {
groupShimmer.hide()
groupEmpty.hide()
groupSuccess.hide()
root.beginDelayedTransition()
updateTxHistoryWidget(emptyList())
groupError.show()
}
private fun updateTxHistoryWidget(dataList: List<HistoryItemData>) = with(txWidget) {
(rvTxHistory.adapter as TxHistoryAdapter).submitList(dataList)
}
}

View file

@ -0,0 +1,74 @@
package com.tangem.tap.features.wallet.ui.wallet.saltPay.rv
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.TransactionStatus
import com.tangem.common.extensions.guard
import com.tangem.tap.common.extensions.toFormattedCurrencyString
import com.tangem.tap.features.wallet.models.PendingTransactionType
import java.text.SimpleDateFormat
import java.util.*
/**
[REDACTED_AUTHOR]
*/
sealed class HistoryItemData(
val viewType: Int,
val itemId: Long,
) {
data class Date(val date: String) : HistoryItemData(0, date.hashCode().toLong())
data class TransactionData(val data: HistoryTransactionData) : HistoryItemData(1, data.hash.hashCode().toLong())
}
data class HistoryTransactionData(
private val transactionData: TransactionData,
private val walletAddress: String,
) {
val isInProgress: Boolean = transactionData.status == TransactionStatus.Unconfirmed
val transactionType: PendingTransactionType = when {
transactionData.sourceAddress.lowercase() == walletAddress.lowercase() -> PendingTransactionType.Outgoing
transactionData.destinationAddress.lowercase() == walletAddress.lowercase() -> PendingTransactionType.Incoming
else -> PendingTransactionType.Unknown
}
val hash: String = transactionData.hash?.let {
"${it.substring(0..5)}...${it.substring(it.length - 4)}"
} ?: ""
val time: String = transactionData.date?.let {
SimpleDateFormat("hh:MM").format(it.time)
} ?: "00:00"
val date: String = calculateDate()
val txSign: String = when (transactionType) {
PendingTransactionType.Incoming -> "+"
PendingTransactionType.Outgoing -> "-"
PendingTransactionType.Unknown -> ""
}
val amountValue: String = transactionData.amount.value
?.toFormattedCurrencyString(8, transactionData.amount.currencySymbol) ?: "0"
private fun calculateDate(): String {
val calendarTx = transactionData.date.guard { return "Unknown" }
val calendarNow = Calendar.getInstance()
val oldDateFormatter = SimpleDateFormat("dd.MM.yyyy")
val date = if (calendarTx.get(Calendar.YEAR) == calendarNow.get(Calendar.YEAR)) {
val dayOfYearTx = calendarTx.get(Calendar.DAY_OF_YEAR)
val dayOfYearNow = calendarNow.get(Calendar.DAY_OF_YEAR)
when {
dayOfYearNow == dayOfYearTx -> "Today"
dayOfYearNow - dayOfYearTx == 1 -> "Tomorrow"
else -> oldDateFormatter.format(calendarTx.time)
}
} else {
oldDateFormatter.format(calendarTx.time)
}
return date
}
}

View file

@ -0,0 +1,112 @@
package com.tangem.tap.features.wallet.ui.wallet.saltPay.rv
import android.graphics.PorterDuff
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.tangem.tap.common.extensions.getColor
import com.tangem.tap.common.extensions.setDrawable
import com.tangem.tap.common.extensions.show
import com.tangem.tap.features.wallet.models.PendingTransactionType
import com.tangem.wallet.R
import com.tangem.wallet.databinding.ItemSaltPayTxHistoryBinding
import com.tangem.wallet.databinding.ItemSaltPayTxHistoryDateBinding
/**
[REDACTED_AUTHOR]
*/
internal class TxHistoryAdapter : ListAdapter<HistoryItemData, BaseTxHistoryVH>(DiffUtilCallback) {
override fun getItemId(position: Int): Long {
return if (currentList.isEmpty()) 0L
else currentList[position].itemId
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseTxHistoryVH {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
0 -> {
val layout = ItemSaltPayTxHistoryDateBinding.inflate(inflater, parent, false)
DateItemVH(layout)
}
1 -> {
val layout = ItemSaltPayTxHistoryBinding.inflate(inflater, parent, false)
TransactionItemVH(layout)
}
else -> throw UnsupportedOperationException()
}
}
override fun getItemViewType(position: Int): Int = currentList[position].viewType
override fun onBindViewHolder(holder: BaseTxHistoryVH, position: Int) {
holder.bind(currentList[position])
}
private object DiffUtilCallback : DiffUtil.ItemCallback<HistoryItemData>() {
override fun areContentsTheSame(oldItem: HistoryItemData, newItem: HistoryItemData) = oldItem == newItem
override fun areItemsTheSame(oldItem: HistoryItemData, newItem: HistoryItemData) = oldItem == newItem
}
}
internal abstract class BaseTxHistoryVH(layout: View) : RecyclerView.ViewHolder(layout) {
abstract fun bind(data: HistoryItemData)
}
private class DateItemVH(
private val binding: ItemSaltPayTxHistoryDateBinding,
) : BaseTxHistoryVH(binding.root) {
override fun bind(data: HistoryItemData) = with(binding) {
val vhData = (data as? HistoryItemData.Date)?.date ?: return
tvTitleDate.text = vhData
}
}
private class TransactionItemVH(
private val binding: ItemSaltPayTxHistoryBinding,
) : BaseTxHistoryVH(binding.root) {
override fun bind(data: HistoryItemData) {
val vhData = (data as? HistoryItemData.TransactionData)?.data ?: return
setupImage(vhData)
setupTitle(vhData)
setupSubtitle(vhData)
setupBalance(vhData)
}
private fun setupImage(data: HistoryTransactionData) = with(binding) {
val drawable = when (data.transactionType) {
PendingTransactionType.Incoming -> R.drawable.ic_tx_incoming
PendingTransactionType.Outgoing -> R.drawable.ic_tx_outgoing
PendingTransactionType.Unknown -> null
}
drawable?.let { imvTx.setDrawable(it) }
if (data.isInProgress) {
imvTx.setColorFilter(imvTx.getColor(R.color.icon_attention), PorterDuff.Mode.SRC_ATOP)
} else {
imvTx.clearColorFilter()
}
}
private fun setupTitle(data: HistoryTransactionData) = with(binding) {
tvTxHash.text = data.hash
}
private fun setupSubtitle(data: HistoryTransactionData) = with(binding) {
tvTxStatus.show(data.isInProgress)
tvTxTime.show(!data.isInProgress)
tvTxTime.text = data.time
}
private fun setupBalance(data: HistoryTransactionData) = with(binding) {
tvTxAmountSign.text = data.txSign
tvTxAmountValue.text = data.amountValue
}
}

View file

@ -53,7 +53,6 @@ class UtorgExchangeService(
override fun isSellAllowed(): Boolean = false
override fun availableForBuy(currency: Currency): Boolean {
return true
if (!isBuyAllowed()) return false
val foundUtorgCurrency = utorgCurrencies.firstOrNull { utorgCurrency ->

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="18dp"
android:height="18dp"
android:viewportWidth="18"
android:viewportHeight="18">
<path
android:pathData="M4.833,13.167L7.5,7.5L13.167,4.833L10.5,10.5L4.833,13.167ZM9,8.25C8.801,8.25 8.61,8.329 8.47,8.47C8.329,8.61 8.25,8.801 8.25,9C8.25,9.199 8.329,9.39 8.47,9.53C8.61,9.671 8.801,9.75 9,9.75C9.199,9.75 9.39,9.671 9.53,9.53C9.671,9.39 9.75,9.199 9.75,9C9.75,8.801 9.671,8.61 9.53,8.47C9.39,8.329 9.199,8.25 9,8.25ZM9,0.667C10.094,0.667 11.178,0.882 12.189,1.301C13.2,1.72 14.119,2.334 14.892,3.107C15.666,3.881 16.28,4.8 16.699,5.811C17.118,6.822 17.333,7.906 17.333,9C17.333,11.21 16.455,13.33 14.892,14.892C13.33,16.455 11.21,17.333 9,17.333C7.906,17.333 6.822,17.118 5.811,16.699C4.8,16.28 3.881,15.666 3.107,14.892C1.545,13.33 0.667,11.21 0.667,9C0.667,6.79 1.545,4.67 3.107,3.107C4.67,1.545 6.79,0.667 9,0.667ZM9,2.333C7.232,2.333 5.536,3.036 4.286,4.286C3.036,5.536 2.333,7.232 2.333,9C2.333,10.768 3.036,12.464 4.286,13.714C5.536,14.964 7.232,15.667 9,15.667C10.768,15.667 12.464,14.964 13.714,13.714C14.964,12.464 15.667,10.768 15.667,9C15.667,7.232 14.964,5.536 13.714,4.286C12.464,3.036 10.768,2.333 9,2.333Z"
android:fillColor="#B0B0B0"/>
</vector>

View file

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<path
android:pathData="M20,20m-20,0a20,20 0,1 1,40 0a20,20 0,1 1,-40 0"
android:strokeAlpha="0.12"
android:fillColor="#B0B0B0"
android:fillAlpha="0.12"/>
<path
android:pathData="M19.08,12.08L21.08,12.08V24.08L26.58,18.58L28,20L20.08,27.92L12.16,20L13.58,18.58L19.08,24.08L19.08,12.08Z"
android:fillColor="#C9C9C9"/>
</vector>

View file

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<path
android:pathData="M20,20m-20,0a20,20 0,1 1,40 0a20,20 0,1 1,-40 0"
android:strokeAlpha="0.12"
android:fillColor="#B0B0B0"
android:fillAlpha="0.12"/>
<path
android:pathData="M21.08,27.92H19.08L19.08,15.92L13.58,21.42L12.16,20L20.08,12.08L28,20L26.58,21.42L21.08,15.92V27.92Z"
android:fillColor="#C9C9C9"/>
</vector>

View file

@ -0,0 +1,89 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="56dp"
android:animateLayoutChanges="true">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_tx"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@drawable/ic_tx_incoming" />
<TextView
android:id="@+id/tv_tx_hash"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="68dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="24dp"
android:letterSpacing="0.01"
android:singleLine="true"
android:textColor="@color/text_primary_1"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@+id/tv_tx_amount_sign"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="33BdfSkjandfjkajksjjkgkajsbga2B" />
<TextView
android:id="@+id/tv_tx_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:letterSpacing="0.03"
android:textColor="@color/text_tertiary"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@+id/tv_tx_hash"
tools:text="12:43" />
<TextView
android:id="@+id/tv_tx_status"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:layout_marginBottom="8dp"
android:ellipsize="end"
android:letterSpacing="0.03"
android:singleLine="true"
android:text="@string/transaction_history_tx_in_progress"
android:textColor="@color/text_attention"
android:textSize="12sp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/tv_tx_amount_sign"
app:layout_constraintStart_toStartOf="@+id/tv_tx_hash" />
<TextView
android:id="@+id/tv_tx_amount_sign"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:letterSpacing="0.02"
android:textColor="@color/text_primary_1"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="@+id/tv_tx_amount_value"
app:layout_constraintEnd_toStartOf="@+id/tv_tx_amount_value"
app:layout_constraintTop_toTopOf="@+id/tv_tx_amount_value"
tools:text="+" />
<TextView
android:id="@+id/tv_tx_amount_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:letterSpacing="0.02"
android:textColor="@color/text_primary_1"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="443" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_title_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:letterSpacing="0.02"
android:textColor="@color/text_tertiary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Today" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<com.skydoves.androidveil.VeilLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:veilLayout_baseColor="@color/lightGray0"
app:veilLayout_highlightColor="@color/lightGray1"
app:veilLayout_radius="4dp"
app:veilLayout_shimmerEnable="true"
app:veilLayout_veiled="true"
tools:veilLayout_veiled="false">
<!--android:background="@drawable/rectangle_twin_background"-->
<!--android:backgroundTint="@color/text_attention"-->
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="56dp">
<View
android:id="@+id/imv_tx"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/tv_tx_hash"
android:layout_width="0dp"
android:layout_height="20dp"
android:layout_marginStart="68dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="24dp"
app:layout_constraintEnd_toStartOf="@+id/tv_tx_amount"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/tv_tx_time"
android:layout_width="50dp"
android:layout_height="16dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@+id/tv_tx_hash" />
<View
android:id="@+id/tv_tx_amount"
android:layout_width="80dp"
android:layout_height="20dp"
android:layout_marginEnd="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.skydoves.androidveil.VeilLayout>

View file

@ -21,12 +21,10 @@
android:id="@+id/tv_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="@string/onboarding_balance_title"
android:textColor="@color/text_tertiary"
android:textSize="14sp"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@id/tv_currency_name"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
@ -34,8 +32,8 @@
android:id="@+id/veil_balance"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
app:layout_constraintBottom_toTopOf="@id/tv_processing"
android:layout_marginTop="6dp"
app:layout_constraintBottom_toTopOf="@id/tv_unreachable"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_title"
@ -65,7 +63,7 @@
android:id="@+id/veil_balance_crypto"
android:layout_width="wrap_content"
android:layout_height="18dp"
android:layout_marginTop="4dp"
android:layout_marginTop="6dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/veil_balance"
app:veilLayout_baseColor="@color/lightGray0"
@ -81,7 +79,6 @@
android:id="@+id/tv_balance_crypto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="2dp"
android:maxLines="1"
android:minWidth="152dp"
android:textColor="@color/text_tertiary"
@ -92,14 +89,14 @@
</com.skydoves.androidveil.VeilLayout>
<TextView
android:id="@+id/tv_processing"
android:id="@+id/tv_unreachable"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:layout_height="18dp"
android:layout_marginTop="6dp"
android:text="@string/wallet_balance_blockchain_unreachable"
android:textColor="@color/warning"
android:textSize="12sp"
android:visibility="gone"
android:visibility="visible"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/veil_balance" />
@ -108,7 +105,7 @@
style="@style/BaseSaltPayButton"
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_marginTop="12dp"
android:layout_marginTop="76dp"
android:text="@string/wallet_button_buy"
android:visibility="gone"
app:icon="@drawable/ic_add"
@ -116,7 +113,7 @@
app:iconTint="@color/selector_saltpay_btn_text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/veil_balance_crypto"
app:layout_constraintTop_toBottomOf="@+id/tv_title"
tools:visibility="visible" />
<TextView

View file

@ -0,0 +1,136 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/card_balance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="12dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:clipToPadding="false"
android:minHeight="272dp"
android:paddingBottom="8dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_tx_history_shimmer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="42dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="@layout/item_salt_pay_tx_history_shimmer" />
<TextView
android:id="@+id/tv_title_tx"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="12dp"
android:letterSpacing="0.01"
android:text="@string/transaction_history_title"
android:textColor="@color/text_tertiary"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_title_explore"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginEnd="16dp"
android:drawableLeft="@drawable/ic_tx_explore"
android:drawablePadding="4dp"
android:letterSpacing="0.01"
android:text="Explore"
android:textColor="@color/text_tertiary"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_tx_list_empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@+id/tv_tx_list_empty"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
app:srcCompat="@drawable/ic_accept_coin" />
<TextView
android:id="@+id/tv_tx_list_empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="20dp"
android:layout_marginEnd="24dp"
android:text="@string/transaction_history_empty_transactions"
android:textColor="@color/text_tertiary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imv_tx_list_empty" />
<TextView
android:id="@+id/tv_tx_list_error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:text="@string/transaction_history_error_failed_to_load"
android:textColor="@color/text_tertiary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_tx_history"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="42dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="@layout/item_salt_pay_tx_history" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_shimmer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="rv_tx_history_shimmer" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="imv_tx_list_empty, tv_tx_list_empty" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="tv_tx_list_error" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_success"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="rv_tx_history" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View file

@ -19,4 +19,14 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<include
android:id="@+id/l_salt_pay_tx_history"
layout="@layout/layout_salt_pay_tx_history"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/l_salt_pay_balance" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -59,8 +59,8 @@ object Versions {
// endregion Other libraries
// region Tangem
const val tangemBlockchainSdk = "develop-152-hotfix-4.0.1-156"
// const val tangemBlockchainSdk = "0.0.1"
// const val tangemBlockchainSdk = "develop-152-hotfix-4.0.1-156"
const val tangemBlockchainSdk = "0.0.1"
const val tangemCardSdk = "develop-179-hotfix-4.0.1-189"
// endregion Tangem

View file

@ -449,6 +449,10 @@
<string name="app_settings_enable_biometrics_description">Go to settings to enable biometric authentication in the Tangem App</string>
<string name="wallet_balance_missing_derivation">Scan the card</string>
<string name="disclaimer_error_loading">Check your internet connection or switch to a different network</string>
<string name="transaction_history_tx_in_progress">In progress…</string>
<string name="transaction_history_error_failed_to_load">Failed to load transactions</string>
<string name="transaction_history_empty_transactions">You don\'t have any transactions yet</string>
<string name="transaction_history_title">Transactions</string>
<!-- Special string -->
<string name="common_custom_string">%s</string>

View file

@ -449,6 +449,10 @@
<string name="app_settings_enable_biometrics_description">Go to settings to enable biometric authentication in the Tangem App</string>
<string name="wallet_balance_missing_derivation">Scan the card</string>
<string name="disclaimer_error_loading">Check your internet connection or switch to a different network</string>
<string name="transaction_history_tx_in_progress">In progress…</string>
<string name="transaction_history_error_failed_to_load">Failed to load transactions</string>
<string name="transaction_history_empty_transactions">You don\'t have any transactions yet</string>
<string name="transaction_history_title">Transactions</string>
<!-- Special string -->
<string name="common_custom_string">%s</string>

View file

@ -449,6 +449,10 @@
<string name="app_settings_enable_biometrics_description">Go to settings to enable biometric authentication in the Tangem App</string>
<string name="wallet_balance_missing_derivation">Scan the card</string>
<string name="disclaimer_error_loading">Check your internet connection or switch to a different network</string>
<string name="transaction_history_tx_in_progress">In progress…</string>
<string name="transaction_history_error_failed_to_load">Failed to load transactions</string>
<string name="transaction_history_empty_transactions">You don\'t have any transactions yet</string>
<string name="transaction_history_title">Transactions</string>
<!-- Special string -->
<string name="common_custom_string">%s</string>

View file

@ -449,6 +449,10 @@
<string name="app_settings_enable_biometrics_description">Перейдите в настройки, чтобы включить биометрическую аутентификацию в приложении Tangem App</string>
<string name="wallet_balance_missing_derivation">Отсканируйте карту</string>
<string name="disclaimer_error_loading">Проверьте подключение с интернетом или переключитесь на другую сеть</string>
<string name="transaction_history_tx_in_progress">В процессе…</string>
<string name="transaction_history_error_failed_to_load">Не удалось загрузить транзакции</string>
<string name="transaction_history_empty_transactions">У вас еще нет транзакций</string>
<string name="transaction_history_title">Транзакции</string>
<!-- Special string -->
<string name="common_custom_string">%s</string>

View file

@ -450,6 +450,10 @@
<string name="app_settings_enable_biometrics_description">Go to settings to enable biometric authentication in the Tangem App</string>
<string name="wallet_balance_missing_derivation">Scan the card</string>
<string name="disclaimer_error_loading">Check your internet connection or switch to a different network</string>
<string name="transaction_history_tx_in_progress">In progress…</string>
<string name="transaction_history_error_failed_to_load">Failed to load transactions</string>
<string name="transaction_history_empty_transactions">You don\'t have any transactions yet</string>
<string name="transaction_history_title">Transactions</string>
<!-- Special string -->
<string name="common_custom_string">%s</string>