Updated on 2026-08-14
This commit is contained in:
commit
9b7851ec16
16 changed files with 211 additions and 17 deletions
|
|
@ -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
|
||||
|
|
@ -32,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) {
|
||||
|
|
@ -47,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)
|
||||
}
|
||||
|
|
|
|||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ data class WarningMessage(
|
|||
@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()) }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
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
|
||||
|
|
@ -140,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
|
||||
}
|
||||
}
|
||||
|
|
@ -41,4 +41,7 @@ fun Card.hasSignedHashes(): Boolean {
|
|||
|
||||
fun Card.signedHashesCount(): Int {
|
||||
return getWallets().map { it.signedHashes ?: 0 }.sum()
|
||||
}
|
||||
}
|
||||
|
||||
val Card.remainingSignatures: Int?
|
||||
get() = this.getSingleWallet()?.remainingSignatures
|
||||
|
|
@ -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
|
||||
|
|
@ -204,5 +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
|
||||
return CardInfo(cardId, issuer)
|
||||
}
|
||||
val signedHashes = this.signedHashesCount()
|
||||
return CardInfo(cardId, issuer, signedHashes)}
|
||||
|
|
@ -26,6 +26,7 @@ data class DetailsState(
|
|||
data class CardInfo(
|
||||
val cardId: String,
|
||||
val issuer: String,
|
||||
val signedHashes: Int,
|
||||
)
|
||||
|
||||
enum class EraseWalletState { Allowed, NotAllowedByCard, NotEmpty }
|
||||
|
|
|
|||
|
|
@ -74,8 +74,12 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
|
|||
}
|
||||
tv_card_id.text = cardId
|
||||
tv_issuer.text = state.cardInfo.issuer
|
||||
tv_signed_hashes.text = state.cardInfo.signedHashes.toString()
|
||||
}
|
||||
|
||||
tv_signed_hashes.show(state.card?.isTwinCard() != true)
|
||||
tv_signed_hashes_title.show(state.card?.isTwinCard() != true)
|
||||
|
||||
tv_disclaimer.setOnClickListener { store.dispatch(DetailsAction.ShowDisclaimer) }
|
||||
|
||||
tv_card_tou.show(state.cardTermsOfUseUrl != null)
|
||||
|
|
|
|||
|
|
@ -6,12 +6,15 @@ import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder
|
|||
import com.tangem.blockchain.common.*
|
||||
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
|
||||
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
|
||||
|
|
@ -110,7 +113,17 @@ 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) {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ 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
|
||||
|
|
@ -48,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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -67,6 +79,7 @@ class WarningsMiddleware {
|
|||
private fun showCardWarningsIfNeeded(globalState: GlobalState?) {
|
||||
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)) {
|
||||
|
|
@ -74,7 +87,8 @@ class WarningsMiddleware {
|
|||
}
|
||||
if (card.getType() == CardType.Release) {
|
||||
if (globalState.scanNoteResponse.verifyResponse?.verificationState ==
|
||||
VerifyCardState.VerifiedOffline) {
|
||||
VerifyCardState.VerifiedOffline
|
||||
) {
|
||||
addWarningMessage(WarningMessagesManager.onlineVerificationFailed())
|
||||
}
|
||||
}
|
||||
|
|
@ -82,8 +96,21 @@ class WarningsMiddleware {
|
|||
}
|
||||
}
|
||||
|
||||
private fun showWarningLowRemainingSignaturesIfNeeded(card: Card) {
|
||||
val remainingSignatures = card.remainingSignatures
|
||||
if (remainingSignatures != null &&
|
||||
remainingSignatures <= WarningMessagesManager.REMAINING_SIGNATURES_WARNING
|
||||
) {
|
||||
addWarningMessage(
|
||||
WarningMessagesManager.remainingSignaturesNotEnough(
|
||||
remainingSignatures
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkIfWarningNeeded(
|
||||
card: Card
|
||||
card: Card
|
||||
): WarningMessage? {
|
||||
if (card.isTwinCard()) return null
|
||||
|
||||
|
|
@ -133,9 +160,15 @@ class WarningsMiddleware {
|
|||
}
|
||||
is SimpleResult.Failure ->
|
||||
if (result.error is BlockchainSdkError.SignatureCountNotMatched) {
|
||||
addWarningMessage(WarningMessagesManager.alreadySignedHashesWarning(), true)
|
||||
addWarningMessage(
|
||||
WarningMessagesManager.alreadySignedHashesWarning(),
|
||||
true
|
||||
)
|
||||
} else if (signedHashes > 0) {
|
||||
addWarningMessage(WarningMessagesManager.alreadySignedHashesWarning(), true)
|
||||
addWarningMessage(
|
||||
WarningMessagesManager.alreadySignedHashesWarning(),
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -153,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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -54,11 +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) {
|
||||
|
|
|
|||
|
|
@ -90,6 +90,30 @@
|
|||
app:layout_constraintTop_toBottomOf="@id/tv_card_id"
|
||||
tools:text="Tangem" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_signed_hashes_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="7dp"
|
||||
android:paddingBottom="7dp"
|
||||
android:text="@string/details_row_title_signed_hashes"
|
||||
android:textColor="@color/darkGray6"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_issuer_title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_signed_hashes"
|
||||
android:layout_width="wrap_content"
|
||||
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" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_disclaimer"
|
||||
android:layout_width="match_parent"
|
||||
|
|
@ -102,7 +126,7 @@
|
|||
android:textSize="16sp"
|
||||
app:drawableEndCompat="@drawable/ic_angle_bracket_right"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_issuer_title" />
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_signed_hashes_title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_card_tou"
|
||||
|
|
|
|||
|
|
@ -111,4 +111,6 @@ this wallet.
|
|||
\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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue