Updated on 2026-08-14

This commit is contained in:
Tangem 2021-09-12 17:29:35 +03:00
commit 126a74addf
15 changed files with 115 additions and 49 deletions

View file

@ -2,5 +2,5 @@
"isWalletPayIdEnabled": true,
"isSendingToPayIdEnabled": true,
"isTopUpEnabled": true,
"isCreatingTwinCardsAllowed": false
"isCreatingTwinCardsAllowed": true
}

@ -1 +1 @@
Subproject commit 415eb6e9ccfcdc3573391b869adeba433f790087
Subproject commit 0cf3af21e6f441e6323a1ba127ee7518168797df

View file

@ -0,0 +1,16 @@
package com.tangem.tap.common.extensions
import android.content.Context
fun Context.readFile(fileName: String): String =
this.openFileInput(fileName).bufferedReader().readText()
fun Context.rewriteFile(content: String, fileName: String) {
this.openFileOutput(fileName, Context.MODE_PRIVATE).use {
it.write(content.toByteArray(), 0, content.length)
}
}
fun Context.readAssetAsString(fileName: String): String {
return this.assets.open("$fileName.json").bufferedReader().readText()
}

View file

@ -98,11 +98,10 @@ class TapWalletManager {
store.dispatch(GlobalAction.SaveScanNoteResponse(data))
store.dispatch(WalletAction.MultiWallet.SetIsMultiwalletAllowed(data.card.isMultiwalletAllowed))
if (data.card.isTwinCard()) {
val secondCardId = TwinsHelper.getTwinsCardId(data.card.cardId)
val cardNumber = TwinsHelper.getTwinCardNumber(data.card.cardId)
if (secondCardId != null && cardNumber != null) {
if (cardNumber != null) {
store.dispatch(WalletAction.TwinsAction.SetTwinCard(
secondCardId, cardNumber, isCreatingTwinCardsAllowed = true
cardNumber, isCreatingTwinCardsAllowed = true
))
}
}

View file

@ -6,6 +6,7 @@ import com.google.firebase.remoteconfig.ktx.remoteConfig
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.common.extensions.readAssetAsString
import com.tangem.tap.domain.configurable.Loader
import timber.log.Timber
@ -22,8 +23,8 @@ class FeaturesLocalLoader(
val featureAdapter: JsonAdapter<FeatureModel> = moshi.adapter(FeatureModel::class.java)
val valuesAdapter: JsonAdapter<ConfigValueModel> = moshi.adapter(ConfigValueModel::class.java)
val jsonFeatures = readAssetAsString(Loader.featuresName)
val jsonConfigValues = readAssetAsString(Loader.configValuesName)
val jsonFeatures = context.readAssetAsString(Loader.featuresName)
val jsonConfigValues = context.readAssetAsString(Loader.configValuesName)
ConfigModel(featureAdapter.fromJson(jsonFeatures), valuesAdapter.fromJson(jsonConfigValues))
} catch (ex: Exception) {
@ -32,10 +33,6 @@ class FeaturesLocalLoader(
}
onComplete(config)
}
private fun readAssetAsString(fileName: String): String {
return context.assets.open("$fileName.json").bufferedReader().readText()
}
}
class FeaturesRemoteLoader(

View file

@ -1,8 +1,6 @@
package com.tangem.tap.domain.twins
import com.tangem.CardSession
import com.tangem.CardSessionRunnable
import com.tangem.Message
import com.tangem.*
import com.tangem.commands.wallet.CreateWalletResponse
import com.tangem.commands.wallet.PurgeWalletCommand
import com.tangem.common.CompletionResult
@ -13,14 +11,26 @@ import com.tangem.tap.domain.extensions.getSingleWallet
import com.tangem.tasks.CreateWalletTask
class CreateSecondTwinWalletTask(
private val firstPublicKey: String,
private val preparingMessage: Message,
private val creatingWalletMessage: Message
private val firstPublicKey: String,
private val firstCardId: String,
private val issuerKeys: KeyPair,
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?.getSingleWallet()?.publicKey != null) {
val card = session.environment.card
if (card?.getSingleWallet()?.publicKey != null) {
if (!card.cardId.startsWith(TwinsHelper.getPairCardSeries(firstCardId) ?: "")) {
callback(CompletionResult.Failure(TangemSdkError.WrongCardType()))
return
}
session.setInitialMessage(preparingMessage)
PurgeWalletCommand(TangemSdkConstants.getDefaultWalletIndex()).run(session) { response ->
when (response) {
@ -44,7 +54,7 @@ class CreateSecondTwinWalletTask(
session.environment.card = session.environment.card?.changeStatusToLoaded()
WriteProtectedIssuerDataTask(
firstPublicKey.hexToBytes(), TwinCardsManager.issuerKeys
firstPublicKey.hexToBytes(), issuerKeys
).run(session) { writeResult ->
when (writeResult) {
is CompletionResult.Success -> callback(result)

View file

@ -1,5 +1,8 @@
package com.tangem.tap.domain.twins
import android.content.Context
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Types
import com.tangem.KeyPair
import com.tangem.Message
import com.tangem.blockchain.extensions.Result
@ -8,17 +11,22 @@ import com.tangem.common.CompletionResult
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString
import com.tangem.crypto.CryptoUtils
import com.tangem.tap.common.extensions.readAssetAsString
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.network.createMoshi
import com.tangem.tap.tangemSdkManager
class TwinCardsManager(private val scanNoteResponse: ScanNoteResponse) {
class TwinCardsManager(private val scanNoteResponse: ScanNoteResponse, context: Context) {
private val currentCardId: String = scanNoteResponse.card.cardId
private val secondCardId: String? = TwinsHelper.getTwinsCardId(currentCardId)
private var currentCardPublicKey: String? = null
private var secondCardPublicKey: String? = null
private val issuerKeyPair: KeyPair = getIssuerKeys(
context, scanNoteResponse.card.issuerPublicKey!!.toHexString()
)
suspend fun createFirstWallet(message: Message): SimpleResult {
val response = tangemSdkManager.runTaskAsync(
CreateFirstTwinWalletTask(), currentCardId, message
@ -39,9 +47,15 @@ class TwinCardsManager(private val scanNoteResponse: ScanNoteResponse) {
preparingMessage: Message,
creatingWalletMessage: Message
): SimpleResult {
val task = CreateSecondTwinWalletTask(
firstPublicKey = currentCardPublicKey!!,
firstCardId = currentCardId,
issuerKeys = issuerKeyPair,
preparingMessage = preparingMessage,
creatingWalletMessage = creatingWalletMessage
)
val response = tangemSdkManager.runTaskAsync(
CreateSecondTwinWalletTask(currentCardPublicKey!!, preparingMessage, creatingWalletMessage),
secondCardId, initialMessage
task, null, initialMessage
)
when (response) {
is CompletionResult.Success -> {
@ -55,7 +69,7 @@ class TwinCardsManager(private val scanNoteResponse: ScanNoteResponse) {
suspend fun complete(message: Message): Result<ScanNoteResponse> {
val response = tangemSdkManager.runTaskAsync(
FinalizeTwinTask(secondCardPublicKey!!.hexToBytes(), issuerKeys),
FinalizeTwinTask(secondCardPublicKey!!.hexToBytes(), issuerKeyPair),
currentCardId, message
)
return when (response) {
@ -65,13 +79,6 @@ class TwinCardsManager(private val scanNoteResponse: ScanNoteResponse) {
}
companion object {
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)
@ -79,5 +86,30 @@ class TwinCardsManager(private val scanNoteResponse: ScanNoteResponse) {
return (cardWalletPublicKey != null &&
CryptoUtils.verify(cardWalletPublicKey, publicKey, signedKey))
}
private fun getIssuerKeys(context: Context, publicKey: String): KeyPair {
val issuer = getIssuers(context).first { it.publicKey == publicKey }
return KeyPair(
publicKey = issuer.publicKey.hexToBytes(),
privateKey = issuer.privateKey.hexToBytes()
)
}
private fun getAdapter(): JsonAdapter<List<Issuer>> {
return createMoshi().adapter(
Types.newParameterizedType(List::class.java, Issuer::class.java)
)
}
private fun getIssuers(context: Context): List<Issuer> {
val file = context.readAssetAsString("tangem-app-config/issuers")
return getAdapter().fromJson(file)!!
}
}
}
}
private class Issuer(
val id: String,
val privateKey: String,
val publicKey: String,
)

View file

@ -24,6 +24,19 @@ class TwinsHelper {
}
}
fun getPairCardSeries(cardId: String): String? {
return when (getTwinCardNumber(cardId) ?: return null) {
TwinCardNumber.First -> {
val index = firstCardSeries.indexOf(cardId.take(4))
secondCardSeries[index]
}
TwinCardNumber.Second -> {
val index = secondCardSeries.indexOf(cardId.take(4))
firstCardSeries[index]
}
}
}
fun getTwinsCardId(cardId: String): String? {
val cardIdWithNewSeries = when (getTwinCardNumber(cardId) ?: return null) {
TwinCardNumber.First -> {

View file

@ -1,5 +1,6 @@
package com.tangem.tap.features.details.redux
import android.content.Context
import com.tangem.Message
import com.tangem.blockchain.common.Wallet
import com.tangem.commands.common.card.Card
@ -62,7 +63,9 @@ sealed class DetailsAction : Action {
object Confirm : CreateTwinWalletAction()
}
data class LaunchFirstStep(val message: Message) : CreateTwinWalletAction() {
data class LaunchFirstStep(
val message: Message, val context: Context
) : CreateTwinWalletAction() {
object Success : CreateTwinWalletAction()
object Failure : CreateTwinWalletAction()
}

View file

@ -49,7 +49,7 @@ class CreateTwinWalletMiddleware {
}
is DetailsAction.CreateTwinWalletAction.LaunchFirstStep -> {
store.state.globalState.scanNoteResponse?.let {
twinsManager = TwinCardsManager(it)
twinsManager = TwinCardsManager(it, action.context)
}
scope.launch {
val result = twinsManager?.createFirstWallet(action.message)

View file

@ -97,11 +97,12 @@ class CreateTwinWalletFragment : Fragment(R.layout.fragment_details_twin_cards),
v_step_3.setBackgroundColor(defaultColor)
btn_tap.setOnClickListener {
store.dispatch(DetailsAction.CreateTwinWalletAction.LaunchFirstStep(
Message(getString(
R.string.details_twins_recreate_title_format,
twinCardNumberString
)
)))
Message(getString(
R.string.details_twins_recreate_title_format,
twinCardNumberString
)),
requireContext()
))
}
btn_tap.text = getString(R.string.details_twins_recreate_button_format,
twinCardNumber.number.toString())

View file

@ -6,7 +6,6 @@ import com.tangem.blockchain.common.address.AddressType
import com.tangem.commands.common.card.Card
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.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.tokens.CardCurrencies
@ -132,7 +131,7 @@ sealed class WalletAction : Action {
object ShowOnboarding : TwinsAction()
object SetOnboardingShown : TwinsAction()
data class SetTwinCard(
val secondCardId: String, val number: TwinCardNumber,
val number: TwinCardNumber,
val isCreatingTwinCardsAllowed: Boolean,
) : TwinsAction()
}

View file

@ -196,7 +196,6 @@ data class TopUpState(
)
data class TwinCardsState(
val secondCardId: String?,
val cardNumber: TwinCardNumber?,
val showTwinOnboarding: Boolean,
val isCreatingTwinCardsAllowed: Boolean

View file

@ -10,7 +10,6 @@ class TwinsReducer {
is WalletAction.TwinsAction.SetTwinCard -> {
state.copy(
twinCardsState = TwinCardsState(
secondCardId = action.secondCardId,
cardNumber = action.number,
showTwinOnboarding = state.twinCardsState?.showTwinOnboarding
?: false,
@ -21,7 +20,7 @@ class TwinsReducer {
is WalletAction.TwinsAction.ShowOnboarding -> {
state.copy(
twinCardsState = state.twinCardsState?.copy(showTwinOnboarding = true)
?: TwinCardsState(null, null,
?: TwinCardsState(cardNumber = null,
showTwinOnboarding = true,
isCreatingTwinCardsAllowed = false)
)

View file

@ -8,7 +8,6 @@ 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
@ -34,10 +33,9 @@ class TwinsOnboardingFragment : Fragment(R.layout.fragment_twin_cards) {
super.onViewCreated(view, savedInstanceState)
store.dispatch(WalletAction.TwinsAction.SetOnboardingShown)
val secondCardId = store.state.walletState.twinCardsState?.secondCardId ?: ""
val secondTwinCardId = TwinsHelper.getTwinCardIdForUser(secondCardId)
val text = getString(R.string.twins_onboarding_description_format, secondTwinCardId)
val secondTwinNumber =
store.state.walletState.twinCardsState?.cardNumber?.pairNumber()?.number ?: ""
val text = getString(R.string.twins_onboarding_description_format, secondTwinNumber)
tv_twin_cards_description_1.text = text
setOnClickListeners()