Updated on 2026-08-14

This commit is contained in:
Tangem 2020-09-25 22:21:44 +03:00
parent e7fe22d1e4
commit 7e60798849
15 changed files with 212 additions and 19 deletions

View file

@ -65,6 +65,8 @@ dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10'
implementation 'com.tangem:blockchain:1.49.0'
implementation 'com.tangem:core:1.57.0'
implementation 'com.tangem:sdk:1.57.0'
//lifecycle
implementation "androidx.lifecycle:lifecycle-runtime:2.2.0"

View file

@ -34,11 +34,11 @@ class TangemSdkManager(val activity: ComponentActivity) {
}
suspend fun setPasscode(cardId: String?): CompletionResult<SetPinResponse> {
return runTaskAsyncReturnOnMain(SetPinCommand.setPin1(null), cardId)
return runTaskAsyncReturnOnMain(SetPinCommand.setPin2(null), cardId)
}
suspend fun setAccessCode(cardId: String?): CompletionResult<SetPinResponse> {
return runTaskAsyncReturnOnMain(SetPinCommand.setPin2(null), cardId)
return runTaskAsyncReturnOnMain(SetPinCommand.setPin1(null), cardId)
}
suspend fun setLongTap(cardId: String?): CompletionResult<SetPinResponse> {

View file

@ -1,8 +1,8 @@
package com.tangem.tap.domain
import androidx.annotation.StringRes
import com.tangem.TangemError
import com.tangem.wallet.R
import java.math.BigDecimal
interface TapErrors
interface MultiMessageError : TapErrors {
@ -34,6 +34,14 @@ sealed class TapError(@StringRes val localizedMessage: Int) : Throwable(), TapEr
) : TapError(-1), MultiMessageError
}
sealed class TapSdkError(override val messageResId: Int?) : Throwable(), TangemError {
final override val code: Int = 1
override var customMessage: String = code.toString()
object CardForDifferentApp : TapSdkError(R.string.error_card_for_different_app)
}
fun TapErrors.assembleErrorIds(): MutableList<Int> {
val idList = mutableListOf<Int>()
when (this) {

View file

@ -79,7 +79,9 @@ class TapWalletManager {
suspend fun onCardScanned(data: ScanNoteResponse) {
withContext(Dispatchers.Main) {
store.dispatch(WalletAction.ResetState)
store.dispatch(GlobalAction.SaveScanNoteResponse(data))
store.dispatch(WalletAction.CheckIfWarningNeeded)
val artworkId = data.verifyResponse?.artworkInfo?.id
if (data.walletManager != null) {
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {

View file

@ -2,14 +2,18 @@ package com.tangem.tap.domain.tasks
import com.tangem.CardSession
import com.tangem.CardSessionRunnable
import com.tangem.TangemError
import com.tangem.TangemSdkError
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.commands.Card
import com.tangem.commands.CardStatus
import com.tangem.commands.CommandResponse
import com.tangem.commands.Product
import com.tangem.commands.verifycard.VerifyCardCommand
import com.tangem.commands.verifycard.VerifyCardResponse
import com.tangem.common.CompletionResult
import com.tangem.tap.domain.TapSdkError
import com.tangem.tasks.ScanTask
data class ScanNoteResponse(
@ -29,6 +33,12 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
is CompletionResult.Success -> {
val card = this.card ?: result.data
val error = getErrorIfExcludedCard(card)
if (error != null) {
callback(CompletionResult.Failure(error))
return@run
}
val walletManager = try {
WalletManagerFactory.makeWalletManager(card)
} catch (exception: Exception) {
@ -50,4 +60,21 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
}
}
}
private fun getErrorIfExcludedCard(card: Card): TangemError? {
if (card.cardData?.productMask?.contains(Product.Note) != true) {
return TapSdkError.CardForDifferentApp
}
if (excludedBatches.contains(card.cardData?.batchId)) {
return TapSdkError.CardForDifferentApp
}
if (card.status == CardStatus.Purged) return TangemSdkError.CardIsPurged()
if (card.status == CardStatus.NotPersonalized) return TangemSdkError.NotPersonalized()
return null
}
companion object {
private val excludedBatches = listOf("0027", "0030", "0031")
}
}

View file

@ -125,7 +125,7 @@ private fun handleSecurityAction(
allowedSecurityOptions.add(SecurityOption.AccessCode)
}
if (allowSetPin2 && (isDefaultPin1 || !prohibitDefaultPin)) {
allowedSecurityOptions.add(SecurityOption.AccessCode)
allowedSecurityOptions.add(SecurityOption.PassCode)
}
state.copy(securityScreenState = state.securityScreenState?.copy(

View file

@ -14,8 +14,10 @@ import java.math.BigDecimal
sealed class WalletAction : Action {
object ResetState : WalletAction()
object LoadData : WalletAction() {
data class Failure(val error: TapError): WalletAction()
data class Failure(val error: TapError) : WalletAction()
}
object LoadWallet : WalletAction() {
@ -24,6 +26,13 @@ sealed class WalletAction : Action {
data class Failure(val errorMessage: String? = null) : WalletAction()
}
object CheckIfWarningNeeded : WalletAction()
object CheckHashesCountOnline : WalletAction()
object NeedToCheckHashesCountOnline : WalletAction()
object ConfirmHashesCount : WalletAction()
data class ShowWarning(val warningType: WarningType) : WalletAction()
object SaveCardId : WalletAction()
object UpdateWallet : WalletAction() {
data class Success(val wallet: Wallet) : WalletAction()
data class Failure(val errorMessage: String? = null) : WalletAction()
@ -50,6 +59,7 @@ sealed class WalletAction : Action {
data class ChooseCurrency(val amounts: List<Amount>?) : WalletAction()
object Cancel : WalletAction()
}
object CreatePayId : WalletAction() {
data class CompleteCreatingPayId(val payId: String) : WalletAction()
data class Success(val payId: String) : WalletAction()
@ -68,7 +78,7 @@ sealed class WalletAction : Action {
}
object ShowQrCode : WalletAction()
object HideQrCode : WalletAction()
object HideDialog : WalletAction()
data class ExploreAddress(val context: Context) : WalletAction()
object CreateWallet : WalletAction()
object EmptyWallet : WalletAction()

View file

@ -5,7 +5,10 @@ import android.net.Uri
import androidx.core.content.ContextCompat
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.SignatureCountValidator
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.commands.Card
import com.tangem.commands.common.network.Result
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.toHexString
@ -17,7 +20,9 @@ import com.tangem.tap.domain.PayIdManager
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.toSendableAmounts
import com.tangem.tap.features.send.redux.PrepareSendScreen
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.network.NetworkStateChanged
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
@ -65,7 +70,10 @@ val walletMiddleware: Middleware<AppState> = { dispatch, state ->
scope.launch { store.state.globalState.tapWalletManager.updateWallet() }
}
is WalletAction.UpdateWallet.Success -> setupWalletUpdate(action.wallet)
is WalletAction.LoadWallet.Success -> setupWalletUpdate(action.wallet)
is WalletAction.LoadWallet.Success -> {
store.dispatch(WalletAction.CheckHashesCountOnline)
setupWalletUpdate(action.wallet)
}
is WalletAction.CreatePayId.CompleteCreatingPayId -> {
scope.launch {
val cardId = store.state.globalState.scanNoteResponse?.card?.cardId
@ -100,13 +108,21 @@ val walletMiddleware: Middleware<AppState> = { dispatch, state ->
}
}
}
is WalletAction.LoadData, is NetworkStateChanged -> {
is WalletAction.LoadData -> {
scope.launch {
store.state.globalState.scanNoteResponse?.let {
store.state.globalState.tapWalletManager.onCardScanned(it)
}
}
}
is NetworkStateChanged -> {
store.state.globalState.scanNoteResponse?.let { scanNoteResponse ->
store.dispatch(WalletAction.CheckHashesCountOnline)
scope.launch {
store.state.globalState.tapWalletManager.onCardScanned(scanNoteResponse)
}
}
}
is WalletAction.CopyAddress -> {
store.state.walletState.addressData?.address?.let {
action.context.copyToClipboard(it)
@ -125,6 +141,20 @@ val walletMiddleware: Middleware<AppState> = { dispatch, state ->
store.dispatch(NavigationAction.NavigateTo(AppScreen.Send))
}
}
is WalletAction.CheckIfWarningNeeded -> {
val card = store.state.globalState.scanNoteResponse?.card
val validator = store.state.globalState.scanNoteResponse?.walletManager
as? SignatureCountValidator
if (card != null && !preferencesStorage.wasCardScannedBefore(card.cardId)) {
val result = checkIfWarningNeeded(card, validator)
if (result != null) store.dispatch(WalletAction.ShowWarning(result))
}
}
is WalletAction.CheckHashesCountOnline -> checkHashesCountOnline()
is WalletAction.SaveCardId -> {
val cardId = store.state.globalState.scanNoteResponse?.card?.cardId
cardId?.let { preferencesStorage.saveScannedCardId(it) }
}
}
next(action)
}
@ -132,7 +162,7 @@ val walletMiddleware: Middleware<AppState> = { dispatch, state ->
}
private fun setupWalletUpdate(wallet: Wallet) {
if (!wallet.recentTransactions.isNullOrEmpty()) {
if (!wallet.recentTransactions.isNullOrEmpty() && !store.state.walletState.updatingWallet) {
scope.launch(Dispatchers.IO) {
delay(10000)
withContext(Dispatchers.Main) {
@ -159,4 +189,48 @@ private fun prepareSendAction(amount: Amount?): Action {
PrepareSendScreen(amountToSend)
}
}
}
private fun checkIfWarningNeeded(
card: Card, signatureCountValidator: SignatureCountValidator? = null
): WarningType? {
// if (card.getType() != CardType.Release) {
// return WarningType.DevCard
// }
return if (signatureCountValidator == null) {
if (card.walletSignedHashes ?: 0 > 0) {
WarningType.CardSignedHashesBefore
} else {
store.dispatch(WalletAction.SaveCardId)
null
}
} else {
store.dispatch(WalletAction.NeedToCheckHashesCountOnline)
null
}
}
private fun checkHashesCountOnline() {
if (store.state.walletState.hashesCountVerified != false) return
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) return
val card = store.state.globalState.scanNoteResponse?.card
val validator = store.state.globalState.scanNoteResponse?.walletManager
as? SignatureCountValidator
scope.launch {
val result = validator?.validateSignatureCount(card?.walletSignedHashes
?: 0)
withContext(Dispatchers.Main) {
when (result) {
SimpleResult.Success -> store.dispatch(WalletAction.ConfirmHashesCount)
is SimpleResult.Failure -> store.dispatch(
WalletAction.ShowWarning(WarningType.CardSignedHashesBefore)
)
}
}
}
}

View file

@ -31,7 +31,8 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
var newState = state.walletState
when (action) {
is WalletAction.EmptyWallet -> newState = WalletState(
is WalletAction.ResetState -> newState = WalletState()
is WalletAction.EmptyWallet -> newState = newState.copy(
state = ProgressState.Done,
currencyData = BalanceWidgetData(BalanceStatus.EmptyCard),
mainButton = WalletMainButton.CreateWalletButton(true)
@ -45,7 +46,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
} else {
AddressData(wallet.address, wallet.shareUrl, wallet.exploreUrl)
}
newState = WalletState(
newState = newState.copy(
state = ProgressState.Error,
error = ErrorType.NoInternetConnection,
addressData = addressData,
@ -56,7 +57,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
)
}
is TapError.UnknownBlockchain -> {
newState = WalletState(
newState = newState.copy(
state = ProgressState.Done,
currencyData = BalanceWidgetData(BalanceStatus.UnknownBlockchain)
)
@ -77,7 +78,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
} else {
null
}
newState = WalletState(
newState = newState.copy(
state = ProgressState.Loading,
cardImage = cardImage,
currencyData = BalanceWidgetData(
@ -109,7 +110,9 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
errorMessage = action.errorMessage
)
)
is WalletAction.UpdateWallet -> newState = newState.copy(updatingWallet = true)
is WalletAction.UpdateWallet.Success -> newState = onWalletLoaded(action.wallet, newState)
is WalletAction.UpdateWallet.Failure -> newState = newState.copy(updatingWallet = false)
is WalletAction.LoadFiatRate -> {
newState.copy(currencyData = newState.currencyData.copy(
fiatAmount = null,
@ -158,7 +161,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
)
)
}
is WalletAction.HideQrCode -> {
is WalletAction.HideDialog -> {
newState = newState.copy(walletDialog = null)
}
is WalletAction.LoadPayId.Success -> newState = newState.copy(
@ -185,7 +188,12 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
)
}
is WalletAction.Send.Cancel -> newState = newState.copy(walletDialog = null)
is WalletAction.ShowWarning ->
newState = newState.copy(walletDialog = WalletDialog.WarningDialog(action.warningType))
is WalletAction.NeedToCheckHashesCountOnline ->
newState = newState.copy(hashesCountVerified = false)
is WalletAction.ConfirmHashesCount ->
newState = newState.copy(hashesCountVerified = true)
}
return newState
}
@ -223,6 +231,7 @@ private fun onWalletLoaded(wallet: Wallet, walletState: WalletState): WalletStat
fiatAmount = fiatAmount
),
pendingTransactions = pendingTransactions,
updatingWallet = false,
mainButton = WalletMainButton.SendButton(sendButtonEnabled)
)
}

View file

@ -15,10 +15,12 @@ data class WalletState(
val cardImage: Artwork? = null,
val wallet: Wallet? = null,
val pendingTransactions: List<PendingTransaction> = emptyList(),
val hashesCountVerified: Boolean? = null,
val addressData: AddressData? = null,
val currencyData: BalanceWidgetData = BalanceWidgetData(),
val payIdData: PayIdData = PayIdData(),
val walletDialog: WalletDialog? = null,
val updatingWallet: Boolean = false,
val mainButton: WalletMainButton = WalletMainButton.SendButton(false)
) : StateType {
val showDetails: Boolean =
@ -33,8 +35,11 @@ sealed class WalletDialog {
data class CreatePayIdDialog(val creatingPayIdState: CreatingPayIdState?) : WalletDialog()
data class SelectAmountToSendDialog(val amounts: List<Amount>?) : WalletDialog()
data class WarningDialog(val type: WarningType) : WalletDialog()
}
enum class WarningType { CardSignedHashesBefore, DevCard }
enum class ProgressState { Loading, Done, Error }

View file

@ -23,6 +23,7 @@ import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.ui.dialogs.AmountToSendDialog
import com.tangem.tap.features.wallet.ui.dialogs.PayIdDialog
import com.tangem.tap.features.wallet.ui.dialogs.QrDialog
import com.tangem.tap.features.wallet.ui.dialogs.WarningDialog
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.card_balance.*
@ -229,6 +230,11 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
this.show(walletDialog.amounts)
}
}
is WalletDialog.WarningDialog -> {
if (dialog == null) dialog = WarningDialog(requireContext()).apply {
this.show(walletDialog.type)
}
}
null -> {
dialog?.dismiss()
dialog = null

View file

@ -18,8 +18,8 @@ class QrDialog(context: Context) : Dialog(context) {
}
fun showQr(qrCode: Bitmap, shareUrl: String, currencyName: CryptoCurrencyName?) {
this.setOnDismissListener { store.dispatch(WalletAction.HideQrCode) }
this.btn_done?.setOnClickListener { store.dispatch(WalletAction.HideQrCode) }
this.setOnDismissListener { store.dispatch(WalletAction.HideDialog) }
this.btn_done?.setOnClickListener { store.dispatch(WalletAction.HideDialog) }
this.tv_qr_dialog_address?.text = shareUrl
this.iv_qrcode?.setImageBitmap(qrCode)

View file

@ -0,0 +1,30 @@
package com.tangem.tap.features.wallet.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WarningType
import com.tangem.tap.store
import com.tangem.wallet.R
class WarningDialog(context: Context) : AlertDialog(context) {
private val dialog: AlertDialog = Builder(context)
.setTitle(context.getString(R.string.generic_warning))
.setPositiveButton(context.getString(R.string.general_ok)) { _, _ ->
dismiss()
}.setOnDismissListener {
store.dispatch(WalletAction.SaveCardId)
store.dispatch(WalletAction.HideDialog)
}
.create()
fun show(warningType: WarningType) {
val messageRes = when (warningType) {
WarningType.CardSignedHashesBefore -> R.string.warning_card_signed_transactions
WarningType.DevCard -> R.string.wallet_warning_dev_card
}
dialog.setMessage(dialog.context.getString(messageRes))
dialog.show()
}
}

View file

@ -48,15 +48,29 @@ class PreferencesStorage(applicationContext: Application) {
fun isFirstLaunch(): Boolean {
val isFirst = !preferences.contains(FIRST_LAUNCH_CHECK_KEY)
if (isFirst) preferences.edit().putInt(FIRST_LAUNCH_CHECK_KEY, System.currentTimeMillis().toInt()).apply()
return isFirst
}
fun saveScannedCardId(cardId: String) {
val scannedCardsIds: String = restoreScannedCardIds()
if (!scannedCardsIds.contains(cardId)) {
preferences.edit().putString(SCANNED_CARDS_IDS_KEY, "$scannedCardsIds$cardId, ").apply()
}
}
fun wasCardScannedBefore(cardId: String): Boolean {
return restoreScannedCardIds().contains(cardId)
}
private fun restoreScannedCardIds(): String =
preferences.getString(SCANNED_CARDS_IDS_KEY, "") ?: ""
companion object {
private const val PREFERENCES_NAME = "tapPrefs"
private const val APP_CURRENCY_KEY = "appCurrency"
private const val FIAT_CURRENCIES_KEY = "fiatCurrencies"
private const val FIRST_LAUNCH_CHECK_KEY = "firstLaunchCheck"
private const val SCANNED_CARDS_IDS_KEY = "scannedCardIds"
}
}

View file

@ -6,6 +6,7 @@
<string name="generic_cancel">Cancel</string>
<string name="generic_retry">Retry</string>
<string name="generic_and">and</string>
<string name="generic_warning">Warning</string>
<string name="notification_address_copied">Address was successfully copied</string>
<string name="notification_no_internet">No internet connection</string>
@ -13,7 +14,7 @@
<string name="home_do_you_have_card">Welcome to Tangem.\nDo you have one of our cards?</string>
<string name="home_welcome_back">Welcome back to Tangem Tap. \nScan your card to start.</string>
<string name="home_button_yes">Yes!</string>
<string name="home_button_yes">Yes. Scan it!</string>
<string name="home_button_scan">Scan card</string>
<string name="home_button_shop">Shop</string>
<string name="home_button_no_card">No</string>
@ -47,10 +48,15 @@
<string name="wallet_dialog_pay_id_description">Your PayID is information unique to you, like your phone number, email or ABN.</string>
<string name="wallet_dialog_qr_code_header">%s wallet</string>
<string name="wallet_warning_dev_card">The card you scanned is a development card. Don\'t accept it as a payment.</string>
<string name="warning_card_signed_transactions">Warning: This card has been already topped up and signed transactions in the past.
Consider immediate withdrawal of all funds if you have received this card from an untrusted source.</string>
<string name="error_payid_already_created">This PayID already exists. Try a different one.</string>
<string name="error_creating_payid">Error response while creating PayID.</string>
<string name="error_card_for_different_app">This card it is not designed to work with Tangem Tap</string>
<string name="error_unknown">Unknown error</string>
<string name="error_payid_verification_failed">PayID verification failed</string>
<string name="error_payid_unsupported_by_blockchain">PayID unsupported by blockchain</string>