Updated on 2026-08-14

This commit is contained in:
Tangem 2021-05-17 14:58:50 +03:00
commit 12585fa412
60 changed files with 1116 additions and 670 deletions

3
.gitignore vendored
View file

@ -6,6 +6,9 @@
# Local configuration file (sdk path, etc)
local.properties
# Google services
# Gradle generated files
.gradle

View file

@ -75,10 +75,9 @@ dependencies {
implementation 'com.google.android.play:core-ktx:1.8.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
implementation 'com.tangem:blockchain:1.166.0'
implementation 'com.tangem:core:1.109.0'
implementation 'com.tangem:sdk:1.109.0'
implementation 'com.tangem:blockchain:develop-8'
implementation 'com.tangem:core:develop-15'
implementation 'com.tangem:sdk:develop-15'
// WebView
implementation "androidx.browser:browser:1.3.0"

View file

@ -629,6 +629,13 @@
"name" : "QASH",
"contractAddress" : "0x618e75ac90b12c6049ba3b27f5d5f8651b0037f6"
},
{
"decimalCount" : 8,
"symbol" : "QCX",
"name" : "QuickX Protocol",
"contractAddress" : "0xf9e5af7b42d31d51677c75bbbd37c1986ec79aee",
"customIcon": "qcx"
},
{
"decimalCount" : 8,
"symbol" : "QRL",

View file

@ -0,0 +1,86 @@
package com.tangem.tap.common.extensions
import android.widget.ImageView
import android.widget.TextView
import com.squareup.picasso.Callback
import com.squareup.picasso.Picasso
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.IconsUtil
import com.tangem.blockchain.common.Token
import com.tangem.wallet.R
fun Picasso.loadCurrenciesIcon(
imageView: ImageView,
textView: TextView,
token: Token? = null,
blockchain: Blockchain?,
) {
val blockchain = blockchain ?: Blockchain.Ethereum
val url = if (token != null) {
IconsUtil.getTokenIconUri(blockchain, token)
} else {
IconsUtil.getBlockchainIconUri(blockchain)
}
imageView.setImageDrawable(null)
imageView.colorFilter = null
textView.text = null
when {
url != null -> {
this.load(url)
.placeholder(R.drawable.shape_circle)
?.into(imageView,
object : Callback {
override fun onError(e: Exception?) {
setOfflineCurrencyImage(imageView, textView, token, blockchain)
}
override fun onSuccess() {
}
})
}
token?.symbol == QCX -> {
this.load(R.drawable.ic_qcx)?.into(imageView)
}
else -> {
setOfflineCurrencyImage(imageView, textView, token, blockchain)
}
}
}
private const val QCX = "QCX"
private fun setOfflineCurrencyImage(
imageView: ImageView,
textView: TextView,
token: Token?,
blockchain: Blockchain,
) {
if (token != null) {
setTokenImage(imageView, textView, token)
} else {
setBlockchainImage(imageView, textView, blockchain)
}
}
private fun setBlockchainImage(
imageView: ImageView,
textView: TextView,
blockchain: Blockchain
) {
imageView.setImageResource(blockchain.getIconRes())
imageView.colorFilter = null
textView.text = null
}
private fun setTokenImage(
imageView: ImageView,
textView: TextView,
token: Token
) {
imageView.setImageResource(R.drawable.shape_circle)
imageView.setColorFilter(token.getColor())
textView.text = token.symbol.take(1)
}

View file

@ -24,7 +24,12 @@ sealed class GlobalAction : Action {
data class Success(val appCurrency: FiatCurrencyName) : GlobalAction()
}
data class UpdateWalletSignedHashes(val walletSignedHashes: Int?) : GlobalAction()
data class UpdateWalletSignedHashes(
val walletSignedHashes: Int?,
val remainingSignatures: Int?,
val walletPublicKey: ByteArray,
) : GlobalAction()
data class HideWarningMessage(val warning: WarningMessage) : GlobalAction()
data class UpdateSecurityOptions(val securityOption: SecurityOption) : GlobalAction()

View file

@ -53,6 +53,9 @@ val globalMiddleware: Middleware<AppState> = { dispatch, appState ->
is GlobalAction.SendFeedback -> {
store.state.globalState.feedbackManager?.send(action.emailData)
}
is GlobalAction.UpdateWalletSignedHashes -> {
store.dispatch(WalletAction.Warnings.CheckRemainingSignatures(action.remainingSignatures))
}
}
nextDispatch(action)
}

View file

@ -1,5 +1,6 @@
package com.tangem.tap.common.redux.global
import com.tangem.commands.wallet.WalletIndex
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.details.redux.SecurityOption
import org.rekotlin.Action
@ -25,16 +26,6 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
is GlobalAction.RestoreAppCurrency.Success -> {
globalState.copy(appCurrency = action.appCurrency)
}
is GlobalAction.UpdateWalletSignedHashes -> {
val card = globalState.scanNoteResponse?.card?.copy(
walletSignedHashes = action.walletSignedHashes
)
if (card != null) {
globalState.copy(scanNoteResponse = globalState.scanNoteResponse.copy(card = card))
} else {
globalState
}
}
is GlobalAction.SetConfigManager -> {
globalState.copy(configManager = action.configManager)
}
@ -42,13 +33,13 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
is GlobalAction.UpdateSecurityOptions -> {
val card = when (action.securityOption) {
SecurityOption.LongTap -> globalState.scanNoteResponse?.card?.copy(
isPin1Default = true, isPin2Default = true
isPin1Default = true, isPin2Default = true
)
SecurityOption.PassCode -> globalState.scanNoteResponse?.card?.copy(
isPin1Default = true, isPin2Default = false
isPin1Default = true, isPin2Default = false
)
SecurityOption.AccessCode -> globalState.scanNoteResponse?.card?.copy(
isPin1Default = false, isPin2Default = true
isPin1Default = false, isPin2Default = true
)
}
if (card != null) {
@ -57,6 +48,22 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
globalState
}
}
is GlobalAction.UpdateWalletSignedHashes -> {
val wallet = globalState.scanNoteResponse?.card
?.wallet(WalletIndex.PublicKey(action.walletPublicKey))
?.copy(
signedHashes = action.walletSignedHashes,
remainingSignatures = action.remainingSignatures
)
val card = globalState.scanNoteResponse?.card
wallet?.let { globalState.scanNoteResponse.card.updateWallet(wallet) }
if (card != null) {
globalState.copy(scanNoteResponse = globalState.scanNoteResponse.copy(card = card))
} else {
globalState
}
}
is GlobalAction.SetFeedbackManager -> {
globalState.copy(feedbackManager = action.feedbackManager)
}

View file

@ -0,0 +1,48 @@
package com.tangem.tap.common.toggleWidget
import android.graphics.drawable.Drawable
import android.view.View
import com.google.android.material.button.MaterialButton
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
/**
[REDACTED_AUTHOR]
*/
open class IndeterminateProgressButtonWidget(
private val button: MaterialButton,
private val progress: View,
initialState: ProgressState = ProgressState.None
) : ViewStateWidget {
private var initialButtonText: CharSequence = button.text
private var icon: Drawable? = null
init {
icon = button.icon
changeState(initialState)
}
override fun changeState(state: WidgetState) {
val progressState = state as? ProgressState ?: return
when (progressState) {
is ProgressState.None -> switchToNone()
is ProgressState.Progress -> switchToProgress()
}
}
protected open fun switchToNone() {
button.isClickable = true
button.icon = icon
button.text = initialButtonText
progress.hide()
}
protected open fun switchToProgress() {
button.isClickable = false
button.icon = null
button.text = ""
progress.show()
}
}

View file

@ -1,85 +0,0 @@
package com.tangem.merchant.common.toggleWidget
import android.view.View
import android.view.ViewGroup
/**
[REDACTED_AUTHOR]
*/
interface ToggleState
interface StateModifier {
fun stateChanged(container: ViewGroup, view: View, state: ToggleState)
}
interface ToggleView {
val mainViewModifiers: MutableList<StateModifier>
val toggleViewModifiers: MutableList<StateModifier>
fun setState(state: ToggleState, andApply: Boolean = true)
fun applyState()
fun getView(): View
fun getMainView(): View
fun getToggleView(): View
}
class ToggleWidget : ToggleView {
private val container: ViewGroup
private val mainView: View
private val toggleView: View
private var state: ToggleState
constructor(
container: ViewGroup,
mainView: View,
toggleView: View,
initialState: ToggleState,
mainViewModifier: List<StateModifier> = mutableListOf(),
loadingViewModifier: List<StateModifier> = mutableListOf()
) {
this.container = container
this.mainView = mainView
this.toggleView = toggleView
this.state = initialState
this.mainViewModifiers.addAll(mainViewModifier)
this.toggleViewModifiers.addAll(loadingViewModifier)
}
constructor(
container: ViewGroup,
mainViewId: Int,
toggleViewId: Int,
initialState: ToggleState,
mainViewModifier: List<StateModifier> = mutableListOf(),
loadingViewModifier: List<StateModifier> = mutableListOf()
) {
this.container = container
this.mainView = container.findViewById(mainViewId)
this.toggleView = container.findViewById(toggleViewId)
this.state = initialState
this.mainViewModifiers.addAll(mainViewModifier)
this.toggleViewModifiers.addAll(loadingViewModifier)
}
override val mainViewModifiers: MutableList<StateModifier> = mutableListOf()
override val toggleViewModifiers: MutableList<StateModifier> = mutableListOf()
override fun setState(state: ToggleState, andApply: Boolean) {
this.state = state
if (andApply) applyState()
}
override fun applyState() {
mainViewModifiers.forEach { it.stateChanged(container, mainView, state) }
toggleViewModifiers.forEach { it.stateChanged(container, toggleView, state) }
}
override fun getView(): View = container
override fun getMainView(): View = mainView
override fun getToggleView(): View = toggleView
}

View file

@ -0,0 +1,16 @@
package com.tangem.tap.common.toggleWidget
/**
[REDACTED_AUTHOR]
*/
interface WidgetState
interface ViewStateWidget {
fun changeState(state: WidgetState)
}
sealed class ProgressState : WidgetState {
object None : ProgressState()
data class Progress(val progress: Int = 0) : ProgressState()
}

View file

@ -1,145 +0,0 @@
package com.tangem.tap.common.toggleWidget
import android.graphics.drawable.Drawable
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.TextView
import androidx.core.widget.TextViewCompat
import com.google.android.material.button.MaterialButton
import com.tangem.merchant.common.toggleWidget.StateModifier
import com.tangem.merchant.common.toggleWidget.ToggleState
import com.tangem.tap.common.extensions.beginDelayedTransition
/**
[REDACTED_AUTHOR]
*/
sealed class ProgressState : ToggleState {
class Progress : ProgressState()
class None : ProgressState()
}
class ReplaceTextStateModifier(
private val initialText: String,
private val replaceText: String = ""
) : StateModifier {
override fun stateChanged(container: ViewGroup, view: View, state: ToggleState) {
val tv = view as? TextView ?: return
when (state) {
is ProgressState.Progress -> {
tv.text = replaceText
}
is ProgressState.None -> {
tv.text = initialText
}
}
}
}
class TextViewDrawableStateModifier(
private val initialDrawable: Drawable?,
private val replaceDrawable: Drawable?,
private val position: Int
) : StateModifier {
companion object {
val LEFT = 1
val RIGHT = 2
}
override fun stateChanged(container: ViewGroup, view: View, state: ToggleState) {
val drawable = when (state) {
is ProgressState.Progress -> replaceDrawable
is ProgressState.None -> initialDrawable
else -> null
}
val drawableChanger = getChanger(view)
drawableChanger?.change(drawable, position)
}
private fun getChanger(view: View): DrawableChanger? = when (view) {
is MaterialButton -> MaterialButtonChanger(view)
is Button -> TextViewChanger(view)
is TextView -> TextViewChanger(view)
else -> null
}
internal interface DrawableChanger {
fun change(drawable: Drawable?, position: Int)
}
internal class TextViewChanger(private val view: TextView) : DrawableChanger {
override fun change(drawable: Drawable?, position: Int) {
when (position) {
LEFT -> setLeft(drawable)
RIGHT -> setRight(drawable)
}
}
private fun setLeft(drawable: Drawable?) {
if (drawable == null) {
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(view, 0, 0, 0, 0)
} else {
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(view, drawable, null, null, null)
}
}
private fun setRight(drawable: Drawable?) {
if (drawable == null) {
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(view, 0, 0, 0, 0)
} else {
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(view, null, null, drawable, null)
}
}
}
internal class MaterialButtonChanger(private val view: MaterialButton) : DrawableChanger {
override fun change(drawable: Drawable?, position: Int) {
when (position) {
LEFT -> setLeft(drawable)
RIGHT -> setRight(drawable)
}
}
private fun setLeft(drawable: Drawable?) {
view.iconGravity = MaterialButton.ICON_GRAVITY_START
view.icon = drawable
}
private fun setRight(drawable: Drawable?) {
view.iconGravity = MaterialButton.ICON_GRAVITY_END
view.icon = drawable
}
}
}
class ShowHideStateModifier(
private val isShowOnLoading: Boolean = true,
private val typeOfHiding: Int = View.INVISIBLE
) : StateModifier {
override fun stateChanged(container: ViewGroup, view: View, state: ToggleState) {
container.beginDelayedTransition()
view.visibility = when (state) {
is ProgressState.Progress -> if (isShowOnLoading) View.VISIBLE else typeOfHiding
is ProgressState.None -> if (isShowOnLoading) typeOfHiding else View.VISIBLE
else -> return
}
}
}
class ClickableStateModifier(
private val isClickableOnLoading: Boolean = false
) : StateModifier {
override fun stateChanged(container: ViewGroup, view: View, state: ToggleState) {
view.isClickable = when (state) {
is ProgressState.Progress -> isClickableOnLoading
is ProgressState.None -> !isClickableOnLoading
else -> return
}
}
}

View file

@ -2,14 +2,21 @@ package com.tangem.tap.domain
import androidx.activity.ComponentActivity
import com.tangem.*
import com.tangem.commands.*
import com.tangem.commands.CommandResponse
import com.tangem.commands.PinType
import com.tangem.commands.SetPinCommand
import com.tangem.commands.SetPinResponse
import com.tangem.commands.common.card.Card
import com.tangem.commands.common.card.CardType
import com.tangem.commands.wallet.PurgeWalletCommand
import com.tangem.commands.wallet.PurgeWalletResponse
import com.tangem.common.CompletionResult
import com.tangem.common.TangemSdkConstants
import com.tangem.common.extensions.calculateSha256
import com.tangem.tangem_sdk_new.extensions.init
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.AnalyticsHandler
import com.tangem.tap.domain.extensions.getDefaultWalletIndex
import com.tangem.tap.domain.tasks.CreateWalletAndRescanTask
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.tasks.ScanNoteTask
@ -32,13 +39,15 @@ class TangemSdkManager(val activity: ComponentActivity) {
initialMessage = Message(activity.getString(R.string.initial_message_scan_header)))
}
suspend fun createWallet(cardId: String?): CompletionResult<ScanNoteResponse> {
suspend fun createWallet(cardId: String?): CompletionResult<Card> {
return runTaskAsyncReturnOnMain(CreateWalletAndRescanTask(), cardId,
initialMessage = Message(activity.getString(R.string.initial_message_create_wallet_body)))
}
suspend fun eraseWallet(cardId: String?): CompletionResult<PurgeWalletResponse> {
return runTaskAsyncReturnOnMain(PurgeWalletCommand(), cardId,
return runTaskAsyncReturnOnMain(PurgeWalletCommand(
TangemSdkConstants.getDefaultWalletIndex()),
cardId,
initialMessage = Message(activity.getString(R.string.initial_message_purge_wallet_body)))
}

View file

@ -0,0 +1,65 @@
package com.tangem.tap.domain
import com.tangem.Message
import com.tangem.TangemSdk
import com.tangem.blockchain.common.TransactionSigner
import com.tangem.commands.SignCommand
import com.tangem.commands.SignResponse
import com.tangem.commands.wallet.WalletIndex
import com.tangem.common.CompletionResult
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class TangemSigner(
private val tangemSdk: TangemSdk,
private val initialMessage: Message,
private val signerCallback: (SignResponse) -> Unit
) : TransactionSigner {
override suspend fun sign(
hash: ByteArray,
cardId: String,
walletPublicKey: ByteArray
): CompletionResult<ByteArray> =
suspendCoroutine { continuation ->
val command = SignCommand(arrayOf(hash), WalletIndex.PublicKey(walletPublicKey))
tangemSdk.startSessionWithRunnable(
runnable = command,
cardId = cardId,
initialMessage = initialMessage,
) { result ->
when (result) {
is CompletionResult.Success -> {
signerCallback(result.data)
continuation.resume(CompletionResult.Success(result.data.signatures.first()))
}
is CompletionResult.Failure ->
continuation.resume(CompletionResult.Failure(result.error))
}
}
}
override suspend fun sign(
hashes: List<ByteArray>,
cardId: String,
walletPublicKey: ByteArray
): CompletionResult<List<ByteArray>> =
suspendCoroutine { continuation ->
val command = SignCommand(hashes.toTypedArray(), WalletIndex.PublicKey(walletPublicKey))
tangemSdk.startSessionWithRunnable(
runnable = command,
cardId = cardId,
initialMessage = initialMessage,
) { result ->
when (result) {
is CompletionResult.Success -> {
signerCallback(result.data)
continuation.resume(CompletionResult.Success(result.data.signatures))
}
is CompletionResult.Failure ->
continuation.resume(CompletionResult.Failure(result.error))
}
}
}
}

View file

@ -11,8 +11,7 @@ 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.extensions.*
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.tokens.CardCurrencies
import com.tangem.tap.domain.twins.TwinsHelper
@ -102,8 +101,10 @@ class TapWalletManager {
))
}
}
if (data.walletManager?.wallet?.blockchain == Blockchain.Ethereum ||
data.walletManager?.wallet?.blockchain == Blockchain.EthereumTestnet) {
val blockchain = data.card.getBlockchain()
if (blockchain == Blockchain.Ethereum ||
blockchain == Blockchain.EthereumTestnet) {
store.dispatch(TokensAction.LoadCardTokens)
}
loadData(data)
@ -112,10 +113,11 @@ class TapWalletManager {
private fun updateConfigManager(data: ScanNoteResponse) {
val configManager = store.state.globalState.configManager
val blockchain = data.card.getBlockchain()
if (TapWorkarounds.isStart2Coin) {
configManager?.turnOff(ConfigManager.isSendingToPayIdEnabled)
configManager?.turnOff(ConfigManager.isTopUpEnabled)
} else if (data.walletManager?.wallet?.blockchain == Blockchain.Bitcoin
} else if (blockchain == Blockchain.Bitcoin
|| data.card.cardData?.blockchainName == Blockchain.Bitcoin.id) {
configManager?.resetToDefault(ConfigManager.isSendingToPayIdEnabled)
configManager?.resetToDefault(ConfigManager.isTopUpEnabled)
@ -128,82 +130,104 @@ class TapWalletManager {
suspend fun loadData(data: ScanNoteResponse) {
withContext(Dispatchers.Main) {
val artworkId = data.verifyResponse?.artworkInfo?.id
if (data.walletManager != null) {
val config = store.state.globalState.configManager?.config ?: return@withContext
val primaryWalletManager = data.walletManager
val primaryBlockchain = data.walletManager.wallet.blockchain
val primaryTokenSymbol = data.card.cardData?.tokenSymbol
val primaryToken = primaryWalletManager.presetTokens.toList()
.firstOrNull { it.symbol == primaryTokenSymbol }
store.dispatch(WalletAction.MultiWallet.SetPrimaryBlockchain(primaryBlockchain))
if (primaryToken != null) {
store.dispatch(WalletAction.MultiWallet.SetPrimaryToken(primaryToken))
when {
data.card.getBlockchain() == Blockchain.Unknown && !data.card.isMultiwalletAllowed -> {
store.dispatch(WalletAction.LoadData.Failure(TapError.UnknownBlockchain))
store.dispatch(WalletAction.LoadArtwork(data.card, artworkId))
}
if (data.card.isMultiwalletAllowed) {
loadMultiWalletData(data.card, primaryBlockchain, primaryWalletManager)
} else {
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(primaryWalletManager))
store.dispatch(WalletAction.MultiWallet.AddBlockchains(listOf(primaryBlockchain)))
data.card.getStatus() == CardStatus.Empty ||
(data.card.isTwinCard() && data.secondTwinPublicKey == null) -> {
store.dispatch(WalletAction.EmptyWallet)
store.dispatch(WalletAction.LoadArtwork(data.card, artworkId))
}
else -> {
val config = store.state.globalState.configManager?.config ?: return@withContext
val blockchain = data.card.getBlockchain()
val primaryWalletManager = if (blockchain != null) {
walletManagerFactory.makeWalletManagerForApp(data.card, blockchain)
} else {
null
}
if (blockchain != null && primaryWalletManager != null) {
val primaryToken = data.card.getToken()
store.dispatch(WalletAction.MultiWallet.SetPrimaryBlockchain(blockchain))
if (primaryToken != null) {
primaryWalletManager.addToken(primaryToken)
store.dispatch(WalletAction.MultiWallet.SetPrimaryToken(primaryToken))
}
if (data.card.isMultiwalletAllowed) {
loadMultiWalletData(data.card, blockchain, primaryWalletManager)
} else {
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(primaryWalletManager))
store.dispatch(WalletAction.MultiWallet.AddBlockchains(listOf(blockchain)))
}
} else {
if (data.card.isMultiwalletAllowed) {
loadMultiWalletData(data.card, blockchain, null)
}
}
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.SetArtworkId(data.verifyResponse?.artworkInfo?.id))
store.dispatch(WalletAction.LoadWallet(config.isTopUpEnabled))
store.dispatch(WalletAction.LoadArtwork(data.card, artworkId))
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))
} else {
store.dispatch(WalletAction.LoadData.Failure(TapError.UnknownBlockchain))
store.dispatch(WalletAction.LoadArtwork(data.card, artworkId))
}
store.dispatch(WalletAction.Warnings.CheckIfNeeded)
}
}
private fun loadMultiWalletData(
card: Card, primaryBlockchain: Blockchain, primaryWalletManager: WalletManager
card: Card, primaryBlockchain: Blockchain?, primaryWalletManager: WalletManager?
) {
val presetTokens = primaryWalletManager.presetTokens.toList()
val presetTokens = primaryWalletManager?.presetTokens?.toList() ?: emptyList()
val savedCurrencies = currenciesRepository.loadCardCurrencies(card.cardId)
if (savedCurrencies == null) {
store.dispatch(WalletAction.MultiWallet.SaveCurrencies(
CardCurrencies(
blockchains = listOf(primaryBlockchain), tokens = presetTokens
)))
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(primaryWalletManager))
store.dispatch(WalletAction.MultiWallet.AddBlockchains(listOf(primaryBlockchain)))
store.dispatch(WalletAction.MultiWallet.AddTokens(presetTokens))
if (primaryBlockchain != null && primaryWalletManager != null) {
store.dispatch(WalletAction.MultiWallet.SaveCurrencies(
CardCurrencies(
blockchains = listOf(primaryBlockchain), tokens = presetTokens
)))
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(primaryWalletManager))
store.dispatch(WalletAction.MultiWallet.AddBlockchains(listOf(primaryBlockchain)))
store.dispatch(WalletAction.MultiWallet.AddTokens(presetTokens))
}
store.dispatch(WalletAction.MultiWallet.FindBlockchainsInUse(card, walletManagerFactory))
store.dispatch(WalletAction.MultiWallet.FindTokensInUse)
} else {
val blockchains = listOf(primaryBlockchain) + savedCurrencies.blockchains
val walletManagers = walletManagerFactory.makeWalletManagers(card, blockchains)
val tokens = presetTokens + savedCurrencies.tokens
val blockchains = savedCurrencies.blockchains
val walletManagers = walletManagerFactory.makeWalletManagersForApp(card, blockchains)
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(walletManagers))
store.dispatch(WalletAction.MultiWallet.AddBlockchains(blockchains))
store.dispatch(WalletAction.MultiWallet.AddTokens(tokens))
store.dispatch(WalletAction.MultiWallet.AddTokens(savedCurrencies.tokens))
store.dispatch(WalletAction.MultiWallet.FindTokensInUse)
}
}
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
when {
data.card.getBlockchain() == Blockchain.Unknown && !data.card.isMultiwalletAllowed -> {
store.dispatch(WalletAction.LoadData.Failure(TapError.UnknownBlockchain))
}
data.card.getStatus() == CardStatus.Empty ||
(data.card.isTwinCard() && data.secondTwinPublicKey == null) -> {
store.dispatch(WalletAction.EmptyWallet)
}
else -> {
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())
}
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))
}
}
}

View file

@ -32,7 +32,7 @@ object TapWorkarounds {
"0030",
"0031",
"0035"
) // Tangem tags
)
private val excludedIssuers = listOf(
"TTM BANK"
@ -43,5 +43,6 @@ val Card.isMultiwalletAllowed: Boolean
get() {
return cardData?.productMask?.contains(Product.TwinCard) != true
&& !TapWorkarounds.isStart2Coin
&& this.curve == EllipticCurve.Secp256k1
&& (this.firmwareVersion.major >= 4 ||
this.getWallets().getOrNull(0)?.curve == EllipticCurve.Secp256k1)
}

View file

@ -1,5 +1,7 @@
package com.tangem.tap.domain.configurable.warningMessage
import android.view.View
import androidx.annotation.StringRes
import com.squareup.moshi.Json
import com.tangem.blockchain.common.Blockchain
@ -7,15 +9,17 @@ import com.tangem.blockchain.common.Blockchain
[REDACTED_AUTHOR]
*/
data class WarningMessage(
val title: String,
val message: String,
val type: Type,
val priority: Priority,
val location: List<Location>,
private val blockchains: List<String>?,
val titleResId: Int? = null,
val messageResId: Int? = null,
val origin: Origin = Origin.Remote,
val title: String,
val message: String,
val type: Type,
val priority: Priority,
val location: List<Location>,
private val blockchains: List<String>?,
@StringRes val titleResId: Int? = null,
@StringRes val messageResId: Int? = null,
val origin: Origin = Origin.Remote,
@StringRes val buttonTextId: Int? = null,
val messageFormatArg: String? = null
) {
val blockchainList: List<Blockchain>? by lazy {
blockchains?.map { Blockchain.fromId(it.toUpperCase()) }
@ -43,6 +47,7 @@ data class WarningMessage(
Temporary, // можно скрыть (кнопка ОК)
AppRating
}
enum class Location {

View file

@ -1,8 +1,11 @@
package com.tangem.tap.domain.configurable.warningMessage
import com.tangem.TangemSdkError
import com.tangem.blockchain.common.Blockchain
import com.tangem.tangem_sdk_new.ui.animation.VoidCallback
import com.tangem.tap.common.extensions.containsAny
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.R
/**
@ -98,6 +101,19 @@ class WarningMessagesManager(
WarningMessage.Origin.Local
)
fun signedHashesMultiWalletWarning(): WarningMessage = WarningMessage(
title = "",
message = "",
type = WarningMessage.Type.Temporary,
priority = WarningMessage.Priority.Info,
location = listOf(WarningMessage.Location.MainScreen),
blockchains = null,
titleResId = R.string.warning_important_security_info,
messageResId = R.string.warning_signed_tx_previously,
origin = WarningMessage.Origin.Local,
buttonTextId = R.string.warning_button_learn_more,
)
fun appRatingWarning(): WarningMessage = WarningMessage(
"",
"",
@ -125,5 +141,20 @@ class WarningMessagesManager(
R.string.warning_failed_to_verify_card_message,
WarningMessage.Origin.Local
)
fun remainingSignaturesNotEnough(remainingSignatures: Int): WarningMessage = WarningMessage(
title = "",
message = "",
type = WarningMessage.Type.Permanent,
priority = WarningMessage.Priority.Critical,
listOf(WarningMessage.Location.MainScreen),
blockchains = null,
titleResId = R.string.alert_title,
messageResId = R.string.warning_low_signatures_format,
origin = WarningMessage.Origin.Local,
messageFormatArg = remainingSignatures.toString()
)
const val REMAINING_SIGNATURES_WARNING = 10
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.tap.domain.extensions
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.commands.common.card.EllipticCurve
import org.stellar.sdk.requests.ErrorResponse
import java.math.BigDecimal
@ -28,4 +29,15 @@ fun Blockchain.minimalAmount(): BigDecimal {
return 1.toBigDecimal().movePointLeft(decimals())
}
fun Blockchain.getCurve(): EllipticCurve? {
return when (this) {
Blockchain.Unknown -> null
Blockchain.Bitcoin, Blockchain.BitcoinTestnet, Blockchain.BitcoinCash, Blockchain.Litecoin,
Blockchain.Ducatus, Blockchain.Ethereum, Blockchain.EthereumTestnet, Blockchain.RSK,
Blockchain.Tezos, Blockchain.XRP, Blockchain.Binance, Blockchain.BinanceTestnet ->
EllipticCurve.Secp256k1
Blockchain.Cardano, Blockchain.CardanoShelley, Blockchain.Stellar -> EllipticCurve.Ed25519
}
}
private const val NODL = "NODL"

View file

@ -0,0 +1,47 @@
package com.tangem.tap.domain.extensions
import com.tangem.FirmwareConstraints
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.commands.common.card.Card
import com.tangem.commands.common.card.CardStatus
import com.tangem.commands.wallet.CardWallet
import com.tangem.commands.wallet.WalletStatus
import com.tangem.common.TangemSdkConstants
fun Card.getToken(): Token? {
val symbol = cardData?.tokenSymbol ?: return null
val contractAddress = cardData?.tokenContractAddress ?: return null
val decimals = cardData?.tokenDecimal ?: return null
return Token(symbol, contractAddress, decimals)
}
fun Card.getBlockchain(): Blockchain? {
val blockchainName: String = cardData?.blockchainName ?: return null
return Blockchain.fromId(blockchainName)
}
fun Card.getSingleWallet(): CardWallet? {
return wallet(TangemSdkConstants.getDefaultWalletIndex())
}
fun Card.getStatus(): CardStatus {
if (firmwareVersion < FirmwareConstraints.AvailabilityVersions.walletData) return status!!
return if (getWallets().any { it.status == WalletStatus.Loaded }) {
CardStatus.Loaded
} else {
CardStatus.Empty
}
}
fun Card.hasSignedHashes(): Boolean {
return getWallets().any { it.status == WalletStatus.Loaded && it.signedHashes ?: 0 > 0 }
}
fun Card.signedHashesCount(): Int {
return getWallets().map { it.signedHashes ?: 0 }.sum()
}
val Card.remainingSignatures: Int?
get() = this.getSingleWallet()?.remainingSignatures

View file

@ -0,0 +1,8 @@
package com.tangem.tap.domain.extensions
import com.tangem.commands.wallet.WalletIndex
import com.tangem.common.TangemSdkConstants
fun TangemSdkConstants.Companion.getDefaultWalletIndex(): WalletIndex {
return WalletIndex.Index(oldCardDefaultWalletIndex)
}

View file

@ -0,0 +1,33 @@
package com.tangem.tap.domain.extensions
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.commands.common.card.Card
import com.tangem.commands.common.card.EllipticCurve
fun WalletManagerFactory.makeWalletManagerForApp(card: Card, blockchain: Blockchain): WalletManager? {
val curve = blockchain.getCurve() ?: return null
val publicKey = card.getWallets().firstOrNull { it.curve == curve }?.publicKey ?: return null
return makeWalletManager(card.cardId, publicKey, blockchain, curve)
}
fun WalletManagerFactory.makeWalletManagersForApp(
card: Card, blockchains: List<Blockchain>
): List<WalletManager> {
return makeWalletManagersForCurve(card, blockchains, EllipticCurve.Secp256k1) +
makeWalletManagersForCurve(card, blockchains, EllipticCurve.Ed25519)
}
fun WalletManagerFactory.makeWalletManagersForCurve(
card: Card, blockchains: List<Blockchain>, curve: EllipticCurve
): List<WalletManager> {
val blockchainsForCurve = blockchains.filter { it.getCurve() == curve }
val walletPublicKey = card.getWallets().firstOrNull { it.curve == curve }?.publicKey
return if (walletPublicKey != null) {
makeWalletManagers(card.cardId, walletPublicKey, blockchainsForCurve, curve)
} else {
emptyList()
}
}

View file

@ -2,26 +2,39 @@ package com.tangem.tap.domain.tasks
import com.tangem.CardSession
import com.tangem.CardSessionRunnable
import com.tangem.commands.ReadCommand
import com.tangem.FirmwareConstraints
import com.tangem.TangemSdkError
import com.tangem.commands.common.card.Card
import com.tangem.common.CompletionResult
import com.tangem.tasks.CreateWalletTask
import com.tangem.tasks.PreflightReadCapable
import com.tangem.tasks.PreflightReadSettings
import com.tangem.tasks.PreflightReadTask
class CreateWalletAndRescanTask : CardSessionRunnable<ScanNoteResponse> {
class CreateWalletAndRescanTask : CardSessionRunnable<Card>, PreflightReadCapable {
override val requiresPin2 = false
override fun preflightReadSettings() = PreflightReadSettings.FullCardRead
override fun run(session: CardSession, callback: (result: CompletionResult<ScanNoteResponse>) -> Unit) {
CreateWalletTask().run(session) { result ->
override fun run(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit) {
val firmwareVerion = session.environment.card?.firmwareVersion
if (firmwareVerion == null) {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
return
}
val task = if (firmwareVerion < FirmwareConstraints.AvailabilityVersions.walletData) {
CreateWalletTask()
} else {
CreateWalletsTask()
}
task.run(session) { result ->
when (result) {
is CompletionResult.Success -> {
ReadCommand().run(session) { readResult ->
when (readResult) {
is CompletionResult.Success -> ScanNoteTask(readResult.data).run(session, callback)
is CompletionResult.Failure -> callback(CompletionResult.Failure(readResult.error))
}
}
}
is CompletionResult.Success ->
PreflightReadTask(PreflightReadSettings.FullCardRead).run(session, callback)
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
}

View file

@ -0,0 +1,64 @@
package com.tangem.tap.domain.tasks
import com.tangem.CardSession
import com.tangem.CardSessionRunnable
import com.tangem.commands.common.card.Card
import com.tangem.commands.common.card.EllipticCurve
import com.tangem.commands.wallet.CreateWalletCommand
import com.tangem.commands.wallet.WalletConfig
import com.tangem.common.CompletionResult
import com.tangem.tasks.PreflightReadSettings
import com.tangem.tasks.PreflightReadTask
class CreateWalletsTask(wallets: List<WalletConfig> = emptyList()) : CardSessionRunnable<Card> {
override val requiresPin2 = false
val wallets = if (wallets.isEmpty()) {
listOf(
WalletConfig(null, null, EllipticCurve.Secp256k1, null),
WalletConfig(null, null, EllipticCurve.Ed25519, null),
WalletConfig(null, null, EllipticCurve.Secp256r1, null),
)
} else {
wallets
}
var index = 0
override fun run(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit) {
val walletConfig = wallets[index]
createWallet(walletConfig, session, callback)
}
private fun createWallet(
walletConfig: WalletConfig, session: CardSession,
callback: (result: CompletionResult<Card>) -> Unit
) {
CreateWalletCommand(
walletConfig, walletIndexValue = walletConfig.curveId!!.toWalletIndex()
).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
if (index == wallets.lastIndex) {
PreflightReadTask(PreflightReadSettings.FullCardRead).run(session, callback)
return@run
}
index += 1
createWallet(wallets[index], session, callback)
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error))
}
}
}
}
}
private fun EllipticCurve.toWalletIndex(): Int {
return when (this) {
EllipticCurve.Secp256k1 -> 0
EllipticCurve.Ed25519 -> 1
EllipticCurve.Secp256r1 -> 2
}
}

View file

@ -1,36 +1,40 @@
package com.tangem.tap.domain.tasks
import com.tangem.*
import com.tangem.blockchain.common.Blockchain
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.verifycard.VerifyCardCommand
import com.tangem.commands.verifycard.VerifyCardResponse
import com.tangem.commands.common.card.EllipticCurve
import com.tangem.commands.verification.VerifyCardCommand
import com.tangem.commands.verification.VerifyCardResponse
import com.tangem.commands.wallet.WalletConfig
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.toHexString
import com.tangem.tap.domain.TapSdkError
import com.tangem.tap.domain.TapWorkarounds.isExcluded
import com.tangem.tap.domain.isMultiwalletAllowed
import com.tangem.tap.domain.extensions.getSingleWallet
import com.tangem.tap.domain.extensions.getStatus
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.domain.twins.isTwinCard
import com.tangem.tap.store
import com.tangem.tasks.PreflightReadCapable
import com.tangem.tasks.PreflightReadSettings
import com.tangem.tasks.ScanTask
data class ScanNoteResponse(
val walletManager: WalletManager?,
val card: Card,
val verifyResponse: VerifyCardResponse? = null,
val secondTwinPublicKey: String? = null,
) : CommandResponse
class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteResponse> {
class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteResponse>, PreflightReadCapable {
override val requiresPin2 = false
override fun preflightReadSettings() = PreflightReadSettings.FullCardRead
override fun run(session: CardSession, callback: (result: CompletionResult<ScanNoteResponse>) -> Unit) {
ScanTask().run(session) { result ->
when (result) {
@ -48,39 +52,25 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
return@run
}
if (card.isTwinCard()) {
dealWithTwinCard(card, session, callback)
return@run
}
val walletManager = try {
getWalletManagerFactory().makeWalletManager(card)
} catch (exception: Exception) {
if (card.isMultiwalletAllowed) {
// Create default Bitcoin WalletManager
getWalletManagerFactory().makeWalletManager(card, Blockchain.Bitcoin)
} else {
return@run callback(CompletionResult.Success(
ScanNoteResponse(null, card)
))
}
}
verifyCard(walletManager, card, null, session, callback)
verifyCard(card, session, callback)
}
}
}
}
private fun verifyCard(
walletManager: WalletManager?, card: Card, publicKey: String? = null,
session: CardSession, callback: (result: CompletionResult<ScanNoteResponse>) -> Unit
card: Card, session: CardSession, callback: (result: CompletionResult<ScanNoteResponse>) -> Unit
) {
VerifyCardCommand(true).run(session) { verifyResult ->
when (verifyResult) {
is CompletionResult.Success -> {
callback(CompletionResult.Success(ScanNoteResponse(
walletManager, card, verifyResult.data, publicKey)))
if (card.isTwinCard()) {
dealWithTwinCard(card, session, verifyResult.data, callback)
} else if (card.firmwareVersion.major >= 4) {
createMissingWalletsIfNeeded(card, session, verifyResult.data, callback)
} else {
callback(CompletionResult.Success(ScanNoteResponse(card, verifyResult.data)))
}
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(TangemSdkError.CardVerificationFailed()))
@ -89,39 +79,73 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
}
}
private fun createMissingWalletsIfNeeded(
card: Card, session: CardSession, verifyResponse: VerifyCardResponse,
callback: (result: CompletionResult<ScanNoteResponse>) -> Unit
) {
if (card.getStatus() == CardStatus.Empty) {
callback(CompletionResult.Success(ScanNoteResponse(card, verifyResponse)))
return
}
val curvesPresent = card.getWallets().map { it.curve }
val curvesToCreate = EllipticCurve.values().subtract(curvesPresent)
if (curvesToCreate.isEmpty()) {
callback(CompletionResult.Success(ScanNoteResponse(card, verifyResponse)))
return
}
val configs = curvesToCreate.map { curve ->
WalletConfig(
isReusable = null, prohibitPurgeWallet = null, curveId = curve,
signingMethods = null
)
}
CreateWalletsTask(configs).run(session) { result ->
when (result) {
is CompletionResult.Success ->
callback(CompletionResult.Success(ScanNoteResponse(result.data, verifyResponse)))
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
private fun dealWithTwinCard(
card: Card, session: CardSession,
card: Card, session: CardSession, verifyResponse: VerifyCardResponse,
callback: (result: CompletionResult<ScanNoteResponse>) -> Unit
) {
ReadIssuerDataCommand().run(session) { readDataResult ->
when (readDataResult) {
is CompletionResult.Success -> {
val publicKey = card.getSingleWallet()?.publicKey
if (publicKey == null) {
callback(CompletionResult.Success(ScanNoteResponse(card, null)))
return@run
}
val verified = TwinCardsManager.verifyTwinPublicKey(
readDataResult.data.issuerData, card.walletPublicKey
readDataResult.data.issuerData, publicKey
)
if (verified) {
val twinPublicKey = readDataResult.data.issuerData.sliceArray(0 until 65)
val walletManager = try {
getWalletManagerFactory().makeMultisigWalletManager(card, twinPublicKey)
} catch (exception: Exception) {
callback(CompletionResult.Success(ScanNoteResponse(null, card)))
return@run
}
verifyCard(walletManager, card, twinPublicKey.toHexString(), session, callback)
callback(CompletionResult.Success(
ScanNoteResponse(card, verifyResponse, twinPublicKey.toHexString())
))
return@run
} else {
callback(CompletionResult.Success(ScanNoteResponse(null, card)))
callback(CompletionResult.Success(ScanNoteResponse(card, null)))
}
}
is CompletionResult.Failure ->
callback(CompletionResult.Success(ScanNoteResponse(null, card)))
callback(CompletionResult.Success(ScanNoteResponse(card, null)))
}
}
}
private fun getErrorIfExcludedCard(card: Card): TangemError? {
if (card.isExcluded()) return TapSdkError.CardForDifferentApp
if (card.status == CardStatus.Purged) return TangemSdkError.CardIsPurged()
if (card.status == CardStatus.Purged) return TangemSdkError.WalletIsPurged()
if (card.status == CardStatus.NotPersonalized) return TangemSdkError.NotPersonalized()
return null
}

View file

@ -7,6 +7,7 @@ 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.commands.common.card.FirmwareVersion
import com.tangem.tap.common.extensions.readJsonFileToString
import com.tangem.tap.network.createMoshi
@ -93,19 +94,27 @@ class CurrenciesRepository(val context: Application) {
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)
fun getBlockchains(cardFirmware: FirmwareVersion?): List<Blockchain> {
return if (cardFirmware == null || cardFirmware.major < 4) {
secp256k1Blochcains
} else {
secp256k1Blochcains + ed25519Blockchains
}
}
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"
private val secp256k1Blochcains = listOf(
Blockchain.Bitcoin, Blockchain.BitcoinCash, Blockchain.Binance, Blockchain.Litecoin,
Blockchain.XRP, Blockchain.Tezos,
Blockchain.Ethereum, Blockchain.RSK)
private val ed25519Blockchains = listOf(Blockchain.CardanoShelley, Blockchain.Stellar)
}
}

View file

@ -2,18 +2,21 @@ package com.tangem.tap.domain.twins
import com.tangem.CardSession
import com.tangem.CardSessionRunnable
import com.tangem.commands.CreateWalletResponse
import com.tangem.commands.PurgeWalletCommand
import com.tangem.commands.common.card.CardStatus
import com.tangem.commands.wallet.CreateWalletResponse
import com.tangem.commands.wallet.PurgeWalletCommand
import com.tangem.common.CompletionResult
import com.tangem.common.TangemSdkConstants
import com.tangem.tap.domain.extensions.getDefaultWalletIndex
import com.tangem.tap.domain.extensions.getSingleWallet
import com.tangem.tasks.CreateWalletTask
class CreateFirstTwinWalletTask : CardSessionRunnable<CreateWalletResponse> {
override val requiresPin2 = false
override fun run(session: CardSession, callback: (result: CompletionResult<CreateWalletResponse>) -> Unit) {
if (session.environment.card?.walletPublicKey != null) {
PurgeWalletCommand().run(session) { response ->
if (session.environment.card?.getSingleWallet()?.publicKey != null) {
PurgeWalletCommand(TangemSdkConstants.getDefaultWalletIndex()).run(session) { response ->
when (response) {
is CompletionResult.Success -> {
session.environment.card = session.environment.card?.copy(status = CardStatus.Empty)

View file

@ -3,11 +3,14 @@ package com.tangem.tap.domain.twins
import com.tangem.CardSession
import com.tangem.CardSessionRunnable
import com.tangem.Message
import com.tangem.commands.CreateWalletResponse
import com.tangem.commands.PurgeWalletCommand
import com.tangem.commands.common.card.CardStatus
import com.tangem.commands.wallet.CreateWalletResponse
import com.tangem.commands.wallet.PurgeWalletCommand
import com.tangem.common.CompletionResult
import com.tangem.common.TangemSdkConstants
import com.tangem.common.extensions.hexToBytes
import com.tangem.tap.domain.extensions.getDefaultWalletIndex
import com.tangem.tap.domain.extensions.getSingleWallet
import com.tangem.tasks.CreateWalletTask
class CreateSecondTwinWalletTask(
@ -18,9 +21,9 @@ class CreateSecondTwinWalletTask(
override val requiresPin2 = true
override fun run(session: CardSession, callback: (result: CompletionResult<CreateWalletResponse>) -> Unit) {
if (session.environment.card?.walletPublicKey != null) {
if (session.environment.card?.getSingleWallet()?.publicKey != null) {
session.setInitialMessage(preparingMessage)
PurgeWalletCommand().run(session) { response ->
PurgeWalletCommand(TangemSdkConstants.getDefaultWalletIndex()).run(session) { response ->
when (response) {
is CompletionResult.Success -> {
session.environment.card =

View file

@ -3,7 +3,7 @@ package com.tangem.tap.domain.twins
import com.tangem.CardSession
import com.tangem.CardSessionRunnable
import com.tangem.KeyPair
import com.tangem.commands.ReadCommand
import com.tangem.commands.read.ReadCommand
import com.tangem.common.CompletionResult
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.tasks.ScanNoteTask

View file

@ -6,9 +6,11 @@ import com.tangem.KeyPair
import com.tangem.TangemSdkError
import com.tangem.commands.*
import com.tangem.common.CompletionResult
import com.tangem.common.TangemSdkConstants
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.guard
import com.tangem.common.files.FileHashHelper
import com.tangem.tap.domain.extensions.getDefaultWalletIndex
class WriteProtectedIssuerDataTask(
private val twinPublicKey: ByteArray, private val issuerKeys: KeyPair
@ -17,14 +19,14 @@ class WriteProtectedIssuerDataTask(
override fun run(session: CardSession, callback: (result: CompletionResult<WriteIssuerDataResponse>) -> Unit) {
val hashes = arrayOf(twinPublicKey.calculateSha256())
SignCommand(hashes).run(session) { signResult ->
SignCommand(hashes, TangemSdkConstants.getDefaultWalletIndex()).run(session) { signResult ->
when (signResult) {
is CompletionResult.Success -> {
ReadIssuerDataCommand().run(session) { readResult ->
when (readResult) {
is CompletionResult.Success -> {
writeIssuerData(
twinPublicKey, issuerKeys, signResult.data.signature,
twinPublicKey, issuerKeys, signResult.data.signatures[0],
readResult.data, session, callback
)
}

View file

@ -5,6 +5,7 @@ import com.tangem.commands.common.card.Card
import com.tangem.commands.common.card.masks.Settings
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.TapWorkarounds
import com.tangem.tap.domain.extensions.signedHashesCount
import com.tangem.tap.domain.extensions.toSendableAmounts
import com.tangem.tap.domain.twins.TwinsHelper
import com.tangem.tap.domain.twins.isTwinCard
@ -83,6 +84,7 @@ private fun handleEraseWallet(action: DetailsAction.EraseWallet, state: DetailsS
return when (action) {
DetailsAction.EraseWallet.Check -> {
val notAllowedByCard = state.card?.settingsMask?.contains(Settings.ProhibitPurgeWallet) == true
|| state.card?.settingsMask?.contains(Settings.IsReusable) == false
val notEmpty = state.wallets.any {
!it.recentTransactions.isNullOrEmpty() || it.amounts.toSendableAmounts().isNotEmpty()
}
@ -203,6 +205,5 @@ private fun prepareAllowedSecurityOptions(card: Card): EnumSet<SecurityOption> {
private fun Card.toCardInfo(): CardInfo? {
val cardId = this.cardId.chunked(4).joinToString(separator = " ")
val issuer = this.cardData?.issuerName ?: return null
val signedHashes = this.walletSignedHashes ?: return null
return CardInfo(cardId, issuer, signedHashes)
}
val signedHashes = this.signedHashesCount()
return CardInfo(cardId, issuer, signedHashes)}

View file

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

View file

@ -18,6 +18,7 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Wallet
import com.tangem.commands.common.card.Card
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.domain.extensions.signedHashesCount
import com.tangem.tap.store
import timber.log.Timber
import java.io.File
@ -161,7 +162,7 @@ class AdditionalEmailInfo {
fun setCardInfo(card: Card) {
cardId = card.cardId
cardFirmwareVersion = card.firmwareVersion.version
signedHashesCount = card.walletSignedHashes?.toString() ?: "0"
signedHashesCount = card.signedHashesCount().toString()
}
fun setWalletsInfo(wallets: List<Wallet>) {
@ -274,7 +275,6 @@ class FeedbackEmail : EmailData {
val builder = StringBuilder()
builder.appendKeyValue("Card ID", infoHolder.cardId)
builder.appendKeyValue("Firmware version", infoHolder.cardFirmwareVersion)
builder.appendKeyValue("Signed hashes", infoHolder.signedHashesCount)
infoHolder.walletsInfo.forEach {
builder.appendKeyValue("Blockchain", it.blockchain.fullName)

View file

@ -135,9 +135,11 @@ internal class AddressPayIdMiddleware {
if (failReason == null) {
noSchemeAddress.getQueryParameter("amount")?.toBigDecimalOrNull()?.let {
dispatch(AmountAction.SetAmount(it, false))
dispatch(AmountActionUi.CheckAmountToSend)
}
dispatch(SetWalletAddress(supposedAddress, isUserInput))
dispatch(TransactionExtrasAction.Prepare(wallet.blockchain, address, null))
dispatch(FeeAction.RequestFee)
} else {
dispatch(SetAddressError(failReason))
dispatch(TransactionExtrasAction.Release)

View file

@ -6,6 +6,7 @@ import com.tangem.common.extensions.isZero
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.send.redux.*
import com.tangem.tap.features.send.redux.states.MainCurrencyType
import com.tangem.tap.features.send.redux.states.SendState
import org.rekotlin.Action
import java.math.BigDecimal
@ -43,6 +44,7 @@ class AmountMiddleware {
}
dispatch(AmountAction.SetAmount(inputValueCrypto, true))
dispatch(AmountActionUi.CheckAmountToSend)
dispatch(FeeAction.RequestFee)
}
@ -78,15 +80,18 @@ class AmountMiddleware {
}
private fun setMaxAmount(appState: AppState?, dispatch: (Action) -> Unit) {
val amountState = appState?.sendState?.amountState ?: return
val sendState = appState?.sendState ?: return
dispatch(AmountAction.SetAmount(amountState.balanceCrypto, false))
dispatch(FeeAction.RequestFee)
if (!amountState.isCoinAmount()) return
dispatch(FeeAction.ChangeLayoutVisibility(main = true, controls = true, chipGroup = true))
dispatch(FeeActionUi.ChangeIncludeFee(true))
dispatch(AmountAction.SetAmount(sendState.amountState.balanceCrypto, false))
dispatch(AmountActionUi.CheckAmountToSend)
if (SendState.isReadyToRequestFee()) {
dispatch(FeeAction.RequestFee)
if (!sendState.amountState.isCoinAmount()) return
dispatch(FeeAction.ChangeLayoutVisibility(main = true, controls = true, chipGroup = true))
dispatch(FeeActionUi.ChangeIncludeFee(true))
}
}
private fun toggleMainCurrency(appState: AppState?, dispatch: (Action) -> Unit) {

View file

@ -10,6 +10,7 @@ import com.tangem.tap.features.send.redux.AmountActionUi
import com.tangem.tap.features.send.redux.FeeAction
import com.tangem.tap.features.send.redux.ReceiptAction
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.send.redux.states.SendState
import com.tangem.tap.scope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -26,7 +27,7 @@ class RequestFeeMiddleware {
val sendState = appState?.sendState ?: return
val walletManager = sendState.walletManager ?: return
if (!sendState.addressPayIdIsReady()) {
if (!SendState.isReadyToRequestFee()) {
dispatch(FeeAction.FeeCalculation.SetFeeError(FeeAction.Error.ADDRESS_OR_AMOUNT_IS_EMPTY))
dispatch(FeeAction.ChangeLayoutVisibility(main = false, chipGroup = true))
dispatch(ReceiptAction.RefreshReceipt)

View file

@ -4,8 +4,9 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras
import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder
import com.tangem.blockchain.common.*
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.Signer
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.commands.SignResponse
import com.tangem.commands.common.card.Card
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
@ -13,6 +14,7 @@ import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.TangemSigner
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.TapWorkarounds
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
@ -111,15 +113,24 @@ private fun sendTransaction(
if (TapWorkarounds.isStart2Coin) {
tangemSdk.config.linkedTerminal = false
}
val signer = Signer(tangemSdk, action.messageForSigner)
val signer = TangemSigner(
tangemSdk = tangemSdk, initialMessage = action.messageForSigner
) { signResponse ->
store.dispatch(
GlobalAction.UpdateWalletSignedHashes(
walletSignedHashes = signResponse.walletSignedHashes,
remainingSignatures = signResponse.walletRemainingSignatures,
walletPublicKey = walletManager.wallet.publicKey
)
)
}
val result = (walletManager as TransactionSender).send(txData, signer)
withContext(Dispatchers.Main) {
when (result) {
is Result.Success -> {
is SimpleResult.Success -> {
tangemSdk.config.linkedTerminal = isLinkedTerminal
FirebaseAnalyticsHandler.triggerEvent(AnalyticsEvent.TRANSACTION_IS_SENT, card)
dispatch(SendAction.SendSuccess)
dispatch(GlobalAction.UpdateWalletSignedHashes(result.data.walletSignedHashes))
dispatch(NavigationAction.PopBackTo())
scope.launch(Dispatchers.IO) {
withContext(Dispatchers.Main) {
@ -131,7 +142,7 @@ private fun sendTransaction(
}
}
}
is Result.Failure -> {
is SimpleResult.Failure -> {
when (result.error) {
is CreateAccountUnderfunded -> {
val error = result.error as CreateAccountUnderfunded

View file

@ -41,7 +41,7 @@ class ReceiptReducer : SendInternalReducer {
val layoutType = determineLayoutType(amountState.mainCurrency.type, amountState.typeOfAmount)
val symbols = determineSymbols(wallet)
val showBlank = !sendState.isReadyToSend()
val showBlank = !SendState.isReadyToSend()
val result = state.copy(
visibleTypeOfReceipt = layoutType,
mainCurrency = amountState.mainCurrency,

View file

@ -5,7 +5,6 @@ import com.tangem.tap.common.CurrencyConverter
import com.tangem.tap.features.send.redux.*
import com.tangem.tap.features.send.redux.states.IdStateHolder
import com.tangem.tap.features.send.redux.states.SendState
import com.tangem.tap.store
import org.rekotlin.Action
import java.math.BigDecimal
@ -60,8 +59,7 @@ private class EmptyReducer : SendInternalReducer {
private class PrepareSendScreenStatesReducer : SendInternalReducer {
override fun handle(action: SendScreenAction, sendState: SendState): SendState {
val prepareAction = action as PrepareSendScreen
val walletManager = action.walletManager
?: store.state.globalState.scanNoteResponse!!.walletManager!!
val walletManager = action.walletManager!!
val amountToExtract = prepareAction.tokenAmount ?: prepareAction.coinAmount!!
val decimals = amountToExtract.decimals

View file

@ -45,13 +45,6 @@ data class SendState(
override val stateId: StateId = StateId.SEND_SCREEN
fun isReadyToSend(): Boolean {
val sendState = store.state.sendState
return addressPayIdIsReady() && sendState.amountState.isReady() && sendState.feeState.isReady()
}
fun addressPayIdIsReady(): Boolean = store.state.sendState.addressPayIdState.isReady()
fun getDecimals(type: MainCurrencyType): Int = when (type) {
MainCurrencyType.FIAT -> 2
MainCurrencyType.CRYPTO -> amountState.amountToExtract?.decimals ?: 0
@ -110,6 +103,17 @@ data class SendState(
AmountType.Reserve -> false
}
}
companion object {
fun addressPayIdIsReady(): Boolean = store.state.sendState.addressPayIdState.isReady()
fun amountIsReady(): Boolean = store.state.sendState.amountState.isReady()
fun isReadyToRequestFee(): Boolean = addressPayIdIsReady() && amountIsReady()
fun isReadyToSend(): Boolean = addressPayIdIsReady() && amountIsReady() &&
store.state.sendState.feeState.isReady()
}
}
enum class SendButtonState {

View file

@ -13,12 +13,10 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.textfield.TextInputEditText
import com.tangem.Message
import com.tangem.merchant.common.toggleWidget.ToggleWidget
import com.tangem.tangem_sdk_new.extensions.dpToPx
import com.tangem.tangem_sdk_new.extensions.hideSoftKeyboard
import com.tangem.tap.common.KeyboardObserver
import com.tangem.tap.common.entities.TapCurrency
import com.tangem.tap.common.extensions.getDrawableCompat
import com.tangem.tap.common.extensions.getFromClipboard
import com.tangem.tap.common.extensions.setOnImeActionListener
import com.tangem.tap.common.qrCodeScan.ScanQrCodeActivity
@ -54,17 +52,11 @@ import java.text.DecimalFormatSymbols
*/
class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
lateinit var sendBtn: ToggleWidget
lateinit var sendBtn: ViewStateWidget
private lateinit var etAmountToSend: TextInputEditText
private lateinit var warningsAdapter: WarningMessagesAdapter
private fun initSendButtonStates() {
sendBtn = ToggleWidget(flSendButtonContainer, btnSend, progress, ProgressState.None())
sendBtn.setupSendButtonStateModifiers(requireContext())
sendBtn.setState(ProgressState.None())
}
private val sendSubscriber = SendStateSubscriber(this)
private lateinit var keyboardObserver: KeyboardObserver
@ -79,12 +71,15 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
setupAmountLayout()
setupFeeLayout()
setupWarningMessages()
}
private fun initSendButtonStates() {
btnSend.setOnClickListener {
store.dispatch(SendActionUi.SendAmountToRecipient(
Message(getString(R.string.initial_message_sign_header))
))
}
sendBtn = IndeterminateProgressButtonWidget(btnSend, progress)
}
private fun setupAddressOrPayIdLayout() {
@ -99,14 +94,12 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
.filter { store.state.sendState.addressPayIdState.viewFieldValue.value != it }
.onEach {
store.dispatch(AddressPayIdActionUi.HandleUserInput(it))
store.dispatch(FeeAction.RequestFee)
}
.launchIn(mainScope)
imvPaste.setOnClickListener {
store.dispatch(PasteAddressPayId(requireContext().getFromClipboard()?.toString() ?: ""))
store.dispatch(TruncateOrRestore(!etAddressOrPayId.isFocused))
store.dispatch(FeeAction.RequestFee)
}
imvQrCode.setOnClickListener {
startActivityForResult(
@ -126,12 +119,6 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
.onEach { store.dispatch(TransactionExtrasAction.XlmMemo.HandleUserInput(it)) }
.launchIn(mainScope)
// groupMemo.setOnCheckedChangeListener { group, checkedId ->
// if (checkedId == -1) return@setOnCheckedChangeListener
//
// store.dispatch(TransactionExtrasAction.XlmMemo.ChangeSelectedMemo(MemoUiHelper.toType(checkedId)))
// }
etDestinationTag.inputtedTextAsFlow()
.debounce(400)
.filter {
@ -154,7 +141,6 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
imvQrCode.postDelayed({
store.dispatch(PasteAddressPayId(scannedCode))
store.dispatch(TruncateOrRestore(!etAddressOrPayId.isFocused))
store.dispatch(FeeAction.RequestFee)
}, 200)
}
@ -176,7 +162,6 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
etAmountToSend.clearFocus()
etAmountToSend.postDelayed(200) { etAmountToSend.hideSoftKeyboard() }
store.dispatch(SetMaxAmount)
store.dispatch(CheckAmountToSend)
}
var snackbarControlledByChangingFocus = false
keyboardObserver = KeyboardObserver(requireActivity())
@ -229,6 +214,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
flExpandCollapse.setOnClickListener {
store.dispatch(ToggleControlsVisibility)
}
chipGroup.check(FeeUiHelper.toId(FeeType.NORMAL))
chipGroup.setOnCheckedChangeListener { group, checkedId ->
if (checkedId == -1) return@setOnCheckedChangeListener
@ -307,37 +293,4 @@ class FeeUiHelper {
}
}
}
}
//class MemoUiHelper {
// companion object {
// fun toId(memo: MemoType): Int {
// return when (memo) {
// MemoType.TEXT -> R.id.chipMemoText
// MemoType.ID -> R.id.chipMemoId
// }
// }
//
// fun toType(id: Int): MemoType {
// return when (id) {
// R.id.chipMemoText -> MemoType.TEXT
// R.id.chipMemoId -> MemoType.ID
// else -> MemoType.TEXT
// }
// }
// }
//}
private fun ToggleWidget.setupSendButtonStateModifiers(context: Context) {
mainViewModifiers.clear()
mainViewModifiers.add(ReplaceTextStateModifier(context.getString(R.string.send_title), ""))
mainViewModifiers.add(
TextViewDrawableStateModifier(
context.getDrawableCompat(R.drawable.ic_arrow_right), null, TextViewDrawableStateModifier.RIGHT
))
mainViewModifiers.add(ClickableStateModifier())
toggleViewModifiers.clear()
toggleViewModifiers.add(ShowHideStateModifier())
}
}

View file

@ -119,15 +119,15 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
when (state.sendButtonState) {
SendButtonState.ENABLED -> {
fg.btnSend.isEnabled = true
sendFragment.sendBtn.setState(ProgressState.None(), true)
sendFragment.sendBtn.changeState(ProgressState.None)
}
SendButtonState.DISABLED -> {
fg.btnSend.isEnabled = false
sendFragment.sendBtn.setState(ProgressState.None(), true)
sendFragment.sendBtn.changeState(ProgressState.None)
}
SendButtonState.PROGRESS -> {
fg.btnSend.isEnabled = true
sendFragment.sendBtn.setState(ProgressState.Progress(), true)
sendFragment.sendBtn.changeState(ProgressState.Progress())
}
}

View file

@ -13,8 +13,9 @@ class TokensMiddleware {
{ action ->
when (action) {
is TokensAction.LoadCurrencies -> {
val cardFirmware = state()?.globalState?.scanNoteResponse?.card?.firmwareVersion
val tokens = currenciesRepository.getPopularTokens()
val blockchains = currenciesRepository.getBlockchains()
val blockchains = currenciesRepository.getBlockchains(cardFirmware)
val currencies = CurrencyListItem.createListOfCurrencies(
blockchains, tokens
)

View file

@ -7,7 +7,10 @@ import androidx.annotation.StringRes
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.squareup.picasso.Callback
import com.squareup.picasso.Picasso
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.IconsUtil
import com.tangem.blockchain.common.Token
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.global.CryptoCurrencyName
@ -16,6 +19,7 @@ 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.lang.Exception
import java.util.*
class CurrenciesAdapter : ListAdapter<CurrencyListItem, RecyclerView.ViewHolder>(DiffUtilCallback) {
@ -104,9 +108,11 @@ class CurrenciesAdapter : ListAdapter<CurrencyListItem, RecyclerView.ViewHolder>
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
Picasso.get().loadCurrenciesIcon(
imageView = view.iv_currency,
textView = view.tv_token_letter,
blockchain = blockchain, token = null
)
view.btn_add_token.setOnClickListener {
store.dispatch(WalletAction.MultiWallet.AddBlockchain(blockchain))
@ -123,10 +129,11 @@ class CurrenciesAdapter : ListAdapter<CurrencyListItem, RecyclerView.ViewHolder>
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)
Picasso.get().loadCurrenciesIcon(
imageView = view.iv_currency,
textView = view.tv_token_letter,
token = token, blockchain = Blockchain.Ethereum
)
view.btn_add_token.setOnClickListener {
store.dispatch(WalletAction.MultiWallet.AddToken(token))
view.btn_add_token.hide()
@ -152,7 +159,10 @@ sealed class CurrencyListItem {
data class TitleListItem(@StringRes val titleResId: Int) : CurrencyListItem()
companion object {
fun createListOfCurrencies(blockchains: List<Blockchain>, tokens: List<Token>): List<CurrencyListItem> {
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)) +

View file

@ -68,6 +68,7 @@ sealed class WalletAction : Action {
object SetNeverToShow : Warnings()
object RemindLater : Warnings()
}
class CheckRemainingSignatures(val remainingSignatures: Int?) : Warnings()
}
data class UpdateWallet(val currency: CryptoCurrencyName? = null) : WalletAction() {
@ -111,6 +112,7 @@ sealed class WalletAction : Action {
object ShowDialog : WalletAction() {
object QrCode : WalletAction()
object ScanFails : WalletAction()
object SignedHashesMultiWalletDialog : WalletAction()
}
object HideDialog : WalletAction()

View file

@ -81,6 +81,9 @@ data class WalletState(
&& (walletData.token == null || walletData.token != store.state.walletState.primaryToken)) {
val walletManager = getWalletManager(walletData.currencyData.currencySymbol)
?: return true
if (walletData.token == null && walletManager.presetTokens.isNotEmpty()) return false
val wallet = walletManager.wallet
if (walletData.blockchain != null) {
return wallet.recentTransactions.toPendingTransactions(wallet.address).isEmpty() &&
@ -112,7 +115,8 @@ sealed class WalletDialog: StateDialog {
) : WalletDialog()
data class SelectAmountToSendDialog(val amounts: List<Amount>?) : WalletDialog()
object ScanFailsDialog: WalletDialog()
object ScanFailsDialog : WalletDialog()
object SignedHashesMultiWalletDialog : WalletDialog()
}
enum class ProgressState { Loading, Done, Error }

View file

@ -10,6 +10,8 @@ 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.domain.extensions.makeWalletManagerForApp
import com.tangem.tap.domain.extensions.makeWalletManagersForApp
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.scope
@ -37,16 +39,16 @@ class MultiWalletMiddleware {
globalState?.scanNoteResponse?.card?.cardId?.let {
currenciesRepository.saveAddedToken(it, action.token)
}
addToken(action.token, walletState)
addToken(action.token, walletState, globalState)
}
is WalletAction.MultiWallet.AddTokens -> {
action.tokens.map { addToken(it, walletState) }
action.tokens.map { addToken(it, walletState, globalState) }
}
is WalletAction.MultiWallet.AddBlockchain -> {
globalState?.scanNoteResponse?.card?.let { card ->
currenciesRepository.saveAddedBlockchain(card.cardId, action.blockchain)
globalState.tapWalletManager.walletManagerFactory
.makeWalletManager(card, action.blockchain)?.let {
.makeWalletManagerForApp(card, action.blockchain)?.let {
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(it))
}
}
@ -60,15 +62,18 @@ class MultiWalletMiddleware {
is WalletAction.MultiWallet.RemoveWallet -> {
val cardId = globalState?.scanNoteResponse?.card?.cardId
if (action.walletData.token != null) {
walletState?.getWalletManagerForToken(action.walletData.token.symbol)
?.removeToken(action.walletData.token)
cardId?.let { currenciesRepository.removeToken(it, action.walletData.token) }
} else if (action.walletData.blockchain != null) {
cardId?.let { currenciesRepository.removeBlockchain(it, action.walletData.blockchain) }
}
}
is WalletAction.MultiWallet.FindBlockchainsInUse -> {
val blockchains = currenciesRepository.getBlockchains()
val cardFirmware = globalState?.scanNoteResponse?.card?.firmwareVersion
val blockchains = currenciesRepository.getBlockchains(cardFirmware)
.filterNot { walletState?.blockchains?.contains(it) == true }
val walletManagers = action.factory.makeWalletManagers(action.card, blockchains)
val walletManagers = action.factory.makeWalletManagersForApp(action.card, blockchains)
scope.launch {
walletManagers.map { walletManager ->
@ -117,8 +122,15 @@ class MultiWalletMiddleware {
}
}
private fun addToken(token: Token, walletState: WalletState?) {
val walletManager = walletState?.getWalletManager(token.symbol)
private fun addToken(token: Token, walletState: WalletState?, globalState: GlobalState?) {
val card = globalState?.scanNoteResponse?.card ?: return
val walletManager = walletState?.getWalletManager(token.symbol) ?:
globalState.tapWalletManager.walletManagerFactory.makeWalletManagerForApp(
card = card,
blockchain = Blockchain.Ethereum
)?.also { walletManager ->
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(walletManager))
}
scope.launch {
val result = walletManager?.addToken(token)
withContext(Dispatchers.Main) {

View file

@ -108,7 +108,11 @@ class WalletMiddleware {
)
when (result) {
is CompletionResult.Success -> {
globalState?.tapWalletManager?.onCardScanned(result.data)
val scanNoteResponse =
globalState?.scanNoteResponse?.copy(card = result.data)
scanNoteResponse?.let {
globalState.tapWalletManager.onCardScanned(scanNoteResponse)
}
}
}
}

View file

@ -6,7 +6,7 @@ import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.commands.common.card.Card
import com.tangem.commands.common.card.CardType
import com.tangem.commands.verifycard.VerifyCardState
import com.tangem.commands.verification.VerifyCardState
import com.tangem.common.extensions.getType
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
@ -14,6 +14,9 @@ import com.tangem.tap.common.extensions.isGreaterThan
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.extensions.getSingleWallet
import com.tangem.tap.domain.extensions.hasSignedHashes
import com.tangem.tap.domain.extensions.remainingSignatures
import com.tangem.tap.domain.isMultiwalletAllowed
import com.tangem.tap.domain.twins.isTwinCard
import com.tangem.tap.features.wallet.redux.WalletAction
@ -46,6 +49,17 @@ class WarningsMiddleware {
is WalletAction.Warnings.AppRating.SetNeverToShow -> {
preferencesStorage.appRatingLaunchObserver.setNeverToShow()
}
is WalletAction.Warnings.CheckRemainingSignatures -> {
if (action.remainingSignatures != null &&
action.remainingSignatures <= WarningMessagesManager.REMAINING_SIGNATURES_WARNING
) {
addWarningMessage(
warning =
WarningMessagesManager.remainingSignaturesNotEnough(action.remainingSignatures),
autoUpdate = true
)
}
}
}
}
@ -63,17 +77,18 @@ class WarningsMiddleware {
}
private fun showCardWarningsIfNeeded(globalState: GlobalState?) {
val validator = globalState?.scanNoteResponse?.walletManager as? SignatureCountValidator
globalState?.scanNoteResponse?.card?.let { card ->
globalState.warningManager?.removeWarnings(WarningMessage.Origin.Local)
showWarningLowRemainingSignaturesIfNeeded(card)
if (card.getType() != CardType.Release) {
addWarningMessage(WarningMessagesManager.devCardWarning())
} else if (!preferencesStorage.wasCardScannedBefore(card.cardId)) {
checkIfWarningNeeded(card, validator)?.let { addWarningMessage(it) }
checkIfWarningNeeded(card)?.let { warning -> addWarningMessage(warning) }
}
if (card.getType() == CardType.Release) {
if (globalState.scanNoteResponse.verifyResponse?.verificationState ==
VerifyCardState.VerifiedOffline) {
VerifyCardState.VerifiedOffline
) {
addWarningMessage(WarningMessagesManager.onlineVerificationFailed())
}
}
@ -81,13 +96,37 @@ class WarningsMiddleware {
}
}
private fun checkIfWarningNeeded(
card: Card, signatureCountValidator: SignatureCountValidator? = null,
): WarningMessage? {
if (card.isTwinCard() || card.isMultiwalletAllowed) return null
private fun showWarningLowRemainingSignaturesIfNeeded(card: Card) {
val remainingSignatures = card.remainingSignatures
if (remainingSignatures != null &&
remainingSignatures <= WarningMessagesManager.REMAINING_SIGNATURES_WARNING
) {
addWarningMessage(
WarningMessagesManager.remainingSignaturesNotEnough(
remainingSignatures
)
)
}
}
return if (signatureCountValidator == null) {
if (card.walletSignedHashes ?: 0 > 0) {
private fun checkIfWarningNeeded(
card: Card
): WarningMessage? {
if (card.isTwinCard()) return null
if (card.isMultiwalletAllowed) {
return if (card.hasSignedHashes()) {
WarningMessagesManager.signedHashesMultiWalletWarning()
} else {
store.dispatch(WalletAction.Warnings.CheckHashesCount.SaveCardId)
null
}
}
val validator = store.state.walletState.walletManagers.firstOrNull()
as? SignatureCountValidator
return if (validator == null) {
if (card.hasSignedHashes()) {
WarningMessagesManager.alreadySignedHashesWarning()
} else {
store.dispatch(WalletAction.Warnings.CheckHashesCount.SaveCardId)
@ -108,10 +147,11 @@ class WarningsMiddleware {
if (card.isTwinCard() || card.isMultiwalletAllowed) return
val validator = store.state.globalState.scanNoteResponse?.walletManager
val validator = store.state.walletState.walletManagers.firstOrNull()
as? SignatureCountValidator
scope.launch {
val result = validator?.validateSignatureCount(card.walletSignedHashes ?: 0)
val signedHashes = card.getSingleWallet()?.signedHashes ?: 0
val result = validator?.validateSignatureCount(signedHashes)
withContext(Dispatchers.Main) {
when (result) {
SimpleResult.Success -> {
@ -120,9 +160,15 @@ class WarningsMiddleware {
}
is SimpleResult.Failure ->
if (result.error is BlockchainSdkError.SignatureCountNotMatched) {
addWarningMessage(WarningMessagesManager.alreadySignedHashesWarning(), true)
} else if (card.walletSignedHashes ?: 0 > 0) {
addWarningMessage(WarningMessagesManager.alreadySignedHashesWarning(), true)
addWarningMessage(
WarningMessagesManager.alreadySignedHashesWarning(),
true
)
} else if (signedHashes > 0) {
addWarningMessage(
WarningMessagesManager.alreadySignedHashesWarning(),
true
)
}
}
}
@ -140,6 +186,9 @@ class WarningsMiddleware {
private fun getWarnings(): List<WarningMessage> {
val warningManager = store.state.globalState.warningManager ?: return emptyList()
return warningManager.getWarnings(WarningMessage.Location.MainScreen, store.state.walletState.blockchains)
return warningManager.getWarnings(
WarningMessage.Location.MainScreen,
store.state.walletState.blockchains
)
}
}

View file

@ -225,6 +225,9 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
is WalletAction.ShowDialog.ScanFails -> {
newState = newState.copy(walletDialog = WalletDialog.ScanFailsDialog)
}
is WalletAction.ShowDialog.SignedHashesMultiWalletDialog -> {
newState = newState.copy(walletDialog = WalletDialog.SignedHashesMultiWalletDialog)
}
is WalletAction.HideDialog -> {
newState = newState.copy(walletDialog = null)
}

View file

@ -6,10 +6,14 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.squareup.picasso.Callback
import com.squareup.picasso.Picasso
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.IconsUtil
import com.tangem.blockchain.common.Token
import com.tangem.tap.common.extensions.getColor
import com.tangem.tap.common.extensions.getIconRes
import com.tangem.tap.common.extensions.loadCurrenciesIcon
import com.tangem.tap.common.extensions.show
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletData
@ -17,6 +21,11 @@ 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 kotlinx.android.synthetic.main.item_currency_wallet.view.iv_currency
import kotlinx.android.synthetic.main.item_currency_wallet.view.tv_currency_symbol
import kotlinx.android.synthetic.main.item_currency_wallet.view.tv_token_letter
import kotlinx.android.synthetic.main.item_popular_token.view.*
import java.lang.Exception
import java.math.BigDecimal
class WalletAdapter
@ -89,16 +98,15 @@ class WalletAdapter
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)
}
val blockchain = wallet.blockchain
val token = wallet.token
Picasso.get().loadCurrenciesIcon(
imageView = view.iv_currency,
textView = view.tv_token_letter,
token = token, blockchain = blockchain
)
when (wallet.currencyData.status) {
BalanceStatus.VerifiedOnline, BalanceStatus.SameCurrencyTransactionInProgress -> hideWarning()
BalanceStatus.Loading -> {

View file

@ -37,9 +37,11 @@ class WarningMessagesAdapter : ListAdapter<WarningMessage, WarningMessageVH>(Dif
}
object DiffUtilCallback : DiffUtil.ItemCallback<WarningMessage>() {
override fun areContentsTheSame(oldItem: WarningMessage, newItem: WarningMessage) = oldItem == newItem
override fun areContentsTheSame(oldItem: WarningMessage, newItem: WarningMessage) =
oldItem == newItem
override fun areItemsTheSame(oldItem: WarningMessage, newItem: WarningMessage) = oldItem == newItem
override fun areItemsTheSame(oldItem: WarningMessage, newItem: WarningMessage) =
oldItem == newItem
}
}
@ -52,10 +54,13 @@ class WarningMessageVH(val view: View) : RecyclerView.ViewHolder(view) {
}
private fun setText(warning: WarningMessage) {
fun getString(resId: Int?, default: String) = if (resId == null) default else view.getString(resId)
fun getString(resId: Int?, default: String, formatArgs: String? = null) =
if (resId == null) default else view.context.getString(resId, formatArgs)
view.tv_title.text = getString(warning.titleResId, warning.title)
view.tv_message.text = getString(warning.messageResId, warning.message)
view.tv_message.text = getString(
resId = warning.messageResId, default = warning.message, formatArgs = warning.messageFormatArg
)
}
private fun setBgColor(priority: WarningMessage.Priority) {
@ -67,63 +72,83 @@ class WarningMessageVH(val view: View) : RecyclerView.ViewHolder(view) {
view.card_view.setCardBackgroundColor(view.context.resources.getColor(color))
}
private fun setupControlButtons(warning: WarningMessage) {
when (warning.type) {
WarningMessage.Type.Permanent -> {
view.group_controls_temporary.hide()
view.group_controls_rating.hide()
}
WarningMessage.Type.Temporary -> {
view.group_controls_rating.hide()
view.group_controls_temporary.show()
view.btn_got_it.setOnClickListener { store.dispatch(GlobalAction.HideWarningMessage(warning)) }
}
WarningMessage.Type.AppRating -> {
view.group_controls_temporary.hide()
view.group_controls_rating.show()
view.btn_close.setOnClickListener {
FirebaseAnalyticsHandler.triggerEvent(AnalyticsEvent.APP_RATING_DISMISS)
store.dispatch(GlobalAction.HideWarningMessage(warning))
store.dispatch(WalletAction.Warnings.AppRating.RemindLater)
}
view.btn_can_be_better.setOnClickListener {
FirebaseAnalyticsHandler.triggerEvent(AnalyticsEvent.APP_RATING_NEGATIVE)
store.dispatch(WalletAction.Warnings.AppRating.SetNeverToShow)
store.dispatch(GlobalAction.HideWarningMessage(warning))
store.dispatch(GlobalAction.SendFeedback(RateCanBeBetterEmail()))
}
view.btn_really_cool.setOnClickListener {
val activity = view.context.getActivity() ?: return@setOnClickListener
FirebaseAnalyticsHandler.triggerEvent(AnalyticsEvent.APP_RATING_POSITIVE)
store.dispatch(WalletAction.Warnings.AppRating.SetNeverToShow)
val reviewManager = ReviewManagerFactory.create(activity)
val task = reviewManager.requestReviewFlow()
task.addOnCompleteListener {
if (it.isSuccessful) {
val reviewFlow = reviewManager.launchReviewFlow(activity, it.result)
reviewFlow.addOnCompleteListener {
if (it.isSuccessful) {
// send review was succeed
} else {
// send fails
}
}
} else {
Timber.e(task.exception)
private fun setupControlButtons(warning: WarningMessage) = when (warning.type) {
WarningMessage.Type.Permanent -> {
view.group_controls_temporary.hide()
view.group_controls_rating.hide()
}
WarningMessage.Type.Temporary -> {
view.group_controls_rating.hide()
view.group_controls_temporary.show()
val buttonAction =
when (warning.titleResId) {
R.string.warning_important_security_info -> {
View.OnClickListener {
store.dispatch(WalletAction.ShowDialog.SignedHashesMultiWalletDialog)
}
}
else -> {
View.OnClickListener {
store.dispatch(GlobalAction.HideWarningMessage(warning))
}
}.addOnFailureListener {
Timber.e(it)
}
store.dispatch(GlobalAction.HideWarningMessage(warning))
}
val buttonTitle = view.getString(
warning.buttonTextId ?: R.string.how_to_got_it_button
)
view.btn_got_it.setOnClickListener (buttonAction)
view.btn_got_it.text = buttonTitle
}
WarningMessage.Type.AppRating -> {
view.group_controls_temporary.hide()
view.group_controls_rating.show()
view.btn_close.setOnClickListener {
FirebaseAnalyticsHandler.triggerEvent(AnalyticsEvent.APP_RATING_DISMISS)
store.dispatch(GlobalAction.HideWarningMessage(warning))
store.dispatch(WalletAction.Warnings.AppRating.RemindLater)
}
view.btn_can_be_better.setOnClickListener {
FirebaseAnalyticsHandler.triggerEvent(AnalyticsEvent.APP_RATING_NEGATIVE)
store.dispatch(WalletAction.Warnings.AppRating.SetNeverToShow)
store.dispatch(GlobalAction.HideWarningMessage(warning))
store.dispatch(GlobalAction.SendFeedback(RateCanBeBetterEmail()))
}
view.btn_really_cool.setOnClickListener {
val activity = view.context.getActivity() ?: return@setOnClickListener
FirebaseAnalyticsHandler.triggerEvent(AnalyticsEvent.APP_RATING_POSITIVE)
store.dispatch(WalletAction.Warnings.AppRating.SetNeverToShow)
val reviewManager = ReviewManagerFactory.create(activity)
val task = reviewManager.requestReviewFlow()
task.addOnCompleteListener {
if (it.isSuccessful) {
val reviewFlow = reviewManager.launchReviewFlow(activity, it.result)
reviewFlow.addOnCompleteListener {
if (it.isSuccessful) {
// send review was succeed
} else {
// send fails
}
}
} else {
Timber.e(task.exception)
}
}.addOnFailureListener {
Timber.e(it)
}
store.dispatch(GlobalAction.HideWarningMessage(warning))
}
}
}
}
class SpacesItemDecoration(private val spacePx: Int) : ItemDecoration() {
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
outRect.left = spacePx
outRect.right = spacePx

View file

@ -0,0 +1,33 @@
package com.tangem.tap.features.wallet.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.R
class SignedHashesWarningDialog {
companion object {
fun create(context: Context): AlertDialog {
return AlertDialog.Builder(context).apply {
setTitle(context.getString(R.string.warning_important_security_info))
setMessage(R.string.alert_signed_hashes_message)
setPositiveButton(R.string.alert_button_i_understand) { _, _ ->
store.dispatch(WalletAction.Warnings.CheckHashesCount.SaveCardId)
store.dispatch(
GlobalAction.HideWarningMessage(
WarningMessagesManager.signedHashesMultiWalletWarning()
)
)
}
setNegativeButton(R.string.common_cancel) { _, _ -> }
setOnDismissListener {
store.dispatch(WalletAction.HideDialog)
}
}.create()
}
}
}

View file

@ -15,6 +15,7 @@ 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.features.wallet.ui.dialogs.ScanFailsDialog
import com.tangem.tap.features.wallet.ui.dialogs.SignedHashesWarningDialog
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.card_balance.*
@ -34,28 +35,26 @@ class MultiWalletView : WalletView {
override fun changeWalletView(fragment: WalletFragment) {
setFragment(fragment)
onViewCreated()
showMultiWalletView()
setupButtons()
showMultiWalletView(fragment)
setupButtons(fragment)
}
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 showMultiWalletView(fragment: WalletFragment) = with(fragment) {
tv_twin_card_number.hide()
iv_twin_card.hide()
rv_pending_transaction.hide()
l_card_balance.hide()
l_address.hide()
l_buttons_short.hide()
l_buttons_long.hide()
btn_scan_multiwallet?.show()
rv_multiwallet.show()
btn_add_token.show()
}
private fun setupButtons() {
val fragment = fragment ?: return
fragment.btn_scan_multiwallet?.setOnClickListener { store.dispatch(WalletAction.Scan) }
private fun setupButtons(fragment: WalletFragment) = with(fragment) {
btn_scan_multiwallet?.setOnClickListener { store.dispatch(WalletAction.Scan) }
}
override fun setFragment(fragment: WalletFragment) {
@ -96,40 +95,40 @@ class MultiWalletView : WalletView {
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)
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)
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
fragment: WalletFragment, errorTitle: CharSequence, errorDescription: CharSequence,
) = with(fragment) {
l_card_balance.show()
l_balance.hide()
l_balance_error.show()
rv_multiwallet.hide()
btn_add_token.hide()
tv_error_title.text = errorTitle
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)
private fun configureButtonsForEmptyWalletState(fragment: WalletFragment) = with(fragment) {
btn_scan_multiwallet.hide()
l_buttons_long.show()
btn_scan_long.setOnClickListener { store.dispatch(WalletAction.Scan) }
btn_confirm_long.setOnClickListener { store.dispatch(WalletAction.CreateWallet) }
btn_confirm_long.text = fragment.getText(R.string.wallet_button_create_wallet)
}
private fun handleDialogs(walletDialog: StateDialog?) {
@ -139,6 +138,11 @@ class MultiWalletView : WalletView {
is WalletDialog.ScanFailsDialog -> {
if (dialog == null) dialog = ScanFailsDialog.create(context).apply { show() }
}
is WalletDialog.SignedHashesMultiWalletDialog -> {
if (dialog == null) {
dialog = SignedHashesWarningDialog.create(context).apply { show() }
}
}
else -> {
dialog?.dismiss()
dialog = null

View file

@ -19,6 +19,7 @@ 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.features.wallet.ui.dialogs.ScanFailsDialog
import com.tangem.tap.features.wallet.ui.dialogs.SignedHashesWarningDialog
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.card_balance.*
@ -44,17 +45,16 @@ class SingleWalletView : WalletView {
override fun changeWalletView(fragment: WalletFragment) {
setFragment(fragment)
onViewCreated()
showSingleWalletView()
showSingleWalletView(fragment)
}
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()
private fun showSingleWalletView(fragment: WalletFragment) = with(fragment) {
rv_multiwallet.hide()
btn_add_token.hide()
btn_scan_multiwallet?.hide()
rv_pending_transaction.hide()
l_card_balance.show()
l_address.show()
}
override fun onViewCreated() {
@ -70,13 +70,15 @@ class SingleWalletView : WalletView {
}
override fun onNewState(state: WalletState) {
setupTwinCards(state.twinCardsState)
val fragment = fragment ?: return
state.primaryWallet ?: return
setupButtons(state.primaryWallet, state.twinCardsState != null)
setupAddressCard(state.primaryWallet)
setupTwinCards(state.twinCardsState, fragment)
setupButtons(state.primaryWallet, state.twinCardsState != null, fragment)
setupAddressCard(state.primaryWallet, fragment)
showPendingTransactionsIfPresent(state.primaryWallet.pendingTransactions)
setupBalance(state, state.primaryWallet)
handleDialogs(state.walletDialog)
handleDialogs(state.walletDialog, fragment)
}
private fun showPendingTransactionsIfPresent(pendingTransactions: List<PendingTransaction>) {
@ -92,43 +94,45 @@ class SingleWalletView : WalletView {
}
}
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()
}
if (twinCardsState?.showTwinOnboarding == true) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.TwinsOnboarding))
private fun setupTwinCards(
twinCardsState: TwinCardsState?, fragment: WalletFragment
) = with(fragment) {
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 =
this.getString(R.string.wallet_twins_chip_format, number)
}
if (twinCardsState?.cardNumber == null) {
tv_twin_card_number.hide()
iv_twin_card.hide()
}
if (twinCardsState?.showTwinOnboarding == true) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.TwinsOnboarding))
}
}
private fun setupButtons(state: WalletData, isTwinsWallet: Boolean) {
val fragment = fragment ?: return
private fun setupButtons(
state: WalletData, isTwinsWallet: Boolean, fragment: WalletFragment
) = with(fragment){
setupButtonsType(state, fragment)
val btnConfirm = if (state.topUpState.allowed) {
fragment.btn_confirm_short
btn_confirm_short
} else {
fragment.btn_confirm_long
btn_confirm_long
}
val btnScan = if (state.topUpState.allowed) {
fragment.btn_scan_short
btn_scan_short
} else {
fragment.btn_scan_long
btn_scan_long
}
setupConfirmButton(state, btnConfirm, fragment, isTwinsWallet)
@ -137,27 +141,27 @@ class SingleWalletView : WalletView {
store.dispatch(WalletAction.Scan)
}
fragment.btn_copy.setOnClickListener {
btn_copy.setOnClickListener {
state.walletAddresses?.selectedAddress?.address?.let { addressString ->
store.dispatch(WalletAction.CopyAddress(addressString, fragment.requireContext()))
}
}
fragment.btn_show_qr.setOnClickListener { store.dispatch(WalletAction.ShowDialog.QrCode) }
btn_show_qr.setOnClickListener { store.dispatch(WalletAction.ShowDialog.QrCode) }
fragment.btn_top_up.setOnClickListener {
btn_top_up.setOnClickListener {
store.dispatch(
WalletAction.TopUpAction.TopUp(fragment.requireContext(), R.color.backgroundLightGray)
)
}
}
private fun setupButtonsType(state: WalletData, fragment: WalletFragment) {
private fun setupButtonsType(state: WalletData, fragment: WalletFragment) = with(fragment) {
if (state.topUpState.allowed) {
fragment.l_buttons_long.hide()
fragment.l_buttons_short.show()
l_buttons_long.hide()
l_buttons_short.show()
} else {
fragment.l_buttons_long.show()
fragment.l_buttons_short.hide()
l_buttons_long.show()
l_buttons_short.hide()
}
}
@ -185,39 +189,37 @@ class SingleWalletView : WalletView {
}
private fun setupAddressCard(state: WalletData) {
val fragment = fragment ?: return
private fun setupAddressCard(state: WalletData, fragment: WalletFragment) = with(fragment) {
if (state.walletAddresses != null && state.blockchain != null) {
fragment.l_address?.show()
l_address?.show()
if (state.shouldShowMultipleAddress()) {
(fragment.l_address as? ViewGroup)?.beginDelayedTransition()
fragment.chip_group_address_type.show()
fragment.chip_group_address_type.fitChipsByGroupWidth()
(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) fragment.chip_group_address_type.check(checkedId)
if (checkedId != View.NO_ID) chip_group_address_type.check(checkedId)
fragment.chip_group_address_type.setOnCheckedChangeListener { group, checkedId ->
chip_group_address_type.setOnCheckedChangeListener { group, checkedId ->
if (checkedId == -1) return@setOnCheckedChangeListener
val type = MultipleAddressUiHelper.idToType(checkedId, state.blockchain)
type?.let { store.dispatch(WalletAction.ChangeSelectedAddress(type)) }
}
} else {
fragment.chip_group_address_type.hide()
chip_group_address_type.hide()
}
fragment.tv_address.text = state.walletAddresses.selectedAddress.address
fragment.tv_explore?.setOnClickListener {
tv_address.text = state.walletAddresses.selectedAddress.address
tv_explore?.setOnClickListener {
store.dispatch(WalletAction.ExploreAddress(
state.walletAddresses.selectedAddress.exploreUrl,
fragment.requireContext()))
}
} else {
fragment.l_address?.hide()
l_address?.hide()
}
}
private fun handleDialogs(walletDialog: StateDialog?) {
val fragment = fragment ?: return
private fun handleDialogs(walletDialog: StateDialog?, fragment: WalletFragment) {
val context = fragment.context ?: return
when (walletDialog) {
is WalletDialog.QrDialog -> {
@ -237,6 +239,11 @@ class SingleWalletView : WalletView {
is WalletDialog.ScanFailsDialog -> {
if (dialog == null) dialog = ScanFailsDialog.create(context).apply { show() }
}
is WalletDialog.SignedHashesMultiWalletDialog -> {
if (dialog == null) {
dialog = SignedHashesWarningDialog.create(context).apply { show() }
}
}
null -> {
dialog?.dismiss()
dialog = null

View file

@ -44,6 +44,7 @@ class NetworkConnectivity(
(capabilities != null) &&
(capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN) ||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET))
} else {

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View file

@ -108,6 +108,8 @@
android:layout_height="wrap_content"
android:textColor="@color/darkGray1"
android:textSize="16sp"
android:paddingTop="7dp"
android:paddingBottom="7dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_issuer"
tools:text="48 hashes" />

View file

@ -103,5 +103,14 @@ this wallet.
<string name="warning_failed_to_verify_card_message" translatable="false">This card might be a production sample or counterfeit</string>
<string name="warning_important_security_info" translatable="false">Important security information \u26A0</string>
<string name="warning_signed_tx_previously" translatable="false">This card has signed transactions in the past</string>
<string name="warning_button_learn_more" translatable="false">Learn more</string>
<string name="alert_signed_hashes_message" translatable="false">This card is not a bearer note. We cant currently match the signature count on the card with the information on the blockchain. This is normal but in rare cases can mean a previous holder is holding back an offline signature, which is a security concern.
\n\nDo not accept this card as physical payment from someone you dont trust.
\n\nIts perfectly safe in all other respects.
\n\nTangem is the only hardware wallet to offer signature count protection.</string>
<string name="alert_button_i_understand" translatable="false">I understand</string>
<string name="warning_low_signatures_format" translatable="false">There are only %s signatures available on this card. You must withdraw all of your funds.</string>
</resources>

View file

@ -1,4 +1,4 @@
ext.versions = [
kotlin : '1.4.31',
build_gradle: '4.1.3',
kotlin : '1.5.0',
build_gradle: '4.2.0',
]

View file

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip