Updated on 2026-08-14
This commit is contained in:
parent
d5cd5f1951
commit
2ee3bce46a
11 changed files with 22 additions and 357 deletions
|
|
@ -19,7 +19,7 @@ import com.tangem.domain.card.common.TapWorkarounds.isNotSupportedInThatRelease
|
|||
import com.tangem.domain.card.common.TapWorkarounds.isStart2Coin
|
||||
import com.tangem.domain.card.common.TapWorkarounds.isTangemTwins
|
||||
import com.tangem.domain.card.common.TapWorkarounds.isVisa
|
||||
import com.tangem.domain.common.TwinsHelper
|
||||
import com.tangem.domain.card.common.TwinsHelper
|
||||
import com.tangem.domain.wallets.derivations.derivationStyleProvider
|
||||
import com.tangem.domain.card.common.visa.VisaUtilities
|
||||
import com.tangem.domain.card.configs.CardConfig
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import com.tangem.common.card.EllipticCurve
|
|||
import com.tangem.common.core.CardSession
|
||||
import com.tangem.common.core.CardSessionRunnable
|
||||
import com.tangem.common.core.CompletionCallback
|
||||
import com.tangem.domain.common.TwinsHelper
|
||||
import com.tangem.domain.card.common.TwinsHelper
|
||||
import com.tangem.operations.attestation.AttestationTask
|
||||
import com.tangem.operations.wallet.CreateWalletResponse
|
||||
import com.tangem.operations.wallet.CreateWalletTask
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import com.tangem.common.core.CardSession
|
|||
import com.tangem.common.core.CardSessionRunnable
|
||||
import com.tangem.common.core.CompletionCallback
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.domain.common.TwinsHelper
|
||||
import com.tangem.domain.card.common.TwinsHelper
|
||||
import com.tangem.operations.wallet.CreateWalletResponse
|
||||
import com.tangem.operations.wallet.CreateWalletTask
|
||||
import com.tangem.operations.wallet.PurgeWalletCommand
|
||||
|
|
|
|||
|
|
@ -1,78 +0,0 @@
|
|||
package com.tangem.tap.domain.twins
|
||||
|
||||
import com.tangem.Message
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.KeyPair
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.operations.wallet.CreateWalletResponse
|
||||
import com.tangem.tap.common.extensions.inject
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
|
||||
class TwinCardsManager(card: CardDTO) {
|
||||
|
||||
private val issuersConfigStorage by lazy(mode = LazyThreadSafetyMode.NONE) {
|
||||
store.inject(DaggerGraphState::issuersConfigStorage)
|
||||
}
|
||||
|
||||
private val firstCardId: String = card.cardId
|
||||
private val publicKey: String = card.issuer.publicKey.toHexString()
|
||||
|
||||
private var currentCardPublicKey: String? = null
|
||||
private var secondCardPublicKey: String? = null
|
||||
|
||||
suspend fun createFirstWallet(message: Message): CompletionResult<CreateWalletResponse> {
|
||||
val response = tangemSdkManager.createFirstTwinWallet(cardId = firstCardId, initialMessage = message)
|
||||
|
||||
if (response is CompletionResult.Success) {
|
||||
currentCardPublicKey = response.data.wallet.publicKey.toHexString()
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
suspend fun createSecondWallet(
|
||||
initialMessage: Message,
|
||||
preparingMessage: Message,
|
||||
creatingWalletMessage: Message,
|
||||
): CompletionResult<CreateWalletResponse> {
|
||||
val response = tangemSdkManager.createSecondTwinWallet(
|
||||
firstPublicKey = requireNotNull(currentCardPublicKey),
|
||||
firstCardId = firstCardId,
|
||||
issuerKeys = getIssuerKeys(),
|
||||
preparingMessage = preparingMessage,
|
||||
creatingWalletMessage = creatingWalletMessage,
|
||||
initialMessage = initialMessage,
|
||||
)
|
||||
|
||||
if (response is CompletionResult.Success) {
|
||||
secondCardPublicKey = response.data.wallet.publicKey.toHexString()
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
suspend fun complete(message: Message): CompletionResult<ScanResponse> {
|
||||
val response = tangemSdkManager.finalizeTwin(
|
||||
secondCardPublicKey = requireNotNull(secondCardPublicKey).hexToBytes(),
|
||||
issuerKeyPair = getIssuerKeys(),
|
||||
cardId = firstCardId,
|
||||
initialMessage = message,
|
||||
)
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
private suspend fun getIssuerKeys(): KeyPair {
|
||||
val issuer = issuersConfigStorage.getConfig().first { it.publicKey == publicKey }
|
||||
|
||||
return KeyPair(
|
||||
publicKey = issuer.publicKey.hexToBytes(),
|
||||
privateKey = issuer.privateKey.hexToBytes(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,174 +0,0 @@
|
|||
package com.tangem.tap.domain.twins
|
||||
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.ObjectAnimator
|
||||
import android.animation.PropertyValuesHolder
|
||||
import android.view.View
|
||||
import androidx.core.animation.doOnEnd
|
||||
import com.tangem.common.extensions.VoidCallback
|
||||
import com.tangem.domain.common.TwinCardNumber
|
||||
import com.tangem.sdk.ui.widget.leapfrogWidget.LeapView
|
||||
import com.tangem.sdk.ui.widget.leapfrogWidget.LeapViewState
|
||||
import com.tangem.sdk.ui.widget.leapfrogWidget.LeapfrogWidget
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class TwinsCardWidget(
|
||||
val leapfrogWidget: LeapfrogWidget,
|
||||
private val deviceScaleFactor: Float = 1f,
|
||||
val getTopOfAnchorViewForActivateState: () -> Float,
|
||||
) {
|
||||
|
||||
var state: TwinCardsWidgetState? = null
|
||||
private set
|
||||
|
||||
private val animationDuration = 300L
|
||||
|
||||
init {
|
||||
if (leapfrogWidget.getViewsCount() != 2) throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
fun toWelcome(animate: Boolean = true, onEnd: VoidCallback = {}) {
|
||||
if (state == TwinCardsWidgetState.Welcome) return
|
||||
|
||||
state = TwinCardsWidgetState.Welcome
|
||||
val animator = createAnimator(animate)
|
||||
animator.playTogether(
|
||||
createAnimator(TwinCardNumber.First, createWelcomeProperties(TwinCardNumber.First)),
|
||||
createAnimator(TwinCardNumber.Second, createWelcomeProperties(TwinCardNumber.Second)),
|
||||
)
|
||||
animator.doOnEnd { onEnd() }
|
||||
leapfrogWidget.fold(animate) { animator.start() }
|
||||
}
|
||||
|
||||
fun toLeapfrog(animate: Boolean = true, onEnd: VoidCallback = {}) {
|
||||
if (state == TwinCardsWidgetState.Leapfrog) return
|
||||
|
||||
state = TwinCardsWidgetState.Leapfrog
|
||||
val animator = createAnimator(animate)
|
||||
animator.playTogether(
|
||||
createAnimator(TwinCardNumber.First, createLeapfrogProperties(TwinCardNumber.First)),
|
||||
createAnimator(TwinCardNumber.Second, createLeapfrogProperties(TwinCardNumber.Second)),
|
||||
)
|
||||
animator.doOnEnd {
|
||||
leapfrogWidget.initViews()
|
||||
leapfrogWidget.unfold(animate, onEnd)
|
||||
}
|
||||
animator.start()
|
||||
}
|
||||
|
||||
fun toActivate(animate: Boolean = true, onEnd: VoidCallback = {}) {
|
||||
if (state == TwinCardsWidgetState.Activate) return
|
||||
|
||||
state = TwinCardsWidgetState.Activate
|
||||
val animator = createAnimator(animate)
|
||||
animator.playTogether(
|
||||
createAnimator(TwinCardNumber.First, createActivateProperties(TwinCardNumber.First)),
|
||||
createAnimator(TwinCardNumber.Second, createActivateProperties(TwinCardNumber.Second)),
|
||||
)
|
||||
animator.doOnEnd { onEnd() }
|
||||
animator.start()
|
||||
}
|
||||
|
||||
private fun createAnimator(animate: Boolean): AnimatorSet {
|
||||
return AnimatorSet().apply {
|
||||
duration = if (animate) animationDuration else 0
|
||||
}
|
||||
}
|
||||
|
||||
private fun createAnimator(cardNumber: TwinCardNumber, properties: TwinsCardProperties): ObjectAnimator {
|
||||
val view = getLeapViewByCardNumber(cardNumber).view
|
||||
val animator = ObjectAnimator.ofPropertyValuesHolder(view, *properties.createValuesHolders().toTypedArray())
|
||||
view.elevation = properties.elevation
|
||||
return animator
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun createWelcomeProperties(cardNumber: TwinCardNumber): TwinsCardProperties {
|
||||
return when (cardNumber) {
|
||||
TwinCardNumber.First -> {
|
||||
TwinsCardProperties(
|
||||
xTranslation = -120f * deviceScaleFactor,
|
||||
yTranslation = -220f * deviceScaleFactor,
|
||||
rotation = -3f,
|
||||
elevation = 1f,
|
||||
scale = deviceScaleFactor,
|
||||
)
|
||||
}
|
||||
TwinCardNumber.Second -> {
|
||||
TwinsCardProperties(
|
||||
xTranslation = 170f * deviceScaleFactor,
|
||||
yTranslation = 170f * deviceScaleFactor,
|
||||
rotation = -3f,
|
||||
elevation = 0f,
|
||||
scale = deviceScaleFactor,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createLeapfrogProperties(cardNumber: TwinCardNumber): TwinsCardProperties {
|
||||
val twinProperties = TwinsCardProperties.from(getLeapViewByCardNumber(cardNumber).state)
|
||||
return twinProperties.copy(
|
||||
xTranslation = 0f,
|
||||
yTranslation = 0f,
|
||||
rotation = 0f,
|
||||
scale = 1f,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun createActivateProperties(cardNumber: TwinCardNumber): TwinsCardProperties {
|
||||
val topOfAnchorView = getTopOfAnchorViewForActivateState()
|
||||
val twinProperties = TwinsCardProperties.from(getLeapViewByCardNumber(cardNumber).state)
|
||||
return twinProperties.copy(
|
||||
xTranslation = twinProperties.xTranslation,
|
||||
yTranslation = twinProperties.yTranslation - topOfAnchorView,
|
||||
rotation = 0f,
|
||||
scale = (twinProperties.scale - 0.5f) * deviceScaleFactor,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getLeapViewByCardNumber(cardNumber: TwinCardNumber): LeapView {
|
||||
return when (cardNumber) {
|
||||
TwinCardNumber.First -> leapfrogWidget.getViewByPosition(0)
|
||||
TwinCardNumber.Second -> leapfrogWidget.getViewByPosition(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class TwinCardsWidgetState {
|
||||
Welcome, Leapfrog, Activate
|
||||
}
|
||||
|
||||
private data class TwinsCardProperties(
|
||||
val xTranslation: Float = 0f,
|
||||
val yTranslation: Float = 0f,
|
||||
val rotation: Float = 0f,
|
||||
val elevation: Float = 0f,
|
||||
val scale: Float = 1.1f,
|
||||
) {
|
||||
|
||||
fun createValuesHolders(): List<PropertyValuesHolder> {
|
||||
return listOf(
|
||||
PropertyValuesHolder.ofFloat(View.TRANSLATION_X, xTranslation),
|
||||
PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, yTranslation),
|
||||
PropertyValuesHolder.ofFloat(View.ROTATION, rotation),
|
||||
PropertyValuesHolder.ofFloat(View.SCALE_X, scale),
|
||||
PropertyValuesHolder.ofFloat(View.SCALE_Y, scale),
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun from(leapViewState: LeapViewState): TwinsCardProperties {
|
||||
val leapViewProperties = leapViewState.properties
|
||||
|
||||
return TwinsCardProperties(
|
||||
yTranslation = leapViewProperties.yTranslation,
|
||||
elevation = leapViewProperties.elevationEnd,
|
||||
scale = leapViewProperties.scale,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.tap.domain.twins
|
||||
|
||||
import com.tangem.common.core.TangemError
|
||||
import com.tangem.domain.common.TwinCardNumber
|
||||
import com.tangem.domain.card.common.TwinCardNumber
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
import com.tangem.wallet.R
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
package com.tangem.domain.card
|
||||
package com.tangem.domain.card.common
|
||||
|
||||
import com.tangem.domain.common.TwinsHelper
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
|
||||
class TwinsHelperTest {
|
||||
|
||||
|
|
@ -19,7 +18,7 @@ class TwinsHelperTest {
|
|||
|
||||
@Test
|
||||
fun `twins compatibility pack 1 success`() {
|
||||
assertTrue(TwinsHelper.isTwinsCompatible(pack1Twins[0], pack1Twins[1]))
|
||||
Assert.assertTrue(TwinsHelper.isTwinsCompatible(pack1Twins[0], pack1Twins[1]))
|
||||
assertTrue(TwinsHelper.isTwinsCompatible(pack1Twins[1], pack1Twins[0]))
|
||||
}
|
||||
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
package com.tangem.domain.common
|
||||
|
||||
import com.tangem.crypto.CryptoUtils
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
|
||||
object TwinsHelper {
|
||||
|
||||
/**
|
||||
* Card compatibility
|
||||
* cb61 <-> cb62
|
||||
* cb64 <-> cb65
|
||||
*
|
||||
*/
|
||||
private const val FIRST_CARD_FIRST_SERIES = "CB61"
|
||||
private const val SECOND_CARD_FIRST_SERIES = "CB62"
|
||||
private const val FIRST_CARD_SECOND_SERIES = "CB64"
|
||||
private const val SECOND_CARD_SECOND_SERIES = "CB65"
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun verifyTwinPublicKey(issuerData: ByteArray, cardWalletPublicKey: ByteArray?): Boolean {
|
||||
if (issuerData.size < 65 || cardWalletPublicKey == null) return false
|
||||
|
||||
val publicKey = issuerData.sliceArray(0 until 65)
|
||||
val signedKey = issuerData.sliceArray(65 until issuerData.size)
|
||||
return CryptoUtils.verify(cardWalletPublicKey, publicKey, signedKey)
|
||||
}
|
||||
|
||||
fun getTwinCardNumber(cardId: String): TwinCardNumber? {
|
||||
val isFirstCard = cardId.startsWith(FIRST_CARD_FIRST_SERIES) ||
|
||||
cardId.startsWith(FIRST_CARD_SECOND_SERIES)
|
||||
if (isFirstCard) return TwinCardNumber.First
|
||||
|
||||
val isSecondCard = cardId.startsWith(SECOND_CARD_FIRST_SERIES) ||
|
||||
cardId.startsWith(SECOND_CARD_SECOND_SERIES)
|
||||
if (isSecondCard) return TwinCardNumber.Second
|
||||
return null
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
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"
|
||||
}
|
||||
|
||||
/**
|
||||
* Twins compatibility
|
||||
* cb61 <-> cb62
|
||||
* cb64 <-> cb65
|
||||
*/
|
||||
fun isTwinsCompatible(firstCardId: String, secondCardId: String): Boolean {
|
||||
return secondCardId.startsWith(getCompatibleTwinCardId(firstCardId))
|
||||
}
|
||||
|
||||
private fun getCompatibleTwinCardId(cardId: String): String {
|
||||
return when {
|
||||
cardId.startsWith(FIRST_CARD_FIRST_SERIES) -> SECOND_CARD_FIRST_SERIES
|
||||
cardId.startsWith(FIRST_CARD_SECOND_SERIES) -> SECOND_CARD_SECOND_SERIES
|
||||
cardId.startsWith(SECOND_CARD_FIRST_SERIES) -> FIRST_CARD_FIRST_SERIES
|
||||
cardId.startsWith(SECOND_CARD_SECOND_SERIES) -> FIRST_CARD_SECOND_SERIES
|
||||
else -> {
|
||||
"unknown"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class TwinCardNumber(val number: Int) {
|
||||
First(1), Second(2);
|
||||
|
||||
fun pairNumber(): TwinCardNumber = when (this) {
|
||||
First -> Second
|
||||
Second -> First
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Use ScanResponse.isTangemTwin")
|
||||
fun CardDTO.isTangemTwin(): Boolean {
|
||||
return TwinsHelper.getTwinCardNumber(cardId) != null
|
||||
}
|
||||
|
||||
fun CardDTO.getTwinCardNumber(): TwinCardNumber? {
|
||||
return TwinsHelper.getTwinCardNumber(this.cardId)
|
||||
}
|
||||
|
||||
fun CardDTO.getTwinCardIdForUser(): String {
|
||||
return TwinsHelper.getTwinCardIdForUser(this.cardId)
|
||||
}
|
||||
|
|
@ -2,8 +2,8 @@ package com.tangem.domain.wallets.usecase
|
|||
|
||||
import com.tangem.common.card.FirmwareVersion
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.domain.common.TwinCardNumber
|
||||
import com.tangem.domain.common.TwinsHelper
|
||||
import com.tangem.domain.card.common.TwinCardNumber
|
||||
import com.tangem.domain.card.common.TwinsHelper
|
||||
import com.tangem.domain.models.ArtworkModel
|
||||
import com.tangem.domain.wallets.models.Artwork
|
||||
import com.tangem.operations.attestation.ArtworkSize
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ import com.tangem.core.decompose.ui.UiMessageSender
|
|||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.toWrappedList
|
||||
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
|
||||
import com.tangem.domain.card.common.TwinCardNumber
|
||||
import com.tangem.domain.card.common.getTwinCardNumber
|
||||
import com.tangem.domain.card.repository.CardRepository
|
||||
import com.tangem.domain.common.TwinCardNumber
|
||||
import com.tangem.domain.common.getTwinCardNumber
|
||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
|
|
@ -68,6 +68,7 @@ internal class OnboardingTwinModel @Inject constructor(
|
|||
private val params = paramsContainer.require<OnboardingTwinComponent.Params>()
|
||||
private val firstCardTwinNumber = params.scanResponse.card.getTwinCardNumber() ?: error("Not twin")
|
||||
private val cryptoCurrencyStatusJobHolder = JobHolder()
|
||||
private var twinCardsIds = Pair<String?, String?>(null, null)
|
||||
|
||||
private val _uiState = MutableStateFlow(
|
||||
when (params.mode) {
|
||||
|
|
@ -159,6 +160,7 @@ internal class OnboardingTwinModel @Inject constructor(
|
|||
}
|
||||
is CompletionResult.Success -> {
|
||||
if (params.mode == Mode.CreateWallet) {
|
||||
twinCardsIds = twinCardsIds.copy(first = result.data.cardId)
|
||||
cardRepository.startCardActivation(result.data.cardId)
|
||||
}
|
||||
|
||||
|
|
@ -216,6 +218,7 @@ internal class OnboardingTwinModel @Inject constructor(
|
|||
}
|
||||
is CompletionResult.Success -> {
|
||||
if (params.mode == Mode.CreateWallet) {
|
||||
twinCardsIds = twinCardsIds.copy(second = result.data.cardId)
|
||||
cardRepository.startCardActivation(result.data.cardId)
|
||||
}
|
||||
|
||||
|
|
@ -299,7 +302,12 @@ internal class OnboardingTwinModel @Inject constructor(
|
|||
return@coroutineScope
|
||||
}
|
||||
|
||||
cardRepository.finishCardActivation(params.scanResponse.card.cardId)
|
||||
twinCardsIds.first?.let {
|
||||
cardRepository.finishCardActivation(it)
|
||||
}
|
||||
twinCardsIds.second?.let {
|
||||
cardRepository.finishCardActivation(it)
|
||||
}
|
||||
|
||||
params.modelCallbacks.onDone()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit b64f8659fce65e7749d50923b32bfad13e6b8cac
|
||||
Subproject commit 193db8a0247a1f33574dcdd5f9ec4dab2c28ea73
|
||||
Loading…
Add table
Add a link
Reference in a new issue