Updated on 2026-08-14

This commit is contained in:
Tangem 2021-03-17 15:31:03 +03:00
parent 2cfb552d19
commit 8dacd12b74
153 changed files with 4393 additions and 1124 deletions

View file

@ -68,10 +68,12 @@ dependencies {
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'com.google.android.material:material:1.2.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
implementation 'com.google.android.material:material:1.3.0'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
implementation "androidx.core:core-ktx:1.3.2"
implementation 'com.tangem:blockchain:1.141.0'
// implementation 'com.tangem:blockchain:1.141.0'
implementation 'blockchain-sdk-kotlin:blockchain:tangem-20210302.084810-3'
implementation 'com.tangem:core:1.101.0'
implementation 'com.tangem:sdk:1.101.0'
@ -79,10 +81,10 @@ dependencies {
implementation "androidx.browser:browser:1.3.0"
//lifecycle
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"
implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.0"
implementation "androidx.lifecycle:lifecycle-common-java8:2.3.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.0"
implementation "org.rekotlin:rekotlin:1.0.4"

View file

@ -13,6 +13,7 @@ import com.tangem.tap.domain.configurable.config.FeaturesLocalLoader
import com.tangem.tap.domain.configurable.config.FeaturesRemoteLoader
import com.tangem.tap.domain.configurable.warningMessage.RemoteWarningLoader
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.tokens.CurrenciesRepository
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.network.createMoshi
import com.tangem.tap.persistence.PreferencesStorage
@ -26,6 +27,7 @@ val store = Store(
state = AppState()
)
lateinit var preferencesStorage: PreferencesStorage
lateinit var currenciesRepository: CurrenciesRepository
class TapApplication : Application() {
override fun onCreate() {
@ -45,6 +47,7 @@ class TapApplication : Application() {
NetworkConnectivity.createInstance(store, this)
preferencesStorage = PreferencesStorage(this)
PicassoHelper.initPicassoWithCaching(this)
currenciesRepository = CurrenciesRepository(this)
loadConfigs()
}

View file

@ -0,0 +1,6 @@
package com.tangem.tap.common.extensions
import android.content.res.AssetManager
fun AssetManager.readJsonFileToString(fileName: String): String =
this.open("$fileName.json").bufferedReader().readText()

View file

@ -0,0 +1,23 @@
package com.tangem.tap.common.extensions
import androidx.annotation.DrawableRes
import com.tangem.blockchain.common.Blockchain
import com.tangem.wallet.R
@DrawableRes
fun Blockchain.getIconRes(): Int {
return when (this) {
Blockchain.Unknown, Blockchain.Ducatus, Blockchain.BitcoinTestnet, Blockchain.EthereumTestnet,
Blockchain.BinanceTestnet -> 1
Blockchain.Bitcoin -> R.drawable.ic_btc
Blockchain.BitcoinCash -> R.drawable.ic_btc_cash
Blockchain.Litecoin -> R.drawable.ic_ltc
Blockchain.Ethereum -> R.drawable.ic_eth
Blockchain.RSK -> R.drawable.ic_rsk
Blockchain.Cardano, Blockchain.CardanoShelley -> R.drawable.ic_cardano
Blockchain.Binance -> R.drawable.ic_binance
Blockchain.Tezos -> R.drawable.ic_tezos
Blockchain.XRP -> R.drawable.ic_xrp
Blockchain.Stellar -> R.drawable.ic_stellar
}
}

View file

@ -12,6 +12,8 @@ import com.tangem.tap.features.details.ui.twins.TwinWalletWarningFragment
import com.tangem.tap.features.disclaimer.ui.DisclaimerFragment
import com.tangem.tap.features.home.HomeFragment
import com.tangem.tap.features.send.ui.SendFragment
import com.tangem.tap.features.tokens.ui.AddTokensFragment
import com.tangem.tap.features.wallet.ui.WalletDetailsFragment
import com.tangem.tap.features.wallet.ui.WalletFragment
import com.tangem.tap.features.wallet.ui.dialogs.TwinsOnboardingFragment
import com.tangem.wallet.R
@ -46,5 +48,7 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
AppScreen.CreateTwinWalletWarning -> TwinWalletWarningFragment()
AppScreen.CreateTwinWallet -> CreateTwinWalletFragment()
AppScreen.TwinsOnboarding -> TwinsOnboardingFragment()
AppScreen.AddTokens -> AddTokensFragment()
AppScreen.WalletDetails -> WalletDetailsFragment()
}
}

View file

@ -1,11 +1,5 @@
package com.tangem.tap.common.extensions
import android.graphics.Bitmap
import android.graphics.Color
import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import com.google.zxing.qrcode.QRCodeWriter
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import com.tangem.common.extensions.isZero
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.network.coinmarketcap.FiatCurrency
@ -15,27 +9,6 @@ import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.*
fun String.toQrCode(): Bitmap {
val hintMap = Hashtable<EncodeHintType, Any>()
hintMap[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.M // H = 30% damage
hintMap[EncodeHintType.MARGIN] = 2
val qrCodeWriter = QRCodeWriter()
val size = 256
val bitMatrix = qrCodeWriter.encode(this, BarcodeFormat.QR_CODE, size, size, hintMap)
val width = bitMatrix.width
val bmp = Bitmap.createBitmap(width, width, Bitmap.Config.RGB_565)
for (x in 0 until width) {
for (y in 0 until width) {
bmp.setPixel(y, x, if (bitMatrix.get(x, y)) Color.BLACK else Color.WHITE)
}
}
return bmp
}
fun BigDecimal.toFormattedString(
decimals: Int, roundingMode: RoundingMode = RoundingMode.DOWN, locale: Locale = Locale.US
): String {

View file

@ -0,0 +1,62 @@
package com.tangem.tap.common.extensions
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
import android.text.Spannable
import android.text.style.ForegroundColorSpan
import androidx.core.content.ContextCompat
import androidx.core.text.toSpannable
import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import com.google.zxing.qrcode.QRCodeWriter
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import java.util.*
fun String?.ellipsizeBeforeSpace(allowedSize: Int): String {
if (this.isNullOrBlank()) return ""
val size = this.length
val sizeDifference = size - allowedSize
val endIndex = this.indexOf(" ")
val startIndex = endIndex - sizeDifference
val newString = this.removeRange(startIndex, endIndex)
return newString.substring(0 until startIndex) + "..." +
newString.substring(startIndex until newString.length)
}
fun String.colorSegment(
context: Context,
color: Int,
startIndex: Int = 0,
endIndex: Int = this.length
): Spannable {
return this.toSpannable()
.also { spannable ->
spannable.setSpan(
ForegroundColorSpan(ContextCompat.getColor(context, color)),
startIndex,
endIndex,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
fun String.toQrCode(): Bitmap {
val hintMap = Hashtable<EncodeHintType, Any>()
hintMap[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.M // H = 30% damage
hintMap[EncodeHintType.MARGIN] = 2
val qrCodeWriter = QRCodeWriter()
val size = 256
val bitMatrix = qrCodeWriter.encode(this, BarcodeFormat.QR_CODE, size, size, hintMap)
val width = bitMatrix.width
val bmp = Bitmap.createBitmap(width, width, Bitmap.Config.RGB_565)
for (x in 0 until width) {
for (y in 0 until width) {
bmp.setPixel(y, x, if (bitMatrix.get(x, y)) Color.BLACK else Color.WHITE)
}
}
return bmp
}

View file

@ -56,4 +56,13 @@ fun TextInputLayout.enableError(enable: Boolean, errorMessage: String? = null) {
error = null
isErrorEnabled = false
}
}
fun TextView.isEllipsized(): Boolean {
val layout = this.layout
if (layout != null) {
val lines: Int = layout.lineCount
return (lines > 0) && (layout.getEllipsisCount(lines - 1) > 0)
}
return false
}

View file

@ -0,0 +1,11 @@
package com.tangem.tap.common.extensions
import androidx.annotation.ColorInt
import androidx.core.graphics.toColorInt
import com.tangem.blockchain.common.Token
@ColorInt
fun Token.getColor(): Int {
return ("#" + this.contractAddress.subSequence(2..7).toString())
.toColorInt()
}

View file

@ -8,8 +8,6 @@ import android.content.Intent
import android.content.res.Resources
import android.graphics.drawable.Drawable
import android.os.Build
import android.text.Spannable
import android.text.style.ForegroundColorSpan
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
@ -19,7 +17,6 @@ import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.core.text.toSpannable
import androidx.fragment.app.Fragment
import com.google.android.material.card.MaterialCardView
@ -106,24 +103,6 @@ fun Activity.setSystemBarTextColor(setTextDark: Boolean) {
}
}
fun String.colorSegment(
context: Context,
color: Int,
startIndex: Int = 0,
endIndex: Int = this.length
): Spannable {
return this.toSpannable()
.also { spannable ->
spannable.setSpan(
ForegroundColorSpan(ContextCompat.getColor(context, color)),
startIndex,
endIndex,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
fun View.hideKeyboard() {
val inputMethodManager = context.getSystemService(android.content.Context.INPUT_METHOD_SERVICE) as? InputMethodManager
inputMethodManager?.hideSoftInputFromWindow(this.windowToken, 0)

View file

@ -6,6 +6,7 @@ import com.tangem.tap.features.details.redux.DetailsReducer
import com.tangem.tap.features.disclaimer.redux.DisclaimerReducer
import com.tangem.tap.features.home.redux.HomeReducer
import com.tangem.tap.features.send.redux.reducers.SendScreenReducer
import com.tangem.tap.features.tokens.redux.TokensReducer
import com.tangem.tap.features.wallet.redux.WalletReducer
import org.rekotlin.Action
@ -20,7 +21,8 @@ fun appReducer(action: Action, state: AppState?): AppState {
walletState = WalletReducer.reduce(action, state),
sendState = SendScreenReducer.reduce(action, state.sendState),
detailsState = DetailsReducer.reduce(action, state),
disclaimerState = DisclaimerReducer.reduce(action, state)
disclaimerState = DisclaimerReducer.reduce(action, state),
tokensState = TokensReducer.reduce(action, state),
)
}

View file

@ -12,8 +12,10 @@ import com.tangem.tap.features.home.redux.HomeMiddleware
import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.features.send.redux.middlewares.sendMiddleware
import com.tangem.tap.features.send.redux.states.SendState
import com.tangem.tap.features.wallet.redux.WalletMiddleware
import com.tangem.tap.features.tokens.redux.TokensMiddleware
import com.tangem.tap.features.tokens.redux.TokensState
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.middlewares.WalletMiddleware
import org.rekotlin.Middleware
import org.rekotlin.StateType
@ -24,7 +26,8 @@ data class AppState(
val walletState: WalletState = WalletState(),
val sendState: SendState = SendState(),
val detailsState: DetailsState = DetailsState(),
val disclaimerState: DisclaimerState = DisclaimerState()
val disclaimerState: DisclaimerState = DisclaimerState(),
val tokensState: TokensState = TokensState(),
) : StateType {
companion object {
@ -35,7 +38,8 @@ data class AppState(
WalletMiddleware().walletMiddleware,
sendMiddleware,
DetailsMiddleware().detailsMiddleware,
DisclaimerMiddleware().disclaimerMiddleware
DisclaimerMiddleware().disclaimerMiddleware,
TokensMiddleware().tokensMiddleware,
)
}
}

View file

@ -6,14 +6,10 @@ import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.features.details.redux.SecurityOption
import org.rekotlin.Action
import java.math.BigDecimal
sealed class GlobalAction : Action {
data class SaveScanNoteResponse(val scanNoteResponse: ScanNoteResponse) : GlobalAction()
data class SetFiatRate(
val fiatRates: Pair<CryptoCurrencyName, BigDecimal>
) : GlobalAction()
data class ChangeAppCurrency(val appCurrency: FiatCurrencyName) : GlobalAction()
object RestoreAppCurrency : GlobalAction() {
data class Success(val appCurrency: FiatCurrencyName) : GlobalAction()

View file

@ -23,7 +23,7 @@ val globalMiddleware: Middleware<AppState> = { dispatch, appState ->
if (it.hideWarning(action.warning)) {
if (WarningMessagesManager.isAlreadySignedHashesWarning(action.warning)) {
//TODO: No appropriate warningMessage identification. Make it better later
store.dispatch(WalletAction.SaveCardId)
store.dispatch(WalletAction.CheckSignedHashes.SaveCardId)
}
store.dispatch(WalletAction.SetWarnings(it.getWarnings(WarningMessage.Location.MainScreen)))

View file

@ -13,16 +13,11 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
return when (action) {
is GlobalAction.SaveScanNoteResponse ->
globalState.copy(scanNoteResponse = action.scanNoteResponse)
is GlobalAction.SetFiatRate -> {
val rates = globalState.conversionRates.rates.toMutableMap()
rates[action.fiatRates.first] = action.fiatRates.second
globalState.copy(conversionRates = ConversionRates(rates))
}
is GlobalAction.ChangeAppCurrency -> {
globalState.copy(appCurrency = action.appCurrency, conversionRates = ConversionRates(mapOf()))
globalState.copy(appCurrency = action.appCurrency)
}
is GlobalAction.RestoreAppCurrency.Success -> {
globalState.copy(appCurrency = action.appCurrency, conversionRates = ConversionRates(mapOf()))
globalState.copy(appCurrency = action.appCurrency)
}
is GlobalAction.UpdateWalletSignedHashes -> {
val card = globalState.scanNoteResponse?.card?.copy(

View file

@ -9,7 +9,6 @@ import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.network.coinmarketcap.CoinMarketCapService
import org.rekotlin.StateType
import java.math.BigDecimal
data class GlobalState(
val scanNoteResponse: ScanNoteResponse? = null,
@ -17,21 +16,11 @@ data class GlobalState(
val payIdManager: PayIdManager = PayIdManager(),
val coinMarketCapService: CoinMarketCapService = CoinMarketCapService(),
val tangemService: TangemService = TangemService(),
val conversionRates: ConversionRates = ConversionRates(emptyMap()),
val configManager: ConfigManager? = null,
val warningManager: WarningMessagesManager? = null,
val appCurrency: FiatCurrencyName = DEFAULT_FIAT_CURRENCY
) : StateType
data class ConversionRates(
val rates: Map<CryptoCurrencyName, BigDecimal>,
) {
fun getRate(cryptoCurrency: CryptoCurrencyName): BigDecimal? {
return rates[cryptoCurrency]
}
}
typealias CryptoCurrencyName = String
typealias FiatCurrencyName = String

View file

@ -10,6 +10,6 @@ data class NavigationState(
) : StateType
enum class AppScreen {
Home, Wallet, Send, Details, DetailsConfirm, DetailsSecurity, Disclaimer,
CreateTwinWalletWarning, CreateTwinWallet, TwinsOnboarding
Home, Wallet, WalletDetails, Send, Details, DetailsConfirm, DetailsSecurity, Disclaimer,
CreateTwinWalletWarning, CreateTwinWallet, TwinsOnboarding, AddTokens
}

View file

@ -2,46 +2,13 @@ package com.tangem.tap.domain
import com.tangem.blockchain.common.Blockchain
import com.tangem.commands.common.network.Result
import com.tangem.tap.network.payid.PayIdService
import com.tangem.tap.network.payid.PayIdVerifyService
import com.tangem.tap.network.payid.SetPayIdResponse
import com.tangem.tap.network.payid.VerifyPayIdResponse
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import retrofit2.HttpException
import java.util.*
class PayIdManager {
private val payIdService = PayIdService()
suspend fun getPayId(cardId: String, publicKey: String): Result<String?> = withContext(Dispatchers.IO) {
val result = payIdService.getPayId(cardId, publicKey)
when (result) {
is Result.Success -> return@withContext Result.Success(result.data.payId)
is Result.Failure -> {
(result.error as? HttpException)?.let {
if (it.code() == 404) return@withContext Result.Success(null)
}
return@withContext result
}
}
}
suspend fun setPayId(
cardId: String, publicKey: String, payId: String, address: String, blockchain: Blockchain
): Result<SetPayIdResponse> = withContext(Dispatchers.IO) {
val result = payIdService.setPayId(cardId, publicKey, payId, address, blockchain.getPayIdNetwork())
when (result) {
is Result.Success -> return@withContext result
is Result.Failure -> {
(result.error as? HttpException)?.let {
if (it.code() == 409) return@withContext Result.Failure(TapError.PayIdAlreadyCreated)
}
return@withContext result
}
}
}
suspend fun verifyPayId(payId: String, blockchain: Blockchain): Result<VerifyPayIdResponse> = withContext(Dispatchers.IO) {
val splitPayId = payId.split("\$")

View file

@ -0,0 +1,13 @@
package com.tangem.tap.domain
class TapBlockchainConfig {
companion object {
val blockchairApiKey: String = "A___0Shpsu4KagE7oSabrw20DfXAqWlT"
val blockcypherTokens: Set<String> = setOf(
"aa8184b0e0894b88a5688e01b3dc1e82",
"56c4ca23c6484c8f8864c32fde4def8d",
"66a8a37c5e9d4d2c9bb191acfe7f93aa"
)
val infuraProjectId: String = "613a0b14833145968b1f656240c7d245"
}
}

View file

@ -1,23 +1,22 @@
package com.tangem.tap.domain
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.*
import com.tangem.commands.common.card.CardStatus
import com.tangem.commands.common.network.Result
import com.tangem.common.extensions.toHexString
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.currenciesRepository
import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.extensions.amountToCreateAccount
import com.tangem.tap.domain.extensions.isNoAccountError
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.tasks.isMultiwalletAllowed
import com.tangem.tap.domain.twins.TwinsHelper
import com.tangem.tap.domain.twins.isTwinCard
import com.tangem.tap.features.tokens.redux.TokensAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.network.coinmarketcap.CoinMarketCapService
@ -28,29 +27,25 @@ import java.math.BigDecimal
class TapWalletManager {
private val payIdManager = PayIdManager()
private val coinMarketCapService = CoinMarketCapService()
suspend fun loadWalletData() {
val walletManager = store.state.globalState.scanNoteResponse?.walletManager
if (walletManager == null) {
store.dispatch(WalletAction.LoadWallet.Failure())
return
private val blockchainSdkConfig by lazy {
store.state.globalState.configManager?.config?.blockchainSdkConfig ?: BlockchainSdkConfig()
}
private val walletManagerFactory: WalletManagerFactory
by lazy { WalletManagerFactory(blockchainSdkConfig) }
suspend fun loadWalletData(walletManager: WalletManager) {
val result = try {
walletManager.update()
Result.Success(walletManager.wallet)
} catch (exception: Exception) {
Result.Failure(exception)
}
loadWallet(walletManager)
handleUpdateWalletResult(result, walletManager)
}
suspend fun loadPayId() {
val result = loadPayIdIfNeeded()
result?.let { handlePayIdResult(it) }
}
suspend fun updateWallet() {
val walletManager = store.state.globalState.scanNoteResponse?.walletManager
if (walletManager == null) {
withContext(Dispatchers.Main) { store.dispatch(WalletAction.UpdateWallet.Failure()) }
return
}
suspend fun updateWallet(walletManager: WalletManager) {
val result = try {
walletManager.update()
Result.Success(walletManager.wallet)
@ -68,12 +63,18 @@ class TapWalletManager {
}
suspend fun loadFiatRate(fiatCurrency: FiatCurrencyName) {
val wallet = store.state.globalState.scanNoteResponse?.walletManager?.wallet ?: return
suspend fun loadFiatRate(fiatCurrency: FiatCurrencyName, wallet: Wallet) {
val currencyList = wallet.getTokens().map { it.symbol }.toMutableList()
currencyList.add(wallet.blockchain.currency)
loadFiatRate(fiatCurrency, currencyList)
}
suspend fun loadFiatRate(fiatCurrency: FiatCurrencyName, cryptoCurrencyName: CryptoCurrencyName) {
val currencyList = listOf(cryptoCurrencyName)
loadFiatRate(fiatCurrency, currencyList)
}
private suspend fun loadFiatRate(fiatCurrency: FiatCurrencyName, currencyList: List<CryptoCurrencyName>) {
val results = mutableListOf<Pair<CryptoCurrencyName, Result<BigDecimal>?>>()
currencyList.forEach { results.add(it to coinMarketCapService.getRate(it, fiatCurrency)) }
handleFiatRatesResult(results)
@ -88,22 +89,20 @@ class TapWalletManager {
store.state.globalState.warningManager?.setBlockchain(data.walletManager?.wallet?.blockchain)
val configManager = store.state.globalState.configManager
if (TapWorkarounds.isStart2Coin) {
configManager?.turnOff(ConfigManager.isWalletPayIdEnabled)
configManager?.turnOff(ConfigManager.isSendingToPayIdEnabled)
configManager?.turnOff(ConfigManager.isTopUpEnabled)
} else if (data.walletManager?.wallet?.blockchain == Blockchain.Bitcoin
|| data.card.cardData?.blockchainName == Blockchain.Bitcoin.id){
configManager?.turnOff(ConfigManager.isWalletPayIdEnabled)
|| data.card.cardData?.blockchainName == Blockchain.Bitcoin.id) {
configManager?.resetToDefault(ConfigManager.isSendingToPayIdEnabled)
configManager?.resetToDefault(ConfigManager.isTopUpEnabled)
} else {
configManager?.resetToDefault(ConfigManager.isWalletPayIdEnabled)
configManager?.resetToDefault(ConfigManager.isSendingToPayIdEnabled)
configManager?.resetToDefault(ConfigManager.isTopUpEnabled)
}
withContext(Dispatchers.Main) {
store.dispatch(WalletAction.ResetState)
store.dispatch(GlobalAction.SaveScanNoteResponse(data))
store.dispatch(WalletAction.MultiWallet.SetIsMultiwalletAllowed(data.card.isMultiwalletAllowed))
if (data.card.isTwinCard()) {
val secondCardId = TwinsHelper.getTwinsCardId(data.card.cardId)
val cardNumber = TwinsHelper.getTwinCardNumber(data.card.cardId)
@ -113,13 +112,17 @@ class TapWalletManager {
))
}
}
if (data.walletManager?.wallet?.blockchain == Blockchain.Ethereum ||
data.walletManager?.wallet?.blockchain == Blockchain.EthereumTestnet) {
store.dispatch(TokensAction.LoadCardTokens)
}
loadData(data)
}
}
suspend fun loadData(data: ScanNoteResponse) {
withContext(Dispatchers.Main) {
store.dispatch(WalletAction.CheckIfWarningNeeded)
store.dispatch(WalletAction.CheckSignedHashes.CheckIfWarningNeeded)
val artworkId = data.verifyResponse?.artworkInfo?.id
if (data.walletManager != null) {
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
@ -128,11 +131,40 @@ class TapWalletManager {
}
val config = store.state.globalState.configManager?.config ?: return@withContext
store.dispatch(WalletAction.LoadWallet(
data.walletManager.wallet, data.verifyResponse?.artworkInfo?.id, config.isTopUpEnabled))
val primaryWalletManager = data.walletManager
val primaryBlockchain = listOf(data.walletManager.wallet.blockchain)
val primaryToken = primaryWalletManager.presetTokens.toList()
store.dispatch(WalletAction.MultiWallet.SetPrimaryBlockchain(primaryBlockchain[0]))
if (primaryWalletManager.presetTokens.isNotEmpty()) {
store.dispatch(WalletAction.MultiWallet.SetPrimaryToken(primaryToken.first()))
}
if (data.card.isMultiwalletAllowed) {
val savedCurrencies = currenciesRepository.loadCardCurrencies(data.card.cardId)
val tokens = if (savedCurrencies.tokens.isNotEmpty()) {
primaryToken + savedCurrencies.tokens
} else {
primaryToken
}
val walletManagers = listOf(primaryWalletManager) +
currenciesRepository.getBlockchains()
.filterNot { it == primaryWalletManager.wallet.blockchain }
.mapNotNull { walletManagerFactory.makeWalletManager(data.card, it) }
val otherBlockhains = savedCurrencies.blockchains
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(walletManagers))
store.dispatch(WalletAction.MultiWallet.AddBlockchains(primaryBlockchain + otherBlockhains))
store.dispatch(WalletAction.MultiWallet.AddTokens(tokens))
} else {
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(listOf(primaryWalletManager)))
store.dispatch(WalletAction.MultiWallet.AddBlockchains(primaryBlockchain))
}
store.dispatch(WalletAction.SetArtworkId(data.verifyResponse?.artworkInfo?.id))
store.dispatch(WalletAction.LoadWallet(config.isTopUpEnabled))
store.dispatch(WalletAction.LoadArtwork(data.card, artworkId))
store.dispatch(WalletAction.LoadFiatRate)
store.dispatch(WalletAction.LoadPayId)
store.dispatch(WalletAction.LoadFiatRate())
} else if (data.card.status == CardStatus.Empty || data.card.isTwinCard()) {
store.dispatch(WalletAction.EmptyWallet)
store.dispatch(WalletAction.LoadArtwork(data.card, artworkId))
@ -143,14 +175,22 @@ class TapWalletManager {
}
}
private suspend fun loadWallet(walletManager: WalletManager) {
val result = try {
walletManager.update()
Result.Success(walletManager.wallet)
} catch (exception: Exception) {
Result.Failure(exception)
suspend fun reloadData(data: ScanNoteResponse) {
withContext(Dispatchers.Main) {
if (data.walletManager != null) {
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
store.dispatch(WalletAction.LoadData.Failure(TapError.NoInternetConnection))
return@withContext
}
val config = store.state.globalState.configManager?.config ?: return@withContext
store.dispatch(WalletAction.LoadWallet(config.isTopUpEnabled))
store.dispatch(WalletAction.LoadFiatRate())
} else if (data.card.status == CardStatus.Empty || data.card.isTwinCard()) {
store.dispatch(WalletAction.EmptyWallet)
} else {
store.dispatch(WalletAction.LoadData.Failure(TapError.UnknownBlockchain))
}
}
handleUpdateWalletResult(result, walletManager)
}
private suspend fun handleUpdateWalletResult(result: Result<Wallet>, walletManager: WalletManager) {
@ -164,59 +204,24 @@ class TapWalletManager {
val token = walletManager.wallet.getFirstToken()
val amountToCreateAccount = blockchain.amountToCreateAccount(token)
if (amountToCreateAccount != null) {
store.dispatch(WalletAction.LoadWallet.NoAccount(amountToCreateAccount.toString()))
store.dispatch(WalletAction.LoadWallet.NoAccount(
walletManager.wallet,
amountToCreateAccount.toString()))
return@withContext
}
}
store.dispatch(WalletAction.LoadWallet.Failure(result.error?.localizedMessage))
store.dispatch(WalletAction.LoadWallet.Failure(walletManager.wallet, result.error?.localizedMessage))
}
}
}
}
private suspend fun loadPayIdIfNeeded(): Result<String?>? {
val scanNoteResponse = store.state.globalState.scanNoteResponse
if (store.state.globalState.configManager?.config?.isWalletPayIdEnabled == false
|| scanNoteResponse?.walletManager?.wallet?.blockchain?.isPayIdSupported() == false) {
return null
}
val cardId = scanNoteResponse?.card?.cardId
val publicKey = scanNoteResponse?.card?.cardPublicKey
if (cardId == null || publicKey == null) {
return null
}
return payIdManager.getPayId(cardId, publicKey.toHexString())
}
private suspend fun handlePayIdResult(result: Result<String?>) {
withContext(Dispatchers.Main) {
when (result) {
is Result.Success -> {
val config = store.state.globalState.configManager?.config
if (config?.isWalletPayIdEnabled == false) {
store.dispatch(WalletAction.DisablePayId)
return@withContext
}
val payId = result.data
if (payId == null) {
store.dispatch(WalletAction.LoadPayId.NotCreated)
} else {
store.dispatch(WalletAction.LoadPayId.Success(payId))
}
}
is Result.Failure -> store.dispatch(WalletAction.LoadPayId.Failure)
}
}
}
private suspend fun handleFiatRatesResult(results: List<Pair<CryptoCurrencyName, Result<BigDecimal>?>>) {
withContext(Dispatchers.Main) {
results.map {
when (it.second) {
is Result.Success -> {
val rate = it.first to (it.second as Result.Success<BigDecimal>).data
store.dispatch(GlobalAction.SetFiatRate(rate))
store.dispatch(WalletAction.LoadFiatRate.Success(rate))
}
is Result.Failure -> store.dispatch(WalletAction.LoadFiatRate.Failure)

View file

@ -1,5 +1,6 @@
package com.tangem.tap.domain.configurable.config
import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.tangem_sdk_new.ui.animation.VoidCallback
import com.tangem.tap.domain.configurable.Loader
@ -10,7 +11,7 @@ data class Config(
val coinMarketCapKey: String = "f6622117-c043-47a0-8975-9d673ce484de",
val moonPayApiKey: String = "pk_test_kc90oYTANy7UQdBavDKGfL4K9l6VEPE",
val moonPayApiSecretKey: String = "sk_test_V8w4M19LbDjjYOt170s0tGuvXAgyEb1C",
val isWalletPayIdEnabled: Boolean = true,
val blockchainSdkConfig: BlockchainSdkConfig = BlockchainSdkConfig(),
val isSendingToPayIdEnabled: Boolean = true,
val isTopUpEnabled: Boolean = false,
val isCreatingTwinCardsAllowed: Boolean = false
@ -39,7 +40,6 @@ class ConfigManager(
fun turnOff(name: String) {
when (name) {
isWalletPayIdEnabled -> config = config.copy(isWalletPayIdEnabled = false)
isSendingToPayIdEnabled -> config = config.copy(isSendingToPayIdEnabled = false)
isTopUpEnabled -> config = config.copy(isTopUpEnabled = false)
isCreatingTwinCardsAllowed -> config = config.copy(isCreatingTwinCardsAllowed = false)
@ -48,7 +48,6 @@ class ConfigManager(
fun resetToDefault(name: String) {
when (name) {
isWalletPayIdEnabled -> config = config.copy(isWalletPayIdEnabled = defaultConfig.isWalletPayIdEnabled)
isSendingToPayIdEnabled -> config = config.copy(isSendingToPayIdEnabled = defaultConfig.isSendingToPayIdEnabled)
isTopUpEnabled -> config = config.copy(isTopUpEnabled = defaultConfig.isTopUpEnabled)
isCreatingTwinCardsAllowed -> config =
@ -60,13 +59,11 @@ class ConfigManager(
val model = featureModel ?: return
config = config.copy(
isWalletPayIdEnabled = model.isWalletPayIdEnabled,
isTopUpEnabled = model.isTopUpEnabled,
isSendingToPayIdEnabled = model.isSendingToPayIdEnabled,
isCreatingTwinCardsAllowed = model.isCreatingTwinCardsAllowed
)
defaultConfig = defaultConfig.copy(
isWalletPayIdEnabled = model.isWalletPayIdEnabled,
isTopUpEnabled = model.isTopUpEnabled,
isSendingToPayIdEnabled = model.isSendingToPayIdEnabled,
isCreatingTwinCardsAllowed = model.isCreatingTwinCardsAllowed
@ -78,22 +75,28 @@ class ConfigManager(
config = config.copy(
coinMarketCapKey = values.coinMarketCapKey,
moonPayApiKey = values.moonPayApiKey,
moonPayApiSecretKey = values.moonPayApiSecretKey
moonPayApiSecretKey = values.moonPayApiSecretKey,
blockchainSdkConfig = BlockchainSdkConfig(
blockchairApiKey = values.blockchairApiKey,
blockcypherTokens = values.blockcypherTokens,
infuraProjectId = values.infuraProjectId
)
)
defaultConfig = defaultConfig.copy(
coinMarketCapKey = values.coinMarketCapKey,
moonPayApiKey = values.moonPayApiKey,
moonPayApiSecretKey = values.moonPayApiSecretKey
moonPayApiSecretKey = values.moonPayApiSecretKey,
blockchainSdkConfig = BlockchainSdkConfig(
blockchairApiKey = values.blockchairApiKey,
blockcypherTokens = values.blockcypherTokens,
infuraProjectId = values.infuraProjectId
)
)
}
companion object {
const val isWalletPayIdEnabled = "isWalletPayIdEnabled"
const val isSendingToPayIdEnabled = "isSendingToPayIdEnabled"
const val isCreatingTwinCardsAllowed = "isCreatingTwinCardsAllowed"
const val isTopUpEnabled = "isTopUpEnabled"
const val coinMarketCapKey = "coinMarketCapKey"
const val moonPayApiKey = "moonPayApiKey"
const val moonPayApiSecretKey = "moonPayApiSecretKey"
}
}

View file

@ -15,6 +15,9 @@ class ConfigValueModel(
val coinMarketCapKey: String,
val moonPayApiKey: String,
val moonPayApiSecretKey: String,
val blockchairApiKey: String?,
val blockcypherTokens: Set<String>?,
val infuraProjectId: String?,
)
class ConfigModel(val features: FeatureModel?, val configValues: ConfigValueModel?) {

View file

@ -4,32 +4,41 @@ import com.tangem.CardSession
import com.tangem.CardSessionRunnable
import com.tangem.TangemError
import com.tangem.TangemSdkError
import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.commands.CommandResponse
import com.tangem.commands.ReadIssuerDataCommand
import com.tangem.commands.common.card.Card
import com.tangem.commands.common.card.CardStatus
import com.tangem.commands.common.card.EllipticCurve
import com.tangem.commands.common.card.masks.Product
import com.tangem.commands.verifycard.VerifyCardCommand
import com.tangem.commands.verifycard.VerifyCardResponse
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.toHexString
import com.tangem.tap.domain.TapSdkError
import com.tangem.tap.domain.TapWorkarounds
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.domain.twins.isTwinCard
import com.tangem.tap.store
import com.tangem.tasks.ScanTask
data class ScanNoteResponse(
val walletManager: WalletManager?,
val card: Card,
val verifyResponse: VerifyCardResponse? = null,
val secondTwinPublicKey: String? = null
val secondTwinPublicKey: String? = null,
val multiwalletCard: Boolean = false,
) : CommandResponse
class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteResponse> {
override val requiresPin2 = false
private val blockchainSdkConfig = store.state.globalState.configManager?.config
?.blockchainSdkConfig ?: BlockchainSdkConfig()
private val walletManagerFactory = WalletManagerFactory(blockchainSdkConfig)
override fun run(session: CardSession, callback: (result: CompletionResult<ScanNoteResponse>) -> Unit) {
ScanTask().run(session) { result ->
when (result) {
@ -53,7 +62,7 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
}
val walletManager = try {
WalletManagerFactory.makeWalletManager(card)
walletManagerFactory.makeWalletManager(card)
} catch (exception: Exception) {
return@run callback(CompletionResult.Success(ScanNoteResponse(null, card)))
}
@ -72,7 +81,8 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
when (verifyResult) {
is CompletionResult.Success -> {
callback(CompletionResult.Success(ScanNoteResponse(
walletManager, card, verifyResult.data, publicKey
walletManager, card, verifyResult.data, publicKey,
batchesAllowingMultiwallet.contains(card.cardData?.batchId)
)))
}
is CompletionResult.Failure -> {
@ -95,7 +105,7 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
if (verified) {
val twinPublicKey = readDataResult.data.issuerData.sliceArray(0 until 65)
val walletManager = try {
WalletManagerFactory.makeMultisigWalletManager(card, twinPublicKey)
walletManagerFactory.makeMultisigWalletManager(card, twinPublicKey)
} catch (exception: Exception) {
callback(CompletionResult.Success(ScanNoteResponse(null, card)))
return@run
@ -130,4 +140,13 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
companion object {
private val excludedBatches = listOf("0027", "0030", "0031")
}
}
}
val Card.isMultiwalletAllowed: Boolean
get() {
return cardData?.productMask?.contains(Product.TwinCard) != true
&& !TapWorkarounds.isStart2Coin
&& this.curve == EllipticCurve.Secp256k1
}
private val batchesAllowingMultiwallet = listOf("FFFF")

View file

@ -0,0 +1,148 @@
package com.tangem.tap.domain.tokens
import android.app.Application
import android.content.Context
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Types
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.tap.common.extensions.readJsonFileToString
import com.tangem.tap.network.createMoshi
class CurrenciesRepository(val context: Application) {
private val moshi = createMoshi()
private val tokensAdapter: JsonAdapter<List<TokenDao>> = moshi.adapter(
Types.newParameterizedType(List::class.java, TokenDao::class.java)
)
private val blockchainsAdapter: JsonAdapter<List<Blockchain>> = moshi.adapter(
Types.newParameterizedType(List::class.java, Blockchain::class.java)
)
fun loadCardCurrencies(cardId: String): CardCurrencies {
return CardCurrencies(loadSavedTokens(cardId), loadSavedBlockchains(cardId))
}
fun saveCardCurrencies(cardId: String, currencies: CardCurrencies) {
saveTokens(cardId, currencies.tokens)
saveBlockchains(cardId, currencies.blockchains)
}
fun saveAddedToken(cardId: String, token: Token) {
val tokens = loadSavedTokens(cardId) + token
saveTokens(cardId, tokens)
}
fun saveAddedBlockchain(cardId: String, blockchain: Blockchain) {
val blockchains = loadSavedBlockchains(cardId) + blockchain
saveBlockchains(cardId, blockchains)
}
fun removeToken(cardId: String, token: Token) {
val tokens = loadSavedTokens(cardId).filterNot { it == token }
saveTokens(cardId, tokens)
}
fun removeBlockchain(cardId: String, blockchain: Blockchain) {
val blockchains = loadSavedBlockchains(cardId).filterNot { it == blockchain }
saveBlockchains(cardId, blockchains)
}
private fun loadSavedTokens(cardId: String): List<Token> {
return try {
val json = context.readFileText(getFileNameForTokens(cardId))
tokensAdapter.fromJson(json)!!.map { it.toToken() }
} catch (exception: Exception) {
emptyList()
}
}
private fun saveTokens(cardId: String, tokens: List<Token>) {
val json = tokensAdapter.toJson(tokens.map { TokenDao.fromToken(it) })
context.rewriteFile(json, getFileNameForTokens(cardId))
}
private fun loadSavedBlockchains(cardId: String): List<Blockchain> {
return try {
val json = context.readFileText(getFileNameForBlockchains(cardId))
blockchainsAdapter.fromJson(json) ?: emptyList()
} catch (exception: Exception) {
emptyList()
}
}
private fun saveBlockchains(cardId: String, blockchains: List<Blockchain>) {
val json = blockchainsAdapter.toJson(blockchains)
context.rewriteFile(json, getFileNameForBlockchains(cardId))
}
private fun Context.readFileText(fileName: String): String =
this.openFileInput(fileName).bufferedReader().readText()
private fun Context.rewriteFile(content: String, fileName: String) {
this.openFileOutput(fileName, Context.MODE_PRIVATE).use {
it.write(content.toByteArray(), 0, content.length)
}
}
fun getPopularTokens(): List<Token> {
val json = context.assets.readJsonFileToString(POPULAR_TOKENS_FILE_NAME)
return tokensAdapter.fromJson(json)!!.map { it.toToken() }
}
fun getBlockchains(): List<Blockchain> {
return listOf(
Blockchain.Bitcoin, Blockchain.BitcoinCash, Blockchain.Binance, Blockchain.Litecoin,
Blockchain.XRP, Blockchain.Tezos,
Blockchain.Ethereum, Blockchain.RSK)
}
companion object {
private const val POPULAR_TOKENS_FILE_NAME = "erc20_tokens"
private const val FILE_NAME_PREFIX_TOKENS = "tokens"
private const val FILE_NAME_PREFIX_BLOCKCHAINS = "blockchains"
fun getFileNameForTokens(cardId: String): String = "${FILE_NAME_PREFIX_TOKENS}_$cardId"
fun getFileNameForBlockchains(cardId: String): String = "${FILE_NAME_PREFIX_BLOCKCHAINS}_$cardId"
}
}
@JsonClass(generateAdapter = true)
data class TokenDao(
val name: String,
val symbol: String,
val contractAddress: String,
val decimalCount: Int
) {
fun toToken(): Token {
return Token(name = name, symbol = symbol, contractAddress = contractAddress, decimals = decimalCount)
}
companion object {
fun fromToken(token: Token): TokenDao {
return TokenDao(name = token.name, symbol = token.symbol, contractAddress = token.contractAddress, decimalCount = token.decimals)
}
}
}
@JsonClass(generateAdapter = true)
data class CardCurrenciesDao(
val tokens: List<TokenDao>,
val blockchains: List<Blockchain>
) {
fun toCardCurrencies(): CardCurrencies {
return CardCurrencies(tokens = tokens.map { it.toToken() }, blockchains = blockchains)
}
companion object {
fun fromCardCurrencies(cardCurrencies: CardCurrencies): CardCurrenciesDao {
return CardCurrenciesDao(tokens = cardCurrencies.tokens.map { TokenDao.fromToken(it) }, blockchains = cardCurrencies.blockchains)
}
}
}
data class CardCurrencies(
val tokens: List<Token>,
val blockchains: List<Blockchain>
)

View file

@ -17,7 +17,7 @@ sealed class DetailsAction : Action {
data class PrepareScreen(
val card: Card,
val scanNoteResponse: ScanNoteResponse,
val wallet: Wallet?,
val wallets: List<Wallet>,
val isCreatingTwinWalletAllowed: Boolean?,
val fiatCurrencyName: FiatCurrencyName,
val fiatCurrencies: List<FiatCurrencyName>? = null,

View file

@ -104,7 +104,7 @@ class DetailsMiddleware {
is DetailsAction.AppCurrencyAction.SelectAppCurrency -> {
preferencesStorage.saveAppCurrency(action.fiatCurrencyName)
store.dispatch(GlobalAction.ChangeAppCurrency(action.fiatCurrencyName))
store.dispatch(WalletAction.LoadFiatRate)
store.dispatch(WalletAction.LoadFiatRate())
}
}
}
@ -152,9 +152,7 @@ class DetailsMiddleware {
store.dispatch(DetailsAction.ManageSecurity.SaveChanges.Failure)
}
}
}
}
}
}

View file

@ -66,14 +66,15 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: Deta
} else {
null
}
return DetailsState(
card = action.card, wallet = action.wallet,
card = action.card, wallets = action.wallets,
cardInfo = action.card.toCardInfo(),
appCurrencyState = AppCurrencyState(
action.fiatCurrencyName
),
securityScreenState = SecurityScreenState(currentOption = securityOption),
createTwinWalletState = twinsState
createTwinWalletState = twinsState,
)
}
@ -81,8 +82,9 @@ private fun handleEraseWallet(action: DetailsAction.EraseWallet, state: DetailsS
return when (action) {
DetailsAction.EraseWallet.Check -> {
val notAllowedByCard = state.card?.settingsMask?.contains(Settings.ProhibitPurgeWallet) == true
val notEmpty = state.wallet?.recentTransactions?.isNullOrEmpty() != true ||
state.wallet.amounts.toSendableAmounts().isNotEmpty()
val notEmpty = state.wallets.any {
!it.recentTransactions.isNullOrEmpty() || it.amounts.toSendableAmounts().isNotEmpty()
}
val eraseWalletState = when {
notAllowedByCard -> EraseWalletState.NotAllowedByCard
notEmpty -> EraseWalletState.NotEmpty

View file

@ -12,7 +12,7 @@ import java.util.*
data class DetailsState(
val card: Card? = null,
val wallet: Wallet? = null,
val wallets: List<Wallet> = emptyList(),
val cardInfo: CardInfo? = null,
val appCurrencyState: AppCurrencyState = AppCurrencyState(),
val eraseWalletState: EraseWalletState? = null,

View file

@ -18,7 +18,7 @@ class CreateTwinWalletMiddleware {
fun handle(action: DetailsAction.CreateTwinWalletAction) {
when (action) {
is DetailsAction.CreateTwinWalletAction.ShowWarning -> {
val wallet = store.state.detailsState.wallet
val wallet = store.state.detailsState.wallets.getOrNull(0)
if (wallet == null) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.CreateTwinWalletWarning))
return

View file

@ -77,7 +77,6 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
tv_signed_hashes.show(state.card?.isTwinCard() != true)
tv_signed_hashes_title.show(state.card?.isTwinCard() != true)
tv_disclaimer.setOnClickListener { store.dispatch(DetailsAction.ShowDisclaimer) }
if (state.createTwinWalletState != null) {

View file

@ -24,7 +24,9 @@ object ReleaseSendState : Action
data class PrepareSendScreen(
val coinAmount: Amount?,
val coinRate: BigDecimal?,
val tokenAmount: Amount? = null,
val tokenRate: BigDecimal? = null
) : SendScreenAction
// Address or PayId

View file

@ -20,9 +20,11 @@ import com.tangem.tap.features.send.redux.*
import com.tangem.tap.features.send.redux.FeeAction.RequestFee
import com.tangem.tap.features.send.redux.states.SendButtonState
import com.tangem.tap.features.send.redux.states.TransactionExtrasState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.scope
import com.tangem.tap.tangemSdk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.rekotlin.Action
@ -116,6 +118,12 @@ private fun sendTransaction(
dispatch(SendAction.SendSuccess)
dispatch(GlobalAction.UpdateWalletSignedHashes(result.data.walletSignedHashes))
dispatch(NavigationAction.PopBackTo())
scope.launch(Dispatchers.IO) {
delay(10000)
withContext(Dispatchers.Main) {
dispatch(WalletAction.UpdateWallet(walletManager.wallet.blockchain.currency))
}
}
}
is Result.Failure -> {
when (result.error) {
@ -142,7 +150,7 @@ private fun sendTransaction(
}
// make it easier latter by handling an appropriate enumError or, like on iOS,
// accept a string identifier of the error message
message.contains("Target account is not created. To create account send 1+ XLM.")-> {
message.contains("Target account is not created. To create account send 1+ XLM.") -> {
dispatch(SendAction.SendError(TapError.XmlError.AssetAccountNotCreated))
}
else -> {

View file

@ -63,12 +63,10 @@ private class PrepareSendScreenStatesReducer : SendInternalReducer {
val amountToExtract = prepareAction.tokenAmount ?: prepareAction.coinAmount!!
val decimals = amountToExtract.decimals
val coinConverter = createCurrencyConverter(walletManager.wallet.blockchain.currency, decimals)
val tokenConverter = createCurrencyConverter(prepareAction.tokenAmount?.currencySymbol ?: "", decimals)
return sendState.copy(
walletManager = walletManager,
coinConverter = coinConverter,
tokenConverter = tokenConverter,
coinConverter = action.coinRate?.let { CurrencyConverter(it, decimals) },
tokenConverter = action.tokenRate?. let { CurrencyConverter(it, decimals) },
amountState = sendState.amountState.copy(
amountToExtract = amountToExtract,
typeOfAmount = amountToExtract.type,
@ -79,11 +77,6 @@ private class PrepareSendScreenStatesReducer : SendInternalReducer {
)
)
}
private fun createCurrencyConverter(currency: String, decimals: Int): CurrencyConverter? {
val rate = store.state.globalState.conversionRates.getRate(currency)
return if (rate == null) null else CurrencyConverter(rate, decimals)
}
}
internal fun updateLastState(sendState: SendState, lastChangedState: IdStateHolder): SendState {

View file

@ -34,8 +34,8 @@ import com.tangem.tap.features.send.redux.FeeActionUi.*
import com.tangem.tap.features.send.redux.states.FeeType
import com.tangem.tap.features.send.redux.states.MainCurrencyType
import com.tangem.tap.features.send.ui.stateSubscribers.SendStateSubscriber
import com.tangem.tap.features.wallet.ui.SpacesItemDecoration
import com.tangem.tap.features.wallet.ui.WarningMessagesAdapter
import com.tangem.tap.features.wallet.ui.adapters.SpacesItemDecoration
import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter
import com.tangem.tap.mainScope
import com.tangem.tap.store
import com.tangem.wallet.R

View file

@ -22,7 +22,7 @@ import com.tangem.tap.features.send.redux.states.*
import com.tangem.tap.features.send.ui.FeeUiHelper
import com.tangem.tap.features.send.ui.SendFragment
import com.tangem.tap.features.send.ui.dialogs.TezosWarningDialog
import com.tangem.tap.features.wallet.ui.WarningMessagesAdapter
import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.btn_expand_collapse.*

View file

@ -0,0 +1,21 @@
package com.tangem.tap.features.tokens.redux
import com.tangem.blockchain.common.Token
import com.tangem.tap.features.tokens.ui.adapters.CurrencyListItem
import com.tangem.tap.features.wallet.redux.WalletData
import org.rekotlin.Action
sealed class TokensAction : Action {
object LoadCurrencies : TokensAction() {
data class Success(val currencies: List<CurrencyListItem>) : TokensAction()
}
object LoadCardTokens : TokensAction() {
data class Success(val tokens: List<Token>) : TokensAction()
}
data class SetAddedCurrencies(val wallets: List<WalletData>) : TokensAction()
}

View file

@ -0,0 +1,28 @@
package com.tangem.tap.features.tokens.redux
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.currenciesRepository
import com.tangem.tap.features.tokens.ui.adapters.CurrencyListItem
import com.tangem.tap.store
import org.rekotlin.Middleware
class TokensMiddleware {
val tokensMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
when (action) {
is TokensAction.LoadCurrencies -> {
val tokens = currenciesRepository.getPopularTokens()
val blockchains = currenciesRepository.getBlockchains()
val currencies = CurrencyListItem.createListOfCurrencies(
blockchains, tokens
)
store.dispatch(TokensAction.LoadCurrencies.Success(currencies))
}
}
next(action)
}
}
}
}

View file

@ -0,0 +1,30 @@
package com.tangem.tap.features.tokens.redux
import com.tangem.tap.common.redux.AppState
import org.rekotlin.Action
class TokensReducer {
companion object {
fun reduce(action: Action, state: AppState): TokensState = internalReduce(action, state)
}
}
private fun internalReduce(action: Action, state: AppState): TokensState {
if (action !is TokensAction) return state.tokensState
val tokensState = state.tokensState
return when (action) {
is TokensAction.LoadCurrencies.Success -> {
tokensState.copy(currencies = action.currencies)
}
is TokensAction.SetAddedCurrencies -> {
tokensState.copy(addedCurrencies = action.wallets.mapNotNull { it.currencyData.currencySymbol })
}
is TokensAction.LoadCardTokens.Success -> {
tokensState.copy(addedTokens = LinkedHashSet(
action.tokens.map { TokenWithAmount(it, null) }
))
}
else -> tokensState
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.tap.features.tokens.redux
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Token
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.features.tokens.ui.adapters.CurrencyListItem
import org.rekotlin.StateType
data class TokensState(
val addedTokens: LinkedHashSet<TokenWithAmount> = LinkedHashSet(),
val addedCurrencies: List<CryptoCurrencyName> = emptyList(),
val currencies: List<CurrencyListItem> = emptyList(),
) : StateType
data class TokenWithAmount(
val token: Token,
val amount: Amount? = null
)

View file

@ -0,0 +1,119 @@
package com.tangem.tap.features.tokens.ui
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.transition.TransitionInflater
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.tokens.redux.TokensState
import com.tangem.tap.features.tokens.ui.adapters.CurrenciesAdapter
import com.tangem.tap.mainScope
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_add_tokens.*
import kotlinx.android.synthetic.main.fragment_details_twin_cards.toolbar
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.*
import org.rekotlin.StoreSubscriber
class AddTokensFragment : Fragment(R.layout.fragment_add_tokens),
StoreSubscriber<TokensState> {
private lateinit var viewAdapter: CurrenciesAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
}
})
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.tokensState == newState.tokensState
}.select { it.tokensState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(activity as? AppCompatActivity)?.setSupportActionBar(toolbar)
toolbar.setNavigationOnClickListener { activity?.onBackPressed() }
setupPopularTokensRecyclerView()
}
private fun setupPopularTokensRecyclerView() {
viewAdapter = CurrenciesAdapter()
rv_popular_tokens.layoutManager = LinearLayoutManager(context)
rv_popular_tokens.adapter = viewAdapter
}
override fun newState(state: TokensState) {
if (activity == null) return
viewAdapter.addedCurrencies = state.addedCurrencies
viewAdapter.submitUnfilteredList(state.currencies)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.menu_search -> {
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.popular_tokens, menu)
val menuItem = menu.findItem(R.id.menu_search)
val searchView: SearchView = menuItem.actionView as SearchView
searchView.queryHint = "Type here to search"
searchView.maxWidth = android.R.attr.width
searchView.inputtedTextAsFlow()
.debounce(400)
.distinctUntilChanged()
.onEach {
viewAdapter.filter(searchView.query)
}
.launchIn(mainScope)
return super.onCreateOptionsMenu(menu, inflater);
}
}
fun SearchView.inputtedTextAsFlow(): Flow<String> = callbackFlow {
val watcher = setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
offer(newText ?: "")
return false
}
})
awaitClose { (watcher) }
}

View file

@ -0,0 +1,165 @@
package com.tangem.tap.features.tokens.ui.adapters
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.StringRes
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.item_currency_subtitle.view.*
import kotlinx.android.synthetic.main.item_popular_token.view.*
import java.util.*
class CurrenciesAdapter : ListAdapter<CurrencyListItem, RecyclerView.ViewHolder>(DiffUtilCallback) {
var addedCurrencies: List<CryptoCurrencyName> = emptyList()
private var unfilteredList = listOf<CurrencyListItem>()
fun submitUnfilteredList(list: List<CurrencyListItem>) {
unfilteredList = list
submitList(list)
}
override fun getItemViewType(position: Int): Int {
return when (currentList[position]) {
is CurrencyListItem.TitleListItem -> 0
is CurrencyListItem.BlockchainListItem, is CurrencyListItem.TokenListItem -> 1
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
0 -> TitleViewHolder(LayoutInflater.from(parent.context)
.inflate(R.layout.item_currency_subtitle, parent, false))
1 -> CurrenciesViewHolder(LayoutInflater.from(parent.context)
.inflate(R.layout.item_popular_token, parent, false))
else -> CurrenciesViewHolder(LayoutInflater.from(parent.context)
.inflate(R.layout.item_popular_token, parent, false))
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val listItem = currentList[position]
if (holder is TitleViewHolder && listItem is CurrencyListItem.TitleListItem) {
holder.bind(listItem)
} else if (holder is CurrenciesViewHolder) {
holder.bind(listItem, addedCurrencies)
}
}
object DiffUtilCallback : DiffUtil.ItemCallback<CurrencyListItem>() {
override fun areContentsTheSame(
oldItem: CurrencyListItem, newItem: CurrencyListItem
) = oldItem == newItem
override fun areItemsTheSame(
oldItem: CurrencyListItem, newItem: CurrencyListItem
) = oldItem == newItem
}
fun filter(query: CharSequence?) {
val list = mutableListOf<CurrencyListItem>()
if (!query.isNullOrEmpty()) {
val queryNormalized = query.toString().toLowerCase(Locale.US)
list.addAll(
unfilteredList.filter { element ->
when (element) {
is CurrencyListItem.BlockchainListItem -> {
element.blockchain.currency.toLowerCase(Locale.US).contains(queryNormalized)
|| element.blockchain.fullName.toLowerCase(Locale.US).contains(queryNormalized)
}
is CurrencyListItem.TokenListItem -> {
element.token.name.toLowerCase(Locale.US).contains(queryNormalized)
|| element.token.symbol.toLowerCase(Locale.US).contains(queryNormalized)
}
else -> false
}
})
} else {
list.addAll(unfilteredList)
}
submitList(list)
}
class CurrenciesViewHolder(val view: View) :
RecyclerView.ViewHolder(view) {
fun bind(currency: CurrencyListItem, addedCurrencies: List<CryptoCurrencyName>) {
when (currency) {
is CurrencyListItem.BlockchainListItem -> {
val blockchain = currency.blockchain
view.tv_currency_name.text = blockchain.fullName
view.tv_currency_symbol.text = blockchain.currency
val isAdded = addedCurrencies.any { it == blockchain.currency }
view.btn_add_token.show(!isAdded)
view.btn_token_added.show(isAdded)
view.iv_currency.setImageResource(currency.blockchain.getIconRes())
view.iv_currency.colorFilter = null
view.tv_token_letter.text = null
view.btn_add_token.setOnClickListener {
store.dispatch(WalletAction.MultiWallet.AddBlockchain(blockchain))
view.btn_add_token.hide()
view.btn_token_added.show()
}
}
is CurrencyListItem.TokenListItem -> {
val token = currency.token
view.tv_currency_name.text = token.name
view.tv_currency_symbol.text = token.symbol
val isAdded = addedCurrencies.any { it == token.symbol }
view.btn_add_token.show(!isAdded)
view.btn_token_added.show(isAdded)
view.iv_currency.setImageResource(R.drawable.shape_circle)
view.iv_currency.setColorFilter(token.getColor())
view.tv_token_letter.text = token.symbol.take(1)
view.btn_add_token.setOnClickListener {
store.dispatch(WalletAction.MultiWallet.AddToken(token))
view.btn_add_token.hide()
view.btn_token_added.show()
}
}
}
}
}
class TitleViewHolder(val view: View) :
RecyclerView.ViewHolder(view) {
fun bind(title: CurrencyListItem.TitleListItem) {
view.tv_subtitle.text = view.getString(title.titleResId).toUpperCase(Locale.US)
}
}
}
sealed class CurrencyListItem {
data class TokenListItem(val token: Token) : CurrencyListItem()
data class BlockchainListItem(val blockchain: Blockchain) : CurrencyListItem()
data class TitleListItem(@StringRes val titleResId: Int) : CurrencyListItem()
companion object {
fun createListOfCurrencies(blockchains: List<Blockchain>, tokens: List<Token>): List<CurrencyListItem> {
val blockchainsTitle = R.string.add_tokens_subtitle_blockchains
val tokensTitle = R.string.add_tokens_subtitle_tokens
return listOf(TitleListItem(blockchainsTitle)) +
blockchains.map { BlockchainListItem(it) } +
listOf(TitleListItem(tokensTitle)) +
tokens.map { TokenListItem(it) }
}
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.tap.features.wallet.models
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.TransactionStatus
import com.tangem.tap.common.extensions.toFormattedString
@ -47,4 +48,13 @@ fun List<TransactionData>.toPendingTransactions(walletAddress: String): List<Pen
fun List<PendingTransaction>.removeUnknownTransactions(): List<PendingTransaction> {
return this.filter { it.type != PendingTransactionType.Unknown }
}
fun TransactionData.toPendingTransactionForToken(token: Token, walletAddress: String): PendingTransaction? {
if (this.amount.currencySymbol != token.symbol) return null
return this.toPendingTransaction(walletAddress)
}
fun List<TransactionData>.toPendingTransactionsForToken(token: Token, walletAddress: String): List<PendingTransaction> {
return this.mapNotNull { it.toPendingTransactionForToken(token, walletAddress) }
}

View file

@ -1,11 +1,9 @@
package com.tangem.tap.features.wallet.redux
import android.content.Context
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.address.AddressType
import com.tangem.commands.common.card.Card
import com.tangem.tap.common.redux.ErrorAction
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.domain.TapError
@ -23,73 +21,94 @@ sealed class WalletAction : Action {
data class Failure(val error: TapError) : WalletAction()
}
data class LoadWallet(
val wallet: Wallet, val artworkId: String?, val allowTopUp: Boolean
) : WalletAction() {
data class LoadWallet(val allowTopUp: Boolean? = null, val currency: CryptoCurrencyName? = null) : WalletAction() {
data class Success(val wallet: Wallet) : WalletAction()
data class NoAccount(val amountToCreateAccount: String) : WalletAction()
data class Failure(val errorMessage: String? = null) : WalletAction()
data class NoAccount(val wallet: Wallet, val amountToCreateAccount: String) : WalletAction()
data class Failure(val wallet: Wallet, val errorMessage: String? = null) : WalletAction()
}
object CheckIfWarningNeeded : WalletAction()
object CheckHashesCountOnline : WalletAction()
object NeedToCheckHashesCountOnline : WalletAction()
object ConfirmHashesCount : WalletAction()
data class SetWarnings(val warningList: List<WarningMessage>) : WalletAction()
object SaveCardId : WalletAction()
data class SetArtworkId(val artworkId: String?) : WalletAction()
object UpdateWallet : WalletAction() {
// sealed class ProcessWallet : WalletAction() {
// data class LoadWallet(val artworkId: String?, val allowTopUp: Boolean
// ) : ProcessWallet() {
// data class Success(val wallet: Wallet) : ProcessWallet()
// data class NoAccount(val wallet: Wallet, val amountToCreateAccount: String) : ProcessWallet()
// data class Failure(val wallet: Wallet, val errorMessage: String? = null) : ProcessWallet()
// }
//
// data class UpdateWallet(val currency: CryptoCurrencyName? = null) : ProcessWallet() {
// object ScheduleUpdatingWallet : ProcessWallet()
// data class Success(val wallet: Wallet) : ProcessWallet()
// data class Failure(val errorMessage: String? = null) : ProcessWallet()
// }
// }
sealed class MultiWallet : WalletAction() {
data class SetIsMultiwalletAllowed(val isMultiwalletAllowed: Boolean) : MultiWallet()
data class AddWalletManagers(val walletManagers: List<WalletManager>) : MultiWallet()
data class AddBlockchain(val blockchain: Blockchain) : MultiWallet()
data class AddBlockchains(val blockchains: List<Blockchain>) : MultiWallet()
data class AddTokens(val tokens: List<Token>) : MultiWallet()
data class AddToken(val token: Token) : MultiWallet()
data class TokenLoaded(val amount: Amount) : MultiWallet()
data class SelectWallet(val walletData: WalletData?) : MultiWallet()
data class RemoveWallet(val walletData: WalletData) : MultiWallet()
data class SetPrimaryBlockchain(val blockchain: Blockchain) : MultiWallet()
data class SetPrimaryToken(val token: Token) : MultiWallet()
}
sealed class CheckSignedHashes : WalletAction() {
object CheckIfWarningNeeded : CheckSignedHashes()
object CheckHashesCountOnline : CheckSignedHashes()
object NeedToCheckHashesCountOnline : CheckSignedHashes()
object ConfirmHashesCount : CheckSignedHashes()
object SaveCardId : CheckSignedHashes()
}
data class SetWarnings(val warningList: List<WarningMessage>) : WalletAction()
data class UpdateWallet(val currency: CryptoCurrencyName? = null) : WalletAction() {
object ScheduleUpdatingWallet : WalletAction()
data class Success(val wallet: Wallet) : WalletAction()
data class Failure(val errorMessage: String? = null) : WalletAction()
}
object LoadFiatRate : WalletAction() {
data class Success(val fiatRates: Pair<CryptoCurrencyName, BigDecimal?>) : WalletAction()
data class LoadFiatRate(
val wallet: Wallet? = null, val currency: CryptoCurrencyName? = null
) : WalletAction() {
data class Success(val fiatRate: Pair<CryptoCurrencyName, BigDecimal?>) : WalletAction()
object Failure : WalletAction()
}
object LoadPayId : WalletAction() {
data class Success(val payId: String) : WalletAction()
object NotCreated : WalletAction()
object Failure : WalletAction()
}
object DisablePayId : WalletAction()
data class LoadArtwork(val card: Card, val artworkId: String?) : WalletAction() {
data class Success(val artwork: Artwork) : WalletAction()
object Failure : WalletAction()
}
object Scan : WalletAction()
data class Send(val amount: Amount? = null) : WalletAction() {
data class ChooseCurrency(val amounts: List<Amount>?) : WalletAction()
object Cancel : WalletAction()
}
object CreatePayId : WalletAction() {
data class CompleteCreatingPayId(val payId: String) : WalletAction()
data class Success(val payId: String) : WalletAction()
object EmptyField : WalletAction(), ErrorAction {
override val error = TapError.PayIdEmptyField
}
class Failure(override val error: TapError) : WalletAction(), ErrorAction
object Cancel : WalletAction()
}
data class CopyAddress(val context: Context) : WalletAction() {
data class CopyAddress(val address: String, val context: Context) : WalletAction() {
object Success : WalletAction(), NotificationAction {
override val messageResource = R.string.wallet_notification_address_copied
}
}
data class ShareAddress(val address: String, val context: Context) : WalletAction()
object ShowQrCode : WalletAction()
object HideDialog : WalletAction()
data class ExploreAddress(val context: Context) : WalletAction()
data class ExploreAddress(val exploreUrl: String, val context: Context) : WalletAction()
object CreateWallet : WalletAction()
object EmptyWallet : WalletAction()
object Scan : WalletAction()
data class Send(val amount: Amount? = null) : WalletAction() {
data class ChooseCurrency(val amounts: List<Amount>?) : WalletAction()
object Cancel : WalletAction()
}
sealed class TopUpAction : WalletAction() {
data class TopUp(val context: Context, val toolbarColor: Int) : TopUpAction()

View file

@ -1,316 +0,0 @@
package com.tangem.tap.features.wallet.redux
import android.content.Intent
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.ContextCompat
import com.tangem.blockchain.common.*
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.commands.common.card.Card
import com.tangem.commands.common.card.CardType
import com.tangem.commands.common.network.Result
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.getType
import com.tangem.common.extensions.toHexString
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.common.extensions.copyToClipboard
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.PayIdManager
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.TopUpHelper
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.extensions.toSendableAmounts
import com.tangem.tap.domain.twins.TwinsHelper
import com.tangem.tap.domain.twins.isTwinCard
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.twins.CreateTwinWallet
import com.tangem.tap.features.send.redux.PrepareSendScreen
import com.tangem.tap.features.wallet.models.toPendingTransactions
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.network.NetworkStateChanged
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.rekotlin.Action
import org.rekotlin.Middleware
class WalletMiddleware {
private val topUpMiddleware = TopUpMiddleware()
val walletMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
when (action) {
is WalletAction.TopUpAction -> topUpMiddleware.handle(action)
is WalletAction.LoadWallet -> {
scope.launch {
store.state.globalState.tapWalletManager.loadWalletData()
}
}
is WalletAction.LoadPayId -> {
scope.launch {
store.state.globalState.tapWalletManager.loadPayId()
}
}
is WalletAction.LoadFiatRate -> {
scope.launch {
store.state.globalState.tapWalletManager.loadFiatRate(store.state.globalState.appCurrency)
}
}
is WalletAction.CreateWallet -> {
if (store.state.walletState.twinCardsState != null) {
store.dispatch(DetailsAction.CreateTwinWalletAction.ShowWarning(
store.state.globalState.scanNoteResponse?.card?.cardId?.let {
TwinsHelper.getTwinCardNumber(it)
},
CreateTwinWallet.CreateWallet
))
} else {
scope.launch {
val result = tangemSdkManager.createWallet(
store.state.globalState.scanNoteResponse?.card?.cardId
)
when (result) {
is CompletionResult.Success -> {
store.state.globalState.tapWalletManager
.onCardScanned(result.data)
}
}
}
}
}
is WalletAction.UpdateWallet -> {
if (store.state.walletState.state == ProgressState.Done) {
scope.launch { store.state.globalState.tapWalletManager.updateWallet() }
}
}
is WalletAction.UpdateWallet.Success -> setupWalletUpdate(action.wallet)
is WalletAction.LoadWallet.Success -> {
store.dispatch(WalletAction.CheckHashesCountOnline)
if (!store.state.walletState.updatingWallet) setupWalletUpdate(action.wallet)
}
is WalletAction.CreatePayId.CompleteCreatingPayId -> {
scope.launch {
val cardId = store.state.globalState.scanNoteResponse?.card?.cardId
val wallet = store.state.globalState.scanNoteResponse?.walletManager?.wallet
val publicKey = store.state.globalState.scanNoteResponse?.card?.cardPublicKey
if (cardId != null && wallet != null && publicKey != null) {
val result = PayIdManager().setPayId(
cardId, publicKey.toHexString(),
action.payId, wallet.address, wallet.blockchain
)
withContext(Dispatchers.Main) {
when (result) {
is Result.Success ->
store.dispatch(WalletAction.CreatePayId.Success(action.payId))
is Result.Failure -> {
val error = result.error as? TapError
?: TapError.PayIdCreatingError
store.dispatch(WalletAction.CreatePayId.Failure(error))
}
}
}
}
}
}
is WalletAction.Scan -> {
scope.launch {
val result = tangemSdkManager.scanNote(FirebaseAnalyticsHandler)
when (result) {
is CompletionResult.Success -> {
tangemSdkManager.changeDisplayedCardIdNumbersCount(result.data.card)
store.state.globalState.tapWalletManager
.onCardScanned(result.data, true)
if (store.state.walletState.twinCardsState != null) {
val showOnboarding = !preferencesStorage.wasTwinsOnboardingShown()
if (showOnboarding) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.TwinsOnboarding))
}
}
}
}
}
}
is WalletAction.LoadData -> {
scope.launch {
store.state.globalState.scanNoteResponse?.let {
store.state.globalState.tapWalletManager.loadData(it)
}
}
}
is NetworkStateChanged -> {
store.state.globalState.scanNoteResponse?.let { scanNoteResponse ->
store.dispatch(WalletAction.CheckHashesCountOnline)
scope.launch {
store.state.globalState.tapWalletManager.onCardScanned(scanNoteResponse)
}
}
}
is WalletAction.CopyAddress -> {
store.state.walletState.walletAddresses?.selectedAddress?.address?.let {
action.context.copyToClipboard(it)
store.dispatch(WalletAction.CopyAddress.Success)
}
}
is WalletAction.ExploreAddress -> {
val uri = Uri.parse(store.state.walletState.walletAddresses?.selectedAddress?.exploreUrl)
val intent = Intent(Intent.ACTION_VIEW, uri)
ContextCompat.startActivity(action.context, intent, null)
}
is WalletAction.Send -> {
val newAction = prepareSendAction(action.amount)
store.dispatch(newAction)
if (newAction is PrepareSendScreen) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Send))
}
}
is WalletAction.CheckIfWarningNeeded -> {
val globalState = store.state.globalState
val validator = globalState.scanNoteResponse?.walletManager as? SignatureCountValidator
globalState.scanNoteResponse?.card?.let { card ->
store.state.globalState.warningManager?.removeWarnings(WarningMessage.Origin.Local)
if (card.getType() != CardType.Release) addWarningMessage(WarningMessagesManager.devCardWarning())
if (!preferencesStorage.wasCardScannedBefore(card.cardId)) {
checkIfWarningNeeded(card, validator)?.let { addWarningMessage(it) }
}
updateWarningMessages()
}
}
is WalletAction.CheckHashesCountOnline -> checkHashesCountOnline()
is WalletAction.SaveCardId -> {
val cardId = store.state.globalState.scanNoteResponse?.card?.cardId
cardId?.let { preferencesStorage.saveScannedCardId(it) }
}
is WalletAction.TwinsAction.SetTwinCard -> {
val showOnboarding = !preferencesStorage.wasTwinsOnboardingShown()
if (showOnboarding) store.dispatch(WalletAction.TwinsAction.ShowOnboarding)
}
is WalletAction.TwinsAction.SetOnboardingShown -> {
preferencesStorage.saveTwinsOnboardingShown()
}
}
next(action)
}
}
}
private fun setupWalletUpdate(wallet: Wallet) {
if (!wallet.recentTransactions.toPendingTransactions(wallet.address).isNullOrEmpty()) {
store.dispatch(WalletAction.UpdateWallet.ScheduleUpdatingWallet)
scope.launch(Dispatchers.IO) {
delay(10000)
withContext(Dispatchers.Main) {
store.dispatch(WalletAction.UpdateWallet)
}
}
}
}
private fun prepareSendAction(amount: Amount?): Action {
return if (amount != null) {
if (amount.type is AmountType.Token) {
PrepareSendScreen(store.state.walletState.wallet?.amounts?.get(AmountType.Coin), amount)
} else {
PrepareSendScreen(amount)
}
} else {
val amounts = store.state.walletState.wallet?.amounts?.toSendableAmounts()
if (amounts?.size ?: 0 > 1) {
WalletAction.Send.ChooseCurrency(amounts)
} else {
val amountToSend = amounts?.first()
PrepareSendScreen(amountToSend)
}
}
}
private fun checkIfWarningNeeded(
card: Card, signatureCountValidator: SignatureCountValidator? = null,
): WarningMessage? {
if (card.isTwinCard()) return null
return if (signatureCountValidator == null) {
if (card.walletSignedHashes ?: 0 > 0) {
WarningMessagesManager.alreadySignedHashesWarning()
} else {
store.dispatch(WalletAction.SaveCardId)
null
}
} else {
store.dispatch(WalletAction.NeedToCheckHashesCountOnline)
null
}
}
private fun checkHashesCountOnline() {
if (store.state.walletState.hashesCountVerified != false) return
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) return
val card = store.state.globalState.scanNoteResponse?.card
if (card == null || preferencesStorage.wasCardScannedBefore(card.cardId)) return
if (card.isTwinCard()) return
val validator = store.state.globalState.scanNoteResponse?.walletManager
as? SignatureCountValidator
scope.launch {
val result = validator?.validateSignatureCount(card.walletSignedHashes ?: 0)
withContext(Dispatchers.Main) {
when (result) {
SimpleResult.Success -> {
store.dispatch(WalletAction.ConfirmHashesCount)
store.dispatch(WalletAction.SaveCardId)
}
is SimpleResult.Failure ->
if (result.error is BlockchainSdkError.SignatureCountNotMatched) {
addWarningMessage(WarningMessagesManager.alreadySignedHashesWarning(), true)
} else if (card.walletSignedHashes ?: 0 > 0) {
addWarningMessage(WarningMessagesManager.alreadySignedHashesWarning(), true)
}
}
}
}
}
private fun addWarningMessage(warning: WarningMessage, autoUpdate: Boolean = false) {
store.state.globalState.warningManager?.addWarning(warning)
if (autoUpdate) updateWarningMessages()
}
private fun updateWarningMessages() {
val warningManager = store.state.globalState.warningManager ?: return
store.dispatch(WalletAction.SetWarnings(warningManager.getWarnings(WarningMessage.Location.MainScreen)))
}
}
private class TopUpMiddleware {
fun handle(action: WalletAction.TopUpAction) {
when (action) {
is WalletAction.TopUpAction.TopUp -> {
val config = store.state.globalState.configManager?.config ?: return
val defaultAddress = store.state.walletState.walletAddresses!!.list[0].address
val url = TopUpHelper.getUrl(
store.state.walletState.currencyData.currencySymbol!!,
defaultAddress,
config.moonPayApiKey,
config.moonPayApiSecretKey
)
val customTabsIntent = CustomTabsIntent.Builder()
.setToolbarColor(action.toolbarColor)
.build()
customTabsIntent.launchUrl(action.context, Uri.parse(url));
}
}
}
}

View file

@ -1,25 +1,26 @@
package com.tangem.tap.features.wallet.redux
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Wallet
import com.tangem.commands.common.network.TangemService
import com.tangem.common.extensions.isZero
import com.tangem.common.extensions.toHexString
import com.tangem.tap.common.extensions.toFiatString
import com.tangem.tap.common.extensions.toFormattedCurrencyString
import com.tangem.tap.common.extensions.toFormattedString
import com.tangem.tap.common.extensions.toQrCode
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.features.wallet.models.removeUnknownTransactions
import com.tangem.tap.features.wallet.models.toPendingTransactions
import com.tangem.tap.features.wallet.redux.reducers.MultiWalletReducer
import com.tangem.tap.features.wallet.redux.reducers.OnWalletLoadedReducer
import com.tangem.tap.features.wallet.redux.reducers.TwinsReducer
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import com.tangem.tap.features.wallet.ui.TokenData
import com.tangem.tap.store
import org.rekotlin.Action
import java.math.BigDecimal
class WalletReducer {
companion object {
@ -29,11 +30,19 @@ class WalletReducer {
private fun internalReduce(action: Action, state: AppState): WalletState {
val multiWalletReducer = MultiWalletReducer()
val twinsReducer = TwinsReducer()
val onWalletLoadedReducer = OnWalletLoadedReducer()
if (action !is WalletAction) return state.walletState
var newState = state.walletState
when (action) {
is WalletAction.CheckSignedHashes -> newState = handleCheckSignedHashesActions(action, newState)
is WalletAction.TwinsAction -> newState = twinsReducer.reduce(action, newState)
is WalletAction.MultiWallet -> newState = multiWalletReducer.reduce(action, newState)
is WalletAction.ResetState -> newState = WalletState()
is WalletAction.EmptyWallet -> {
val creatingWalletAllowed = !(newState.twinCardsState != null &&
@ -41,121 +50,149 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
newState = newState.copy(
state = ProgressState.Done,
currencyData = BalanceWidgetData(BalanceStatus.EmptyCard),
mainButton = WalletMainButton.CreateWalletButton(creatingWalletAllowed),
topUpState = TopUpState(false)
)
wallets = listOf(WalletData(
currencyData = BalanceWidgetData(BalanceStatus.EmptyCard),
mainButton = WalletMainButton.CreateWalletButton(creatingWalletAllowed),
topUpState = TopUpState(false)
)))
}
is WalletAction.LoadData.Failure -> {
when (action.error) {
is TapError.NoInternetConnection -> {
val wallet = state.globalState.scanNoteResponse?.walletManager?.wallet
newState = newState.copy(
state = ProgressState.Error,
error = ErrorType.NoInternetConnection,
walletAddresses = createAddressList(wallet, newState.walletAddresses),
currencyData = BalanceWidgetData(
status = BalanceStatus.Unreachable,
currency = wallet?.blockchain?.fullName,
token = wallet?.getFirstToken()?.symbol?.let {
TokenData("", tokenSymbol = it)
}),
mainButton = WalletMainButton.SendButton(false),
topUpState = TopUpState(false)
)
}
is TapError.UnknownBlockchain -> {
newState = newState.copy(
state = ProgressState.Done,
currencyData = BalanceWidgetData(BalanceStatus.UnknownBlockchain),
topUpState = TopUpState(false)
wallets = listOf(WalletData(
currencyData = BalanceWidgetData(BalanceStatus.UnknownBlockchain),
topUpState = TopUpState(false)
))
)
}
}
}
is WalletAction.LoadData -> {
newState = newState.copy(
state = ProgressState.Loading,
error = null,
)
}
is WalletAction.LoadWallet -> {
val wallet = action.wallet
if (action.currency == null) {
val wallets = newState.wallets.map { wallet ->
wallet.copy(
currencyData = wallet.currencyData.copy(
status = BalanceStatus.Loading,
currency = wallet.currencyData.currency,
currencySymbol = wallet.currencyData.currencySymbol,
),
mainButton = WalletMainButton.SendButton(false),
topUpState = TopUpState(allowed = action.allowTopUp
?: wallet.topUpState.allowed)
)
}
newState = newState.copy(
state = ProgressState.Loading,
wallets = wallets
)
} else {
val walletManager = newState.getWalletManager(action.currency) ?: return newState
val currencies = listOf(walletManager.wallet.blockchain.currency + walletManager.presetTokens.map { it.symbol })
val newWallets = newState.wallets.filter { currencies.contains(it.currencyData.currencySymbol) }
.map { wallet ->
wallet.copy(
currencyData = wallet.currencyData.copy(
status = BalanceStatus.Loading,
currency = wallet.currencyData.currency,
currencySymbol = wallet.currencyData.currencySymbol,
),
mainButton = WalletMainButton.SendButton(false),
topUpState = TopUpState(
allowed = action.allowTopUp ?: wallet.topUpState.allowed
)
)
}
val wallets = newState.replaceSomeWallets(newWallets)
newState = newState.copy(wallets = wallets)
}
}
is WalletAction.LoadWallet.Success -> newState = onWalletLoadedReducer.reduce(action.wallet, newState)
is WalletAction.UpdateWallet.Success -> {
newState = onWalletLoadedReducer.reduce(action.wallet, newState)
// newState = newState.copy(updatingWallet = newState.pendingTransactions.isNotEmpty())
}
is WalletAction.LoadWallet.NoAccount -> {
val walletData = newState.getWalletData(action.wallet.blockchain.currency)?.copy(
currencyData = BalanceWidgetData(
BalanceStatus.NoAccount, action.wallet.blockchain.fullName,
currencySymbol = action.wallet.blockchain.currency,
amountToCreateAccount = action.amountToCreateAccount
)
)
val wallets = newState.replaceWalletInWallets(walletData)
val state = if (wallets.any { it.currencyData.status == BalanceStatus.Loading }) {
ProgressState.Loading
} else {
ProgressState.Done
}
newState = newState.copy(
state = state,
wallets = wallets
)
}
is WalletAction.LoadWallet.Failure -> {
val walletData = newState.getWalletData(action.wallet.blockchain.currency)
val newWalletData = walletData?.copy(
currencyData = walletData.currencyData.copy(
status = BalanceStatus.Unreachable,
errorMessage = action.errorMessage
),
topUpState = TopUpState(false)
)
val wallets = newState.replaceWalletInWallets(newWalletData)
val state = if (wallets.any { it.currencyData.status == BalanceStatus.Loading }) {
ProgressState.Loading
} else {
ProgressState.Done
}
newState = newState.copy(
state = state, wallets = wallets
)
}
is WalletAction.SetArtworkId -> {
val cardImage = if (newState.cardImage?.artworkId == action.artworkId) {
newState.cardImage
} else {
null
}
newState = newState.copy(
state = ProgressState.Loading,
cardImage = cardImage,
currencyData = BalanceWidgetData(
BalanceStatus.Loading,
wallet.blockchain.fullName,
currencySymbol = wallet.blockchain.currency,
token = wallet.getFirstToken()?.symbol?.let {
TokenData("", tokenSymbol = it)
}
),
walletAddresses = createAddressList(wallet, newState.walletAddresses),
mainButton = WalletMainButton.SendButton(false),
topUpState = TopUpState(allowed = action.allowTopUp)
)
newState = newState.copy(cardImage = cardImage)
}
is WalletAction.LoadWallet.Success -> newState = onWalletLoaded(action.wallet, newState)
is WalletAction.LoadWallet.NoAccount -> {
val wallet = state.globalState.scanNoteResponse?.walletManager?.wallet
newState = newState.copy(
state = ProgressState.Done, wallet = wallet,
currencyData = BalanceWidgetData(
BalanceStatus.NoAccount, wallet?.blockchain?.fullName,
currencySymbol = wallet?.blockchain?.currency,
amountToCreateAccount = action.amountToCreateAccount
)
)
}
is WalletAction.LoadWallet.Failure -> newState = newState.copy(
state = ProgressState.Done,
currencyData = newState.currencyData.copy(
status = BalanceStatus.Unreachable,
errorMessage = action.errorMessage
),
topUpState = TopUpState(false)
)
is WalletAction.UpdateWallet -> {
if (store.state.walletState.state == ProgressState.Done) {
newState = newState.copy(updatingWallet = true)
}
}
is WalletAction.UpdateWallet.ScheduleUpdatingWallet ->
newState = newState.copy(updatingWallet = true)
is WalletAction.UpdateWallet.Success -> {
newState = onWalletLoaded(action.wallet, newState)
newState = newState.copy(updatingWallet = newState.pendingTransactions.isNotEmpty())
}
is WalletAction.UpdateWallet.Failure -> newState = newState.copy(updatingWallet = false)
is WalletAction.LoadFiatRate -> {
newState.copy(currencyData = newState.currencyData.copy(
fiatAmount = null,
token = newState.currencyData.token?.copy(fiatAmount = null))
)
}
is WalletAction.LoadFiatRate.Success -> {
val rate = action.fiatRates.second ?: return newState
val currency = action.fiatRates.first
val fiatAmount = if (currency == newState.wallet?.blockchain?.currency) {
newState.wallet?.amounts?.get(AmountType.Coin)?.value
?.toFiatString(rate, state.globalState.appCurrency)
} else {
newState.currencyData.fiatAmount
}
val token = newState.wallet?.getFirstToken()
val tokenFiatAmount = if (currency == token?.symbol) {
newState.wallet?.getTokenAmount(token)?.value?.toFiatString(rate, state.globalState.appCurrency)
} else {
newState.currencyData.token?.fiatAmount
}
newState = newState.copy(currencyData = newState.currencyData.copy(
fiatAmount = fiatAmount,
token = newState.currencyData.token?.copy(fiatAmount = tokenFiatAmount)
))
}
// is WalletAction.UpdateWallet -> {
// if (store.state.walletState.state == ProgressState.Done) {
// newState = newState.copy(updatingWallet = true)
// }
// }
// is WalletAction.UpdateWallet.ScheduleUpdatingWallet ->
// newState = newState.copy(updatingWallet = true)
// is WalletAction.UpdateWallet.Failure -> newState = newState.copy(updatingWallet = false)
// is WalletAction.LoadFiatRate -> {
// newState.copy(currencyData = newState.currencyData.copy(
// fiatAmount = null,
// token = newState.currencyData.token?.copy(fiatAmount = null))
// )
// }
is WalletAction.LoadFiatRate.Success ->
newState = setNewFiatRate(action.fiatRate, state.globalState.appCurrency, newState)
is WalletAction.LoadArtwork -> {
val cardId = action.card.cardId
val cardPublicKey = action.card.cardPublicKey?.toHexString()
@ -177,38 +214,18 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
newState = newState.copy(cardImage = Artwork(artworkId = artworkUrl))
}
is WalletAction.ShowQrCode -> {
val selectedWalletData = newState.getWalletData(newState.selectedWallet)
newState = newState.copy(
walletDialog = WalletDialog.QrDialog(
newState.walletAddresses?.selectedAddress?.shareUrl?.toQrCode(),
newState.walletAddresses?.selectedAddress?.shareUrl,
newState.currencyData.currency
selectedWalletData?.walletAddresses?.selectedAddress?.shareUrl?.toQrCode(),
selectedWalletData?.walletAddresses?.selectedAddress?.shareUrl,
selectedWalletData?.currencyData?.currency
)
)
}
is WalletAction.HideDialog -> {
newState = newState.copy(walletDialog = null)
}
is WalletAction.LoadPayId.Success -> newState = newState.copy(
payIdData = PayIdData(PayIdState.Created, action.payId)
)
is WalletAction.LoadPayId.NotCreated -> newState = newState.copy(
payIdData = PayIdData(PayIdState.NotCreated, null)
)
is WalletAction.DisablePayId -> newState = newState.copy(
payIdData = PayIdData(PayIdState.Disabled, null)
)
is WalletAction.CreatePayId, is WalletAction.CreatePayId.Failure ->
newState = newState.copy(
walletDialog = WalletDialog.CreatePayIdDialog(CreatingPayIdState.EnterPayId)
)
is WalletAction.CreatePayId.CompleteCreatingPayId -> newState = newState.copy(
walletDialog = WalletDialog.CreatePayIdDialog(CreatingPayIdState.Waiting)
)
is WalletAction.CreatePayId.Success -> newState = newState.copy(
payIdData = PayIdData(PayIdState.Created, action.payId),
walletDialog = null
)
is WalletAction.CreatePayId.Cancel -> newState = newState.copy(walletDialog = null)
is WalletAction.Send.ChooseCurrency -> {
newState = newState.copy(
walletDialog = WalletDialog.SelectAmountToSendDialog(action.amounts)
@ -216,43 +233,18 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
}
is WalletAction.Send.Cancel -> newState = newState.copy(walletDialog = null)
is WalletAction.SetWarnings -> newState = newState.copy(mainWarningsList = action.warningList)
is WalletAction.NeedToCheckHashesCountOnline ->
newState = newState.copy(hashesCountVerified = false)
is WalletAction.ConfirmHashesCount ->
newState = newState.copy(hashesCountVerified = true)
is WalletAction.TopUpAction -> {
newState = newState.copy(topUpState = handleTopUpActions(action, newState.topUpState))
}
is WalletAction.TopUpAction -> return newState
is WalletAction.ChangeSelectedAddress -> {
val walletAddresses = newState.walletAddresses ?: return newState
val selectedWalletData = newState.getWalletData(newState.selectedWallet)
val walletAddresses = newState.getWalletData(selectedWalletData?.currencyData?.currencySymbol)?.walletAddresses
?: return newState
val address = walletAddresses.list.firstOrNull { it.type == action.type }
?: return newState
newState = newState.copy(walletAddresses = WalletAddresses(address, walletAddresses.list))
}
is WalletAction.TwinsAction.SetTwinCard -> {
newState = newState.copy(
twinCardsState = TwinCardsState(
secondCardId = action.secondCardId,
cardNumber = action.number,
showTwinOnboarding = newState.twinCardsState?.showTwinOnboarding
?: false,
isCreatingTwinCardsAllowed = action.isCreatingTwinCardsAllowed
)
)
}
is WalletAction.TwinsAction.ShowOnboarding -> {
newState = newState.copy(
twinCardsState = newState.twinCardsState?.copy(showTwinOnboarding = true)
?: TwinCardsState(null, null,
showTwinOnboarding = true,
isCreatingTwinCardsAllowed = false)
)
}
is WalletAction.TwinsAction.SetOnboardingShown -> {
newState = newState.copy(
twinCardsState = newState.twinCardsState?.copy(showTwinOnboarding = false)
val wallets = newState.replaceWalletInWallets(
selectedWalletData?.copy(walletAddresses = WalletAddresses(address, walletAddresses.list))
)
newState = newState.copy(wallets = wallets)
}
}
return newState
@ -281,56 +273,89 @@ fun createAddressList(wallet: Wallet?, walletAddresses: WalletAddresses? = null)
return WalletAddresses(listOfAddressData[indexOfSelectedWallet], listOfAddressData)
}
private fun handleTopUpActions(action: WalletAction.TopUpAction, state: TopUpState): TopUpState {
private fun handleCheckSignedHashesActions(action: WalletAction.CheckSignedHashes, state: WalletState): WalletState {
return when (action) {
is WalletAction.TopUpAction.TopUp -> state
WalletAction.CheckSignedHashes.CheckHashesCountOnline -> state
WalletAction.CheckSignedHashes.CheckIfWarningNeeded -> state
WalletAction.CheckSignedHashes.ConfirmHashesCount -> state.copy(hashesCountVerified = true)
WalletAction.CheckSignedHashes.NeedToCheckHashesCountOnline -> state.copy(hashesCountVerified = false)
WalletAction.CheckSignedHashes.SaveCardId -> state
}
}
private fun onWalletLoaded(
wallet: Wallet, walletState: WalletState, topUpAllowed: Boolean? = null
private fun setNewFiatRate(
fiatRate: Pair<CryptoCurrencyName, BigDecimal?>,
appCurrency: FiatCurrencyName, state: WalletState
): WalletState {
val fiatCurrencySymbol = store.state.globalState.appCurrency
val rate = fiatRate.second ?: return state
val rateFormatted = rate.toFormattedCurrencyString(2, appCurrency)
val currency = fiatRate.first
if (!state.isMultiwalletAllowed) {
return setSingeWalletFiatRate(rate, rateFormatted, currency, appCurrency, state)
} else {
return setMultiWalletFiatRate(rate, rateFormatted, currency, appCurrency, state)
}
}
private fun setMultiWalletFiatRate(
rate: BigDecimal, rateFormatted: String, currency: CryptoCurrencyName,
appCurrency: FiatCurrencyName, state: WalletState
): WalletState {
val walletData = state.getWalletData(currency) ?: return state
val wallet = state.walletManagers.find { it.wallet.blockchain.currency == currency }?.wallet
if (wallet != null) {
val newWalletData = state.getWalletData(currency)?.copy(
currencyData = walletData.currencyData.copy(
fiatAmount = wallet.amounts[AmountType.Coin]?.value
?.toFiatString(rate, appCurrency)),
fiatRate = rate, fiatRateString = rateFormatted
)
return state.copy(wallets = state.replaceWalletInWallets(newWalletData))
} else {
val ethereumWallet = state.walletManagers.find { it.wallet.blockchain == Blockchain.Ethereum }?.wallet
val token = ethereumWallet?.getTokens()?.find { it.symbol == currency } ?: return state
val tokenFiatAmount = ethereumWallet.getTokenAmount(token)?.value?.toFiatString(rate, appCurrency)
val newWalletData = state.getWalletData(currency)?.copy(
currencyData = walletData.currencyData.copy(
fiatAmount = tokenFiatAmount),
fiatRate = rate, fiatRateString = rateFormatted
)
return state.copy(wallets = state.replaceWalletInWallets(newWalletData))
}
}
private fun setSingeWalletFiatRate(
rate: BigDecimal, rateFormatted: String, currency: CryptoCurrencyName,
appCurrency: FiatCurrencyName, state: WalletState
): WalletState {
val wallet = state.walletManagers[0].wallet
val token = wallet.getFirstToken()
val tokenData = if (token != null) {
val tokenAmount = wallet.getTokenAmount(token)
if (tokenAmount != null) {
val tokenFiatRate = store.state.globalState.conversionRates.getRate(tokenAmount.currencySymbol)
val tokenFiatAmount = tokenFiatRate?.let { tokenAmount.value?.toFiatString(it, fiatCurrencySymbol) }
TokenData(tokenAmount.value?.toFormattedString(tokenAmount.decimals) ?: "",
tokenAmount.currencySymbol, tokenFiatAmount)
} else {
null
}
} else {
null
}
val amount = wallet.amounts[AmountType.Coin]?.value
val formattedAmount = amount?.toFormattedCurrencyString(
wallet.blockchain.decimals(),
wallet.blockchain.currency)
val fiatRate = store.state.globalState.conversionRates.getRate(wallet.blockchain.currency)
val fiatAmount = fiatRate?.let { amount?.toFiatString(it, fiatCurrencySymbol) }
val pendingTransactions = wallet.recentTransactions
.toPendingTransactions(wallet.address)
if (currency == state.primaryWallet?.currencyData?.currencySymbol) {
val fiatAmount = wallet.amounts[AmountType.Coin]?.value
?.toFiatString(rate, appCurrency)
val walletData = state.primaryWallet.copy(
currencyData = state.primaryWallet.currencyData.copy(fiatAmount = fiatAmount),
fiatRate = rate,
fiatRateString = rateFormatted
)
return state.copy(wallets = listOf(walletData))
} else if (currency == token?.symbol) {
val tokenFiatAmount = wallet.getTokenAmount(token)?.value?.toFiatString(rate, appCurrency)
val tokenData = state.primaryWallet?.currencyData?.token?.copy(
fiatAmount = tokenFiatAmount,
fiatRate = rate,
fiatRateString = rateFormatted
)
val sendButtonEnabled = amount?.isZero() == false && pendingTransactions.isEmpty()
val balanceStatus = if (pendingTransactions.isNotEmpty()) {
BalanceStatus.TransactionInProgress
} else {
BalanceStatus.VerifiedOnline
val walletData = state.primaryWallet?.copy(
currencyData = state.primaryWallet.currencyData.copy(
token = tokenData
))
val wallets = walletData?.let { listOf(walletData) } ?: emptyList()
return state.copy(wallets = wallets)
}
return walletState.copy(
state = ProgressState.Done, wallet = wallet,
currencyData = BalanceWidgetData(
balanceStatus, wallet.blockchain.fullName,
currencySymbol = wallet.blockchain.currency,
formattedAmount,
token = tokenData,
fiatAmount = fiatAmount
),
pendingTransactions = pendingTransactions.removeUnknownTransactions(),
mainButton = WalletMainButton.SendButton(sendButtonEnabled)
)
return state
}

View file

@ -1,47 +1,98 @@
package com.tangem.tap.features.wallet.redux
import android.graphics.Bitmap
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.address.AddressType
import com.tangem.blockchain.extensions.isAboveZero
import com.tangem.tap.common.entities.Button
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.extensions.toSendableAmounts
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.models.toPendingTransactions
import com.tangem.tap.features.wallet.models.toPendingTransactionsForToken
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import com.tangem.tap.store
import org.rekotlin.StateType
import java.math.BigDecimal
data class WalletState(
val state: ProgressState = ProgressState.Done,
val error: ErrorType? = null,
val cardImage: Artwork? = null,
val wallet: Wallet? = null,
val pendingTransactions: List<PendingTransaction> = emptyList(),
val hashesCountVerified: Boolean? = null,
val walletAddresses: WalletAddresses? = null,
val currencyData: BalanceWidgetData = BalanceWidgetData(),
val payIdData: PayIdData = PayIdData(),
val walletDialog: WalletDialog? = null,
val updatingWallet: Boolean = false,
val mainButton: WalletMainButton = WalletMainButton.SendButton(false),
val topUpState: TopUpState = TopUpState(),
val twinCardsState: TwinCardsState? = null,
val mainWarningsList: List<WarningMessage> = mutableListOf()
val mainWarningsList: List<WarningMessage> = mutableListOf(),
val wallets: List<WalletData> = emptyList(),
val walletManagers: List<WalletManager> = emptyList(),
val isMultiwalletAllowed: Boolean = false,
val cardCurrency: CryptoCurrencyName? = null,
val selectedWallet: CryptoCurrencyName? = null,
val primaryBlockchain: Blockchain? = null,
val primaryToken: Token? = null
) : StateType {
val showDetails: Boolean =
currencyData.status != com.tangem.tap.features.wallet.ui.BalanceStatus.EmptyCard &&
currencyData.status != com.tangem.tap.features.wallet.ui.BalanceStatus.UnknownBlockchain
val showMultipleAddress: Boolean
get() {
val listOfAddresses = walletAddresses?.list ?: return false
return (wallet?.blockchain == Blockchain.Bitcoin ||
wallet?.blockchain == Blockchain.BitcoinTestnet ||
wallet?.blockchain == Blockchain.CardanoShelley) &&
listOfAddresses.size > 1
val primaryWallet = if (wallets.isNotEmpty()) wallets[0] else null
// val primaryWalletManager = if (walletManagers.isNotEmpty()) walletManagers[0] else null
val shouldShowDetails: Boolean =
primaryWallet?.currencyData?.status != com.tangem.tap.features.wallet.ui.BalanceStatus.EmptyCard &&
primaryWallet?.currencyData?.status != com.tangem.tap.features.wallet.ui.BalanceStatus.UnknownBlockchain
fun getWalletManager(currencyName: CryptoCurrencyName?): WalletManager? {
if (currencyName == null) return null
val walletManager = walletManagers.find { it.wallet.blockchain.currency == currencyName }
if (walletManager != null) return walletManager
val ethereumWalletManager = walletManagers.find { it.wallet.blockchain == Blockchain.Ethereum }
return if (ethereumWalletManager?.presetTokens?.find { it.symbol == currencyName } != null) {
ethereumWalletManager
} else {
null
}
}
fun getWalletData(currencyName: CryptoCurrencyName?): WalletData? {
if (currencyName == null) return null
return wallets.find { it.currencyData.currencySymbol == currencyName }
}
fun getSelectedWalletData(): WalletData? {
return wallets.find { it.currencyData.currencySymbol == selectedWallet }
}
fun canBeRemoved(walletData: WalletData?): Boolean {
if (walletData == null) return false
if (walletData.blockchain != store.state.walletState.primaryBlockchain
&& (walletData.token == null || walletData.token != store.state.walletState.primaryToken)) {
val walletManager = getWalletManager(walletData.currencyData.currencySymbol)
?: return true
val wallet = walletManager.wallet
if (walletData.blockchain != null) {
return wallet.recentTransactions.toPendingTransactions(wallet.address).isEmpty() &&
wallet.amounts.toSendableAmounts().isEmpty()
} else if (walletData.token != null) (
return wallet.recentTransactions.toPendingTransactionsForToken(
walletData.token, wallet.address).isEmpty()
&& wallet.amounts[AmountType.Token(token = walletData.token)]?.isAboveZero() != true
)
}
return false
}
fun replaceWalletInWallets(walletData: WalletData?): List<WalletData> {
if (walletData == null) return wallets
return wallets.filter { it.currencyData.currency != walletData.currencyData.currency } + walletData
}
fun replaceSomeWallets(newWallets: List<WalletData>): List<WalletData> {
val currencies = newWallets.map { it.currencyData.currency }
return wallets.filter { !currencies.contains(it.currencyData.currency) } + newWallets
}
}
sealed class WalletDialog {
@ -49,24 +100,13 @@ sealed class WalletDialog {
val qrCode: Bitmap?, val shareUrl: String?, val currencyName: CryptoCurrencyName?
) : WalletDialog()
data class CreatePayIdDialog(val creatingPayIdState: CreatingPayIdState?) : WalletDialog()
data class SelectAmountToSendDialog(val amounts: List<Amount>?) : WalletDialog()
data class TwinsOnboardingFragment(val secondCardId: String): WalletDialog()
}
enum class ProgressState { Loading, Done, Error }
enum class ErrorType { NoInternetConnection }
enum class PayIdState { Disabled, Loading, NotCreated, Created, ErrorLoading }
data class PayIdData(
val payIdState: PayIdState = PayIdState.Loading,
val payId: String? = null
)
enum class CreatingPayIdState { EnterPayId, Waiting }
sealed class WalletMainButton(enabled: Boolean) : Button(enabled) {
class SendButton(enabled: Boolean) : WalletMainButton(enabled)
class CreateWalletButton(enabled: Boolean) : WalletMainButton(enabled)
@ -81,7 +121,8 @@ data class AddressData(
val address: String,
val type: AddressType,
val shareUrl: String,
val exploreUrl: String
val exploreUrl: String,
val qrCode: Bitmap? = null
)
data class Artwork(
@ -110,4 +151,27 @@ data class TwinCardsState(
val cardNumber: TwinCardNumber?,
val showTwinOnboarding: Boolean,
val isCreatingTwinCardsAllowed: Boolean
)
)
data class WalletData(
val pendingTransactions: List<PendingTransaction> = emptyList(),
val hashesCountVerified: Boolean? = null,
val walletAddresses: WalletAddresses? = null,
val currencyData: BalanceWidgetData = BalanceWidgetData(),
val updatingWallet: Boolean = false,
val topUpState: TopUpState = TopUpState(),
val allowToSend: Boolean = true,
val fiatRateString: String? = null,
val fiatRate: BigDecimal? = null,
val mainButton: WalletMainButton = WalletMainButton.SendButton(false),
val token: Token? = null,
val blockchain: Blockchain? = null,
) {
fun shouldShowMultipleAddress(): Boolean {
val listOfAddresses = walletAddresses?.list ?: return false
return (currencyData.currencySymbol == Blockchain.Bitcoin.currency ||
currencyData.currencySymbol == Blockchain.BitcoinTestnet.currency ||
currencyData.currencySymbol == Blockchain.CardanoShelley.currency) &&
listOfAddresses.size > 1
}
}

View file

@ -0,0 +1,103 @@
package com.tangem.tap.features.wallet.redux.middlewares
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.SignatureCountValidator
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.commands.common.card.Card
import com.tangem.commands.common.card.CardType
import com.tangem.common.extensions.getType
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.tasks.isMultiwalletAllowed
import com.tangem.tap.domain.twins.isTwinCard
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class CheckSignedHashesMiddleware {
fun handle(action: WalletAction.CheckSignedHashes, globalState: GlobalState?) {
when (action) {
is WalletAction.CheckSignedHashes.CheckIfWarningNeeded -> {
val validator = globalState?.scanNoteResponse?.walletManager as? SignatureCountValidator
globalState?.scanNoteResponse?.card?.let { card ->
globalState.warningManager?.removeWarnings(WarningMessage.Origin.Local)
if (card.getType() != CardType.Release) addWarningMessage(WarningMessagesManager.devCardWarning())
if (!preferencesStorage.wasCardScannedBefore(card.cardId)) {
checkIfWarningNeeded(card, validator)?.let { addWarningMessage(it) }
}
updateWarningMessages()
}
}
is WalletAction.CheckSignedHashes.CheckHashesCountOnline -> checkHashesCountOnline()
is WalletAction.CheckSignedHashes.SaveCardId -> {
val cardId = globalState?.scanNoteResponse?.card?.cardId
cardId?.let { preferencesStorage.saveScannedCardId(it) }
}
}
}
private fun checkIfWarningNeeded(
card: Card, signatureCountValidator: SignatureCountValidator? = null,
): WarningMessage? {
if (card.isTwinCard() || card.isMultiwalletAllowed) return null
return if (signatureCountValidator == null) {
if (card.walletSignedHashes ?: 0 > 0) {
WarningMessagesManager.alreadySignedHashesWarning()
} else {
store.dispatch(WalletAction.CheckSignedHashes.SaveCardId)
null
}
} else {
store.dispatch(WalletAction.CheckSignedHashes.NeedToCheckHashesCountOnline)
null
}
}
private fun checkHashesCountOnline() {
if (store.state.walletState.hashesCountVerified != false) return
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) return
val card = store.state.globalState.scanNoteResponse?.card
if (card == null || preferencesStorage.wasCardScannedBefore(card.cardId)) return
if (card.isTwinCard() || card.isMultiwalletAllowed) return
val validator = store.state.globalState.scanNoteResponse?.walletManager
as? SignatureCountValidator
scope.launch {
val result = validator?.validateSignatureCount(card.walletSignedHashes ?: 0)
withContext(Dispatchers.Main) {
when (result) {
SimpleResult.Success -> {
store.dispatch(WalletAction.CheckSignedHashes.ConfirmHashesCount)
store.dispatch(WalletAction.CheckSignedHashes.SaveCardId)
}
is SimpleResult.Failure ->
if (result.error is BlockchainSdkError.SignatureCountNotMatched) {
addWarningMessage(WarningMessagesManager.alreadySignedHashesWarning(), true)
} else if (card.walletSignedHashes ?: 0 > 0) {
addWarningMessage(WarningMessagesManager.alreadySignedHashesWarning(), true)
}
}
}
}
}
private fun addWarningMessage(warning: WarningMessage, autoUpdate: Boolean = false) {
store.state.globalState.warningManager?.addWarning(warning)
if (autoUpdate) updateWarningMessages()
}
private fun updateWarningMessages() {
val warningManager = store.state.globalState.warningManager ?: return
store.dispatch(WalletAction.SetWarnings(warningManager.getWarnings(WarningMessage.Location.MainScreen)))
}
}

View file

@ -0,0 +1,72 @@
package com.tangem.tap.features.wallet.redux.middlewares
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.TokenManager
import com.tangem.blockchain.extensions.Result
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.currenciesRepository
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class MultiWalletMiddleware {
fun handle(
action: WalletAction.MultiWallet, walletState: WalletState?, globalState: GlobalState?
) {
when (action) {
is WalletAction.MultiWallet.SelectWallet -> {
if (action.walletData != null) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.WalletDetails))
}
}
is WalletAction.MultiWallet.AddToken -> {
globalState?.scanNoteResponse?.card?.cardId?.let {
currenciesRepository.saveAddedToken(it, action.token)
}
addToken(action.token, walletState)
}
is WalletAction.MultiWallet.AddTokens -> {
action.tokens.map { addToken(it, walletState) }
}
is WalletAction.MultiWallet.AddBlockchain -> {
globalState?.scanNoteResponse?.card?.cardId?.let {
currenciesRepository.saveAddedBlockchain(it, action.blockchain)
}
store.dispatch(WalletAction.LoadFiatRate(currency = action.blockchain.currency))
}
is WalletAction.MultiWallet.RemoveWallet -> {
val cardId = globalState?.scanNoteResponse?.card?.cardId
if (action.walletData.token != null) {
cardId?.let { currenciesRepository.removeToken(it, action.walletData.token) }
} else if (action.walletData.blockchain != null) {
cardId?.let { currenciesRepository.removeBlockchain(it, action.walletData.blockchain) }
}
}
}
}
private fun addToken(token: Token, walletState: WalletState?) {
val tokenManager =
(walletState?.getWalletManager(Blockchain.Ethereum.currency) as? TokenManager)
scope.launch {
val result = tokenManager?.addToken(token)
withContext(Dispatchers.Main) {
when (result) {
is Result.Success -> {
store.dispatch(WalletAction.LoadFiatRate(currency = token.symbol))
store.dispatch(WalletAction.MultiWallet.TokenLoaded(result.data))
}
is Result.Failure -> null
null -> null
}
}
}
}
}

View file

@ -0,0 +1,29 @@
package com.tangem.tap.features.wallet.redux.middlewares
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
import com.tangem.tap.domain.TopUpHelper
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
class TopUpMiddleware {
fun handle(action: WalletAction.TopUpAction) {
when (action) {
is WalletAction.TopUpAction.TopUp -> {
val selectedWalletData = store.state.walletState.getSelectedWalletData()
val config = store.state.globalState.configManager?.config ?: return
val defaultAddress = selectedWalletData?.walletAddresses!!.list[0].address
val url = TopUpHelper.getUrl(
selectedWalletData.currencyData.currencySymbol!!,
defaultAddress,
config.moonPayApiKey,
config.moonPayApiSecretKey
)
val customTabsIntent = CustomTabsIntent.Builder()
.setToolbarColor(action.toolbarColor)
.build()
customTabsIntent.launchUrl(action.context, Uri.parse(url));
}
}
}
}

View file

@ -0,0 +1,19 @@
package com.tangem.tap.features.wallet.redux.middlewares
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.preferencesStorage
import com.tangem.tap.store
class TwinsMiddleware {
fun handle(action: WalletAction.TwinsAction) {
when (action) {
is WalletAction.TwinsAction.SetTwinCard -> {
val showOnboarding = !preferencesStorage.wasTwinsOnboardingShown()
if (showOnboarding) store.dispatch(WalletAction.TwinsAction.ShowOnboarding)
}
WalletAction.TwinsAction.SetOnboardingShown -> {
preferencesStorage.saveTwinsOnboardingShown()
}
}
}
}

View file

@ -0,0 +1,230 @@
package com.tangem.tap.features.wallet.redux.middlewares
import android.content.Intent
import android.net.Uri
import androidx.core.content.ContextCompat
import com.tangem.blockchain.common.*
import com.tangem.common.CompletionResult
import com.tangem.tap.*
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.common.extensions.copyToClipboard
import com.tangem.tap.common.extensions.shareText
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.extensions.toSendableAmounts
import com.tangem.tap.domain.twins.TwinsHelper
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.twins.CreateTwinWallet
import com.tangem.tap.features.send.redux.PrepareSendScreen
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletData
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.network.NetworkStateChanged
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
class WalletMiddleware {
private val topUpMiddleware = TopUpMiddleware()
private val twinsMiddleware = TwinsMiddleware()
private val checkSignedHashesMiddleware = CheckSignedHashesMiddleware()
private val multiWalletMiddleware = MultiWalletMiddleware()
val walletMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
val globalState = state()?.globalState
val walletState = state()?.walletState
when (action) {
is WalletAction.TopUpAction -> topUpMiddleware.handle(action)
is WalletAction.TwinsAction -> twinsMiddleware.handle(action)
is WalletAction.CheckSignedHashes ->
checkSignedHashesMiddleware.handle(action, globalState)
is WalletAction.MultiWallet ->
multiWalletMiddleware.handle(action, walletState, globalState)
is WalletAction.LoadWallet -> {
scope.launch {
if (action.currency == null) {
walletState?.walletManagers?.map { walletManager ->
globalState?.tapWalletManager?.loadWalletData(walletManager)
}
} else {
val walletManager = walletState?.getWalletManager(action.currency)
walletManager?.let { globalState?.tapWalletManager?.loadWalletData(it) }
}
}
}
is WalletAction.LoadFiatRate -> {
scope.launch {
if (action.wallet != null) {
globalState?.tapWalletManager?.loadFiatRate(
globalState.appCurrency, action.wallet
)
} else if (action.currency != null) {
globalState?.tapWalletManager?.loadFiatRate(
globalState.appCurrency, action.currency
)
} else {
walletState?.wallets?.filter { it.blockchain != null }
?.map { walletState.getWalletManager(it.blockchain?.currency) }
?.mapNotNull { walletManager ->
walletManager?.let {
globalState?.tapWalletManager?.loadFiatRate(
globalState.appCurrency, walletManager.wallet
)
}
}
}
}
}
is WalletAction.CreateWallet -> {
if (walletState?.twinCardsState != null) {
store.dispatch(DetailsAction.CreateTwinWalletAction.ShowWarning(
globalState?.scanNoteResponse?.card?.cardId?.let {
TwinsHelper.getTwinCardNumber(it)
},
CreateTwinWallet.CreateWallet
))
} else {
scope.launch {
val result = tangemSdkManager.createWallet(
globalState?.scanNoteResponse?.card?.cardId
)
when (result) {
is CompletionResult.Success -> {
globalState?.tapWalletManager?.onCardScanned(result.data)
}
}
}
}
}
is WalletAction.UpdateWallet -> {
if (action.currency != null) {
scope.launch {
val walletManager = walletState?.getWalletManager(action.currency)
walletManager?.let { globalState?.tapWalletManager?.updateWallet(it) }
}
} else {
scope.launch {
if (walletState?.state == ProgressState.Done) {
walletState.walletManagers.map { walletManager ->
globalState?.tapWalletManager?.updateWallet(walletManager)
}
}
}
}
}
is WalletAction.LoadWallet.Success -> {
store.dispatch(WalletAction.CheckSignedHashes.CheckHashesCountOnline)
}
is WalletAction.Scan -> {
scope.launch {
val result = tangemSdkManager.scanNote(FirebaseAnalyticsHandler)
when (result) {
is CompletionResult.Success -> {
tangemSdkManager.changeDisplayedCardIdNumbersCount(result.data.card)
globalState?.tapWalletManager
?.onCardScanned(result.data, true)
if (walletState?.twinCardsState != null) {
val showOnboarding = !preferencesStorage.wasTwinsOnboardingShown()
if (showOnboarding) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.TwinsOnboarding))
}
}
}
}
}
}
is WalletAction.LoadData -> {
scope.launch {
val scanNoteResponse = globalState?.scanNoteResponse ?: return@launch
if (!walletState?.wallets.isNullOrEmpty()) {
globalState.tapWalletManager.reloadData(scanNoteResponse)
} else {
globalState.tapWalletManager.loadData(scanNoteResponse)
}
}
}
is NetworkStateChanged -> {
globalState?.scanNoteResponse?.let { scanNoteResponse ->
store.dispatch(WalletAction.CheckSignedHashes.CheckHashesCountOnline)
scope.launch {
globalState.tapWalletManager.loadData(scanNoteResponse)
}
}
}
is WalletAction.CopyAddress -> {
action.context.copyToClipboard(action.address)
store.dispatch(WalletAction.CopyAddress.Success)
}
is WalletAction.ShareAddress -> {
action.context.shareText(action.address)
}
is WalletAction.ExploreAddress -> {
val uri = Uri.parse(action.exploreUrl)
val intent = Intent(Intent.ACTION_VIEW, uri)
ContextCompat.startActivity(action.context, intent, null)
}
is WalletAction.Send -> {
val newAction = prepareSendAction(action.amount, state()?.walletState)
store.dispatch(newAction)
if (newAction is PrepareSendScreen) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Send))
}
}
}
next(action)
}
}
}
private fun prepareSendAction(amount: Amount?, state: WalletState?): Action {
val selectedWalletData = state?.getSelectedWalletData()
val currency = selectedWalletData?.currencyData?.currencySymbol
val wallet = currency?.let { state.getWalletManager(currency)?.wallet }
return if (amount != null) {
if (amount.type is AmountType.Token) {
prepareSendActionForToken(amount, state, selectedWalletData, wallet)
} else {
PrepareSendScreen(amount, selectedWalletData?.fiatRate)
}
} else {
val amounts = wallet?.amounts?.toSendableAmounts()
if (currency != null && state.isMultiwalletAllowed) {
val amountToSend = amounts?.find { it.currencySymbol == currency }
?: return WalletAction.Send.ChooseCurrency(amounts)
if (amountToSend.type is AmountType.Token) {
prepareSendActionForToken(amount, state, selectedWalletData, wallet)
} else {
PrepareSendScreen(amountToSend, selectedWalletData.fiatRate)
}
} else {
if (amounts?.size ?: 0 > 1) {
WalletAction.Send.ChooseCurrency(amounts)
} else {
val amountToSend = amounts?.first()
PrepareSendScreen(amountToSend, selectedWalletData?.fiatRate)
}
}
}
}
private fun prepareSendActionForToken(
amount: Amount?, state: WalletState?, selectedWalletData: WalletData?, wallet: Wallet?
): PrepareSendScreen {
val coinRate = state?.getWalletData(wallet?.blockchain?.currency)?.fiatRate
val tokenRate = if (state?.isMultiwalletAllowed == true) {
selectedWalletData?.fiatRate
} else {
selectedWalletData?.currencyData?.token?.fiatRate
}
return PrepareSendScreen(
wallet?.amounts?.get(AmountType.Coin), coinRate,
amount, tokenRate)
}
}

View file

@ -0,0 +1,160 @@
package com.tangem.tap.features.wallet.redux.reducers
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.isZero
import com.tangem.tap.common.extensions.toFiatString
import com.tangem.tap.common.extensions.toFormattedCurrencyString
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.features.wallet.models.removeUnknownTransactions
import com.tangem.tap.features.wallet.models.toPendingTransactions
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import com.tangem.tap.features.wallet.ui.TokenData
import com.tangem.tap.store
class MultiWalletReducer {
fun reduce(action: WalletAction.MultiWallet, state: WalletState): WalletState {
return when (action) {
is WalletAction.MultiWallet.AddWalletManagers -> {
state.copy(
walletManagers = action.walletManagers
)
}
is WalletAction.MultiWallet.AddBlockchains -> {
val wallets = action.blockchains.map { blockchain ->
val wallet = state.getWalletManager(blockchain.currency)?.wallet
val cardToken = if (!state.isMultiwalletAllowed) {
wallet?.getFirstToken()?.symbol?.let { TokenData("", tokenSymbol = it) }
} else {
null
}
WalletData(
currencyData = BalanceWidgetData(
BalanceStatus.Loading,
blockchain.fullName,
currencySymbol = blockchain.currency,
token = cardToken
),
walletAddresses = createAddressList(wallet),
mainButton = WalletMainButton.SendButton(false),
topUpState = TopUpState(allowed = false),
blockchain = blockchain
)
}
val selectedWallet = if (!state.isMultiwalletAllowed) {
wallets[0].currencyData.currencySymbol
} else {
state.selectedWallet
}
state.copy(
wallets = wallets,
selectedWallet = selectedWallet
)
}
is WalletAction.MultiWallet.AddBlockchain -> {
val wallet = state.getWalletManager(action.blockchain.currency)?.wallet
val walletData = WalletData(
currencyData = BalanceWidgetData(
BalanceStatus.Loading,
action.blockchain.fullName,
currencySymbol = action.blockchain.currency,
),
walletAddresses = createAddressList(wallet),
mainButton = WalletMainButton.SendButton(false),
topUpState = TopUpState(allowed = false),
blockchain = action.blockchain
)
val newState = state.copy(wallets = state.replaceWalletInWallets(walletData))
if (wallet != null && wallet.amounts[AmountType.Coin]?.value != null) {
OnWalletLoadedReducer().reduce(wallet, newState)
} else {
newState
}
}
is WalletAction.MultiWallet.AddTokens -> {
if (!state.isMultiwalletAllowed) return state
val walletAddresses = createAddressList(state.getWalletManager(Blockchain.Ethereum.currency)?.wallet)
val wallets = action.tokens.map { token ->
WalletData(
currencyData = BalanceWidgetData(
BalanceStatus.Loading,
currency = token.name,
currencySymbol = token.symbol
),
walletAddresses = walletAddresses,
mainButton = WalletMainButton.SendButton(false),
topUpState = TopUpState(allowed = false),
token = token
)
}
state.copy(wallets = state.wallets + wallets)
}
is WalletAction.MultiWallet.AddToken -> {
if (!state.isMultiwalletAllowed) return state
val walletAddresses = createAddressList(state.getWalletManager(Blockchain.Ethereum.currency)?.wallet)
val wallet = WalletData(
currencyData = BalanceWidgetData(
BalanceStatus.Loading,
currency = action.token.name,
currencySymbol = action.token.symbol
),
walletAddresses = walletAddresses,
mainButton = WalletMainButton.SendButton(false),
topUpState = TopUpState(allowed = false),
token = action.token
)
val wallets = state.replaceWalletInWallets(wallet)
state.copy(wallets = wallets)
}
is WalletAction.MultiWallet.TokenLoaded -> {
val pendingTransactions = state.getWalletManager(Blockchain.Ethereum.currency)
?.wallet?.let { wallet ->
wallet.recentTransactions.toPendingTransactions(wallet.address)
} ?: emptyList()
val sendButtonEnabled = action.amount.value?.isZero() == false && pendingTransactions.isEmpty()
val balanceStatus = if (pendingTransactions.isNotEmpty()) {
BalanceStatus.TransactionInProgress
} else {
BalanceStatus.VerifiedOnline
}
val tokenWalletData = state.getWalletData(action.amount.currencySymbol)
val newTokenWalletData = tokenWalletData?.copy(
currencyData = tokenWalletData.currencyData.copy(
status = balanceStatus,
amount = action.amount.value?.toFormattedCurrencyString(
action.amount.decimals, action.amount.currencySymbol
),
fiatAmount = tokenWalletData.fiatRate?.let {
action.amount.value
?.toFiatString(it, store.state.globalState.appCurrency)
}
),
pendingTransactions = pendingTransactions.removeUnknownTransactions(),
mainButton = WalletMainButton.SendButton(sendButtonEnabled)
)
val wallets = state.replaceWalletInWallets(newTokenWalletData)
state.copy(wallets = wallets)
}
is WalletAction.MultiWallet.SetIsMultiwalletAllowed ->
state.copy(isMultiwalletAllowed = action.isMultiwalletAllowed)
is WalletAction.MultiWallet.SelectWallet ->
state.copy(selectedWallet = action.walletData?.currencyData?.currencySymbol)
is WalletAction.MultiWallet.RemoveWallet -> {
state.copy(wallets = state.wallets.filterNot {
it.currencyData.currencySymbol == action.walletData.currencyData.currencySymbol
})
}
is WalletAction.MultiWallet.SetPrimaryBlockchain ->
state.copy(primaryBlockchain = action.blockchain)
is WalletAction.MultiWallet.SetPrimaryToken ->
state.copy(primaryToken = action.token)
}
}
}

View file

@ -0,0 +1,182 @@
package com.tangem.tap.features.wallet.redux.reducers
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Wallet
import com.tangem.common.extensions.isZero
import com.tangem.tap.common.extensions.toFiatString
import com.tangem.tap.common.extensions.toFormattedCurrencyString
import com.tangem.tap.common.extensions.toFormattedString
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.features.wallet.models.removeUnknownTransactions
import com.tangem.tap.features.wallet.models.toPendingTransactions
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletData
import com.tangem.tap.features.wallet.redux.WalletMainButton
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import com.tangem.tap.features.wallet.ui.TokenData
import com.tangem.tap.store
import java.math.RoundingMode
class OnWalletLoadedReducer {
fun reduce(wallet: Wallet, walletState: WalletState, topUpAllowed: Boolean? = null): WalletState {
if (!walletState.isMultiwalletAllowed) {
return onSingleWalletLoaded(wallet, walletState, topUpAllowed)
} else {
val fiatCurrencySymbol = store.state.globalState.appCurrency
val amount = wallet.amounts[AmountType.Coin]?.value
if (walletState.getWalletData(wallet.blockchain.currency) == null && amount?.isZero() != false) {
return walletState
}
if (wallet.blockchain != Blockchain.Ethereum) {
val formattedAmount = amount?.toFormattedCurrencyString(
wallet.blockchain.decimals(),
wallet.blockchain.currency)
val pendingTransactions = wallet.recentTransactions
.toPendingTransactions(wallet.address)
val sendButtonEnabled = amount?.isZero() == false && pendingTransactions.isEmpty()
val balanceStatus = if (pendingTransactions.isNotEmpty()) {
BalanceStatus.TransactionInProgress
} else {
BalanceStatus.VerifiedOnline
}
val walletData = walletState.getWalletData(wallet.blockchain.currency)
?: WalletData()
val newWalletData = walletData.copy(
currencyData = walletData.currencyData.copy(
status = balanceStatus, currency = wallet.blockchain.fullName,
currencySymbol = wallet.blockchain.currency,
amount = formattedAmount,
fiatAmount = walletData.fiatRate?.let { amount?.toFiatString(it, fiatCurrencySymbol) }
),
pendingTransactions = pendingTransactions.removeUnknownTransactions(),
mainButton = WalletMainButton.SendButton(sendButtonEnabled)
)
val wallets = walletState.replaceWalletInWallets(newWalletData)
val state = if (wallets.any { it.currencyData.status == BalanceStatus.Loading }) {
ProgressState.Loading
} else {
ProgressState.Done
}
return walletState.copy(
state = state, wallets = wallets
)
} else {
val formattedAmount = amount?.toFormattedCurrencyString(
wallet.blockchain.decimals(),
wallet.blockchain.currency)
val pendingTransactions = wallet.recentTransactions
.toPendingTransactions(wallet.address)
val sendButtonEnabled = amount?.isZero() == false && pendingTransactions.isEmpty()
val balanceStatus = if (pendingTransactions.isNotEmpty()) {
BalanceStatus.TransactionInProgress
} else {
BalanceStatus.VerifiedOnline
}
val ethereumWalletData = walletState.getWalletData(wallet.blockchain.currency)
?: WalletData()
val newEthereumWalletData = ethereumWalletData.copy(
currencyData = ethereumWalletData.currencyData.copy(
status = balanceStatus,
amount = formattedAmount,
currency = wallet.blockchain.fullName,
currencySymbol = wallet.blockchain.currency,
fiatAmount = ethereumWalletData.fiatRate?.let {
amount?.toFiatString(it, fiatCurrencySymbol)
}
),
pendingTransactions = pendingTransactions.removeUnknownTransactions(),
mainButton = WalletMainButton.SendButton(sendButtonEnabled)
)
val tokens = wallet.getTokens().mapNotNull { token ->
val tokenWalletData = walletState.getWalletData(token.symbol)
tokenWalletData?.copy(
currencyData = tokenWalletData.currencyData.copy(
status = balanceStatus,
amount = wallet.getTokenAmount(token)?.value?.toFormattedCurrencyString(
token.decimals, token.symbol
),
fiatAmount = tokenWalletData.fiatRate?.let {
wallet.getTokenAmount(token)?.value
?.toFiatString(it, fiatCurrencySymbol)
}
),
pendingTransactions = pendingTransactions.removeUnknownTransactions(),
mainButton = WalletMainButton.SendButton(sendButtonEnabled)
)
}
val wallets = walletState.replaceSomeWallets((tokens + newEthereumWalletData).filterNotNull())
val state = if (wallets.any { it.currencyData.status == BalanceStatus.Loading }) {
ProgressState.Loading
} else {
ProgressState.Done
}
return walletState.copy(
state = state, wallets = wallets
)
}
}
}
private fun onSingleWalletLoaded(wallet: Wallet, walletState: WalletState, topUpAllowed: Boolean? = null
): WalletState {
if (wallet.blockchain != walletState.primaryBlockchain) return walletState
val fiatCurrencySymbol = store.state.globalState.appCurrency
val token = wallet.getFirstToken()
val tokenData = if (token != null) {
val tokenAmount = wallet.getTokenAmount(token)
if (tokenAmount != null) {
val tokenFiatRate = walletState.primaryWallet?.currencyData?.token?.fiatRate
val tokenFiatAmount = tokenFiatRate?.let { tokenAmount.value?.toFiatString(it, fiatCurrencySymbol) }
TokenData(tokenAmount.value?.toFormattedString(tokenAmount.decimals) ?: "",
tokenAmount.currencySymbol, tokenFiatAmount)
} else {
null
}
} else {
null
}
val amount = wallet.amounts[AmountType.Coin]?.value
val formattedAmount = amount?.toFormattedCurrencyString(
wallet.blockchain.decimals(),
wallet.blockchain.currency)
val fiatRate = walletState.primaryWallet?.fiatRate
val fiatAmountRaw = fiatRate?.multiply(amount)?.setScale(2, RoundingMode.DOWN)
val fiatAmount = fiatRate?.let { amount?.toFiatString(it, fiatCurrencySymbol) }
val pendingTransactions = wallet.recentTransactions
.toPendingTransactions(wallet.address)
val sendButtonEnabled = amount?.isZero() == false && pendingTransactions.isEmpty()
val balanceStatus = if (pendingTransactions.isNotEmpty()) {
BalanceStatus.TransactionInProgress
} else {
BalanceStatus.VerifiedOnline
}
val walletData = walletState.primaryWallet?.copy(
currencyData = BalanceWidgetData(
balanceStatus, wallet.blockchain.fullName,
currencySymbol = wallet.blockchain.currency,
formattedAmount,
token = tokenData,
fiatAmount = fiatAmount,
fiatAmountRaw = fiatAmountRaw
),
pendingTransactions = pendingTransactions.removeUnknownTransactions(),
mainButton = WalletMainButton.SendButton(sendButtonEnabled)
)
val wallets = walletData?.let { listOf(walletData) } ?: emptyList()
return walletState.copy(
state = ProgressState.Done, wallets = wallets
)
}
}

View file

@ -0,0 +1,36 @@
package com.tangem.tap.features.wallet.redux.reducers
import com.tangem.tap.features.wallet.redux.TwinCardsState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
class TwinsReducer {
fun reduce(action: WalletAction.TwinsAction, state: WalletState): WalletState {
return when (action) {
is WalletAction.TwinsAction.SetTwinCard -> {
state.copy(
twinCardsState = TwinCardsState(
secondCardId = action.secondCardId,
cardNumber = action.number,
showTwinOnboarding = state.twinCardsState?.showTwinOnboarding
?: false,
isCreatingTwinCardsAllowed = action.isCreatingTwinCardsAllowed
)
)
}
is WalletAction.TwinsAction.ShowOnboarding -> {
state.copy(
twinCardsState = state.twinCardsState?.copy(showTwinOnboarding = true)
?: TwinCardsState(null, null,
showTwinOnboarding = true,
isCreatingTwinCardsAllowed = false)
)
}
is WalletAction.TwinsAction.SetOnboardingShown -> {
state.copy(
twinCardsState = state.twinCardsState?.copy(showTwinOnboarding = false)
)
}
}
}
}

View file

@ -8,6 +8,7 @@ import com.tangem.wallet.R
import kotlinx.android.synthetic.main.card_balance.*
import kotlinx.android.synthetic.main.layout_balance.*
import kotlinx.android.synthetic.main.layout_balance_error.*
import java.math.BigDecimal
enum class BalanceStatus {
VerifiedOnline,
@ -25,6 +26,7 @@ data class BalanceWidgetData(
val currencySymbol: String? = null,
val amount: String? = null,
val fiatAmount: String? = null,
val fiatAmountRaw: BigDecimal? = null,
val token: TokenData? = null,
val amountToCreateAccount: String? = null,
val errorMessage: String? = null
@ -33,14 +35,16 @@ data class BalanceWidgetData(
data class TokenData(
val amount: String,
val tokenSymbol: String,
val fiatAmount: String? = null
val fiatAmount: String? = null,
val fiatRateString: String? = null,
val fiatRate: BigDecimal? = null,
)
class BalanceWidget(
val fragment: Fragment,
val data: BalanceWidgetData,
val isTwinCard: Boolean,
private val isTwinCard: Boolean,
) {
fun setup() {

View file

@ -0,0 +1,50 @@
package com.tangem.tap.features.wallet.ui
import android.view.View
import com.tangem.blockchain.blockchains.bitcoin.BitcoinAddressType
import com.tangem.blockchain.blockchains.cardano.CardanoAddressType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.address.AddressType
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.wallet.R
class MultipleAddressUiHelper {
companion object {
fun typeToId(type: AddressType): Int {
return when (type) {
is BitcoinAddressType.Legacy -> R.id.chip_legacy
is BitcoinAddressType.Segwit -> R.id.chip_default
is CardanoAddressType.Byron -> R.id.chip_legacy
is CardanoAddressType.Shelley -> R.id.chip_default
else -> View.NO_ID
}
}
fun idToType(id: Int, currencyName: CryptoCurrencyName?): AddressType? {
val blockchain = currencyName?.let { currency ->
Blockchain.fromCurrency(currency)
}
return idToType(id, blockchain)
}
fun idToType(id: Int, blockchain: Blockchain?): AddressType? {
return when (id) {
R.id.chip_default -> {
when (blockchain) {
Blockchain.Bitcoin -> BitcoinAddressType.Segwit
Blockchain.CardanoShelley -> CardanoAddressType.Shelley
else -> null
}
}
R.id.chip_legacy -> {
when (blockchain) {
Blockchain.Bitcoin -> BitcoinAddressType.Legacy
Blockchain.CardanoShelley -> CardanoAddressType.Byron
else -> null
}
}
else -> null
}
}
}
}

View file

@ -0,0 +1,289 @@
package com.tangem.tap.features.wallet.ui
import android.app.Dialog
import android.os.Bundle
import android.view.*
import androidx.activity.OnBackPressedCallback
import androidx.annotation.IdRes
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.transition.TransitionInflater
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletData
import com.tangem.tap.features.wallet.redux.WalletDialog
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.ui.adapters.PendingTransactionsAdapter
import com.tangem.tap.features.wallet.ui.dialogs.AmountToSendDialog
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_details_twin_cards.*
import kotlinx.android.synthetic.main.fragment_wallet.*
import kotlinx.android.synthetic.main.fragment_wallet_details.*
import kotlinx.android.synthetic.main.fragment_wallet_details.rv_pending_transaction
import kotlinx.android.synthetic.main.fragment_wallet_details.toolbar
import kotlinx.android.synthetic.main.item_currency_wallet.view.*
import kotlinx.android.synthetic.main.layout_balance_error.*
import kotlinx.android.synthetic.main.layout_balance_wallet_details.*
import kotlinx.android.synthetic.main.layout_wallet_details.*
import org.rekotlin.StoreSubscriber
class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), StoreSubscriber<WalletState> {
private lateinit var pendingTransactionAdapter: PendingTransactionsAdapter
private var dialog: Dialog? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(WalletAction.MultiWallet.SelectWallet(null))
store.dispatch(NavigationAction.PopBackTo())
}
})
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.walletState == newState.walletState
}.select { it.walletState }
}
store.dispatch(WalletAction.UpdateWallet())
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(activity as? AppCompatActivity)?.setSupportActionBar(toolbar)
toolbar.setNavigationOnClickListener { activity?.onBackPressed() }
setupTransactionsRecyclerView()
setupButtons()
}
private fun setupTransactionsRecyclerView() {
pendingTransactionAdapter = PendingTransactionsAdapter()
rv_pending_transaction.layoutManager = LinearLayoutManager(requireContext())
rv_pending_transaction.adapter = pendingTransactionAdapter
}
private fun setupButtons() {
btn_confirm.text = getString(R.string.wallet_button_send)
btn_confirm.setOnClickListener { store.dispatch(WalletAction.Send()) }
btn_share.setOnClickListener { store.dispatch(WalletAction.ShowQrCode) }
btn_top_up.setOnClickListener {
store.dispatch(
WalletAction.TopUpAction.TopUp(requireContext(), R.color.backgroundLightGray)
)
}
}
override fun newState(state: WalletState) {
if (activity == null) return
if (state.selectedWallet == null) return
val selectedWallet = state.getSelectedWalletData() ?: return
tv_currency_title.text = selectedWallet.currencyData.currency
showPendingTransactionsIfPresent(selectedWallet.pendingTransactions)
setupAddressCard(selectedWallet)
setupBalanceData(selectedWallet.currencyData)
btn_confirm.isEnabled = selectedWallet.mainButton.enabled
btn_copy.setOnClickListener {
selectedWallet.walletAddresses?.selectedAddress?.address?.let { addressString ->
store.dispatch(WalletAction.CopyAddress(addressString, requireContext()))
}
}
btn_share.setOnClickListener {
selectedWallet.walletAddresses?.selectedAddress?.address?.let { addressString ->
store.dispatch(WalletAction.ShareAddress(addressString, requireContext()))
}
}
handleDialogs(state.walletDialog)
handleCurrencyIcon(selectedWallet)
srl_wallet_details.setOnRefreshListener {
if (selectedWallet.currencyData.status != BalanceStatus.Loading) {
store.dispatch(WalletAction.LoadWallet(
selectedWallet.topUpState.allowed,
selectedWallet.currencyData.currencySymbol
))
}
}
if (selectedWallet.currencyData.status != BalanceStatus.Loading) {
srl_wallet_details.isRefreshing = false
}
}
private fun handleCurrencyIcon(wallet: WalletData) {
val blockchain = wallet.currencyData.currencySymbol?.let { Blockchain.fromCurrency(it) }
if (blockchain != null && blockchain != Blockchain.Unknown) {
tv_token_letter.text = null
iv_currency.colorFilter = null
iv_currency.setImageResource(blockchain.getIconRes())
} else {
tv_token_letter.text = wallet.currencyData.currencySymbol?.take(1)
wallet.token?.getColor()?.let { iv_currency.setColorFilter(it) }
iv_currency.setImageResource(R.drawable.shape_circle)
}
}
private fun showPendingTransactionsIfPresent(pendingTransactions: List<PendingTransaction>) {
pendingTransactionAdapter.submitList(pendingTransactions)
rv_pending_transaction?.show(pendingTransactions.isNotEmpty())
}
private fun setupAddressCard(state: WalletData) {
if (state.walletAddresses != null) {
if (state.shouldShowMultipleAddress()) {
(card_balance as? ViewGroup)?.beginDelayedTransition()
chip_group_address_type.show()
chip_group_address_type.fitChipsByGroupWidth()
val checkedId = MultipleAddressUiHelper.typeToId(state.walletAddresses.selectedAddress.type)
if (checkedId != View.NO_ID) chip_group_address_type.check(checkedId)
chip_group_address_type.setOnCheckedChangeListener { group, checkedId ->
if (checkedId == -1) return@setOnCheckedChangeListener
val type = MultipleAddressUiHelper.idToType(checkedId, state.currencyData.currencySymbol)
type?.let { store.dispatch(WalletAction.ChangeSelectedAddress(type)) }
}
} else {
chip_group_address_type.hide()
}
tv_address.text = state.walletAddresses.selectedAddress.address
tv_explore?.setOnClickListener {
store.dispatch(WalletAction.ExploreAddress(
state.walletAddresses.selectedAddress.exploreUrl,
requireContext()))
}
iv_qr_code.setImageBitmap(state.walletAddresses.selectedAddress.address.toQrCode())
}
}
private fun setupBalanceData(data: BalanceWidgetData) {
when (data.status) {
BalanceStatus.Loading -> {
l_balance.show()
l_balance_error.hide()
tv_fiat_amount.hide()
tv_amount.text = ""
showStatus(R.id.tv_status_loading)
showBalanceWithoutToken(data, false)
}
BalanceStatus.VerifiedOnline, BalanceStatus.TransactionInProgress -> {
l_balance.show()
l_balance_error.hide()
val statusView = if (data.status == BalanceStatus.VerifiedOnline) {
R.id.tv_status_verified
} else {
tv_status_error.text =
getText(R.string.wallet_balance_tx_in_progress)
R.id.group_error
}
showStatus(statusView)
tv_status_error_message.hide()
showBalanceWithoutToken(data, true)
}
BalanceStatus.Unreachable -> {
l_balance.show()
l_balance_error.hide()
tv_fiat_amount.hide()
tv_amount.text = ""
tv_status_error_message.text = data.errorMessage
tv_status_error.text =
getString(R.string.wallet_balance_blockchain_unreachable)
showStatus(R.id.group_error)
tv_status_error_message.show(!data.errorMessage.isNullOrBlank())
}
BalanceStatus.NoAccount -> {
l_balance.hide()
l_balance_error.show()
tv_error_title.text = getText(R.string.wallet_error_no_account)
tv_error_descriptions.text =
getString(
R.string.wallet_error_no_account_subtitle_format,
data.amountToCreateAccount, data.currencySymbol
)
}
}
}
private fun showStatus(@IdRes viewRes: Int) {
group_error.show(viewRes == R.id.group_error)
tv_status_loading.show(viewRes == R.id.tv_status_loading)
tv_status_verified.show(viewRes == R.id.tv_status_verified)
}
private fun showBalanceWithoutToken(data: BalanceWidgetData, showAmount: Boolean) {
tv_amount.text = if (showAmount) data.amount else ""
if (showAmount) {
tv_fiat_amount.show()
tv_fiat_amount.text = data.fiatAmount
}
}
private fun handleDialogs(walletDialog: WalletDialog?) {
when (walletDialog) {
is WalletDialog.SelectAmountToSendDialog -> {
if (dialog == null) dialog = AmountToSendDialog(requireContext()).apply {
this.show(walletDialog.amounts)
}
}
null -> {
dialog?.dismiss()
dialog = null
}
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.meny_remove -> {
store.state.walletState.getSelectedWalletData()?.let { walletData ->
store.dispatch(WalletAction.MultiWallet.RemoveWallet(walletData))
store.dispatch(WalletAction.MultiWallet.SelectWallet(null))
store.dispatch(NavigationAction.PopBackTo())
true
}
false
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
if (store.state.walletState.canBeRemoved(store.state.walletState.getSelectedWalletData())) {
inflater.inflate(R.menu.wallet_details, menu)
}
}
}

View file

@ -1,9 +1,11 @@
package com.tangem.tap.features.wallet.ui
import android.app.Dialog
import android.os.Bundle
import android.view.*
import android.view.Menu
import android.view.Menu.NONE
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
@ -12,39 +14,32 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.transition.TransitionInflater
import com.google.android.material.snackbar.Snackbar
import com.squareup.picasso.Picasso
import com.tangem.blockchain.blockchains.bitcoin.BitcoinAddressType
import com.tangem.blockchain.blockchains.cardano.CardanoAddressType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.address.AddressType
import com.tangem.tangem_sdk_new.extensions.dpToPx
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.ui.dialogs.AmountToSendDialog
import com.tangem.tap.features.wallet.ui.dialogs.PayIdDialog
import com.tangem.tap.features.wallet.ui.dialogs.QrDialog
import com.tangem.tap.features.wallet.ui.adapters.SpacesItemDecoration
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.SingleWalletView
import com.tangem.tap.features.wallet.ui.wallet.WalletView
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.card_balance.*
import kotlinx.android.synthetic.main.fragment_wallet.*
import kotlinx.android.synthetic.main.layout_address.*
import kotlinx.android.synthetic.main.layout_send_fee.*
import kotlinx.android.synthetic.main.layout_wallet_long_buttons.*
import kotlinx.android.synthetic.main.layout_wallet_short_buttons.*
import org.rekotlin.StoreSubscriber
class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<WalletState> {
private var dialog: Dialog? = null
private var snackbar: Snackbar? = null
private lateinit var pendingTransactionAdapter: PendingTransactionsAdapter
private lateinit var warningsAdapter: WarningMessagesAdapter
private var walletView: WalletView = SingleWalletView()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
@ -65,12 +60,14 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
oldState.walletState == newState.walletState
}.select { it.walletState }
}
store.dispatch(WalletAction.UpdateWallet)
walletView.setFragment(this)
store.dispatch(WalletAction.UpdateWallet())
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
walletView.removeFragment()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -81,7 +78,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
}
setupWarningsRecyclerView()
setupTransactionsRecyclerView()
walletView.changeWalletView(this)
}
private fun setupWarningsRecyclerView() {
@ -92,79 +89,44 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
rv_warning_messages.adapter = warningsAdapter
}
private fun setupTransactionsRecyclerView() {
pendingTransactionAdapter = PendingTransactionsAdapter()
rv_pending_transaction.layoutManager = LinearLayoutManager(context)
rv_pending_transaction.adapter = pendingTransactionAdapter
}
override fun newState(state: WalletState) {
if (activity == null) return
state.twinCardsState?.cardNumber?.let { cardNumber ->
tv_twin_card_number.show()
iv_twin_card.show()
val number = when (cardNumber) {
TwinCardNumber.First -> "1"
TwinCardNumber.Second -> "2"
}
tv_twin_card_number.text = getString(R.string.wallet_twins_chip_format, number)
}
if (state.twinCardsState?.cardNumber == null) {
tv_twin_card_number.hide()
iv_twin_card.hide()
if (state.isMultiwalletAllowed && walletView is SingleWalletView) {
walletView = MultiWalletView()
walletView.changeWalletView(this)
} else if (!state.isMultiwalletAllowed && walletView is MultiWalletView) {
walletView = SingleWalletView()
walletView.changeWalletView(this)
}
walletView.onNewState(state)
if (!state.showDetails) {
if (!state.shouldShowDetails) {
toolbar.menu.removeItem(R.id.details_menu)
} else if (toolbar.menu.findItem(R.id.details_menu) == null) {
toolbar.menu.add(R.menu.wallet, R.id.details_menu, NONE, R.string.details_title)
}
srl_wallet.setOnRefreshListener {
store.dispatch(WalletAction.LoadData)
}
setupNoInternetHandling(state)
setupButtons(state)
setupAddressCard(state)
setupCardImage(state.cardImage)
warningsAdapter.submitList(state.mainWarningsList)
rv_warning_messages.show(state.mainWarningsList.isNotEmpty())
showWarningsIfPresent(state.mainWarningsList)
pendingTransactionAdapter.submitList(state.pendingTransactions)
rv_pending_transaction.show(state.pendingTransactions.isNotEmpty())
srl_wallet.setOnRefreshListener {
if (state.state != ProgressState.Loading) {
store.dispatch(WalletAction.LoadData)
}
}
handleDialogs(state.walletDialog)
l_balance.show()
BalanceWidget(this, state.currencyData, state.twinCardsState != null).setup()
if (state.state == ProgressState.Done) {
if (state.state != ProgressState.Loading) {
srl_wallet.isRefreshing = false
}
}
when (state.payIdData.payIdState) {
PayIdState.Disabled, PayIdState.Loading -> group_payid.hide()
PayIdState.NotCreated -> {
group_payid.show()
tv_create_payid.show()
tv_create_payid.setOnClickListener { store.dispatch(WalletAction.CreatePayId) }
tv_payid_address.hide()
}
PayIdState.Created -> {
group_payid.show()
tv_create_payid.hide()
tv_payid_address.show()
tv_payid_address.text = state.payIdData.payId
}
PayIdState.ErrorLoading -> {
}
}
private fun showWarningsIfPresent(warnings: List<WarningMessage>) {
warningsAdapter.submitList(warnings)
rv_warning_messages.show(warnings.isNotEmpty())
}
private fun setupNoInternetHandling(state: WalletState) {
@ -174,98 +136,14 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
snackbar = Snackbar.make(
coordinator_wallet, getString(R.string.wallet_notification_no_internet),
Snackbar.LENGTH_INDEFINITE
).setAction(getString(R.string.common_retry))
{ store.dispatch(WalletAction.LoadData) }
.also { it.show() }
).setAction(getString(R.string.common_retry)) { store.dispatch(WalletAction.LoadData) }
snackbar?.show()
}
} else {
snackbar?.dismiss()
}
}
private fun setupButtons(state: WalletState) {
if (state.topUpState.allowed) {
l_buttons_long.hide()
l_buttons_short.show()
} else {
l_buttons_long.show()
l_buttons_short.hide()
}
val btnConfirm = if (state.topUpState.allowed) {
btn_confirm_short
} else {
btn_confirm_long
}
val btnScan = if (state.topUpState.allowed) {
btn_scan_short
} else {
btn_scan_long
}
btnScan.setOnClickListener {
store.dispatch(WalletAction.Scan)
}
btn_copy.setOnClickListener { store.dispatch(WalletAction.CopyAddress(requireContext())) }
btn_show_qr.setOnClickListener { store.dispatch(WalletAction.ShowQrCode) }
val buttonTitle = when (state.mainButton) {
is WalletMainButton.SendButton -> R.string.wallet_button_send
is WalletMainButton.CreateWalletButton -> {
if (state.twinCardsState == null) {
R.string.wallet_button_create_wallet
} else {
R.string.wallet_button_create_twin_wallet
}
}
}
btnConfirm.text = getString(buttonTitle)
btnConfirm.isEnabled = state.mainButton.enabled
btnConfirm.setOnClickListener {
when (state.mainButton) {
is WalletMainButton.SendButton -> store.dispatch(WalletAction.Send())
is WalletMainButton.CreateWalletButton -> store.dispatch(WalletAction.CreateWallet)
}
}
btn_top_up.setOnClickListener {
store.dispatch(
WalletAction.TopUpAction.TopUp(requireContext(), R.color.backgroundLightGray)
)
}
}
private fun setupAddressCard(state: WalletState) {
if (state.walletAddresses != null) {
l_address?.show()
if (state.showMultipleAddress) {
(l_address as? ViewGroup)?.beginDelayedTransition()
chip_group_address_type.show()
chip_group_address_type.fitChipsByGroupWidth()
val checkedId = MultipleAddressUiHelper.typeToId(state.walletAddresses.selectedAddress.type)
if (checkedId != View.NO_ID) chip_group_address_type.check(checkedId)
chip_group_address_type.setOnCheckedChangeListener { group, checkedId ->
if (checkedId == -1) return@setOnCheckedChangeListener
MultipleAddressUiHelper.idToType(checkedId, state.wallet?.blockchain)?.let {
store.dispatch(WalletAction.ChangeSelectedAddress(it))
}
}
} else {
chip_group_address_type.hide()
}
tv_address.text = state.walletAddresses.selectedAddress.address
tv_explore?.setOnClickListener {
store.dispatch(WalletAction.ExploreAddress(requireContext()))
}
} else {
l_address?.hide()
}
}
private fun setupCardImage(cardImage: Artwork?) {
Picasso.get()
.load(cardImage?.artworkId)
@ -274,47 +152,13 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
?.into(iv_card)
}
private fun handleDialogs(walletDialog: WalletDialog?) {
when (walletDialog) {
is WalletDialog.QrDialog -> {
if (walletDialog.qrCode != null && walletDialog.shareUrl != null) {
if (dialog == null) dialog = QrDialog(requireContext()).apply {
this.showQr(
walletDialog.qrCode, walletDialog.shareUrl, walletDialog.currencyName
)
}
}
}
is WalletDialog.CreatePayIdDialog -> {
when (walletDialog.creatingPayIdState) {
CreatingPayIdState.EnterPayId -> {
if (dialog == null) dialog = PayIdDialog(requireContext()).apply {
this.show()
}
(dialog as? PayIdDialog)?.stopProgress()
}
CreatingPayIdState.Waiting -> (dialog as? PayIdDialog)?.showProgress()
}
}
is WalletDialog.SelectAmountToSendDialog -> {
if (dialog == null) dialog = AmountToSendDialog(requireContext()).apply {
this.show(walletDialog.amounts)
}
}
null -> {
dialog?.dismiss()
dialog = null
}
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.details_menu -> {
store.state.globalState.scanNoteResponse?.let { scanNoteResponse ->
store.dispatch(DetailsAction.PrepareScreen(
scanNoteResponse.card, scanNoteResponse,
store.state.walletState.wallet,
store.state.walletState.walletManagers.map { it.wallet },
store.state.globalState.configManager?.config?.isCreatingTwinCardsAllowed,
store.state.globalState.appCurrency
))
@ -328,41 +172,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
if (store.state.walletState.showDetails) inflater.inflate(R.menu.wallet, menu)
if (store.state.walletState.shouldShowDetails) inflater.inflate(R.menu.wallet, menu)
}
}
class MultipleAddressUiHelper {
companion object {
fun typeToId(type: AddressType): Int {
return when (type) {
is BitcoinAddressType.Legacy -> R.id.chip_legacy
is BitcoinAddressType.Segwit -> R.id.chip_default
is CardanoAddressType.Byron -> R.id.chip_legacy
is CardanoAddressType.Shelley -> R.id.chip_default
else -> View.NO_ID
}
}
fun idToType(id: Int, blockchain: Blockchain?): AddressType? {
return when (id) {
R.id.chip_default -> {
when (blockchain) {
Blockchain.Bitcoin -> BitcoinAddressType.Segwit
Blockchain.CardanoShelley -> CardanoAddressType.Shelley
else -> null
}
}
R.id.chip_legacy -> {
when (blockchain) {
Blockchain.Bitcoin -> BitcoinAddressType.Legacy
Blockchain.CardanoShelley -> CardanoAddressType.Byron
else -> null
}
}
else -> null
}
}
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.tap.features.wallet.ui
package com.tangem.tap.features.wallet.ui.adapters
import android.view.LayoutInflater
import android.view.View

View file

@ -0,0 +1,137 @@
package com.tangem.tap.features.wallet.ui.adapters
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.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.tap.common.extensions.*
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletData
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.item_currency_wallet.view.*
import java.math.BigDecimal
class WalletAdapter
: ListAdapter<WalletData, WalletAdapter.WalletsViewHolder>(DiffUtilCallback) {
override fun getItemId(position: Int): Long {
return currentList[position].currencyData.currencySymbol?.hashCode()?.toLong() ?: 0
}
fun submitList(list: List<WalletData>, primaryBlockchain: Blockchain?, primaryToken: Token? = null) {
val listModified = list.toMutableList()
val primaryBlockchainWallet = when (
val index = listModified.indexOfFirst { it.blockchain == primaryBlockchain }
) {
-1 -> null
else -> listModified.removeAt(index)
}
val primaryTokenWallet = if (primaryToken == null) null else when (
val index = listModified.indexOfFirst { it.token == primaryToken }
) {
-1 -> null
else -> listModified.removeAt(index)
}
if (list.all { it.currencyData.fiatAmount == null }) {
val sortedList = listOfNotNull(primaryBlockchainWallet, primaryTokenWallet) + listModified
super.submitList(sortedList)
return
}
val sorted = listModified.sortedWith(
compareByDescending<WalletData> { it.currencyData.fiatAmountRaw ?: BigDecimal.ZERO }
.thenBy { it.currencyData.currencySymbol }
)
val sortedList = listOfNotNull(primaryBlockchainWallet, primaryTokenWallet) + sorted
super.submitList(sortedList)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WalletsViewHolder {
val layout = LayoutInflater.from(parent.context)
.inflate(R.layout.item_currency_wallet, parent, false)
return WalletsViewHolder(layout)
}
override fun onBindViewHolder(holder: WalletsViewHolder, position: Int) {
holder.bind(currentList[position])
}
object DiffUtilCallback : DiffUtil.ItemCallback<WalletData>() {
override fun areContentsTheSame(
oldItem: WalletData, newItem: WalletData
) = oldItem == newItem
override fun areItemsTheSame(
oldItem: WalletData, newItem: WalletData
) = oldItem == newItem
}
class WalletsViewHolder(val view: View) :
RecyclerView.ViewHolder(view) {
fun bind(wallet: WalletData) {
view.tv_currency.text = wallet.currencyData.currency
view.tv_amount.text = wallet.currencyData.amount
if (view.tv_amount.isEllipsized()) {
val allowedSize = view.tv_amount.text.length - 4
view.tv_amount.text = wallet.currencyData.amount?.ellipsizeBeforeSpace(allowedSize)
}
view.tv_amount_fiat.text = wallet.currencyData.fiatAmount
view.tv_exchange_rate.text = wallet.fiatRateString
view.card_wallet.setOnClickListener {
store.dispatch(WalletAction.MultiWallet.SelectWallet(wallet))
}
val blockchain = wallet.currencyData.currencySymbol?.let { Blockchain.fromCurrency(it) }
if (blockchain != null && blockchain != Blockchain.Unknown) {
view.tv_token_letter.text = null
view.iv_currency.colorFilter = null
view.iv_currency.setImageResource(blockchain.getIconRes())
} else {
view.tv_token_letter.text = wallet.currencyData.currencySymbol?.take(1)
wallet.token?.getColor()?.let { view.iv_currency.setColorFilter(it) }
view.iv_currency.setImageResource(R.drawable.shape_circle)
}
when (wallet.currencyData.status) {
BalanceStatus.VerifiedOnline -> hideWarning()
BalanceStatus.Loading -> {
hideWarning()
if (wallet.currencyData.amount == null) {
view.tv_exchange_rate.text = view.context.getText(R.string.wallet_balance_loading)
}
}
BalanceStatus.TransactionInProgress ->
showWarning(view.context.getString(R.string.wallet_balance_tx_in_progress))
BalanceStatus.Unreachable ->
showWarning(view.context.getString(R.string.wallet_balance_blockchain_unreachable))
BalanceStatus.NoAccount ->
showWarning(view.context.getString(R.string.wallet_error_no_account))
else -> {
}
}
}
private fun showWarning(message: String) {
toggleWarning(true)
view.tv_status_error_message.text = message
}
private fun hideWarning() {
toggleWarning(false)
}
private fun toggleWarning(show: Boolean) {
view.tv_exchange_rate.show(!show)
view.tv_status_error_message.show(show)
}
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.tap.features.wallet.ui
package com.tangem.tap.features.wallet.ui.adapters
import android.graphics.Rect
import android.view.LayoutInflater

View file

@ -1,47 +0,0 @@
package com.tangem.tap.features.wallet.ui.dialogs
import android.app.Dialog
import android.content.Context
import android.view.View
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.notificationsHandler
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.dialog_create_payid.*
class PayIdDialog(context: Context) : Dialog(context) {
init {
this.setContentView(R.layout.dialog_create_payid)
}
override fun show() {
notificationsHandler?.replaceBaseLayout(this.cl_payid_dialog)
this.setOnDismissListener {
notificationsHandler?.returnBaseLayout()
store.dispatch(WalletAction.CreatePayId.Cancel)
}
this.btn_create_payid?.setOnClickListener {
if (this.et_payid.text.isNullOrBlank()) {
store.dispatch(WalletAction.CreatePayId.EmptyField)
} else {
val payid = this.et_payid.text!!.toString() +
this.context.getString(R.string.wallet_create_payid_domain)
store.dispatch(WalletAction.CreatePayId.CompleteCreatingPayId(payid))
}
}
super.show()
}
fun showProgress() {
this.btn_create_payid?.visibility = View.INVISIBLE
this.pb_create_payid?.show()
}
fun stopProgress() {
this.btn_create_payid?.show()
this.pb_create_payid?.hide()
}
}

View file

@ -0,0 +1,126 @@
package com.tangem.tap.features.wallet.ui.wallet
import androidx.recyclerview.widget.LinearLayoutManager
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.tokens.redux.TokensAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.WalletFragment
import com.tangem.tap.features.wallet.ui.adapters.WalletAdapter
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.card_balance.*
import kotlinx.android.synthetic.main.fragment_wallet.*
import kotlinx.android.synthetic.main.layout_balance_error.*
import kotlinx.android.synthetic.main.layout_wallet_long_buttons.*
class MultiWalletView : WalletView {
private var fragment: WalletFragment? = null
private lateinit var walletsAdapter: WalletAdapter
override fun changeWalletView(fragment: WalletFragment) {
setFragment(fragment)
onViewCreated()
showMultiWalletView()
setupButtons()
}
private fun showMultiWalletView() {
val fragment = fragment ?: return
fragment.tv_twin_card_number.hide()
fragment.iv_twin_card.hide()
fragment.rv_pending_transaction.hide()
fragment.l_card_balance.hide()
fragment.l_address.hide()
fragment.l_buttons_short.hide()
fragment.l_buttons_long.hide()
fragment.btn_scan_multiwallet?.show()
fragment.rv_multiwallet.show()
fragment.btn_add_token.show()
}
private fun setupButtons() {
val fragment = fragment ?: return
fragment.btn_scan_multiwallet?.setOnClickListener { store.dispatch(WalletAction.Scan) }
}
override fun setFragment(fragment: WalletFragment) {
this.fragment = fragment
}
override fun removeFragment() {
this.fragment = null
}
override fun onViewCreated() {
setupWalletsRecyclerView()
}
private fun setupWalletsRecyclerView() {
val fragment = fragment ?: return
walletsAdapter = WalletAdapter()
walletsAdapter.setHasStableIds(true)
fragment.rv_multiwallet.layoutManager = LinearLayoutManager(fragment.requireContext())
fragment.rv_multiwallet.adapter = walletsAdapter
}
override fun onNewState(state: WalletState) {
val fragment = fragment ?: return
walletsAdapter.submitList(state.wallets, state.primaryBlockchain, state.primaryToken)
fragment.btn_add_token?.setOnClickListener {
store.dispatch(TokensAction.LoadCurrencies)
store.dispatch(TokensAction.SetAddedCurrencies(state.wallets))
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens))
}
handleErrorStates(state, fragment)
}
private fun handleErrorStates(state: WalletState, fragment: WalletFragment) {
when (state.primaryWallet?.currencyData?.status) {
BalanceStatus.EmptyCard -> {
showErrorState(
fragment,
fragment.getText(R.string.wallet_error_empty_card),
fragment.getString(R.string.wallet_error_empty_card_subtitle)
)
configureButtonsForEmptyWalletState(fragment)
}
BalanceStatus.UnknownBlockchain -> {
showErrorState(
fragment,
fragment.getText(R.string.wallet_error_unsupported_blockchain),
fragment.getString(R.string.wallet_error_unsupported_blockchain_subtitle)
)
}
}
}
private fun showErrorState(
fragment: WalletFragment, errorTitle: CharSequence, errorDescription: CharSequence
) {
fragment.l_card_balance.show()
fragment.l_balance.hide()
fragment.l_balance_error.show()
fragment.rv_multiwallet.hide()
fragment.btn_add_token.hide()
fragment.tv_error_title.text = errorTitle
fragment.tv_error_descriptions.text = errorDescription
}
private fun configureButtonsForEmptyWalletState(fragment: WalletFragment) {
fragment.btn_scan_multiwallet.hide()
fragment.l_buttons_long.show()
fragment.btn_scan_long.setOnClickListener { store.dispatch(WalletAction.Scan) }
fragment.btn_confirm_long.setOnClickListener { store.dispatch(WalletAction.CreateWallet) }
fragment.btn_confirm_long.text = fragment.getText(R.string.wallet_button_create_wallet)
}
}

View file

@ -0,0 +1,236 @@
package com.tangem.tap.features.wallet.ui.wallet
import android.app.Dialog
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import androidx.recyclerview.widget.LinearLayoutManager
import com.tangem.tap.common.extensions.*
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.ui.BalanceWidget
import com.tangem.tap.features.wallet.ui.MultipleAddressUiHelper
import com.tangem.tap.features.wallet.ui.WalletFragment
import com.tangem.tap.features.wallet.ui.adapters.PendingTransactionsAdapter
import com.tangem.tap.features.wallet.ui.dialogs.AmountToSendDialog
import com.tangem.tap.features.wallet.ui.dialogs.QrDialog
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.card_balance.*
import kotlinx.android.synthetic.main.fragment_wallet.*
import kotlinx.android.synthetic.main.layout_address.*
import kotlinx.android.synthetic.main.layout_wallet_long_buttons.*
import kotlinx.android.synthetic.main.layout_wallet_short_buttons.*
class SingleWalletView : WalletView {
private lateinit var pendingTransactionAdapter: PendingTransactionsAdapter
private var fragment: WalletFragment? = null
private var dialog: Dialog? = null
override fun setFragment(fragment: WalletFragment) {
this.fragment = fragment
}
override fun removeFragment() {
fragment = null
}
override fun changeWalletView(fragment: WalletFragment) {
setFragment(fragment)
onViewCreated()
showSingleWalletView()
}
private fun showSingleWalletView() {
val fragment = fragment ?: return
fragment.rv_multiwallet.hide()
fragment.btn_add_token.hide()
fragment.btn_scan_multiwallet?.hide()
fragment.rv_pending_transaction.hide()
fragment.l_card_balance.show()
fragment.l_address.show()
}
override fun onViewCreated() {
setupTransactionsRecyclerView()
}
private fun setupTransactionsRecyclerView() {
val fragment = fragment ?: return
pendingTransactionAdapter = PendingTransactionsAdapter()
fragment.rv_pending_transaction.layoutManager = LinearLayoutManager(fragment.requireContext())
fragment.rv_pending_transaction.adapter = pendingTransactionAdapter
}
override fun onNewState(state: WalletState) {
setupTwinCards(state.twinCardsState)
state.primaryWallet ?: return
setupButtons(state.primaryWallet, state.twinCardsState != null)
setupAddressCard(state.primaryWallet)
showPendingTransactionsIfPresent(state.primaryWallet.pendingTransactions)
setupBalance(state, state.primaryWallet)
handleDialogs(state.walletDialog)
}
private fun showPendingTransactionsIfPresent(pendingTransactions: List<PendingTransaction>) {
pendingTransactionAdapter.submitList(pendingTransactions)
fragment?.rv_pending_transaction?.show(pendingTransactions.isNotEmpty())
}
private fun setupBalance(state: WalletState, primaryWallet: WalletData) {
fragment?.apply {
this.l_balance.show()
BalanceWidget(this, primaryWallet.currencyData, state.twinCardsState != null).setup()
}
}
private fun setupTwinCards(twinCardsState: TwinCardsState?) {
fragment?.apply {
twinCardsState?.cardNumber?.let { cardNumber ->
this.tv_twin_card_number.show()
this.iv_twin_card.show()
val number = when (cardNumber) {
TwinCardNumber.First -> "1"
TwinCardNumber.Second -> "2"
}
this.tv_twin_card_number.text =
this.getString(R.string.wallet_twins_chip_format, number)
}
if (twinCardsState?.cardNumber == null) {
this.tv_twin_card_number.hide()
this.iv_twin_card.hide()
}
}
}
private fun setupButtons(state: WalletData, isTwinsWallet: Boolean) {
val fragment = fragment ?: return
setupButtonsType(state, fragment)
val btnConfirm = if (state.topUpState.allowed) {
fragment.btn_confirm_short
} else {
fragment.btn_confirm_long
}
val btnScan = if (state.topUpState.allowed) {
fragment.btn_scan_short
} else {
fragment.btn_scan_long
}
setupConfirmButton(state, btnConfirm, fragment, isTwinsWallet)
btnScan.setOnClickListener {
store.dispatch(WalletAction.Scan)
}
fragment.btn_copy.setOnClickListener {
state.walletAddresses?.selectedAddress?.address?.let { addressString ->
store.dispatch(WalletAction.CopyAddress(addressString, fragment.requireContext()))
}
}
fragment.btn_show_qr.setOnClickListener { store.dispatch(WalletAction.ShowQrCode) }
fragment.btn_top_up.setOnClickListener {
store.dispatch(
WalletAction.TopUpAction.TopUp(fragment.requireContext(), R.color.backgroundLightGray)
)
}
}
private fun setupButtonsType(state: WalletData, fragment: WalletFragment) {
if (state.topUpState.allowed) {
fragment.l_buttons_long.hide()
fragment.l_buttons_short.show()
} else {
fragment.l_buttons_long.show()
fragment.l_buttons_short.hide()
}
}
private fun setupConfirmButton(
state: WalletData, btnConfirm: Button, fragment: WalletFragment, isTwinsWallet: Boolean
) {
val buttonTitle = when (state.mainButton) {
is WalletMainButton.SendButton -> R.string.wallet_button_send
is WalletMainButton.CreateWalletButton -> {
if (!isTwinsWallet) {
R.string.wallet_button_create_wallet
} else {
R.string.wallet_button_create_twin_wallet
}
}
}
btnConfirm.text = fragment.getString(buttonTitle)
btnConfirm.isEnabled = state.mainButton.enabled
btnConfirm.setOnClickListener {
when (state.mainButton) {
is WalletMainButton.SendButton -> store.dispatch(WalletAction.Send())
is WalletMainButton.CreateWalletButton -> store.dispatch(WalletAction.CreateWallet)
}
}
}
private fun setupAddressCard(state: WalletData) {
val fragment = fragment ?: return
if (state.walletAddresses != null) {
fragment.l_address?.show()
if (state.shouldShowMultipleAddress()) {
(fragment.l_address as? ViewGroup)?.beginDelayedTransition()
fragment.chip_group_address_type.show()
fragment.chip_group_address_type.fitChipsByGroupWidth()
val checkedId = MultipleAddressUiHelper.typeToId(state.walletAddresses.selectedAddress.type)
if (checkedId != View.NO_ID) fragment.chip_group_address_type.check(checkedId)
fragment.chip_group_address_type.setOnCheckedChangeListener { group, checkedId ->
if (checkedId == -1) return@setOnCheckedChangeListener
val type = MultipleAddressUiHelper.idToType(checkedId, state.currencyData.currencySymbol)
type?.let { store.dispatch(WalletAction.ChangeSelectedAddress(type)) }
}
} else {
fragment.chip_group_address_type.hide()
}
fragment.tv_address.text = state.walletAddresses.selectedAddress.address
fragment.tv_explore?.setOnClickListener {
store.dispatch(WalletAction.ExploreAddress(
state.walletAddresses.selectedAddress.exploreUrl,
fragment.requireContext()))
}
} else {
fragment.l_address?.hide()
}
}
private fun handleDialogs(walletDialog: WalletDialog?) {
val fragment = fragment ?: return
when (walletDialog) {
is WalletDialog.QrDialog -> {
if (walletDialog.qrCode != null && walletDialog.shareUrl != null) {
if (dialog == null) dialog = QrDialog(fragment.requireContext()).apply {
this.showQr(
walletDialog.qrCode, walletDialog.shareUrl, walletDialog.currencyName
)
}
}
}
is WalletDialog.SelectAmountToSendDialog -> {
if (dialog == null) dialog = AmountToSendDialog(fragment.requireContext()).apply {
this.show(walletDialog.amounts)
}
}
null -> {
dialog?.dismiss()
dialog = null
}
}
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.tap.features.wallet.ui.wallet
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.ui.WalletFragment
interface WalletView {
fun setFragment(fragment: WalletFragment)
fun removeFragment()
fun changeWalletView(fragment: WalletFragment)
fun onViewCreated()
fun onNewState(state: WalletState)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 699 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 970 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

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