Updated on 2026-08-14

This commit is contained in:
Tangem 2021-09-23 11:28:43 +03:00
parent 25e8887043
commit 5eb8b71a1f
21 changed files with 316 additions and 160 deletions

View file

@ -77,8 +77,8 @@ dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
implementation 'com.tangem:blockchain:develop-38'
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-70'
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-70'
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-78'
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-78'
// WebView
implementation "androidx.browser:browser:1.3.0"

View file

@ -14,4 +14,11 @@ fun <T> MutableList<T>.removeBy(predicate: (T) -> Boolean): Boolean {
val toRemove = this.filter(predicate)
this.removeAll(toRemove)
return toRemove.isNotEmpty()
}
fun <T> MutableList<T>.replaceBy(item: T, predicate: (T) -> Boolean) {
val toRemove = this.filter(predicate)
val indexes = toRemove.map { indexOf(it) }
this.removeAll(toRemove)
indexes.forEach { this.add(it, item) }
}

View file

@ -9,6 +9,7 @@ import com.tangem.tap.domain.MultiMessageError
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.assembleErrors
import com.tangem.tap.notificationsHandler
import com.tangem.wallet.BuildConfig
import org.rekotlin.Action
import org.rekotlin.Middleware
import java.lang.ref.WeakReference
@ -63,23 +64,29 @@ fun getMessageString(context: Context, message: Int, args: List<Any>?): String {
val notificationsMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
when (action) {
is NotificationAction -> notificationsHandler?.showNotification(action.messageResource)
is ToastNotificationAction -> notificationsHandler?.showToastNotification(action.messageResource)
is ErrorAction -> {
when (action.error) {
is MultiMessageError -> {
val multiError = action.error as MultiMessageError
notificationsHandler?.showNotification(multiError.assembleErrors(), multiError.builder)
}
else -> {
val args = (action.error as? ArgError)?.args ?: listOf()
notificationsHandler?.showNotification(action.error.localizedMessage, args)
}
}
handleNotificationAction(action)
next(action)
}
}
}
private fun handleNotificationAction(action: Action) {
if (action is Debug && !BuildConfig.DEBUG) return
when (action) {
is NotificationAction -> notificationsHandler?.showNotification(action.messageResource)
is ToastNotificationAction -> notificationsHandler?.showToastNotification(action.messageResource)
is ErrorAction -> {
when (action.error) {
is MultiMessageError -> {
val multiError = action.error as MultiMessageError
notificationsHandler?.showNotification(multiError.assembleErrors(), multiError.builder)
}
else -> {
val args = (action.error as? ArgError)?.args ?: listOf()
notificationsHandler?.showNotification(action.error.localizedMessage, args)
}
}
next(action)
}
}
}
@ -94,4 +101,10 @@ interface NotificationAction : Action {
interface ErrorAction : Action {
val error: TapError
}
}
// Processed only in the debug builds
interface Debug
interface DebugNotification : Debug, NotificationAction
interface DebugToastNotification : Debug, ToastNotificationAction
interface DebugErrorAction : Debug, ErrorAction

View file

@ -22,7 +22,7 @@ import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.domain.tasks.CreateWalletAndRescanTask
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.tasks.ScanNoteTask
import com.tangem.tap.domain.twins.isTwinCard
import com.tangem.tap.domain.twins.isTangemTwin
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -35,16 +35,22 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
analyticsHandler: AnalyticsHandler, messageRes: Int? = null,
): CompletionResult<ScanNoteResponse> {
analyticsHandler.triggerEvent(AnalyticsEvent.READY_TO_SCAN, null)
val result = runTaskAsyncReturnOnMain(ScanNoteTask(),
initialMessage = Message(
context.getString(messageRes ?: R.string.initial_message_scan_header)
))
if (result is CompletionResult.Failure) {
return runTaskAsyncReturnOnMain(
ScanNoteTask(),
initialMessage = Message(context.getString(messageRes ?: R.string.initial_message_scan_header))
).also { sendScanFailuresToAnalytics(analyticsHandler, it) }
}
private fun sendScanFailuresToAnalytics(
analyticsHandler: AnalyticsHandler,
result: CompletionResult<ScanNoteResponse>
) {
if (result is CompletionResult.Failure && result.error is TangemSdkError) {
(result.error as? TangemSdkError)?.let { error ->
analyticsHandler.logCardSdkError(error, FirebaseAnalyticsHandler.ActionToLog.Scan)
}
}
return result
}
suspend fun createWallet(cardId: String?): CompletionResult<Card> {
@ -106,7 +112,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
}
fun changeDisplayedCardIdNumbersCount(card: Card) {
tangemSdk.config.cardIdDisplayedNumbersCount = if (card.isTwinCard()) 4 else null
tangemSdk.config.cardIdDisplayedNumbersCount = if (card.isTangemTwin()) 4 else null
}
companion object {

View file

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

View file

@ -21,6 +21,7 @@ sealed class TapError(
) : Throwable(), TapErrors, ArgError {
object UnknownError : TapError(R.string.send_error_unknown)
object ScanCardError : TapError(R.string.scan_card_error)
data class CustomError(val customMessage: String) : TapError(R.string.common_custom_string, listOf(customMessage))
object PayIdAlreadyCreated : TapError(R.string.wallet_create_payid_error_already_created)
object PayIdCreatingError : TapError(R.string.wallet_create_payid_error_message)
@ -38,8 +39,8 @@ sealed class TapError(
object DustChange : TapError(R.string.send_error_dust_change)
data class CreateAccountUnderfunded(override val args: List<Any>) : TapError(R.string.send_error_no_target_account)
sealed class XmlError {
object AssetAccountNotCreated: TapError(R.string.send_error_no_account_xlm)
sealed class XmlError {
object AssetAccountNotCreated : TapError(R.string.send_error_no_account_xlm)
}
data class ValidateTransactionErrors(

View file

@ -15,7 +15,7 @@ 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
import com.tangem.tap.domain.twins.isTwinCard
import com.tangem.tap.domain.twins.isTangemTwin
import com.tangem.tap.features.tokens.redux.TokensAction
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.WalletAction
@ -98,7 +98,7 @@ class TapWalletManager {
store.dispatch(GlobalAction.SaveScanNoteResponse(data))
store.dispatch(WalletAction.SetIfTestnetCard(data.card.isTestCard))
store.dispatch(WalletAction.MultiWallet.SetIsMultiwalletAllowed(data.card.isMultiwalletAllowed))
if (data.card.isTwinCard()) {
if (data.card.isTangemTwin()) {
val secondCardId = TwinsHelper.getTwinsCardId(data.card.cardId)
val cardNumber = TwinsHelper.getTwinCardNumber(data.card.cardId)
if (secondCardId != null && cardNumber != null) {
@ -143,7 +143,7 @@ class TapWalletManager {
store.dispatch(WalletAction.LoadData.Failure(TapError.UnknownBlockchain))
}
data.card.wallets.isEmpty() ||
(data.card.isTwinCard() && data.secondTwinPublicKey == null) -> {
(data.card.isTangemTwin() && data.secondTwinPublicKey == null) -> {
store.dispatch(WalletAction.EmptyWallet)
}
else -> {
@ -152,7 +152,7 @@ class TapWalletManager {
val blockchain = data.getBlockchain()
val primaryWalletManager = walletManagerFactory.makePrimaryWalletManager(data)
if (blockchain != null && primaryWalletManager != null) {
if (blockchain != Blockchain.Unknown && primaryWalletManager != null) {
val primaryToken = data.getPrimaryToken()
store.dispatch(WalletAction.MultiWallet.SetPrimaryBlockchain(blockchain))
@ -236,7 +236,7 @@ class TapWalletManager {
store.dispatch(WalletAction.LoadData.Failure(TapError.UnknownBlockchain))
}
data.card.wallets.isEmpty() ||
(data.card.isTwinCard() && data.secondTwinPublicKey == null) -> {
(data.card.isTangemTwin() && data.secondTwinPublicKey == null) -> {
store.dispatch(WalletAction.EmptyWallet)
}
else -> {

View file

@ -4,10 +4,10 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.common.card.Card
import com.tangem.common.card.EllipticCurve
import com.tangem.common.card.FirmwareVersion
import com.tangem.tap.domain.TapWorkarounds.isNote
import com.tangem.tap.domain.TapWorkarounds.isStart2Coin
import com.tangem.tap.domain.TapWorkarounds.isTangemNote
import com.tangem.tap.domain.extensions.getSingleWallet
import com.tangem.tap.domain.twins.isTwinCard
import com.tangem.tap.domain.twins.isTangemTwin
import java.util.*
object TapWorkarounds {
@ -28,26 +28,10 @@ object TapWorkarounds {
return excludedBatch || excludedIssuerName
}
fun Card.isNote(): Boolean {
return notesBatches.contains(batchId)
}
fun Card.isTangemNote(): Boolean = tangemNoteBatches.contains(batchId)
fun Card.isTangemWallet(): Boolean = tangemWalletBatches.contains(batchId)
fun Card.isMultiCurrencyWallet(): Boolean {
return multiCurrencyWalletsBatches.contains(batchId)
}
val Card.noteCurrency: Blockchain?
get() {
return when (batchId) {
"AB01" -> Blockchain.Bitcoin
"AB02" -> Blockchain.Ethereum
"AB03" -> Blockchain.CardanoShelley
"AB04" -> Blockchain.Dogecoin
"AB05" -> Blockchain.Binance
"AB06" -> Blockchain.XRP
else -> null
}
}
fun Card.getTangemNoteBlockchain(): Blockchain? = tangemNoteBatches[batchId]
private const val START_2_COIN_ISSUER = "start2coin"
private const val TEST_CARD_BATCH = "99FF"
@ -64,21 +48,21 @@ object TapWorkarounds {
"TTM BANK"
)
private val notesBatches = listOf(
"AB01",
"AB02",
"AB03",
"AB04",
"AB05",
"AB06",
)
private val tangemWalletBatches = listOf("AC01")
private val multiCurrencyWalletsBatches = listOf("AC01")
private val tangemNoteBatches = mapOf(
"AB01" to Blockchain.Bitcoin,
"AB02" to Blockchain.Ethereum,
"AB03" to Blockchain.CardanoShelley,
"AB04" to Blockchain.Dogecoin,
"AB05" to Blockchain.Binance,
"AB06" to Blockchain.XRP,
)
}
val Card.isMultiwalletAllowed: Boolean
get() {
return !isTwinCard() && !isStart2Coin && !isNote()
return !isTangemTwin() && !isStart2Coin && !isTangemNote()
&& (firmwareVersion >= FirmwareVersion.MultiWalletAvailable ||
getSingleWallet()?.curve == EllipticCurve.Secp256k1)
}

View file

@ -0,0 +1,38 @@
package com.tangem.tap.domain
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import com.squareup.picasso.Picasso
import com.squareup.picasso.Target
import com.tangem.common.services.Result
/**
[REDACTED_AUTHOR]
*/
class UrlBitmapLoader {
fun loadBitmap(url: String, callback: (Result<Bitmap>) -> Unit) {
Picasso.get().load(url).into(DownloadTarget(callback))
}
fun loadBitmap(url: String, target: DownloadTarget) {
Picasso.get().load(url).into(target)
}
}
// It adds the ability to trigger multiple downloads with a unique callback.
open class DownloadTarget(
val callback: (Result<Bitmap>) -> Unit,
) : Target {
override fun onBitmapLoaded(bitmap: Bitmap, from: Picasso.LoadedFrom) {
callback(Result.Success(bitmap))
}
override fun onBitmapFailed(e: java.lang.Exception?, errorDrawable: Drawable?) {
callback.invoke(Result.Failure(e))
}
override fun onPrepareLoad(placeHolderDrawable: Drawable?) {
}
}

View file

@ -2,6 +2,9 @@ package com.tangem.tap.domain.extensions
import com.tangem.common.card.Card
import com.tangem.common.card.CardWallet
import com.tangem.common.extensions.toHexString
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.features.wallet.redux.Artwork
fun Card.getSingleWallet(): CardWallet? {
return wallets.firstOrNull()
@ -9,6 +12,10 @@ fun Card.getSingleWallet(): CardWallet? {
fun Card.hasWallets(): Boolean = wallets.isNotEmpty()
fun Card.hasNoWallets(): Boolean = wallets.isEmpty()
fun Card.hasSingleWallet(): Boolean = wallets.size == 1
fun Card.hasSignedHashes(): Boolean {
return wallets.any { it.totalSignedHashes ?: 0 > 0 }
}
@ -17,6 +24,17 @@ fun Card.signedHashesCount(): Int {
return wallets.map { it.totalSignedHashes ?: 0 }.sum()
}
fun Card.getArtworkUrl(artworkId: String?): String? {
return when {
artworkId != null -> {
OnlineCardVerifier.getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId)
}
cardId.startsWith(Artwork.SERGIO_CARD_ID) -> Artwork.SERGIO_CARD_URL
cardId.startsWith(Artwork.MARTA_CARD_ID) -> Artwork.MARTA_CARD_URL
else -> null
}
}
val Card.remainingSignatures: Int?
get() = this.getSingleWallet()?.remainingSignatures

View file

@ -9,7 +9,7 @@ import com.tangem.common.card.EllipticCurve
import com.tangem.common.extensions.hexToBytes
import com.tangem.tap.domain.TapWorkarounds.isTestCard
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.twins.isTwinCard
import com.tangem.tap.domain.twins.isTangemTwin
fun WalletManagerFactory.makeWalletManagerForApp(
card: Card,
@ -54,16 +54,18 @@ fun WalletManagerFactory.makePrimaryWalletManager(
): WalletManager? {
val card = data.card
val blockchain = if (card.isTestCard) {
data.getBlockchain()?.getTestnetVersion()
data.getBlockchain().getTestnetVersion()
} else {
data.getBlockchain()
}
val supportedCurves = blockchain?.getSupportedCurves() ?: return null
val wallets = card.wallets.filter { wallet -> supportedCurves.contains(wallet.curve) }
val wallet = selectWallet(wallets)
val publicKey = wallet?.publicKey ?: return null
val curveToUse = wallet.curve ?: return null
return if (card.isTwinCard() && data.secondTwinPublicKey != null) {
val curveToUse = wallet.curve
return if (card.isTangemTwin() && data.secondTwinPublicKey != null) {
makeMultisigWalletManager(
cardId = card.cardId,
walletPublicKey = publicKey, pairPublicKey = data.secondTwinPublicKey.hexToBytes(),

View file

@ -2,9 +2,7 @@ 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.Token
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.card.FirmwareVersion
@ -17,12 +15,12 @@ import com.tangem.operations.CommandResponse
import com.tangem.operations.ScanTask
import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand
import com.tangem.tap.domain.TapSdkError
import com.tangem.tap.domain.TapWorkarounds.getTangemNoteBlockchain
import com.tangem.tap.domain.TapWorkarounds.isExcluded
import com.tangem.tap.domain.TapWorkarounds.noteCurrency
import com.tangem.tap.domain.TapWorkarounds.isTangemNote
import com.tangem.tap.domain.extensions.getSingleWallet
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.domain.twins.isTwinCard
import com.tangem.tap.store
import com.tangem.tap.domain.twins.isTangemTwin
data class ScanNoteResponse(
val card: Card,
@ -30,9 +28,9 @@ data class ScanNoteResponse(
val secondTwinPublicKey: String? = null,
) : CommandResponse {
fun getBlockchain(): Blockchain? {
if (card.noteCurrency != null) return card.noteCurrency
val blockchainName: String = walletData?.blockchain ?: return null
fun getBlockchain(): Blockchain {
if (card.isTangemNote()) return card.getTangemNoteBlockchain() ?: return Blockchain.Unknown
val blockchainName: String = walletData?.blockchain ?: return Blockchain.Unknown
return Blockchain.fromId(blockchainName)
}
@ -67,7 +65,7 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
return@run
}
if (card.isTwinCard()) {
if (card.isTangemTwin()) {
dealWithTwinCard(card, session, callback)
} else if (card.firmwareVersion >= FirmwareVersion.MultiWalletAvailable) {
createMissingWalletsIfNeeded(card, session, callback)
@ -146,20 +144,7 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
private fun getErrorIfExcludedCard(card: Card): TangemError? {
if (card.isExcluded()) return TapSdkError.CardForDifferentApp
// Disable new multi-currency HD wallet cards on the old version of the app
if (card.isMultiCurrencyWallet()) return UpdateAppToUseThisCard()
// if (card.isMultiCurrencyWallet()) return UpdateAppToUseThisCard()
return null
}
private fun getWalletManagerFactory(): WalletManagerFactory {
val blockchainSdkConfig = store.state.globalState.configManager?.config
?.blockchainSdkConfig ?: BlockchainSdkConfig()
return WalletManagerFactory(blockchainSdkConfig)
}
}
class UpdateAppToUseThisCard : TangemError {
override val code: Int = 50005
override var customMessage: String = code.toString()
override val messageResId: Int = R.string.error_update_app
}
}

View file

@ -75,7 +75,7 @@ enum class TwinCardNumber(val number: Int) {
}
}
fun Card.isTwinCard(): Boolean {
fun Card.isTangemTwin(): Boolean {
return getTwinCardNumber(cardId) != null
}

View file

@ -8,10 +8,10 @@ import com.tangem.tap.domain.extensions.isWalletDataSupported
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
import com.tangem.tap.domain.twins.isTangemTwin
import com.tangem.tap.features.details.redux.twins.CreateTwinWalletReducer
import com.tangem.tap.features.details.redux.twins.CreateTwinWalletState
import com.tangem.tap.features.wallet.models.toPendingTransactions
import com.tangem.tap.features.wallet.models.hasPendingTransactions
import org.rekotlin.Action
import java.util.*
@ -46,7 +46,7 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
}
private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: DetailsState): DetailsState {
val twinsState = if (action.card.isTwinCard()) {
val twinsState = if (action.card.isTangemTwin()) {
CreateTwinWalletState(
scanResponse = action.scanNoteResponse,
twinCardNumber = TwinsHelper.getTwinCardNumber(action.card.cardId),
@ -72,12 +72,10 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: Deta
private fun handleEraseWallet(action: DetailsAction.EraseWallet, state: DetailsState): DetailsState {
return when (action) {
DetailsAction.EraseWallet.Check -> {
val notAllowedByCard =
state.card?.settings?.isPermanentWallet == true
|| state.card?.isWalletDataSupported == true
val notAllowedByAnyWallet = state.card?.wallets?.any { it.settings.isPermanent } ?: false
val notAllowedByCard = notAllowedByAnyWallet || state.card?.isWalletDataSupported == true
val notEmpty = state.wallets.any {
!it.recentTransactions.toPendingTransactions(it.address).isNullOrEmpty()
|| it.amounts.toSendableAmounts().isNotEmpty()
it.hasPendingTransactions() || it.amounts.toSendableAmounts().isNotEmpty()
}
val eraseWalletState = when {
notAllowedByCard -> EraseWalletState.NotAllowedByCard

View file

@ -12,7 +12,7 @@ import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.twins.getTwinCardIdForUser
import com.tangem.tap.domain.twins.isTwinCard
import com.tangem.tap.domain.twins.isTangemTwin
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.details.redux.SecurityOption
@ -68,7 +68,7 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
if (state.cardInfo != null) {
val cardId = if (state.card?.isTwinCard() == true) {
val cardId = if (state.card?.isTangemTwin() == true) {
state.card.getTwinCardIdForUser()
} else {
state.cardInfo.cardId
@ -78,8 +78,8 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
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_signed_hashes.show(state.card?.isTangemTwin() != true)
tv_signed_hashes_title.show(state.card?.isTangemTwin() != true)
tv_disclaimer.setOnClickListener { store.dispatch(DetailsAction.ShowDisclaimer) }

View file

@ -3,6 +3,7 @@ package com.tangem.tap.features.wallet.models
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.TransactionStatus
import com.tangem.blockchain.common.Wallet
import com.tangem.tap.common.extensions.toFormattedString
data class PendingTransaction(
@ -57,4 +58,12 @@ fun TransactionData.toPendingTransactionForToken(token: Token, walletAddress: St
fun List<TransactionData>.toPendingTransactionsForToken(token: Token, walletAddress: String): List<PendingTransaction> {
return this.mapNotNull { it.toPendingTransactionForToken(token, walletAddress) }
}
fun Wallet.getPendingTransactions(): List<PendingTransaction> {
return recentTransactions.toPendingTransactions(address)
}
fun Wallet.hasPendingTransactions(): Boolean {
return recentTransactions.toPendingTransactions(address).isNotEmpty()
}

View file

@ -17,7 +17,7 @@ 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.domain.twins.isTangemTwin
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.preferencesStorage
@ -41,7 +41,7 @@ class WarningsMiddleware {
is WalletAction.Warnings.CheckHashesCount.CheckHashesCountOnline -> checkHashesCountOnline()
is WalletAction.Warnings.CheckHashesCount.SaveCardId -> {
val cardId = globalState?.scanNoteResponse?.card?.cardId
cardId?.let { preferencesStorage.saveScannedCardId(it) }
cardId?.let { preferencesStorage.usedCardsPrefStorage.scanned(it) }
}
is WalletAction.Warnings.AppRating.RemindLater -> {
preferencesStorage.appRatingLaunchObserver.applyDelayedShowing()
@ -91,7 +91,7 @@ class WarningsMiddleware {
showWarningLowRemainingSignaturesIfNeeded(card)
if (card.firmwareVersion.type != FirmwareVersion.FirmwareType.Release) {
addWarningMessage(WarningMessagesManager.devCardWarning())
} else if (!preferencesStorage.wasCardScannedBefore(card.cardId)) {
} else if (!preferencesStorage.usedCardsPrefStorage.wasScanned(card.cardId)) {
checkIfWarningNeeded(card)?.let { warning -> addWarningMessage(warning) }
}
if (card.firmwareVersion.type == FirmwareVersion.FirmwareType.Release) {
@ -119,7 +119,7 @@ class WarningsMiddleware {
private fun checkIfWarningNeeded(
card: Card,
): WarningMessage? {
if (card.isTwinCard()) return null
if (card.isTangemTwin()) return null
if (card.isMultiwalletAllowed) {
return if (card.hasSignedHashes()) {
@ -150,9 +150,9 @@ class WarningsMiddleware {
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) return
val card = store.state.globalState.scanNoteResponse?.card
if (card == null || preferencesStorage.wasCardScannedBefore(card.cardId)) return
if (card == null || preferencesStorage.usedCardsPrefStorage.wasScanned(card.cardId)) return
if (card.isTwinCard() || card.isMultiwalletAllowed) return
if (card.isTangemTwin() || card.isMultiwalletAllowed) return
val validator = store.state.walletState.walletManagers.firstOrNull()
as? SignatureCountValidator

View file

@ -2,12 +2,11 @@ package com.tangem.tap.features.wallet.redux.reducers
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Wallet
import com.tangem.common.extensions.toHexString
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.getArtworkUrl
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.features.wallet.redux.*
@ -215,29 +214,12 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
is WalletAction.LoadFiatRate.Success ->
newState = setNewFiatRate(action.fiatRate, state.globalState.appCurrency, newState)
is WalletAction.LoadArtwork -> {
val cardId = action.card.cardId
val cardPublicKey = action.card.cardPublicKey.toHexString()
val artworkUrl = when {
action.artworkId != null -> {
OnlineCardVerifier.getUrlForArtwork(cardId, cardPublicKey, action.artworkId)
}
action.card.cardId.startsWith(Artwork.SERGIO_CARD_ID) -> {
Artwork.SERGIO_CARD_URL
}
action.card.cardId.startsWith(Artwork.MARTA_CARD_ID) -> {
Artwork.MARTA_CARD_URL
}
newState.twinCardsState?.cardNumber != null -> {
when (newState.twinCardsState?.cardNumber) {
val artworkUrl = action.card.getArtworkUrl(action.artworkId)
?: when (newState.twinCardsState?.cardNumber) {
TwinCardNumber.First -> Artwork.TWIN_CARD_1
TwinCardNumber.Second -> Artwork.TWIN_CARD_2
null -> Artwork.DEFAULT_IMG_URL
else -> Artwork.DEFAULT_IMG_URL
}
}
else -> {
Artwork.DEFAULT_IMG_URL
}
}
newState = newState.copy(cardImage = Artwork(artworkId = artworkUrl))
}
is WalletAction.ShowDialog.QrCode -> {

View file

@ -19,10 +19,13 @@ class PreferencesStorage(applicationContext: Application) {
private val preferences: SharedPreferences = applicationContext.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
val appRatingLaunchObserver: AppRatingLaunchObserver
val usedCardsPrefStorage: UsedCardsPrefStorage
init {
incrementLaunchCounter()
appRatingLaunchObserver = AppRatingLaunchObserver(preferences, getCountOfLaunches())
usedCardsPrefStorage = UsedCardsPrefStorage(preferences)
usedCardsPrefStorage.migrate()
}
private val fiatCurrenciesAdapter: JsonAdapter<List<FiatCurrency>> by lazy {
@ -54,20 +57,11 @@ class PreferencesStorage(applicationContext: Application) {
fun getCountOfLaunches(): Int = preferences.getInt(APP_LAUNCH_COUNT_KEY, 1)
fun saveScannedCardId(cardId: String) {
val scannedCardsIds: String = restoreScannedCardIds()
if (!scannedCardsIds.contains(cardId)) {
preferences.edit().putString(SCANNED_CARDS_IDS_KEY, "$scannedCardsIds$cardId, ").apply()
}
}
@Deprecated("Use UsedCardsPrefStorage instead")
fun wasCardScannedBefore(cardId: String): Boolean {
return restoreScannedCardIds().contains(cardId)
return usedCardsPrefStorage.wasScanned(cardId)
}
private fun restoreScannedCardIds(): String =
preferences.getString(SCANNED_CARDS_IDS_KEY, "") ?: ""
fun saveDisclaimerAccepted() {
preferences.edit().putBoolean(DISCLAIMER_ACCEPTED_KEY, true).apply()
}
@ -93,7 +87,6 @@ class PreferencesStorage(applicationContext: Application) {
private const val PREFERENCES_NAME = "tapPrefs"
private const val APP_CURRENCY_KEY = "appCurrency"
private const val FIAT_CURRENCIES_KEY = "fiatCurrencies"
private const val SCANNED_CARDS_IDS_KEY = "scannedCardIds"
private const val DISCLAIMER_ACCEPTED_KEY = "disclaimerAccepted"
private const val TWINS_ONBOARDING_SHOWN_KEY = "twinsOnboardingShown"
private const val APP_LAUNCH_COUNT_KEY = "launchCount"
@ -102,8 +95,8 @@ class PreferencesStorage(applicationContext: Application) {
}
class AppRatingLaunchObserver(
private val preferences: SharedPreferences,
private val launchCounts: Int,
private val preferences: SharedPreferences,
private val launchCounts: Int,
) {
private val K_SHOW_RATING_AT_LAUNCH_COUNT = "showRatingDialogAtLaunchCount"
private val K_FUNDS_FOUND_DATE = "fundsFoundDate"

View file

@ -0,0 +1,107 @@
package com.tangem.tap.persistence
import android.content.SharedPreferences
import androidx.core.content.edit
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.tap.common.extensions.replaceBy
import timber.log.Timber
/**
[REDACTED_AUTHOR]
*/
class UsedCardsPrefStorage(
private val preferences: SharedPreferences,
) {
private val jsonConverter = MoshiJsonConverter.INSTANCE
internal fun migrate() {
val scannedIds = preferences.getString(SCANNED_CARDS_IDS_KEY, null) ?: return
val usedCardsInfo = scannedIds.split(",").map { UsedCardInfo(it.trim(), true) }
if (save(usedCardsInfo.toMutableList())) {
preferences.edit { this.remove(SCANNED_CARDS_IDS_KEY) }
}
}
fun scanned(cardId: String) {
val restoredList = restore()
val foundItem = findCardInfo(cardId, restoredList)?.copy(isScanned = true)
?: UsedCardInfo(cardId, true)
save(foundItem, restoredList)
}
fun wasScanned(cardId: String): Boolean {
return findCardInfo(cardId)?.isScanned ?: false
}
fun activationStarted(cardId: String) {
val restoredList = restore()
val foundItem = findCardInfo(cardId, restoredList)?.copy(isActivationStarted = true)
?: UsedCardInfo(cardId, isActivationStarted = true)
save(foundItem, restoredList)
}
fun activationIsStarted(cardId: String): Boolean {
return findCardInfo(cardId)?.isActivationStarted ?: false
}
fun activated(cardId: String) {
val restoredList = restore()
val foundItem = findCardInfo(cardId, restoredList)?.copy(isActivated = true)
?: UsedCardInfo(cardId, isActivated = true)
save(foundItem, restoredList)
}
fun wasActivated(cardId: String): Boolean {
return findCardInfo(cardId)?.isActivated ?: false
}
private fun findCardInfo(cardId: String, list: MutableList<UsedCardInfo>? = null): UsedCardInfo? {
val findInList = list ?: restore()
return findInList.firstOrNull { it.cardId == cardId }
}
private fun save(usedCardInfo: UsedCardInfo?, usedCardsInfo: MutableList<UsedCardInfo>) {
val info = usedCardInfo ?: return
usedCardsInfo.replaceBy(info) { it.cardId == info.cardId }
save(usedCardsInfo)
}
private fun save(list: MutableList<UsedCardInfo>): Boolean {
return try {
val json = jsonConverter.toJson(list)
preferences.edit { putString(USED_CARDS_INFO, json) }
true
} catch (ex: Exception) {
Timber.e(ex)
false
}
}
private fun restore(): MutableList<UsedCardInfo> {
val json = preferences.getString(USED_CARDS_INFO, null) ?: return mutableListOf()
return try {
jsonConverter.fromJson(json)!!
} catch (ex: Exception) {
preferences.edit(true) { remove(USED_CARDS_INFO) }
mutableListOf()
}
}
companion object {
private const val USED_CARDS_INFO = "usedCardsInfo"
private const val SCANNED_CARDS_IDS_KEY = "scannedCardIds"
}
}
private data class UsedCardInfo(
val cardId: String,
val isScanned: Boolean = false,
val isActivationStarted: Boolean = false,
val isActivated: Boolean = false,
)

View file

@ -1,4 +1,6 @@
<resources>
<string name="scan_card_error" translatable="false">Scan card failed. Try again</string>
<string name="wallet_button_buy" translatable="false">Buy</string>
<string name="wallet_button_sell" translatable="false">Sell</string>
<string name="wallet_button_trade" translatable="false">Trade</string>
@ -168,4 +170,28 @@ this wallet.
<string name="wallet_dialog_choose_trade_action" translatable="false">Do you want to buy or sell crypto?</string>
<string name="error_update_app" translatable="false">Oops, the current version of the application is not ready to work with this card, please check for updates.</string>
<!-- Onboarding -->
<string name="home_welcome_header" translatable="false">Welcome to Tangem</string>
<string name="home_welcome_body" translatable="false">The safest way to buy, use and\n store cryptocurrency</string>
<string name="home_button_get_new_card" translatable="false">Get a new card</string>
<string name="onboarding_title" translatable="false">Activating card</string>
<string name="onboarding_button_what_does_it_mean" translatable="false">What does it mean?</string>
<string name="onboarding_create_wallet_header" translatable="false">Create a wallet</string>
<string name="onboarding_create_wallet_body" translatable="false">Lets generate all the keys on your card and create a secure wallet</string>
<string name="onboarding_create_wallet_button_create_wallet" translatable="false">Create wallet</string>
<string name="onboarding_top_up_header" translatable="false">Top up your wallet</string>
<string name="onboarding_top_up_body" translatable="false">To get started, simply top up the card with any amount</string>
<string name="onboarding_top_up_button_but_crypto" translatable="false">Buy crypto</string>
<string name="onboarding_top_up_button_show_wallet_address" translatable="false">Show the wallets address</string>
<string name="onboarding_dialog_wallet_address" translatable="false">Scan the address to top up your wallet</string>
<string name="onboarding_done_header" translatable="false">Done!</string>
<string name="onboarding_done_body" translatable="false">Your crypto card is activated and ready to be used</string>
<string name="onboarding_done_button_continue" translatable="false">Continue</string>
<string name="onboarding_error_create_primary_wallet" translatable="false">Internal error: can\'t create wallet manager</string>
</resources>