Updated on 2026-08-14
This commit is contained in:
commit
12585fa412
60 changed files with 1116 additions and 670 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -6,6 +6,9 @@
|
|||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
# Google services
|
||||
|
||||
|
||||
# Gradle generated files
|
||||
.gradle
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -629,6 +629,13 @@
|
|||
"name" : "QASH",
|
||||
"contractAddress" : "0x618e75ac90b12c6049ba3b27f5d5f8651b0037f6"
|
||||
},
|
||||
{
|
||||
"decimalCount" : 8,
|
||||
"symbol" : "QCX",
|
||||
"name" : "QuickX Protocol",
|
||||
"contractAddress" : "0xf9e5af7b42d31d51677c75bbbd37c1986ec79aee",
|
||||
"customIcon": "qcx"
|
||||
},
|
||||
{
|
||||
"decimalCount" : 8,
|
||||
"symbol" : "QRL",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)))
|
||||
}
|
||||
|
||||
|
|
|
|||
65
app/src/main/java/com/tangem/tap/domain/TangemSigner.kt
Normal file
65
app/src/main/java/com/tangem/tap/domain/TangemSigner.kt
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
47
app/src/main/java/com/tangem/tap/domain/extensions/Card.kt
Normal file
47
app/src/main/java/com/tangem/tap/domain/extensions/Card.kt
Normal 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
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)) +
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
BIN
app/src/main/res/drawable-ldpi/ic_qcx.png
Normal file
BIN
app/src/main/res/drawable-ldpi/ic_qcx.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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 can’t 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 don’t trust.
|
||||
\n\nIt’s 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>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
ext.versions = [
|
||||
kotlin : '1.4.31',
|
||||
build_gradle: '4.1.3',
|
||||
kotlin : '1.5.0',
|
||||
build_gradle: '4.2.0',
|
||||
]
|
||||
|
|
|
|||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue