Updated on 2026-08-14

This commit is contained in:
Tangem 2021-01-12 16:12:07 +03:00
commit 7bb49ef13a
45 changed files with 484 additions and 196 deletions

View file

@ -71,6 +71,9 @@ dependencies {
implementation 'com.google.android.material:material:1.2.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
implementation 'com.tangem:blockchain:1.123.0'
implementation 'com.tangem:core:1.90.0'
implementation 'com.tangem:sdk:1.90.0'
implementation 'com.tangem:blockchain:1.123.0'
implementation 'com.tangem:core:1.92.0'
implementation 'com.tangem:sdk:1.92.0'

View file

@ -1,5 +1,6 @@
{
"isWalletPayIdEnabled": true,
"isTopUpEnabled": true,
"isSendingToPayIdEnabled": true,
"isCreatingTwinCardsAllowed": false
}

View file

@ -1,5 +1,6 @@
{
"isWalletPayIdEnabled": false,
"isTopUpEnabled": false,
"isWalletPayIdEnabled": true,
"isTopUpEnabled": true,
"isSendingToPayIdEnabled": true,
"isCreatingTwinCardsAllowed": true
}

View file

@ -87,7 +87,8 @@ class MainActivity : AppCompatActivity() {
val tag = intent.getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG)
if (tag != null) {
intent.action = null
store.dispatch(NavigationAction.NavigateTo(AppScreen.Home, false))
store.dispatch(NavigationAction.NavigateTo(AppScreen.Home))
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
store.dispatch(HomeAction.ReadCard)
}
}

View file

@ -18,13 +18,9 @@ import com.tangem.wallet.R
fun FragmentActivity.openFragment(screen: AppScreen, addToBackStack: Boolean = true) {
val transaction = this.supportFragmentManager.beginTransaction()
.replace(
R.id.fragment_container,
fragmentFactory(screen),
screen.name
)
transaction.replace(R.id.fragment_container, fragmentFactory(screen), screen.name)
if (addToBackStack && screen != AppScreen.Home) transaction.addToBackStack(null)
transaction.commit();
transaction.commitAllowingStateLoss()
}
fun FragmentActivity.popBackTo(screen: AppScreen?, inclusive: Boolean = false) {

View file

@ -3,6 +3,7 @@ package com.tangem.tap.domain
import androidx.activity.ComponentActivity
import com.tangem.*
import com.tangem.commands.*
import com.tangem.commands.common.card.Card
import com.tangem.commands.common.card.CardType
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.calculateSha256
@ -12,6 +13,7 @@ import com.tangem.tap.common.analytics.AnalyticsHandler
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.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -65,7 +67,7 @@ class TangemSdkManager(val activity: ComponentActivity) {
}
suspend fun <T : CommandResponse> runTaskAsync(
runnable: CardSessionRunnable<T>, cardId: String? = null, initialMessage: Message? = null
runnable: CardSessionRunnable<T>, cardId: String? = null, initialMessage: Message? = null,
): CompletionResult<T> =
withContext(Dispatchers.IO) {
suspendCoroutine { continuation ->
@ -76,9 +78,13 @@ class TangemSdkManager(val activity: ComponentActivity) {
}
private suspend fun <T : CommandResponse> runTaskAsyncReturnOnMain(
runnable: CardSessionRunnable<T>, cardId: String? = null, initialMessage: Message? = null
runnable: CardSessionRunnable<T>, cardId: String? = null, initialMessage: Message? = null,
): CompletionResult<T> {
val result = runTaskAsync(runnable, cardId, initialMessage)
return withContext(Dispatchers.Main) { result }
}
fun changeDisplayedCardIdNumbersCount(card: Card) {
tangemSdk.config.cardIdDisplayedNumbersCount = if (card.isTwinCard()) 4 else null
}
}

View file

@ -1,10 +1,10 @@
package com.tangem.tap.domain
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.commands.common.card.CardStatus
import com.tangem.commands.common.card.masks.Product
import com.tangem.commands.common.network.Result
import com.tangem.common.extensions.toHexString
import com.tangem.tap.common.analytics.AnalyticsEvent
@ -16,7 +16,8 @@ import com.tangem.tap.domain.config.ConfigManager
import com.tangem.tap.domain.extensions.amountToCreateAccount
import com.tangem.tap.domain.extensions.isNoAccountError
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.features.send.redux.AddressPayIdActionUi
import com.tangem.tap.domain.twins.TwinsHelper
import com.tangem.tap.domain.twins.isTwinCard
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.network.coinmarketcap.CoinMarketCapService
@ -86,23 +87,27 @@ class TapWalletManager {
val configManager = store.state.globalState.configManager
if (TapWorkarounds.isStart2Coin) {
configManager?.turnOff(ConfigManager.isWalletPayIdEnabled)
configManager?.turnOff(ConfigManager.isSendingToPayIdEnabled)
configManager?.turnOff(ConfigManager.isTopUpEnabled)
} else if (data.walletManager?.wallet?.blockchain == Blockchain.Bitcoin
|| data.card.cardData?.blockchainName == Blockchain.Bitcoin.id){
configManager?.turnOff(ConfigManager.isWalletPayIdEnabled)
configManager?.resetToDefault(ConfigManager.isSendingToPayIdEnabled)
configManager?.resetToDefault(ConfigManager.isTopUpEnabled)
} else {
configManager?.resetToDefault(ConfigManager.isWalletPayIdEnabled)
configManager?.resetToDefault(ConfigManager.isSendingToPayIdEnabled)
configManager?.resetToDefault(ConfigManager.isTopUpEnabled)
}
withContext(Dispatchers.Main) {
store.dispatch(WalletAction.ResetState)
store.dispatch(GlobalAction.SaveScanNoteResponse(data))
store.dispatch(AddressPayIdActionUi.ChangePayIdState(configManager?.config?.isWalletPayIdEnabled
?: false))
if (data.card.cardData?.productMask?.contains(Product.TwinCard) == true) {
if (data.card.isTwinCard()) {
val secondCardId = TwinsHelper.getTwinsCardId(data.card.cardId)
val cardNumber = TwinsHelper.getTwinCardNumber(data.card.cardId)
if (secondCardId != null && cardNumber != null) {
store.dispatch(WalletAction.TwinsAction.SetTwinCard(
secondCardId, cardNumber,
configManager?.config?.isCreatingTwinCardsAllowed ?: false
secondCardId, cardNumber, isCreatingTwinCardsAllowed = true
))
}
}
@ -126,8 +131,7 @@ class TapWalletManager {
store.dispatch(WalletAction.LoadArtwork(data.card, artworkId))
store.dispatch(WalletAction.LoadFiatRate)
store.dispatch(WalletAction.LoadPayId)
} else if (data.card.status == CardStatus.Empty ||
data.card.cardData?.productMask?.contains(Product.TwinCard) == true) {
} else if (data.card.status == CardStatus.Empty || data.card.isTwinCard()) {
store.dispatch(WalletAction.EmptyWallet)
store.dispatch(WalletAction.LoadArtwork(data.card, artworkId))
} else {

View file

@ -10,6 +10,7 @@ data class Config(
val moonPayApiKey: String = "pk_test_kc90oYTANy7UQdBavDKGfL4K9l6VEPE",
val moonPayApiSecretKey: String = "sk_test_V8w4M19LbDjjYOt170s0tGuvXAgyEb1C",
val isWalletPayIdEnabled: Boolean = true,
val isSendingToPayIdEnabled: Boolean = true,
val isTopUpEnabled: Boolean = false,
val isCreatingTwinCardsAllowed: Boolean = false
)
@ -29,15 +30,16 @@ class ConfigManager(
setupFeature(config.features)
setupKey(config.configValues)
}
remoteLoader.loadConfig { config ->
setupFeature(config.features)
onComplete?.invoke()
}
// remoteLoader.loadConfig { config ->
// setupFeature(config.features)
// onComplete?.invoke()
// }
}
fun turnOff(name: String) {
when (name) {
isWalletPayIdEnabled -> config = config.copy(isWalletPayIdEnabled = false)
isSendingToPayIdEnabled -> config = config.copy(isSendingToPayIdEnabled = false)
isTopUpEnabled -> config = config.copy(isTopUpEnabled = false)
isCreatingTwinCardsAllowed -> config = config.copy(isCreatingTwinCardsAllowed = false)
}
@ -46,6 +48,7 @@ class ConfigManager(
fun resetToDefault(name: String) {
when (name) {
isWalletPayIdEnabled -> config = config.copy(isWalletPayIdEnabled = defaultConfig.isWalletPayIdEnabled)
isSendingToPayIdEnabled -> config = config.copy(isSendingToPayIdEnabled = defaultConfig.isSendingToPayIdEnabled)
isTopUpEnabled -> config = config.copy(isTopUpEnabled = defaultConfig.isTopUpEnabled)
isCreatingTwinCardsAllowed -> config =
config.copy(isCreatingTwinCardsAllowed = defaultConfig.isCreatingTwinCardsAllowed)
@ -58,11 +61,13 @@ class ConfigManager(
config = config.copy(
isWalletPayIdEnabled = model.isWalletPayIdEnabled,
isTopUpEnabled = model.isTopUpEnabled,
isSendingToPayIdEnabled = model.isSendingToPayIdEnabled,
isCreatingTwinCardsAllowed = model.isCreatingTwinCardsAllowed
)
defaultConfig = defaultConfig.copy(
isWalletPayIdEnabled = model.isWalletPayIdEnabled,
isTopUpEnabled = model.isTopUpEnabled,
isSendingToPayIdEnabled = model.isSendingToPayIdEnabled,
isCreatingTwinCardsAllowed = model.isCreatingTwinCardsAllowed
)
}
@ -83,6 +88,7 @@ class ConfigManager(
companion object {
const val isWalletPayIdEnabled = "isWalletPayIdEnabled"
const val isSendingToPayIdEnabled = "isSendingToPayIdEnabled"
const val isCreatingTwinCardsAllowed = "isCreatingTwinCardsAllowed"
const val isTopUpEnabled = "isTopUpEnabled"
const val coinMarketCapKey = "coinMarketCapKey"

View file

@ -7,20 +7,17 @@ import com.tangem.TangemSdkError
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.common.card.masks.Product
import com.tangem.commands.file.ReadFileDataCommand
import com.tangem.commands.verifycard.VerifyCardCommand
import com.tangem.commands.verifycard.VerifyCardResponse
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString
import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
import com.tangem.tap.domain.TapSdkError
import com.tangem.tap.domain.TwinsHelper
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.domain.twins.isTwinCard
import com.tangem.tasks.ScanTask
data class ScanNoteResponse(
@ -50,7 +47,7 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
return@run
}
if (card.cardData?.productMask?.contains(Product.TwinCard) == true) {
if (card.isTwinCard()) {
dealWithTwinCard(card, session, callback)
return@run
}
@ -75,7 +72,7 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
when (verifyResult) {
is CompletionResult.Success -> {
callback(CompletionResult.Success(ScanNoteResponse(
walletManager, card, verifyResult.data
walletManager, card, verifyResult.data, publicKey
)))
}
is CompletionResult.Failure -> {
@ -89,20 +86,21 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
card: Card, session: CardSession,
callback: (result: CompletionResult<ScanNoteResponse>) -> Unit
) {
ReadFileDataCommand(readPrivateFiles = true).run(session) { filesResult ->
when (filesResult) {
ReadIssuerDataCommand().run(session) { readDataResult ->
when (readDataResult) {
is CompletionResult.Success -> {
val decoder = Tlv.deserialize(filesResult.data.fileData)?.let { TlvDecoder(it) }
val name = decoder?.decodeOptional<String>(TlvTag.FileName)
if (name != null && name == TwinsHelper.TWIN_FILE_NAME) {
val publicKey = decoder.decodeOptional<ByteArray>(TlvTag.FileData)?.toHexString()
val verified = TwinCardsManager.verifyTwinPublicKey(
readDataResult.data.issuerData, card.walletPublicKey
)
if (verified) {
val twinPublicKey = readDataResult.data.issuerData.sliceArray(0 until 65)
val walletManager = try {
WalletManagerFactory.makeMultisigWalletManager(card, publicKey!!.hexToBytes())
WalletManagerFactory.makeMultisigWalletManager(card, twinPublicKey)
} catch (exception: Exception) {
callback(CompletionResult.Success(ScanNoteResponse(null, card)))
return@run
}
verifyCard(walletManager, card, publicKey, session, callback)
verifyCard(walletManager, card, twinPublicKey.toHexString(), session, callback)
return@run
} else {
callback(CompletionResult.Success(ScanNoteResponse(null, card)))
@ -111,13 +109,13 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
is CompletionResult.Failure ->
callback(CompletionResult.Success(ScanNoteResponse(null, card)))
}
}
}
private fun getErrorIfExcludedCard(card: Card): TangemError? {
val productMask = card.cardData?.productMask ?: return TangemSdkError.CardError()
if (!productMask.contains(Product.Note) && !productMask.contains(Product.TwinCard)) {
val productMask = card.cardData?.productMask
if (productMask != null && // product mask is on cards v2.30 and later
!productMask.contains(Product.Note) && !productMask.contains(Product.TwinCard)) {
return TapSdkError.CardForDifferentApp
}
if (excludedBatches.contains(card.cardData?.batchId)) {

View file

@ -7,7 +7,6 @@ import com.tangem.commands.PurgeWalletCommand
import com.tangem.commands.common.card.CardStatus
import com.tangem.common.CompletionResult
import com.tangem.tasks.CreateWalletTask
import com.tangem.tasks.file.DeleteFilesTask
class CreateFirstTwinWalletTask : CardSessionRunnable<CreateWalletResponse> {
override val requiresPin2 = false
@ -18,24 +17,13 @@ class CreateFirstTwinWalletTask : CardSessionRunnable<CreateWalletResponse> {
when (response) {
is CompletionResult.Success -> {
session.environment.card = session.environment.card?.copy(status = CardStatus.Empty)
finishTask(session, callback)
CreateWalletTask().run(session) { callback(it) }
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(response.error))
}
}
} else {
finishTask(session, callback)
}
}
private fun finishTask(session: CardSession, callback: (result: CompletionResult<CreateWalletResponse>) -> Unit) {
DeleteFilesTask().run(session) { deleteResponse ->
when (deleteResponse) {
is CompletionResult.Success ->
CreateWalletTask().run(session) { callback(it) }
is CompletionResult.Failure ->
callback(CompletionResult.Failure(deleteResponse.error))
}
CreateWalletTask().run(session) { callback(it) }
}
}
}

View file

@ -2,19 +2,24 @@ 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.file.WriteFileDataCommand
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.hexToBytes
import com.tangem.tasks.CreateWalletTask
import com.tangem.tasks.file.DeleteFilesTask
class CreateSecondTwinWalletTask(private val firstPublicKey: String) : CardSessionRunnable<CreateWalletResponse> {
class CreateSecondTwinWalletTask(
private val firstPublicKey: String,
private val preparingMessage: Message,
private val creatingWalletMessage: Message
) : CardSessionRunnable<CreateWalletResponse> {
override val requiresPin2 = true
override fun run(session: CardSession, callback: (result: CompletionResult<CreateWalletResponse>) -> Unit) {
if (session.environment.card?.walletPublicKey != null) {
session.setInitialMessage(preparingMessage)
PurgeWalletCommand().run(session) { response ->
when (response) {
is CompletionResult.Success -> {
@ -31,20 +36,24 @@ class CreateSecondTwinWalletTask(private val firstPublicKey: String) : CardSessi
}
private fun finishTask(session: CardSession, callback: (result: CompletionResult<CreateWalletResponse>) -> Unit) {
DeleteFilesTask().run(session) { deleteResponse ->
when (deleteResponse) {
session.setInitialMessage(creatingWalletMessage)
CreateWalletTask().run(session) { result ->
when (result) {
is CompletionResult.Success -> {
val file = TwinCardsManager.createFileWithPublicKey(firstPublicKey)
WriteFileDataCommand(file).run(session) { result ->
when (result) {
is CompletionResult.Success -> CreateWalletTask().run(session, callback)
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
session.environment.card =
session.environment.card?.copy(status = CardStatus.Loaded)
WriteProtectedIssuerDataTask(
firstPublicKey.hexToBytes(), TwinCardsManager.issuerKeys
).run(session) { writeResult ->
when (writeResult) {
is CompletionResult.Success -> callback(result)
is CompletionResult.Failure ->
callback(CompletionResult.Failure(writeResult.error))
}
}
}
is CompletionResult.Failure ->
callback(CompletionResult.Failure(deleteResponse.error))
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
}
}

View file

@ -0,0 +1,31 @@
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.common.CompletionResult
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.tasks.ScanNoteTask
class FinalizeTwinTask(
private val twinPublicKey: ByteArray, private val issuerKeys: KeyPair
) : CardSessionRunnable<ScanNoteResponse> {
override val requiresPin2 = true
override fun run(session: CardSession, callback: (result: CompletionResult<ScanNoteResponse>) -> Unit) {
WriteProtectedIssuerDataTask(twinPublicKey, issuerKeys).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.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
}

View file

@ -1,18 +1,13 @@
package com.tangem.tap.domain.twins
import com.tangem.KeyPair
import com.tangem.Message
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.commands.file.FileData
import com.tangem.commands.file.WriteFileDataCommand
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString
import com.tangem.common.tlv.TlvEncoder
import com.tangem.common.tlv.TlvTag
import com.tangem.common.tlv.serialize
import com.tangem.tap.domain.TwinsHelper
import com.tangem.crypto.CryptoUtils
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.tangemSdkManager
@ -39,9 +34,14 @@ class TwinCardsManager(private val scanNoteResponse: ScanNoteResponse) {
}
suspend fun createSecondWallet(message: Message): SimpleResult {
suspend fun createSecondWallet(
initialMessage: Message,
preparingMessage: Message,
creatingWalletMessage: Message
): SimpleResult {
val response = tangemSdkManager.runTaskAsync(
CreateSecondTwinWalletTask(currentCardPublicKey!!), secondCardId, message
CreateSecondTwinWalletTask(currentCardPublicKey!!, preparingMessage, creatingWalletMessage),
secondCardId, initialMessage
)
when (response) {
is CompletionResult.Success -> {
@ -55,29 +55,29 @@ class TwinCardsManager(private val scanNoteResponse: ScanNoteResponse) {
suspend fun complete(message: Message): Result<ScanNoteResponse> {
val response = tangemSdkManager.runTaskAsync(
WriteFileDataCommand(createFileWithPublicKey(secondCardPublicKey!!)),
FinalizeTwinTask(secondCardPublicKey!!.hexToBytes(), issuerKeys),
currentCardId, message
)
return when (response) {
is CompletionResult.Success -> {
val walletManager = WalletManagerFactory.makeMultisigWalletManager(
scanNoteResponse.card, secondCardPublicKey!!.hexToBytes()
)
return Result.Success(scanNoteResponse.copy(
walletManager = walletManager,
secondTwinPublicKey = secondCardPublicKey
))
}
is CompletionResult.Success -> Result.Success(response.data)
is CompletionResult.Failure -> Result.failure(response.error)
}
}
companion object {
fun createFileWithPublicKey(publicKey: String): FileData {
val nameTlv = TlvEncoder().encode(TlvTag.FileName, "TwinPublicKey")
val pubKey = TlvEncoder().encode(TlvTag.FileData, publicKey.hexToBytes())
val tlvs = listOf(nameTlv, pubKey).serialize()
return FileData.DataProtectedByPasscode(tlvs)
val issuerKeys = KeyPair(
privateKey = "F9F4C50636C9E6FC65F92655BD5C21C85A5F6A34DCD0F1E75FCEA1980FE242F5".hexToBytes(),
publicKey = ("048196AA4B410AC44A3B9CCE18E7BE226AEA070ACC83A9CF67540F" +
"AC49AF25129F6A538A28AD6341358E3C4F9963064F" +
"7E365372A651D374E5C23CDD37FD099BF2").hexToBytes()
)
fun verifyTwinPublicKey(issuerData: ByteArray, cardWalletPublicKey: ByteArray?): Boolean {
if (issuerData.size < 65) return false
val publicKey = issuerData.sliceArray(0 until 65)
val signedKey = issuerData.sliceArray(65 until issuerData.size)
return (cardWalletPublicKey != null &&
CryptoUtils.verify(cardWalletPublicKey, publicKey, signedKey))
}
}
}

View file

@ -1,5 +1,7 @@
package com.tangem.tap.domain
package com.tangem.tap.domain.twins
import com.tangem.commands.common.card.Card
import com.tangem.commands.common.card.masks.Product
import com.tangem.tap.common.extensions.isEven
class TwinsHelper {
@ -8,10 +10,10 @@ class TwinsHelper {
fun getTwinCardNumber(cardId: String): TwinCardNumber? {
return when {
firstCardBatches.map { cardId.startsWith(it) }.contains(true) -> {
firstCardSeries.map { cardId.startsWith(it) }.contains(true) -> {
TwinCardNumber.First
}
secondCardBatches.map { cardId.startsWith(it) }.contains(true) -> {
secondCardSeries.map { cardId.startsWith(it) }.contains(true) -> {
TwinCardNumber.Second
}
else -> {
@ -21,28 +23,36 @@ class TwinsHelper {
}
fun getTwinsCardId(cardId: String): String? {
val cardIdWithNewBatch = when (getTwinCardNumber(cardId) ?: return null) {
val cardIdWithNewSeries = when (getTwinCardNumber(cardId) ?: return null) {
TwinCardNumber.First -> {
val batchIndex = if (cardId.startsWith(firstCardBatches[1])) 1 else 2
cardId.replace(firstCardBatches[batchIndex], secondCardBatches[batchIndex])
val index = if (cardId.startsWith(firstCardSeries[0])) 0 else 1
cardId.replace(firstCardSeries[index], secondCardSeries[index])
}
TwinCardNumber.Second -> {
val batchIndex = if (cardId.startsWith(secondCardBatches[1])) 1 else 2
cardId.replace(secondCardBatches[batchIndex], firstCardBatches[batchIndex])
val index = if (cardId.startsWith(secondCardSeries[0])) 0 else 1
cardId.replace(secondCardSeries[index], firstCardSeries[index])
}
}
val cardIdWithoutChecksum = cardIdWithNewBatch.dropLast(1)
val cardIdWithoutChecksum = cardIdWithNewSeries.dropLast(1)
val checkSum = cardIdWithoutChecksum.calculateLuhn()
return cardIdWithoutChecksum + checkSum
}
private val firstCardBatches = listOf("CB61", "CB64")
private val secondCardBatches = listOf("CB62", "CB65")
fun getTwinCardIdForUser(cardId: String): String {
if (cardId.length < 16) return cardId
val twinCardId = cardId.substring(11..14)
val twinCardNumber = getTwinCardNumber(cardId)?.number ?: 1
return "$twinCardId #$twinCardNumber"
}
private val firstCardSeries = listOf("CB61", "CB64")
private val secondCardSeries = listOf("CB62", "CB65")
}
}
private fun String.calculateLuhn(): Int {
return 10 - this.reversed()
val checksum = this.reversed()
.mapIndexed { index, c ->
val digit = if (c in '0'..'9') c - '0' else c - 'A'
if (!index.isEven()) {
@ -51,8 +61,9 @@ private fun String.calculateLuhn(): Int {
val newDigit = digit * 2
if (newDigit >= 10) newDigit - 9 else newDigit
}
}
.sum() % 10
}.sum()
.rem(10)
return (10 - checksum) % 10
}
enum class TwinCardNumber(val number: Int) {
@ -62,4 +73,12 @@ enum class TwinCardNumber(val number: Int) {
First -> Second
Second -> First
}
}
fun Card.isTwinCard(): Boolean {
return this.cardData?.productMask?.contains(Product.TwinCard) == true
}
fun Card.getTwinCardIdForUser(): String {
return TwinsHelper.getTwinCardIdForUser(this.cardId)
}

View file

@ -0,0 +1,59 @@
package com.tangem.tap.domain.twins
import com.tangem.CardSession
import com.tangem.CardSessionRunnable
import com.tangem.KeyPair
import com.tangem.TangemSdkError
import com.tangem.commands.*
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.guard
import com.tangem.common.files.FileHashHelper
class WriteProtectedIssuerDataTask(
private val twinPublicKey: ByteArray, private val issuerKeys: KeyPair
) : CardSessionRunnable<WriteIssuerDataResponse> {
override val requiresPin2 = true
override fun run(session: CardSession, callback: (result: CompletionResult<WriteIssuerDataResponse>) -> Unit) {
val hashes = arrayOf(twinPublicKey.calculateSha256())
SignCommand(hashes).run(session) { signResult ->
when (signResult) {
is CompletionResult.Success -> {
ReadIssuerDataCommand().run(session) { readResult ->
when (readResult) {
is CompletionResult.Success -> {
writeIssuerData(
twinPublicKey, issuerKeys, signResult.data.signature,
readResult.data, session, callback
)
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(readResult.error))
}
}
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(signResult.error))
}
}
}
private fun writeIssuerData(
twinPublicKey: ByteArray, issuerKeys: KeyPair, cardSignature: ByteArray,
readResponse: ReadIssuerDataResponse,
session: CardSession, callback: (result: CompletionResult<WriteIssuerDataResponse>
) -> Unit) {
val cardId = session.environment.card?.cardId.guard {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
return
}
val counter = (readResponse.issuerDataCounter ?: 0) + 1
val data = twinPublicKey + cardSignature
val signedByIssuer = FileHashHelper.prepareHashes(
cardId, data, counter, issuerKeys.privateKey
)
WriteIssuerDataCommand(
data, signedByIssuer.finalizingSignature!!,
counter, issuerKeys.publicKey
).run(session, callback)
}
}

View file

@ -5,8 +5,8 @@ import com.tangem.blockchain.common.Wallet
import com.tangem.commands.common.card.Card
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.domain.TwinCardNumber
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.features.details.redux.twins.CreateTwinWallet
import com.tangem.tap.network.coinmarketcap.FiatCurrency
import com.tangem.wallet.R
@ -45,12 +45,16 @@ sealed class DetailsAction : Action {
}
sealed class CreateTwinWalletAction : DetailsAction() {
object ShowWarning : CreateTwinWalletAction()
data class ShowWarning(
val twinCardNumber: TwinCardNumber?,
val createTwinWallet: CreateTwinWallet = CreateTwinWallet.RecreateWallet
) : CreateTwinWalletAction()
object NotEmpty : CreateTwinWalletAction(), NotificationAction {
override val messageResource = R.string.details_notification_erase_wallet_not_possible
}
object ShowAlert : CreateTwinWalletAction()
object HideAlert : CreateTwinWalletAction()
data class Proceed(
val twinCardNumber: TwinCardNumber?, val createTwinWallet: CreateTwinWallet
) : CreateTwinWalletAction()
object Proceed: CreateTwinWalletAction()
object Cancel : CreateTwinWalletAction() {
object Confirm : CreateTwinWalletAction()
@ -61,7 +65,11 @@ sealed class DetailsAction : Action {
object Failure : CreateTwinWalletAction()
}
data class LaunchSecondStep(val message: Message) : CreateTwinWalletAction() {
data class LaunchSecondStep(
val initialMessage: Message,
val preparingMessage: Message,
val creatingWalletMessage: Message,
) : CreateTwinWalletAction() {
object Success : CreateTwinWalletAction()
object Failure : CreateTwinWalletAction()
}

View file

@ -2,12 +2,12 @@ package com.tangem.tap.features.details.redux
import com.tangem.commands.common.card.Card
import com.tangem.commands.common.card.masks.Product
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.TwinsHelper
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.features.details.redux.twins.CreateTwinWalletReducer
import com.tangem.tap.features.details.redux.twins.CreateTwinWalletState
import org.rekotlin.Action
@ -55,7 +55,7 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: Deta
SecurityOption.LongTap
}
}
val twinsState = if (action.card.cardData?.productMask?.contains(Product.TwinCard) == true) {
val twinsState = if (action.card.isTwinCard()) {
CreateTwinWalletState(
scanResponse = action.scanNoteResponse,
twinCardNumber = TwinsHelper.getTwinCardNumber(action.card.cardId),

View file

@ -2,9 +2,9 @@ package com.tangem.tap.features.details.redux.twins
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
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.extensions.toSendableAmounts
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.scope
@ -17,8 +17,19 @@ class CreateTwinWalletMiddleware {
var twinsManager: TwinCardsManager? = null
fun handle(action: DetailsAction.CreateTwinWalletAction) {
when (action) {
DetailsAction.CreateTwinWalletAction.ShowWarning ->
store.dispatch(NavigationAction.NavigateTo(AppScreen.CreateTwinWalletWarning))
is DetailsAction.CreateTwinWalletAction.ShowWarning -> {
val wallet = store.state.detailsState.wallet
if (wallet == null) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.CreateTwinWalletWarning))
return
}
val notEmpty = wallet.recentTransactions.isNotEmpty() || wallet.amounts.toSendableAmounts().isNotEmpty()
if (notEmpty) {
store.dispatch(DetailsAction.CreateTwinWalletAction.NotEmpty)
} else {
store.dispatch(NavigationAction.NavigateTo(AppScreen.CreateTwinWalletWarning))
}
}
is DetailsAction.CreateTwinWalletAction.Proceed ->
store.dispatch(NavigationAction.NavigateTo(AppScreen.CreateTwinWallet))
is DetailsAction.CreateTwinWalletAction.Cancel -> {
@ -34,7 +45,7 @@ class CreateTwinWalletMiddleware {
}
is DetailsAction.CreateTwinWalletAction.Cancel.Confirm -> {
twinsManager = null
store.dispatch(NavigationAction.PopBackTo())
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
}
is DetailsAction.CreateTwinWalletAction.LaunchFirstStep -> {
store.state.globalState.scanNoteResponse?.let {
@ -60,7 +71,8 @@ class CreateTwinWalletMiddleware {
}
is DetailsAction.CreateTwinWalletAction.LaunchSecondStep ->
scope.launch {
val result = twinsManager?.createSecondWallet(action.message)
val result = twinsManager?.createSecondWallet(action.initialMessage,
action.preparingMessage, action.creatingWalletMessage)
withContext(Dispatchers.Main) {
when (result) {
SimpleResult.Success ->
@ -93,8 +105,11 @@ class CreateTwinWalletMiddleware {
}
}
is DetailsAction.CreateTwinWalletAction.LaunchThirdStep.Success -> {
store.dispatch(GlobalAction.SaveScanNoteResponse(action.scanNoteResponse))
scope.launch {
store.state.globalState.tapWalletManager.onCardScanned(action.scanNoteResponse)
}
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}
DetailsAction.CreateTwinWalletAction.LaunchThirdStep.Failure -> {

View file

@ -9,9 +9,17 @@ class CreateTwinWalletReducer {
action: DetailsAction.CreateTwinWalletAction, state: DetailsState
): DetailsState {
return when (action) {
DetailsAction.CreateTwinWalletAction.ShowWarning -> {
state
is DetailsAction.CreateTwinWalletAction.ShowWarning -> {
state.copy(createTwinWalletState = CreateTwinWalletState(
scanResponse = null,
twinCardNumber = action.twinCardNumber
?: state.createTwinWalletState?.twinCardNumber,
createTwinWallet = action.createTwinWallet,
showAlert = false,
allowRecreatingWallet = state.createTwinWalletState?.allowRecreatingWallet
))
}
DetailsAction.CreateTwinWalletAction.NotEmpty -> state
DetailsAction.CreateTwinWalletAction.ShowAlert -> {
state.copy(createTwinWalletState = state.createTwinWalletState?.copy(
showAlert = true
@ -23,14 +31,7 @@ class CreateTwinWalletReducer {
))
}
is DetailsAction.CreateTwinWalletAction.Proceed -> {
state.copy(createTwinWalletState = CreateTwinWalletState(
scanResponse = null,
twinCardNumber = action.twinCardNumber
?: state.createTwinWalletState?.twinCardNumber,
createTwinWallet = action.createTwinWallet,
showAlert = false,
step = CreateTwinWalletStep.FirstStep,
))
state
}
DetailsAction.CreateTwinWalletAction.Cancel -> state
DetailsAction.CreateTwinWalletAction.Cancel.Confirm -> state

View file

@ -1,7 +1,7 @@
package com.tangem.tap.features.details.redux.twins
import com.tangem.tap.domain.TwinCardNumber
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.twins.TwinCardNumber
data class CreateTwinWalletState(
val scanResponse: ScanNoteResponse?,

View file

@ -8,9 +8,12 @@ import androidx.transition.TransitionInflater
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
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.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.features.details.redux.twins.CreateTwinWallet
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_details.*
@ -61,11 +64,20 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
if (state.cardInfo != null) {
tv_card_id.text = state.cardInfo.cardId
val cardId = if (state.card?.isTwinCard() == true) {
state.card.getTwinCardIdForUser()
} else {
state.cardInfo.cardId
}
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) }
if (state.createTwinWalletState != null) {
@ -73,7 +85,9 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
tv_erase_wallet.show()
tv_erase_wallet.text = getText(R.string.details_row_title_twins_recreate)
tv_erase_wallet.setOnClickListener {
store.dispatch(DetailsAction.CreateTwinWalletAction.ShowWarning)
store.dispatch(DetailsAction.CreateTwinWalletAction.ShowWarning(
null, CreateTwinWallet.RecreateWallet
))
}
} else {
tv_erase_wallet.hide()

View file

@ -8,12 +8,14 @@ import androidx.core.content.ContextCompat.getColor
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.squareup.picasso.Picasso
import com.tangem.Message
import com.tangem.tap.domain.TwinCardNumber
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.details.redux.twins.CreateTwinWallet
import com.tangem.tap.features.details.redux.twins.CreateTwinWalletStep
import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_details_twin_cards.*
@ -56,6 +58,18 @@ class CreateTwinWalletFragment : Fragment(R.layout.fragment_details_twin_cards),
toolbar.setNavigationOnClickListener {
store.dispatch(DetailsAction.CreateTwinWalletAction.Cancel)
}
Picasso.get()
.load(Artwork.TWIN_CARD_1)
.placeholder(R.drawable.card_placeholder)
?.error(R.drawable.card_placeholder)
?.into(iv_card_1)
Picasso.get()
.load(Artwork.TWIN_CARD_2)
.placeholder(R.drawable.card_placeholder)
?.error(R.drawable.card_placeholder)
?.into(iv_card_2)
}
@ -96,14 +110,14 @@ class CreateTwinWalletFragment : Fragment(R.layout.fragment_details_twin_cards),
tv_step_number.text =
getString(R.string.details_twins_recreate_step_format, "2")
v_step_1.setBackgroundColor(defaultColor)
v_step_1.setBackgroundColor(selectedColor)
v_step_2.setBackgroundColor(selectedColor)
v_step_3.setBackgroundColor(defaultColor)
btn_tap.setOnClickListener {
store.dispatch(DetailsAction.CreateTwinWalletAction.LaunchSecondStep(
Message(getString(
R.string.details_twins_recreate_title_format, twinCardNumberString)
)
Message(getString(R.string.details_twins_recreate_title_format, twinCardNumberString)),
Message(getString(R.string.details_twins_recreate_title_preparing)),
Message(getString(R.string.details_twins_recreate_title_creating_wallet)),
))
}
btn_tap.text = getString(R.string.details_twins_recreate_button_format, "2")
@ -114,8 +128,8 @@ class CreateTwinWalletFragment : Fragment(R.layout.fragment_details_twin_cards),
tv_step_number.text =
getString(R.string.details_twins_recreate_step_format, "3")
v_step_1.setBackgroundColor(defaultColor)
v_step_2.setBackgroundColor(defaultColor)
v_step_1.setBackgroundColor(selectedColor)
v_step_2.setBackgroundColor(selectedColor)
v_step_3.setBackgroundColor(selectedColor)
btn_tap.setOnClickListener {
store.dispatch(DetailsAction.CreateTwinWalletAction.LaunchThirdStep(
@ -133,16 +147,21 @@ class CreateTwinWalletFragment : Fragment(R.layout.fragment_details_twin_cards),
tv_twin_title.text = getString(R.string.details_twins_recreate_title_format, cardNumber)
if (state.createTwinWalletState?.showAlert == true) {
dialog = MaterialAlertDialogBuilder(requireContext())
.setMessage(R.string.details_twins_recreate_alert)
.setPositiveButton(R.string.common_ok) { _, _ ->
store.dispatch(DetailsAction.CreateTwinWalletAction.Cancel.Confirm)
}
.setNegativeButton(R.string.common_cancel) { _, _ ->
store.dispatch(DetailsAction.CreateTwinWalletAction.HideAlert)
}
.create()
dialog?.show()
if (dialog == null) {
dialog = MaterialAlertDialogBuilder(requireContext())
.setMessage(R.string.details_twins_recreate_alert)
.setPositiveButton(R.string.common_ok) { _, _ ->
store.dispatch(DetailsAction.CreateTwinWalletAction.Cancel.Confirm)
}
.setNegativeButton(R.string.common_cancel) { _, _ ->
dialog?.cancel()
}
.setOnCancelListener {
store.dispatch(DetailsAction.CreateTwinWalletAction.HideAlert)
}
.create()
dialog?.show()
}
} else {
dialog?.cancel()
dialog = null

View file

@ -5,12 +5,15 @@ import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.squareup.picasso.Picasso
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.twins.CreateTwinWallet
import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_details_twin_cards_warning.*
import kotlinx.android.synthetic.main.layout_twin_cards_orange.*
class TwinWalletWarningFragment : Fragment(R.layout.fragment_details_twin_cards_warning) {
@ -28,13 +31,29 @@ class TwinWalletWarningFragment : Fragment(R.layout.fragment_details_twin_cards_
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val createTwinWallet = store.state.detailsState.createTwinWalletState?.createTwinWallet
if (createTwinWallet == CreateTwinWallet.CreateWallet) {
tv_twin_cards_description.text = getText(R.string.details_twins_recreate_subtitle)
} else if (createTwinWallet == CreateTwinWallet.RecreateWallet) {
tv_twin_cards_description.text = getText(R.string.details_twins_recreate_warning)
}
btn_cancel.setOnClickListener { store.dispatch(NavigationAction.PopBackTo()) }
btn_start.setOnClickListener {
store.dispatch(
DetailsAction.CreateTwinWalletAction.Proceed(
null, createTwinWallet = CreateTwinWallet.RecreateWallet
))
DetailsAction.CreateTwinWalletAction.Proceed)
}
Picasso.get()
.load(Artwork.TWIN_CARD_1)
.placeholder(R.drawable.card_placeholder)
?.error(R.drawable.card_placeholder)
?.into(iv_twin_card_1)
Picasso.get()
.load(Artwork.TWIN_CARD_2)
.placeholder(R.drawable.card_placeholder)
?.error(R.drawable.card_placeholder)
?.into(iv_twin_card_2)
}

View file

@ -34,6 +34,7 @@ class HomeMiddleware {
withContext(Dispatchers.Main) {
when (result) {
is CompletionResult.Success -> {
tangemSdkManager.changeDisplayedCardIdNumbersCount(result.data.card)
store.dispatch(GlobalAction.RestoreAppCurrency)
store.state.globalState.tapWalletManager.onCardScanned(result.data)
showDisclaimerOrNavigateToWallet()

View file

@ -23,7 +23,7 @@ object ReleaseSendState : Action
data class PrepareSendScreen(
val coinAmount: Amount?,
val tokenAmount: Amount? = null
val tokenAmount: Amount? = null,
) : SendScreenAction
// Address or PayId
@ -34,7 +34,7 @@ sealed class AddressPayIdActionUi : SendScreenActionUi {
object CheckAddressPayId : AddressPayIdActionUi()
data class SetTruncateHandler(val handler: (String) -> String) : AddressPayIdActionUi()
data class TruncateOrRestore(val truncate: Boolean) : AddressPayIdActionUi()
data class ChangePayIdState(val walletPayIdEnabled: Boolean) : AddressPayIdActionUi()
data class ChangePayIdState(val sendingToPayIdEnabled: Boolean) : AddressPayIdActionUi()
}
@ -48,7 +48,7 @@ sealed class TransactionExtrasAction : SendScreenActionUi {
object Release : TransactionExtrasAction()
sealed class XlmMemo : TransactionExtrasAction() {
// data class ChangeSelectedMemo(val memoType: XlmMemoType) : XlmMemo()
// data class ChangeSelectedMemo(val memoType: XlmMemoType) : XlmMemo()
data class HandleUserInput(val data: String) : XlmMemo()
}

View file

@ -124,7 +124,7 @@ internal class AddressPayIdMiddleware {
private fun isValidBlockchainAddressAndNotTheSameAsWallet(wallet: Wallet, address: String): Error? {
return if (wallet.blockchain.validateAddress(address)) {
if (wallet.address != address) {
if (wallet.addresses.all { it.value != address } ) {
null
} else {
Error.ADDRESS_SAME_AS_WALLET
@ -167,6 +167,6 @@ internal class AddressPayIdMiddleware {
}
private fun isPayIdEnabled(): Boolean {
return store.state.globalState.configManager?.config?.isWalletPayIdEnabled ?: false
return store.state.globalState.configManager?.config?.isSendingToPayIdEnabled ?: false
}
}

View file

@ -42,6 +42,7 @@ val sendMiddleware: Middleware<AppState> = { dispatch, appState ->
is RequestFee -> RequestFeeMiddleware().handle(appState(), dispatch)
is SendActionUi.SendAmountToRecipient ->
verifyAndSendTransaction(action, appState(), dispatch)
is PrepareSendScreen -> setIfSendingToPayIdEnabled(appState(), dispatch)
}
nextDispatch(action)
}
@ -195,3 +196,9 @@ fun createValidateTransactionError(errorList: EnumSet<TransactionError>, walletM
return TapError.ValidateTransactionErrors(tapErrors) { it.joinToString("\r\n") }
}
private fun setIfSendingToPayIdEnabled(appState: AppState?, dispatch: (Action) -> Unit) {
val isSendingToPayIdEnabled =
appState?.globalState?.configManager?.config?.isSendingToPayIdEnabled ?: false
dispatch(AddressPayIdActionUi.ChangePayIdState(isSendingToPayIdEnabled))
}

View file

@ -30,7 +30,7 @@ class AddressPayIdReducer : SendInternalReducer {
is AddressPayIdActionUi.PasteAddressPayId -> return sendState
is AddressPayIdActionUi.CheckClipboard -> return sendState
is AddressPayIdActionUi.CheckAddressPayId -> return sendState
is AddressPayIdActionUi.ChangePayIdState -> state.copy(walletPayIdEnabled = action.walletPayIdEnabled)
is AddressPayIdActionUi.ChangePayIdState -> state.copy(sendingToPayIdEnabled = action.sendingToPayIdEnabled)
}
return updateLastState(sendState.copy(addressPayIdState = result), result)
}

View file

@ -10,7 +10,7 @@ data class AddressPayIdState(
val destinationWalletAddress: String? = null,
val error: AddressPayIdVerifyAction.Error? = null,
val truncateHandler: ((String) -> String)? = null,
val walletPayIdEnabled: Boolean = false,
val sendingToPayIdEnabled: Boolean = false,
val pasteIsEnabled: Boolean = false
) : SendScreenState {

View file

@ -126,7 +126,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
val til = fg.tilAddressOrPayId
val parsedError = parseError(til.context, state.error)
val hintResId = if (state.walletPayIdEnabled) {
val hintResId = if (state.sendingToPayIdEnabled) {
R.string.send_destination_hint_address_payid
} else {
R.string.send_destination_hint_address

View file

@ -9,7 +9,7 @@ import com.tangem.tap.common.redux.ErrorAction
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.TwinCardNumber
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.wallet.R
import org.rekotlin.Action
import java.math.BigDecimal

View file

@ -20,8 +20,9 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.PayIdManager
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.TopUpHelper
import com.tangem.tap.domain.TwinsHelper
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.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.twins.CreateTwinWallet
import com.tangem.tap.features.send.redux.PrepareSendScreen
@ -64,7 +65,7 @@ class WalletMiddleware {
}
is WalletAction.CreateWallet -> {
if (store.state.walletState.twinCardsState != null) {
store.dispatch(DetailsAction.CreateTwinWalletAction.Proceed(
store.dispatch(DetailsAction.CreateTwinWalletAction.ShowWarning(
store.state.globalState.scanNoteResponse?.card?.cardId?.let {
TwinsHelper.getTwinCardNumber(it)
},
@ -124,8 +125,15 @@ class WalletMiddleware {
val result = tangemSdkManager.scanNote(FirebaseAnalyticsHandler)
when (result) {
is CompletionResult.Success -> {
tangemSdkManager.changeDisplayedCardIdNumbersCount(result.data.card)
store.state.globalState.tapWalletManager
.onCardScanned(result.data, true)
if (store.state.walletState.twinCardsState != null) {
val showOnboarding = !preferencesStorage.wasTwinsOnboardingShown()
if (showOnboarding) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.TwinsOnboarding))
}
}
}
}
}
@ -228,6 +236,7 @@ class WalletMiddleware {
if (card.getType() != CardType.Release) {
return WarningType.DevCard
}
if (card.isTwinCard()) return null
return if (signatureCountValidator == null) {
if (card.walletSignedHashes ?: 0 > 0) {
@ -249,6 +258,8 @@ class WalletMiddleware {
val card = store.state.globalState.scanNoteResponse?.card
if (card == null || preferencesStorage.wasCardScannedBefore(card.cardId)) return
if (card.isTwinCard()) return
val validator = store.state.globalState.scanNoteResponse?.walletManager
as? SignatureCountValidator
scope.launch {
@ -276,9 +287,10 @@ private class TopUpMiddleware {
when (action) {
is WalletAction.TopUpAction.TopUp -> {
val config = store.state.globalState.configManager?.config ?: return
val defaultAddress = store.state.walletState.walletAddresses!!.list[0].address
val url = TopUpHelper.getUrl(
store.state.walletState.currencyData.currencySymbol!!,
store.state.walletState.walletAddresses!!.selectedAddress.address,
defaultAddress,
config.moonPayApiKey,
config.moonPayApiSecretKey
)

View file

@ -12,6 +12,7 @@ import com.tangem.tap.common.extensions.toQrCode
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.features.wallet.models.removeUnknownTransactions
import com.tangem.tap.features.wallet.models.toPendingTransactions
import com.tangem.tap.features.wallet.ui.BalanceStatus
@ -164,6 +165,12 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
Artwork.SERGIO_CARD_URL
} else if (action.card.cardId.startsWith(Artwork.MARTA_CARD_ID)) {
Artwork.MARTA_CARD_URL
} else if (newState.twinCardsState?.cardNumber != null) {
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
}

View file

@ -7,7 +7,7 @@ import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.address.AddressType
import com.tangem.tap.common.entities.Button
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.domain.TwinCardNumber
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import org.rekotlin.StateType
@ -94,6 +94,8 @@ data class Artwork(
const val MARTA_CARD_URL = "https://app.tangem.com/cards/card_tg083.png"
const val SERGIO_CARD_ID = "BC01"
const val MARTA_CARD_ID = "BC02"
const val TWIN_CARD_1 = "https://app.tangem.com/cards/card_tg085.png"
const val TWIN_CARD_2 = "https://app.tangem.com/cards/card_tg086.png"
}
}
@ -104,8 +106,8 @@ data class TopUpState(
)
data class TwinCardsState(
val secondCardId: String?,
val cardNumber: TwinCardNumber?,
val showTwinOnboarding: Boolean,
val isCreatingTwinCardsAllowed: Boolean
val secondCardId: String?,
val cardNumber: TwinCardNumber?,
val showTwinOnboarding: Boolean,
val isCreatingTwinCardsAllowed: Boolean
)

View file

@ -40,6 +40,7 @@ data class TokenData(
class BalanceWidget(
val fragment: Fragment,
val data: BalanceWidgetData,
val isTwinCard: Boolean,
) {
fun setup() {
@ -100,8 +101,13 @@ class BalanceWidget(
BalanceStatus.EmptyCard -> {
fragment.l_balance.hide()
fragment.l_balance_error.show()
fragment.tv_error_title.text = fragment.getText(R.string.wallet_error_empty_card)
fragment.tv_error_descriptions.text = fragment.getText(R.string.wallet_error_empty_card_subtitle)
if (isTwinCard) {
fragment.tv_error_title.text = fragment.getText(R.string.wallet_error_empty_twin_card)
fragment.tv_error_descriptions.text = fragment.getText(R.string.wallet_error_empty_twin_card_subtitle)
} else {
fragment.tv_error_title.text = fragment.getText(R.string.wallet_error_empty_card)
fragment.tv_error_descriptions.text = fragment.getText(R.string.wallet_error_empty_card_subtitle)
}
}
BalanceStatus.NoAccount -> {
fragment.l_balance.hide()

View file

@ -16,7 +16,7 @@ import com.tangem.blockchain.common.address.AddressType
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.TwinCardNumber
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.ui.dialogs.AmountToSendDialog
@ -130,7 +130,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
handleDialogs(state.walletDialog)
l_balance.show()
BalanceWidget(this, state.currencyData).setup()
BalanceWidget(this, state.currencyData, state.twinCardsState != null).setup()
if (state.state == ProgressState.Done) {
srl_wallet.isRefreshing = false
@ -201,7 +201,13 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
val buttonTitle = when (state.mainButton) {
is WalletMainButton.SendButton -> R.string.wallet_button_send
is WalletMainButton.CreateWalletButton -> R.string.wallet_button_create_wallet
is WalletMainButton.CreateWalletButton -> {
if (state.twinCardsState == null) {
R.string.wallet_button_create_wallet
} else {
R.string.wallet_button_create_twin_wallet
}
}
}
btnConfirm.text = getString(buttonTitle)
btnConfirm.isEnabled = state.mainButton.enabled

View file

@ -5,12 +5,16 @@ import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.squareup.picasso.Picasso
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.twins.TwinsHelper
import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_twin_cards.*
import kotlinx.android.synthetic.main.layout_twin_cards.*
class TwinsOnboardingFragment : Fragment(R.layout.fragment_twin_cards) {
@ -31,11 +35,24 @@ class TwinsOnboardingFragment : Fragment(R.layout.fragment_twin_cards) {
store.dispatch(WalletAction.TwinsAction.SetOnboardingShown)
val secondCardId = store.state.walletState.twinCardsState?.secondCardId
val text = getString(R.string.twins_onboarding_description_format, secondCardId)
val secondCardId = store.state.walletState.twinCardsState?.secondCardId ?: ""
val secondTwinCardId = TwinsHelper.getTwinCardIdForUser(secondCardId)
val text = getString(R.string.twins_onboarding_description_format, secondTwinCardId)
tv_twin_cards_description_1.text = text
setOnClickListeners()
Picasso.get()
.load(Artwork.TWIN_CARD_1)
.placeholder(R.drawable.card_placeholder)
?.error(R.drawable.card_placeholder)
?.into(iv_twin_card_1)
Picasso.get()
.load(Artwork.TWIN_CARD_2)
.placeholder(R.drawable.card_placeholder)
?.error(R.drawable.card_placeholder)
?.into(iv_twin_card_2)
}
private fun setOnClickListeners() {

View file

@ -12,6 +12,7 @@ import retrofit2.Converter
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import java.math.BigDecimal
import java.util.concurrent.TimeUnit
fun createRetrofitInstance(
baseUrl: String,
@ -19,6 +20,7 @@ fun createRetrofitInstance(
): Retrofit {
val okHttpBuilder = OkHttpClient.Builder()
interceptors.forEach { okHttpBuilder.addInterceptor(it) }
addTimeOuts(okHttpBuilder)
if (BuildConfig.DEBUG) okHttpBuilder.addInterceptor(createHttpLoggingInterceptor())
return Retrofit.Builder()
@ -28,6 +30,13 @@ fun createRetrofitInstance(
.build()
}
private fun addTimeOuts(okHttpBuilder: OkHttpClient.Builder) {
okHttpBuilder.callTimeout(1, TimeUnit.SECONDS)
okHttpBuilder.connectTimeout(20, TimeUnit.SECONDS)
okHttpBuilder.readTimeout(20, TimeUnit.SECONDS)
okHttpBuilder.writeTimeout(20, TimeUnit.SECONDS)
}
fun createMoshiConverterFactory(): Converter.Factory = MoshiConverterFactory.create(createMoshi())
fun createMoshi(): Moshi = Moshi.Builder()

View file

@ -47,7 +47,7 @@
android:id="@+id/tv_card_id_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="14dp"
android:paddingBottom="7dp"
android:text="@string/details_row_title_cid"
android:textColor="@color/darkGray6"
android:textSize="16sp"
@ -59,7 +59,7 @@
android:id="@+id/tv_card_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="14dp"
android:paddingBottom="7dp"
android:textColor="@color/darkGray1"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
@ -70,7 +70,8 @@
android:id="@+id/tv_issuer_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="14dp"
android:paddingTop="7dp"
android:paddingBottom="7dp"
android:text="@string/details_row_title_issuer"
android:textColor="@color/darkGray6"
android:textSize="16sp"
@ -81,7 +82,8 @@
android:id="@+id/tv_issuer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="14dp"
android:paddingTop="7dp"
android:paddingBottom="7dp"
android:textColor="@color/darkGray1"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
@ -92,6 +94,7 @@
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"

View file

@ -38,12 +38,12 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
android:gravity="start"
android:text="@string/details_twins_recreate_step_format"
android:textColor="@color/colorSecondary"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="@+id/v_step_1"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="@id/v_step_1"
app:layout_constraintTop_toTopOf="parent" />

View file

@ -13,14 +13,22 @@
android:layout_height="match_parent"
android:layout_marginBottom="33dp">
<View
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/l_twin_cards"
android:alpha="0.55"
android:background="@color/warning"/>
<include
android:id="@+id/l_twin_cards"
layout="@layout/layout_twin_cards_orange"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="-40dp"
android:layout_marginBottom="24dp"
app:layout_constraintBottom_toTopOf="@id/tv_twin_title"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0" />
<TextView

View file

@ -12,6 +12,14 @@
android:layout_height="match_parent"
android:layout_marginBottom="33dp">
<View
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/l_twin_cards"
android:alpha="0.22"
android:background="@color/darkGray6"/>
<include
android:id="@+id/l_twin_cards"
layout="@layout/layout_twin_cards"
@ -21,7 +29,6 @@
android:layout_marginBottom="24dp"
android:gravity="top"
app:layout_constraintBottom_toTopOf="@id/tv_twin_title"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0" />
<TextView
@ -61,7 +68,7 @@
android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@id/btn_continue"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
app:layout_constraintStart_toStartOf="parent"/>
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"

View file

@ -152,7 +152,8 @@
android:layout_margin="16dp"
android:src="@drawable/ic_payid"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/v_payid_divider" />
app:layout_constraintTop_toBottomOf="@id/v_payid_divider"
app:layout_constraintBottom_toBottomOf="parent"/>
<TextView
android:id="@+id/tv_create_payid"
@ -172,9 +173,7 @@
android:id="@+id/tv_payid_address"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="middle"
android:padding="16dp"
android:singleLine="true"
android:textAlignment="textEnd"
android:textColor="@color/darkGray1"
android:textSize="13sp"
@ -183,7 +182,7 @@
app:layout_constraintHorizontal_bias="1"
app:layout_constraintStart_toEndOf="@id/iv_payid_icon"
app:layout_constraintTop_toBottomOf="@id/v_payid_divider"
tools:text="roman$payid.tangem.com" />
tools:text="romafdffdfdfn$payid.tangem.com" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_payid"

View file

@ -197,7 +197,7 @@
<string name="initial_message_scan_header">Tap to scan</string>
<string name="initial_message_tap_header">Tap the card</string>
<string name="details_notification_erase_wallet_not_possible">You balance on this wallet is not zero, or you have unconfirmed transactions</string>
<string name="details_notification_erase_wallet_not_possible">Your balance on this wallet is not zero, or you have unconfirmed transactions</string>
<string name="details_notification_security_option_already_active">This is the currently active option</string>

View file

@ -4,6 +4,10 @@
<string name="wallet_address_chip_compatibility" translatable="false">Compatibility</string>
<string name="wallet_address_chip_legacy" translatable="false">Legacy</string>
<string name="wallet_button_create_twin_wallet" translatable="false">Create twin wallet</string>
<string name="wallet_error_empty_twin_card" translatable="false">Create twin wallet</string>
<string name="wallet_error_empty_twin_card_subtitle" translatable="false"> Generate wallet keys on both cards to start using your Twins</string>
<string name="twins_onboarding_title" translatable="false">Tangem Twin</string>
<string name="twins_onboarding_subtitle" translatable="false">One wallet. Two cards.</string>
<string name="twins_onboarding_description_format" translatable="false">This one that you are holding in your hands
@ -14,11 +18,13 @@ this wallet.
<string name="wallet_twins_chip_format" translatable="false">Card %s of 2</string>
<string name="details_row_title_twins_recreate" translatable="false">Re-create twin wallet</string>
<string name="details_twins_recreate_warning" translatable="false">This action is irreversible. If, after deleting the old wallet, someone sends funds to it, then you will not be able to withdraw them.</string>
<string name="details_twins_recreate_warning" translatable="false">This action is irreversible. You will not have access to the old wallet.\nThe wallet re-creation procedure consists of three steps. You must complete it to the end, otherwise you will have to start from the beginning.</string>
<string name="details_twins_recreate_toolbar" translatable="false">Re-create wallet</string>
<string name="details_twins_recreate_step_format" translatable="false">Step %s</string>
<string name="details_twins_recreate_title_format" translatable="false">Tap the #%s twin card</string>
<string name="details_twins_recreate_subtitle" translatable="false">If the process of re-creating the wallet gets interrupted in any way, youll have to start over.</string>
<string name="details_twins_recreate_title_preparing" translatable="false">Preparing card</string>
<string name="details_twins_recreate_title_creating_wallet" translatable="false">Creating wallet</string>
<string name="details_twins_recreate_subtitle" translatable="false">The wallet creation procedure consists of three steps. You must complete it to the end, otherwise you will have to start from the beginning.</string>
<string name="details_twins_recreate_button_format" translatable="false">Tap the card #%s</string>
<string name="details_twins_recreate_alert" translatable="false">You have already started the process of recreating twin wallet. If you interrupt it, you won\'t be able to use your twin cards until you start it again and complete recreating the wallet.</string>