Updated on 2026-08-14

This commit is contained in:
Tangem 2020-12-02 14:39:27 +00:00
commit 9d9eccddcd
53 changed files with 1628 additions and 120 deletions

View file

@ -65,21 +65,21 @@ repositories {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin"
implementation 'androidx.core:core-ktx:1.3.1'
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'com.google.android.material:material:1.2.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
implementation 'com.tangem:blockchain:1.117.0'
implementation 'com.tangem:core:1.83.0'
implementation 'com.tangem:sdk:1.83.0'
implementation 'com.tangem:blockchain:1.119.0'
implementation 'com.tangem:core:1.86.0'
implementation 'com.tangem:sdk:1.86.0'
// WebView
implementation "androidx.browser:browser:1.2.0"
//lifecycle
implementation "androidx.lifecycle:lifecycle-runtime:2.2.0"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"
implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"

View file

@ -6,5 +6,9 @@
{
"name": "isTopUpEnabled",
"value": true
},
{
"name": "isCreatingTwinCardsAllowed",
"value": false
}
]

View file

@ -6,5 +6,9 @@
{
"name": "isTopUpEnabled",
"value": false
},
{
"name": "isCreatingTwinCardsAllowed",
"value": true
}
]

View file

@ -10,7 +10,7 @@ import com.tangem.CardFilter
import com.tangem.Config
import com.tangem.Log
import com.tangem.TangemSdk
import com.tangem.common.extensions.CardType
import com.tangem.commands.common.card.CardType
import com.tangem.tangem_sdk_new.extensions.init
import com.tangem.tap.common.redux.NotificationsHandler
import com.tangem.tap.common.redux.navigation.AppScreen

View file

@ -1,6 +1,7 @@
package com.tangem.tap.common.analytics
import com.tangem.commands.Card
import com.tangem.commands.common.card.Card
interface AnalyticsHandler {
fun triggerEvent(event: AnalyticsEvent, card: Card?)

View file

@ -4,7 +4,7 @@ import android.os.Bundle
import androidx.core.os.bundleOf
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.ktx.Firebase
import com.tangem.commands.Card
import com.tangem.commands.common.card.Card
object FirebaseAnalyticsHandler: AnalyticsHandler {
override fun triggerEvent(event: AnalyticsEvent, card: Card?) {
@ -17,7 +17,7 @@ object FirebaseAnalyticsHandler: AnalyticsHandler {
return bundleOf(
AnalyticsParam.BLOCKCHAIN.param to card.cardData?.blockchainName,
AnalyticsParam.BATCH_ID.param to card.cardData?.batchId,
AnalyticsParam.FIRMWARE.param to card.firmwareVersion
AnalyticsParam.FIRMWARE.param to card.firmwareVersion.version
)
}

View file

@ -7,10 +7,13 @@ import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.features.details.ui.DetailsConfirmFragment
import com.tangem.tap.features.details.ui.DetailsFragment
import com.tangem.tap.features.details.ui.DetailsSecurityFragment
import com.tangem.tap.features.details.ui.twins.CreateTwinWalletFragment
import com.tangem.tap.features.details.ui.twins.TwinWalletWarningFragment
import com.tangem.tap.features.disclaimer.ui.DisclaimerFragment
import com.tangem.tap.features.home.HomeFragment
import com.tangem.tap.features.send.ui.SendFragment
import com.tangem.tap.features.wallet.ui.WalletFragment
import com.tangem.tap.features.wallet.ui.dialogs.TwinsOnboardingFragment
import com.tangem.wallet.R
fun FragmentActivity.openFragment(screen: AppScreen, addToBackStack: Boolean = true) {
@ -44,5 +47,8 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
AppScreen.DetailsConfirm -> DetailsConfirmFragment()
AppScreen.DetailsSecurity -> DetailsSecurityFragment()
AppScreen.Disclaimer -> DisclaimerFragment()
AppScreen.CreateTwinWalletWarning -> TwinWalletWarningFragment()
AppScreen.CreateTwinWallet -> CreateTwinWalletFragment()
AppScreen.TwinsOnboarding -> TwinsOnboardingFragment()
}
}

View file

@ -13,6 +13,8 @@ import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
@ -28,6 +30,11 @@ fun Context.getDrawableCompat(@DrawableRes drawableResId: Int): Drawable? {
return ContextCompat.getDrawable(this, drawableResId)
}
@ColorInt
fun Fragment.getColor(@ColorRes colorRes: Int): Int {
return ContextCompat.getColor(requireContext(), colorRes)
}
fun View.getString(@StringRes id: Int): String {
return context.getString(id)
}
@ -57,22 +64,22 @@ fun View.makeInvisible() {
}
fun Context.dpToPixels(dp: Int): Int =
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), this.resources.displayMetrics
).toInt()
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), this.resources.displayMetrics
).toInt()
fun MaterialCardView.setMargins(
marginLeftDp: Int = 16,
marginTopDp: Int = 8,
marginRightDp: Int = 16,
marginBottomDp: Int = 8
marginLeftDp: Int = 16,
marginTopDp: Int = 8,
marginRightDp: Int = 16,
marginBottomDp: Int = 8
) {
val params = this.layoutParams
(params as ViewGroup.MarginLayoutParams).setMargins(
context.dpToPixels(marginLeftDp),
context.dpToPixels(marginTopDp),
context.dpToPixels(marginRightDp),
context.dpToPixels(marginBottomDp)
context.dpToPixels(marginLeftDp),
context.dpToPixels(marginTopDp),
context.dpToPixels(marginRightDp),
context.dpToPixels(marginBottomDp)
)
this.layoutParams = params
}
@ -82,31 +89,30 @@ fun Activity.setSystemBarTextColor(setTextDark: Boolean) {
val flags = this.window.decorView.systemUiVisibility
// Update the SystemUiVisibility dependening on whether we want a Light or Dark theme.
this.window.decorView.systemUiVisibility =
if (setTextDark) {
flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
} else {
flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
}
if (setTextDark) {
flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
} else {
flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
}
}
}
fun String.colorSegment(
context: Context,
color: Int,
startIndex: Int = 0,
endIndex: Int = this.length
context: Context,
color: Int,
startIndex: Int = 0,
endIndex: Int = this.length
): Spannable {
return this.toSpannable()
.also { spannable ->
spannable.setSpan(
ForegroundColorSpan(ContextCompat.getColor(context, color)),
startIndex,
endIndex,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
.also { spannable ->
spannable.setSpan(
ForegroundColorSpan(ContextCompat.getColor(context, color)),
startIndex,
endIndex,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
fun View.hideKeyboard() {
@ -122,7 +128,8 @@ fun Context.copyToClipboard(value: Any, label: String = "") {
}
fun Context.getFromClipboard(default: CharSequence? = null): CharSequence? {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager ?: return default
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
?: return default
val clipData = clipboard.primaryClip ?: return default
if (clipData.itemCount == 0) return default

View file

@ -9,4 +9,7 @@ data class NavigationState(
val activity: WeakReference<FragmentActivity>? = null
) : StateType
enum class AppScreen { Home, Wallet, Send, Details, DetailsConfirm, DetailsSecurity, Disclaimer }
enum class AppScreen {
Home, Wallet, Send, Details, DetailsConfirm, DetailsSecurity, Disclaimer,
CreateTwinWalletWarning, CreateTwinWallet, TwinsOnboarding
}

View file

@ -3,8 +3,8 @@ package com.tangem.tap.domain
import androidx.activity.ComponentActivity
import com.tangem.*
import com.tangem.commands.*
import com.tangem.commands.common.card.CardType
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.CardType
import com.tangem.common.extensions.calculateSha256
import com.tangem.tangem_sdk_new.extensions.init
import com.tangem.tap.common.analytics.AnalyticsEvent
@ -64,7 +64,7 @@ class TangemSdkManager(val activity: ComponentActivity) {
), cardId, initialMessage = Message(activity.getString(R.string.initial_message_tap_header)))
}
private suspend fun <T : CommandResponse> runTaskAsync(
suspend fun <T : CommandResponse> runTaskAsync(
runnable: CardSessionRunnable<T>, cardId: String? = null, initialMessage: Message? = null
): CompletionResult<T> =
withContext(Dispatchers.IO) {

View file

@ -3,7 +3,8 @@ package com.tangem.tap.domain
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.commands.CardStatus
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
@ -93,7 +94,18 @@ class TapWalletManager {
withContext(Dispatchers.Main) {
store.dispatch(WalletAction.ResetState)
store.dispatch(GlobalAction.SaveScanNoteResponse(data))
store.dispatch(AddressPayIdActionUi.ChangePayIdState(configManager?.config?.isWalletPayIdEnabled ?: false))
store.dispatch(AddressPayIdActionUi.ChangePayIdState(configManager?.config?.isWalletPayIdEnabled
?: false))
if (data.card.cardData?.productMask?.contains(Product.TwinCard) == true) {
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
))
}
}
loadData(data)
}
}
@ -114,7 +126,8 @@ class TapWalletManager {
store.dispatch(WalletAction.LoadArtwork(data.card, artworkId))
store.dispatch(WalletAction.LoadFiatRate)
store.dispatch(WalletAction.LoadPayId)
} else if (data.card.status == CardStatus.Empty) {
} else if (data.card.status == CardStatus.Empty ||
data.card.cardData?.productMask?.contains(Product.TwinCard) == true) {
store.dispatch(WalletAction.EmptyWallet)
store.dispatch(WalletAction.LoadArtwork(data.card, artworkId))
} else {

View file

@ -1,6 +1,6 @@
package com.tangem.tap.domain
import com.tangem.commands.Card
import com.tangem.commands.common.card.Card
import java.util.*
object TapWorkarounds {

View file

@ -11,6 +11,7 @@ data class Config(
val moonPayApiSecretKey: String = "sk_test_V8w4M19LbDjjYOt170s0tGuvXAgyEb1C",
val isWalletPayIdEnabled: Boolean = true,
val isTopUpEnabled: Boolean = false,
val isCreatingTwinCardsAllowed: Boolean = false
)
class ConfigManager(
@ -38,6 +39,7 @@ class ConfigManager(
when (name) {
isWalletPayIdEnabled -> config = config.copy(isWalletPayIdEnabled = false)
isTopUpEnabled -> config = config.copy(isTopUpEnabled = false)
isCreatingTwinCardsAllowed -> config = config.copy(isCreatingTwinCardsAllowed = false)
}
}
@ -45,6 +47,8 @@ class ConfigManager(
when (name) {
isWalletPayIdEnabled -> config = config.copy(isWalletPayIdEnabled = defaultConfig.isWalletPayIdEnabled)
isTopUpEnabled -> config = config.copy(isTopUpEnabled = defaultConfig.isTopUpEnabled)
isCreatingTwinCardsAllowed -> config =
config.copy(isCreatingTwinCardsAllowed = defaultConfig.isCreatingTwinCardsAllowed)
}
}
@ -60,6 +64,10 @@ class ConfigManager(
config = config.copy(isTopUpEnabled = newValue)
defaultConfig = defaultConfig.copy(isTopUpEnabled = newValue)
}
isCreatingTwinCardsAllowed -> {
config = config.copy(isCreatingTwinCardsAllowed = newValue)
defaultConfig = defaultConfig.copy(isCreatingTwinCardsAllowed = newValue)
}
}
}
@ -83,6 +91,7 @@ class ConfigManager(
companion object {
const val isWalletPayIdEnabled = "isWalletPayIdEnabled"
const val isTopUpEnabled = "useTopUp"
const val isCreatingTwinCardsAllowed = "isCreatingTwinCardsAllowed"
const val coinMarketCapKey = "coinMarketCapKey"
const val moonPayApiKey = "moonPayApiKey"
const val moonPayApiSecretKey = "moonPayApiSecretKey"

View file

@ -6,20 +6,28 @@ import com.tangem.TangemError
import com.tangem.TangemSdkError
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.commands.Card
import com.tangem.commands.CardStatus
import com.tangem.commands.CommandResponse
import com.tangem.commands.Product
import com.tangem.commands.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.tasks.ScanTask
data class ScanNoteResponse(
val walletManager: WalletManager?,
val card: Card,
val verifyResponse: VerifyCardResponse? = null
val verifyResponse: VerifyCardResponse? = null,
val secondTwinPublicKey: String? = null
) : CommandResponse
class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteResponse> {
@ -42,31 +50,74 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
return@run
}
if (card.cardData?.productMask?.contains(Product.TwinCard) == true) {
dealWithTwinCard(card, session, callback)
return@run
}
val walletManager = try {
WalletManagerFactory.makeWalletManager(card)
} catch (exception: Exception) {
return@run callback(CompletionResult.Success(ScanNoteResponse(null, card)))
}
VerifyCardCommand(true).run(session) { verifyResult ->
when (verifyResult) {
is CompletionResult.Success -> {
callback(CompletionResult.Success(ScanNoteResponse(
walletManager, card, verifyResult.data
)))
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(TangemSdkError.VerificationFailed()))
}
}
}
verifyCard(walletManager, card, null, session, callback)
}
}
}
}
private fun verifyCard(
walletManager: WalletManager?, card: Card, publicKey: String? = null,
session: CardSession, callback: (result: CompletionResult<ScanNoteResponse>) -> Unit
) {
VerifyCardCommand(true).run(session) { verifyResult ->
when (verifyResult) {
is CompletionResult.Success -> {
callback(CompletionResult.Success(ScanNoteResponse(
walletManager, card, verifyResult.data
)))
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(TangemSdkError.VerificationFailed()))
}
}
}
}
private fun dealWithTwinCard(
card: Card, session: CardSession,
callback: (result: CompletionResult<ScanNoteResponse>) -> Unit
) {
ReadFileDataCommand(readPrivateFiles = true).run(session) { filesResult ->
when (filesResult) {
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 walletManager = try {
WalletManagerFactory.makeMultisigWalletManager(card, publicKey!!.hexToBytes())
} catch (exception: Exception) {
callback(CompletionResult.Success(ScanNoteResponse(null, card)))
return@run
}
verifyCard(walletManager, card, publicKey, session, callback)
return@run
} else {
callback(CompletionResult.Success(ScanNoteResponse(null, card)))
}
}
is CompletionResult.Failure ->
callback(CompletionResult.Success(ScanNoteResponse(null, card)))
}
}
}
private fun getErrorIfExcludedCard(card: Card): TangemError? {
if (card.cardData?.productMask != null &&
card.cardData?.productMask?.contains(Product.Note) != true) {
val productMask = card.cardData?.productMask ?: return TangemSdkError.CardError()
if (!productMask.contains(Product.Note) && !productMask.contains(Product.TwinCard)) {
return TapSdkError.CardForDifferentApp
}
if (excludedBatches.contains(card.cardData?.batchId)) {

View file

@ -0,0 +1,41 @@
package com.tangem.tap.domain.twins
import com.tangem.CardSession
import com.tangem.CardSessionRunnable
import com.tangem.commands.CreateWalletResponse
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
override fun run(session: CardSession, callback: (result: CompletionResult<CreateWalletResponse>) -> Unit) {
if (session.environment.card?.walletPublicKey != null) {
PurgeWalletCommand().run(session) { response ->
when (response) {
is CompletionResult.Success -> {
session.environment.card = session.environment.card?.copy(status = CardStatus.Empty)
finishTask(session, callback)
}
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))
}
}
}
}

View file

@ -0,0 +1,50 @@
package com.tangem.tap.domain.twins
import com.tangem.CardSession
import com.tangem.CardSessionRunnable
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.tasks.CreateWalletTask
import com.tangem.tasks.file.DeleteFilesTask
class CreateSecondTwinWalletTask(private val firstPublicKey: String) : CardSessionRunnable<CreateWalletResponse> {
override val requiresPin2 = true
override fun run(session: CardSession, callback: (result: CompletionResult<CreateWalletResponse>) -> Unit) {
if (session.environment.card?.walletPublicKey != null) {
PurgeWalletCommand().run(session) { response ->
when (response) {
is CompletionResult.Success -> {
session.environment.card =
session.environment.card?.copy(status = CardStatus.Empty)
finishTask(session, callback)
}
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 -> {
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))
}
}
}
is CompletionResult.Failure ->
callback(CompletionResult.Failure(deleteResponse.error))
}
}
}
}

View file

@ -0,0 +1,83 @@
package com.tangem.tap.domain.twins
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.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.tangemSdkManager
class TwinCardsManager(private val scanNoteResponse: ScanNoteResponse) {
private val currentCardId: String = scanNoteResponse.card.cardId
private val secondCardId: String? = TwinsHelper.getTwinsCardId(currentCardId)
private var currentCardPublicKey: String? = null
private var secondCardPublicKey: String? = null
suspend fun createFirstWallet(message: Message): SimpleResult {
val response = tangemSdkManager.runTaskAsync(
CreateFirstTwinWalletTask(), currentCardId, message
)
when (response) {
is CompletionResult.Success -> {
currentCardPublicKey = response.data.walletPublicKey.toHexString()
return SimpleResult.Success
}
is CompletionResult.Failure -> return SimpleResult.failure(response.error)
}
}
suspend fun createSecondWallet(message: Message): SimpleResult {
val response = tangemSdkManager.runTaskAsync(
CreateSecondTwinWalletTask(currentCardPublicKey!!), secondCardId, message
)
when (response) {
is CompletionResult.Success -> {
secondCardPublicKey = response.data.walletPublicKey.toHexString()
return SimpleResult.Success
}
is CompletionResult.Failure -> return SimpleResult.failure(response.error)
}
}
suspend fun complete(message: Message): Result<ScanNoteResponse> {
val response = tangemSdkManager.runTaskAsync(
WriteFileDataCommand(createFileWithPublicKey(secondCardPublicKey!!)),
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.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)
}
}
}

View file

@ -0,0 +1,65 @@
package com.tangem.tap.domain
import com.tangem.tap.common.extensions.isEven
class TwinsHelper {
companion object {
const val TWIN_FILE_NAME = "TwinPublicKey"
fun getTwinCardNumber(cardId: String): TwinCardNumber? {
return when {
firstCardBatches.map { cardId.startsWith(it) }.contains(true) -> {
TwinCardNumber.First
}
secondCardBatches.map { cardId.startsWith(it) }.contains(true) -> {
TwinCardNumber.Second
}
else -> {
null
}
}
}
fun getTwinsCardId(cardId: String): String? {
val cardIdWithNewBatch = when (getTwinCardNumber(cardId) ?: return null) {
TwinCardNumber.First -> {
val batchIndex = if (cardId.startsWith(firstCardBatches[1])) 1 else 2
cardId.replace(firstCardBatches[batchIndex], secondCardBatches[batchIndex])
}
TwinCardNumber.Second -> {
val batchIndex = if (cardId.startsWith(secondCardBatches[1])) 1 else 2
cardId.replace(secondCardBatches[batchIndex], firstCardBatches[batchIndex])
}
}
val cardIdWithoutChecksum = cardIdWithNewBatch.dropLast(1)
val checkSum = cardIdWithoutChecksum.calculateLuhn()
return cardIdWithoutChecksum + checkSum
}
private val firstCardBatches = listOf("CB61", "CB64")
private val secondCardBatches = listOf("CB62", "CB65")
}
}
private fun String.calculateLuhn(): Int {
return 10 - this.reversed()
.mapIndexed { index, c ->
val digit = if (c in '0'..'9') c - '0' else c - 'A'
if (!index.isEven()) {
digit
} else {
val newDigit = digit * 2
if (newDigit >= 10) newDigit - 9 else newDigit
}
}
.sum() % 10
}
enum class TwinCardNumber(val number: Int) {
First(1), Second(2);
fun pairNumber(): TwinCardNumber = when (this) {
First -> Second
Second -> First
}
}

View file

@ -1,9 +1,13 @@
package com.tangem.tap.features.details.redux
import com.tangem.Message
import com.tangem.blockchain.common.Wallet
import com.tangem.commands.Card
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.features.details.redux.twins.CreateTwinWallet
import com.tangem.tap.network.coinmarketcap.FiatCurrency
import com.tangem.wallet.R
import org.rekotlin.Action
@ -12,10 +16,12 @@ sealed class DetailsAction : Action {
data class PrepareScreen(
val card: Card,
val scanNoteResponse: ScanNoteResponse,
val wallet: Wallet?,
val isCreatingTwinWalletAllowed: Boolean?,
val fiatCurrencyName: FiatCurrencyName,
val fiatCurrencies: List<FiatCurrencyName>? = null,
): DetailsAction()
) : DetailsAction()
object ShowDisclaimer : DetailsAction()
@ -23,24 +29,54 @@ sealed class DetailsAction : Action {
sealed class EraseWallet : DetailsAction() {
object Check : EraseWallet()
object Proceed : EraseWallet() {
object NotAllowedByCard: EraseWallet(), NotificationAction {
object NotAllowedByCard : EraseWallet(), NotificationAction {
override val messageResource = R.string.error_purge_prohibited
}
object NotEmpty: EraseWallet(), NotificationAction {
object NotEmpty : EraseWallet(), NotificationAction {
override val messageResource = R.string.details_notification_erase_wallet_not_possible
}
}
object Confirm : EraseWallet()
object Cancel : EraseWallet()
object Failure : EraseWallet()
object Success : EraseWallet()
}
sealed class CreateTwinWalletAction : DetailsAction() {
object ShowWarning : CreateTwinWalletAction()
object ShowAlert : CreateTwinWalletAction()
object HideAlert : CreateTwinWalletAction()
data class Proceed(
val twinCardNumber: TwinCardNumber?, val createTwinWallet: CreateTwinWallet
) : CreateTwinWalletAction()
object Cancel : CreateTwinWalletAction() {
object Confirm : CreateTwinWalletAction()
}
data class LaunchFirstStep(val message: Message) : CreateTwinWalletAction() {
object Success : CreateTwinWalletAction()
object Failure : CreateTwinWalletAction()
}
data class LaunchSecondStep(val message: Message) : CreateTwinWalletAction() {
object Success : CreateTwinWalletAction()
object Failure : CreateTwinWalletAction()
}
data class LaunchThirdStep(val message: Message) : CreateTwinWalletAction() {
data class Success(val scanNoteResponse: ScanNoteResponse) : CreateTwinWalletAction()
object Failure : CreateTwinWalletAction()
}
}
sealed class AppCurrencyAction : DetailsAction() {
data class SetCurrencies(val currencies: List<FiatCurrency>) : AppCurrencyAction()
object ChooseAppCurrency : AppCurrencyAction()
object Cancel: AppCurrencyAction()
data class SelectAppCurrency(val fiatCurrencyName: FiatCurrencyName): AppCurrencyAction()
object Cancel : AppCurrencyAction()
data class SelectAppCurrency(val fiatCurrencyName: FiatCurrencyName) : AppCurrencyAction()
}
sealed class ManageSecurity : DetailsAction() {
@ -50,8 +86,9 @@ sealed class DetailsAction : Action {
object Success : ManageSecurity()
object Failure : ManageSecurity()
}
data class ConfirmSelection(val option: SecurityOption) : ManageSecurity() {
object AlreadySet: ManageSecurity(), NotificationAction {
object AlreadySet : ManageSecurity(), NotificationAction {
override val messageResource = R.string.details_notification_security_option_already_active
}
}

View file

@ -6,6 +6,7 @@ import com.tangem.tap.common.redux.AppState
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.features.details.redux.twins.CreateTwinWalletMiddleware
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.coinmarketcap.CoinMarketCapService
@ -22,6 +23,7 @@ class DetailsMiddleware {
private val eraseWalletMiddleware = EraseWalletMiddleware()
private val appCurrencyMiddleware = AppCurrencyMiddleware()
private val manageSecurityMiddleware = ManageSecurityMiddleware()
private val twinWalletMiddleware = CreateTwinWalletMiddleware()
val detailsMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
@ -30,7 +32,8 @@ class DetailsMiddleware {
is DetailsAction.EraseWallet -> eraseWalletMiddleware.handle(action)
is DetailsAction.AppCurrencyAction -> appCurrencyMiddleware.handle(action)
is DetailsAction.ManageSecurity -> manageSecurityMiddleware.handle(action)
is DetailsAction.ShowDisclaimer -> {
is DetailsAction.CreateTwinWalletAction -> twinWalletMiddleware.handle(action)
is DetailsAction.ShowDisclaimer -> {
store.dispatch(DisclaimerAction.ShowAcceptedDisclaimer)
store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer))
}

View file

@ -1,10 +1,15 @@
package com.tangem.tap.features.details.redux
import com.tangem.commands.Card
import com.tangem.commands.Settings
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.features.details.redux.twins.CreateTwinWalletReducer
import com.tangem.tap.features.details.redux.twins.CreateTwinWalletState
import org.rekotlin.Action
import java.util.*
@ -31,6 +36,9 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
is DetailsAction.ManageSecurity -> {
handleSecurityAction(action, detailsState)
}
is DetailsAction.CreateTwinWalletAction -> {
CreateTwinWalletReducer.handle(action, detailsState)
}
else -> detailsState
}
}
@ -47,13 +55,25 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: Deta
SecurityOption.LongTap
}
}
val twinsState = if (action.card.cardData?.productMask?.contains(Product.TwinCard) == true) {
CreateTwinWalletState(
scanResponse = action.scanNoteResponse,
twinCardNumber = TwinsHelper.getTwinCardNumber(action.card.cardId),
createTwinWallet = null,
showAlert = false,
allowRecreatingWallet = action.isCreatingTwinWalletAllowed
)
} else {
null
}
return DetailsState(
card = action.card, wallet = action.wallet,
cardInfo = action.card.toCardInfo(),
appCurrencyState = AppCurrencyState(
action.fiatCurrencyName
),
securityScreenState = SecurityScreenState(currentOption = securityOption)
securityScreenState = SecurityScreenState(currentOption = securityOption),
createTwinWalletState = twinsState
)
}
@ -169,6 +189,7 @@ private fun handleSecurityAction(
}
}
private fun Card.toCardInfo(): CardInfo? {
val cardId = this.cardId.chunked(4).joinToString(separator = " ")
val issuer = this.cardData?.issuerName ?: return null

View file

@ -1,10 +1,11 @@
package com.tangem.tap.features.details.redux
import com.tangem.blockchain.common.Wallet
import com.tangem.commands.Card
import com.tangem.commands.common.card.Card
import com.tangem.tap.common.entities.Button
import com.tangem.tap.common.entities.TapCurrency.Companion.DEFAULT_FIAT_CURRENCY
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.features.details.redux.twins.CreateTwinWalletState
import com.tangem.tap.network.coinmarketcap.FiatCurrency
import org.rekotlin.StateType
import java.util.*
@ -17,6 +18,7 @@ data class DetailsState(
val eraseWalletState: EraseWalletState? = null,
val confirmScreenState: ConfirmScreenState? = null,
val securityScreenState: SecurityScreenState? = null,
val createTwinWalletState: CreateTwinWalletState? = null
) : StateType
data class CardInfo(
@ -39,4 +41,4 @@ data class AppCurrencyState(
val fiatCurrencyName: FiatCurrencyName = DEFAULT_FIAT_CURRENCY,
val showAppCurrencyDialog: Boolean = false,
val fiatCurrencies: List<FiatCurrency>? = null,
)
)

View file

@ -0,0 +1,104 @@
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.twins.TwinCardsManager
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
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.Proceed ->
store.dispatch(NavigationAction.NavigateTo(AppScreen.CreateTwinWallet))
is DetailsAction.CreateTwinWalletAction.Cancel -> {
val step = store.state.detailsState.createTwinWalletState?.step
if (step != null && step != CreateTwinWalletStep.FirstStep
) {
store.dispatch(DetailsAction.CreateTwinWalletAction.ShowAlert)
} else {
twinsManager = null
store.dispatch(NavigationAction.PopBackTo())
}
}
is DetailsAction.CreateTwinWalletAction.Cancel.Confirm -> {
twinsManager = null
store.dispatch(NavigationAction.PopBackTo())
}
is DetailsAction.CreateTwinWalletAction.LaunchFirstStep -> {
store.state.globalState.scanNoteResponse?.let {
twinsManager = TwinCardsManager(it)
}
scope.launch {
val result = twinsManager?.createFirstWallet(action.message)
withContext(Dispatchers.Main) {
when (result) {
SimpleResult.Success ->
store.dispatch(DetailsAction.CreateTwinWalletAction.LaunchFirstStep.Success)
is SimpleResult.Failure ->
store.dispatch(DetailsAction.CreateTwinWalletAction.LaunchFirstStep.Failure)
}
}
}
}
DetailsAction.CreateTwinWalletAction.LaunchFirstStep.Success -> {
}
DetailsAction.CreateTwinWalletAction.LaunchFirstStep.Failure -> {
}
is DetailsAction.CreateTwinWalletAction.LaunchSecondStep ->
scope.launch {
val result = twinsManager?.createSecondWallet(action.message)
withContext(Dispatchers.Main) {
when (result) {
SimpleResult.Success ->
store.dispatch(DetailsAction.CreateTwinWalletAction.LaunchSecondStep.Success)
is SimpleResult.Failure ->
store.dispatch(DetailsAction.CreateTwinWalletAction.LaunchSecondStep.Failure)
}
}
}
DetailsAction.CreateTwinWalletAction.LaunchSecondStep.Success -> {
}
DetailsAction.CreateTwinWalletAction.LaunchSecondStep.Failure -> {
}
is DetailsAction.CreateTwinWalletAction.LaunchThirdStep -> {
scope.launch {
val result = twinsManager?.complete(action.message)
withContext(Dispatchers.Main) {
when (result) {
is Result.Success ->
store.dispatch(
DetailsAction.CreateTwinWalletAction
.LaunchThirdStep.Success(result.data)
)
is Result.Failure ->
store.dispatch(DetailsAction.CreateTwinWalletAction.LaunchThirdStep.Failure)
}
}
}
}
is DetailsAction.CreateTwinWalletAction.LaunchThirdStep.Success -> {
store.dispatch(GlobalAction.SaveScanNoteResponse(action.scanNoteResponse))
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
}
DetailsAction.CreateTwinWalletAction.LaunchThirdStep.Failure -> {
}
}
}
}

View file

@ -0,0 +1,67 @@
package com.tangem.tap.features.details.redux.twins
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
class CreateTwinWalletReducer {
companion object {
fun handle(
action: DetailsAction.CreateTwinWalletAction, state: DetailsState
): DetailsState {
return when (action) {
DetailsAction.CreateTwinWalletAction.ShowWarning -> {
state
}
DetailsAction.CreateTwinWalletAction.ShowAlert -> {
state.copy(createTwinWalletState = state.createTwinWalletState?.copy(
showAlert = true
))
}
DetailsAction.CreateTwinWalletAction.HideAlert -> {
state.copy(createTwinWalletState = state.createTwinWalletState?.copy(
showAlert = false
))
}
is DetailsAction.CreateTwinWalletAction.Proceed -> {
state.copy(createTwinWalletState = CreateTwinWalletState(
scanResponse = null,
twinCardNumber = action.twinCardNumber
?: state.createTwinWalletState?.twinCardNumber,
createTwinWallet = action.createTwinWallet,
showAlert = false,
step = CreateTwinWalletStep.FirstStep,
))
}
DetailsAction.CreateTwinWalletAction.Cancel -> state
DetailsAction.CreateTwinWalletAction.Cancel.Confirm -> state
is DetailsAction.CreateTwinWalletAction.LaunchFirstStep -> state
DetailsAction.CreateTwinWalletAction.LaunchFirstStep.Success -> {
state.copy(createTwinWalletState = state.createTwinWalletState?.copy(
step = CreateTwinWalletStep.SecondStep
))
}
DetailsAction.CreateTwinWalletAction.LaunchFirstStep.Failure -> state
is DetailsAction.CreateTwinWalletAction.LaunchSecondStep -> state
DetailsAction.CreateTwinWalletAction.LaunchSecondStep.Success ->
state.copy(createTwinWalletState = state.createTwinWalletState?.copy(
step = CreateTwinWalletStep.ThirdStep
))
DetailsAction.CreateTwinWalletAction.LaunchSecondStep.Failure -> {
state
}
is DetailsAction.CreateTwinWalletAction.LaunchThirdStep -> {
state
}
is DetailsAction.CreateTwinWalletAction.LaunchThirdStep.Success -> {
state
}
DetailsAction.CreateTwinWalletAction.LaunchThirdStep.Failure -> {
state
}
}
}
}
}

View file

@ -0,0 +1,17 @@
package com.tangem.tap.features.details.redux.twins
import com.tangem.tap.domain.TwinCardNumber
import com.tangem.tap.domain.tasks.ScanNoteResponse
data class CreateTwinWalletState(
val scanResponse: ScanNoteResponse?,
val step: CreateTwinWalletStep = CreateTwinWalletStep.FirstStep,
val twinCardNumber: TwinCardNumber?,
val createTwinWallet: CreateTwinWallet?,
val showAlert: Boolean,
val allowRecreatingWallet: Boolean? = null
)
enum class CreateTwinWalletStep { FirstStep, SecondStep, ThirdStep }
enum class CreateTwinWallet { CreateWallet, RecreateWallet }

View file

@ -5,6 +5,8 @@ import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
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.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
@ -66,11 +68,26 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
tv_disclaimer.setOnClickListener { store.dispatch(DetailsAction.ShowDisclaimer) }
tv_erase_wallet.setOnClickListener {
store.dispatch(DetailsAction.EraseWallet.Check)
store.dispatch(DetailsAction.EraseWallet.Proceed)
if (state.createTwinWalletState != null) {
if (state.createTwinWalletState.allowRecreatingWallet == true) {
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)
}
} else {
tv_erase_wallet.hide()
}
} else {
tv_erase_wallet.show()
tv_erase_wallet.text = getText(R.string.details_row_title_erase_wallet)
tv_erase_wallet.setOnClickListener {
store.dispatch(DetailsAction.EraseWallet.Check)
store.dispatch(DetailsAction.EraseWallet.Proceed)
}
}
tv_app_currency.text = state.appCurrencyState.fiatCurrencyName
tv_app_currency_title.setOnClickListener {

View file

@ -0,0 +1,152 @@
package com.tangem.tap.features.details.ui.twins
import android.app.Dialog
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.core.content.ContextCompat.getColor
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.Message
import com.tangem.tap.domain.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.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_details_twin_cards.*
import org.rekotlin.StoreSubscriber
class CreateTwinWalletFragment : Fragment(R.layout.fragment_details_twin_cards),
StoreSubscriber<DetailsState> {
private var dialog: Dialog? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(DetailsAction.CreateTwinWalletAction.Cancel)
}
})
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
toolbar.setNavigationOnClickListener {
store.dispatch(DetailsAction.CreateTwinWalletAction.Cancel)
}
}
override fun newState(state: DetailsState) {
if (activity == null) return
toolbar.title = when (state.createTwinWalletState?.createTwinWallet) {
CreateTwinWallet.CreateWallet -> getText(R.string.wallet_button_create_wallet)
CreateTwinWallet.RecreateWallet, null -> getText(R.string.details_twins_recreate_toolbar)
}
val selectedColor = getColor(requireContext(), R.color.colorSecondary)
val defaultColor = getColor(requireContext(), R.color.blue_pale)
val twinCardNumber = state.createTwinWalletState?.twinCardNumber ?: TwinCardNumber.First
val cardNumber = when (state.createTwinWalletState?.step) {
CreateTwinWalletStep.FirstStep -> {
val twinCardNumberString = twinCardNumber.number.toString()
tv_step_number.text =
getString(R.string.details_twins_recreate_step_format, "1")
v_step_1.setBackgroundColor(selectedColor)
v_step_2.setBackgroundColor(defaultColor)
v_step_3.setBackgroundColor(defaultColor)
btn_tap.setOnClickListener {
store.dispatch(DetailsAction.CreateTwinWalletAction.LaunchFirstStep(
Message(getString(
R.string.details_twins_recreate_title_format, twinCardNumber)
)))
}
btn_tap.text = getString(R.string.details_twins_recreate_button_format,
twinCardNumber.number.toString())
twinCardNumberString
}
CreateTwinWalletStep.SecondStep -> {
val twinCardNumberString = twinCardNumber.pairNumber().number.toString()
tv_step_number.text =
getString(R.string.details_twins_recreate_step_format, "2")
v_step_1.setBackgroundColor(defaultColor)
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)
)
))
}
btn_tap.text = getString(R.string.details_twins_recreate_button_format, "2")
twinCardNumberString
}
CreateTwinWalletStep.ThirdStep -> {
val twinCardNumberString = twinCardNumber.number.toString()
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_3.setBackgroundColor(selectedColor)
btn_tap.setOnClickListener {
store.dispatch(DetailsAction.CreateTwinWalletAction.LaunchThirdStep(
Message(getString(
R.string.details_twins_recreate_title_format, twinCardNumberString)
)
))
}
btn_tap.text = getString(R.string.details_twins_recreate_button_format, twinCardNumberString)
twinCardNumberString
}
else -> null
}
btn_tap.text = getString(R.string.details_twins_recreate_button_format, cardNumber)
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()
} else {
dialog?.cancel()
dialog = null
}
}
}

View file

@ -0,0 +1,41 @@
package com.tangem.tap.features.details.ui.twins
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
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.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_details_twin_cards_warning.*
class TwinWalletWarningFragment : Fragment(R.layout.fragment_details_twin_cards_warning) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
}
})
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btn_cancel.setOnClickListener { store.dispatch(NavigationAction.PopBackTo()) }
btn_start.setOnClickListener {
store.dispatch(
DetailsAction.CreateTwinWalletAction.Proceed(
null, createTwinWallet = CreateTwinWallet.RecreateWallet
))
}
}
}

View file

@ -14,7 +14,16 @@ class DisclaimerMiddleware {
when (action) {
is DisclaimerAction.AcceptDisclaimer -> {
preferencesStorage.saveDisclaimerAccepted()
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
if (store.state.walletState.twinCardsState != null) {
val showOnboarding = !preferencesStorage.wasTwinsOnboardingShown()
if (showOnboarding) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.TwinsOnboarding))
} else {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}
} else {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}
}
}
next(action)

View file

@ -54,11 +54,18 @@ class HomeMiddleware {
}
private fun showDisclaimerOrNavigateToWallet() {
if (preferencesStorage.wasDisclaimerAccepted()) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
} else {
if (!preferencesStorage.wasDisclaimerAccepted()) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer))
return
}
if (store.state.walletState.twinCardsState != null) {
val showOnboarding = !preferencesStorage.wasTwinsOnboardingShown()
if (showOnboarding) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.TwinsOnboarding))
return
}
}
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}
companion object {

View file

@ -4,7 +4,7 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.blockchain.common.*
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.Signer
import com.tangem.commands.Card
import com.tangem.commands.common.card.Card
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.common.extensions.stripZeroPlainString

View file

@ -4,11 +4,12 @@ import android.content.Context
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.address.AddressType
import com.tangem.commands.Card
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.TwinCardNumber
import com.tangem.wallet.R
import org.rekotlin.Action
import java.math.BigDecimal
@ -93,5 +94,14 @@ sealed class WalletAction : Action {
data class TopUp(val context: Context, val toolbarColor: Int) : TopUpAction()
}
data class ChangeSelectedAddress(val type: AddressType): WalletAction()
data class ChangeSelectedAddress(val type: AddressType) : WalletAction()
sealed class TwinsAction : WalletAction() {
object ShowOnboarding : TwinsAction()
object SetOnboardingShown : TwinsAction()
data class SetTwinCard(
val secondCardId: String, val number: TwinCardNumber,
val isCreatingTwinCardsAllowed: Boolean
) : TwinsAction()
}
}

View file

@ -6,10 +6,10 @@ import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.ContextCompat
import com.tangem.blockchain.common.*
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.commands.Card
import com.tangem.commands.common.card.Card
import com.tangem.commands.common.card.CardType
import com.tangem.commands.common.network.Result
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.CardType
import com.tangem.common.extensions.getType
import com.tangem.common.extensions.toHexString
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
@ -20,7 +20,10 @@ 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.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.twins.CreateTwinWallet
import com.tangem.tap.features.send.redux.PrepareSendScreen
import com.tangem.tap.features.wallet.models.toPendingTransactions
import com.tangem.tap.network.NetworkConnectivity
@ -60,15 +63,25 @@ class WalletMiddleware {
}
}
is WalletAction.CreateWallet -> {
scope.launch {
val result = tangemSdkManager.createWallet(
store.state.globalState.scanNoteResponse?.card?.cardId
)
when (result) {
is CompletionResult.Success -> {
store.state.globalState.tapWalletManager.onCardScanned(result.data)
}
if (store.state.walletState.twinCardsState != null) {
store.dispatch(DetailsAction.CreateTwinWalletAction.Proceed(
store.state.globalState.scanNoteResponse?.card?.cardId?.let {
TwinsHelper.getTwinCardNumber(it)
},
CreateTwinWallet.CreateWallet
))
} else {
scope.launch {
val result = tangemSdkManager.createWallet(
store.state.globalState.scanNoteResponse?.card?.cardId
)
when (result) {
is CompletionResult.Success -> {
store.state.globalState.tapWalletManager
.onCardScanned(result.data)
}
}
}
}
}
@ -164,6 +177,13 @@ class WalletMiddleware {
val cardId = store.state.globalState.scanNoteResponse?.card?.cardId
cardId?.let { preferencesStorage.saveScannedCardId(it) }
}
is WalletAction.TwinsAction.SetTwinCard -> {
val showOnboarding = !preferencesStorage.wasTwinsOnboardingShown()
if (showOnboarding) store.dispatch(WalletAction.TwinsAction.ShowOnboarding)
}
is WalletAction.TwinsAction.SetOnboardingShown -> {
preferencesStorage.saveTwinsOnboardingShown()
}
}
next(action)
}

View file

@ -34,12 +34,17 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
when (action) {
is WalletAction.ResetState -> newState = WalletState()
is WalletAction.EmptyWallet -> newState = newState.copy(
state = ProgressState.Done,
currencyData = BalanceWidgetData(BalanceStatus.EmptyCard),
mainButton = WalletMainButton.CreateWalletButton(true),
topUpState = TopUpState(false)
)
is WalletAction.EmptyWallet -> {
val creatingWalletAllowed = !(newState.twinCardsState != null &&
newState.twinCardsState?.isCreatingTwinCardsAllowed != true)
newState = newState.copy(
state = ProgressState.Done,
currencyData = BalanceWidgetData(BalanceStatus.EmptyCard),
mainButton = WalletMainButton.CreateWalletButton(creatingWalletAllowed),
topUpState = TopUpState(false)
)
}
is WalletAction.LoadData.Failure -> {
when (action.error) {
is TapError.NoInternetConnection -> {
@ -214,10 +219,35 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
}
is WalletAction.ChangeSelectedAddress -> {
val walletAddresses = newState.walletAddresses ?: return newState
val address = walletAddresses.list.firstOrNull { it.type == action.type } ?: return newState
val address = walletAddresses.list.firstOrNull { it.type == action.type }
?: return newState
newState = newState.copy(walletAddresses = WalletAddresses(address, walletAddresses.list))
}
is WalletAction.TwinsAction.SetTwinCard -> {
newState = newState.copy(
twinCardsState = TwinCardsState(
secondCardId = action.secondCardId,
cardNumber = action.number,
showTwinOnboarding = newState.twinCardsState?.showTwinOnboarding
?: false,
isCreatingTwinCardsAllowed = action.isCreatingTwinCardsAllowed
)
)
}
is WalletAction.TwinsAction.ShowOnboarding -> {
newState = newState.copy(
twinCardsState = newState.twinCardsState?.copy(showTwinOnboarding = true)
?: TwinCardsState(null, null,
showTwinOnboarding = true,
isCreatingTwinCardsAllowed = false)
)
}
is WalletAction.TwinsAction.SetOnboardingShown -> {
newState = newState.copy(
twinCardsState = newState.twinCardsState?.copy(showTwinOnboarding = false)
)
}
}
return newState
}

View file

@ -7,6 +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.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import org.rekotlin.StateType
@ -24,7 +25,8 @@ data class WalletState(
val walletDialog: WalletDialog? = null,
val updatingWallet: Boolean = false,
val mainButton: WalletMainButton = WalletMainButton.SendButton(false),
val topUpState: TopUpState = TopUpState()
val topUpState: TopUpState = TopUpState(),
val twinCardsState: TwinCardsState? = null,
) : StateType {
val showDetails: Boolean =
currencyData.status != com.tangem.tap.features.wallet.ui.BalanceStatus.EmptyCard &&
@ -45,6 +47,7 @@ sealed class WalletDialog {
data class CreatePayIdDialog(val creatingPayIdState: CreatingPayIdState?) : WalletDialog()
data class SelectAmountToSendDialog(val amounts: List<Amount>?) : WalletDialog()
data class WarningDialog(val type: WarningType) : WalletDialog()
data class TwinsOnboardingFragment(val secondCardId: String): WalletDialog()
}
enum class WarningType { CardSignedHashesBefore, DevCard }
@ -97,4 +100,11 @@ data class TopUpState(
val allowed: Boolean = true,
val url: String? = null,
val redirectUrl: String? = null
)
data class TwinCardsState(
val secondCardId: String?,
val cardNumber: TwinCardNumber?,
val showTwinOnboarding: Boolean,
val isCreatingTwinCardsAllowed: Boolean
)

View file

@ -18,6 +18,7 @@ import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
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.features.details.redux.DetailsAction
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.ui.dialogs.AmountToSendDialog
@ -91,6 +92,20 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
override fun newState(state: WalletState) {
if (activity == null) return
state.twinCardsState?.cardNumber?.let { cardNumber ->
tv_twin_card_number.show()
iv_twin_card.show()
val number = when (cardNumber) {
TwinCardNumber.First -> "1"
TwinCardNumber.Second -> "2"
}
tv_twin_card_number.text = getString(R.string.wallet_twins_chip_format, number)
}
if (state.twinCardsState?.cardNumber == null) {
tv_twin_card_number.hide()
iv_twin_card.hide()
}
if (!state.showDetails) {
toolbar.menu.removeItem(R.id.details_menu)
} else if (toolbar.menu.findItem(R.id.details_menu) == null) {
@ -289,9 +304,11 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.details_menu -> {
store.state.globalState.scanNoteResponse?.card?.let { card ->
store.state.globalState.scanNoteResponse?.let { scanNoteResponse ->
store.dispatch(DetailsAction.PrepareScreen(
card, store.state.walletState.wallet,
scanNoteResponse.card, scanNoteResponse,
store.state.walletState.wallet,
store.state.globalState.configManager?.config?.isCreatingTwinCardsAllowed,
store.state.globalState.appCurrency
))
store.dispatch(NavigationAction.NavigateTo(AppScreen.Details))

View file

@ -0,0 +1,47 @@
package com.tangem.tap.features.wallet.ui.dialogs
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
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.*
class TwinsOnboardingFragment : Fragment(R.layout.fragment_twin_cards) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
}
})
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
store.dispatch(WalletAction.TwinsAction.SetOnboardingShown)
val secondCardId = store.state.walletState.twinCardsState?.secondCardId
val text = getString(R.string.twins_onboarding_description_format, secondCardId)
tv_twin_cards_description_1.text = text
setOnClickListeners()
}
private fun setOnClickListeners() {
btn_continue.setOnClickListener {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}
}
}

View file

@ -74,6 +74,14 @@ class PreferencesStorage(applicationContext: Application) {
return preferences.getBoolean(DISCLAIMER_ACCEPTED_KEY, false)
}
fun saveTwinsOnboardingShown() {
preferences.edit().putBoolean(TWINS_ONBOARDING_SHOWN_KEY, true).apply()
}
fun wasTwinsOnboardingShown(): Boolean {
return preferences.getBoolean(TWINS_ONBOARDING_SHOWN_KEY, false)
}
private fun incrementLaunchCounter() {
var count = preferences.getInt(APP_LAUNCH_COUNT_KEY, 0)
preferences.edit { putInt(APP_LAUNCH_COUNT_KEY, ++count) }
@ -83,9 +91,9 @@ class PreferencesStorage(applicationContext: Application) {
private const val PREFERENCES_NAME = "tapPrefs"
private const val APP_CURRENCY_KEY = "appCurrency"
private const val FIAT_CURRENCIES_KEY = "fiatCurrencies"
private const val FIRST_LAUNCH_CHECK_KEY = "firstLaunchCheck"
private const val SCANNED_CARDS_IDS_KEY = "scannedCardIds"
private const val DISCLAIMER_ACCEPTED_KEY = "disclaimerAccepted"
private const val TWINS_ONBOARDING_SHOWN_KEY = "twinsOnboardingShown"
private const val APP_LAUNCH_COUNT_KEY = "launchCount"
}

View file

@ -0,0 +1,26 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="375dp"
android:height="471dp"
android:viewportWidth="375"
android:viewportHeight="471">
<path
android:pathData="M-65.241,418.887l452.126,-172.218l26.339,66.399l-452.126,172.218z"
android:strokeAlpha="0.35"
android:fillColor="#D1D1D6"
android:fillAlpha="0.35"/>
<path
android:pathData="M-91.81,315l474.237,-180.641l39.37,99.249l-474.237,180.641z"
android:strokeAlpha="0.2"
android:fillColor="#636366"
android:fillAlpha="0.2"/>
<path
android:pathData="M-114,31.889l452.126,-172.218l70.731,178.307l-452.126,172.218z"
android:strokeAlpha="0.22"
android:fillColor="#1C1C1E"
android:fillAlpha="0.22"/>
<path
android:pathData="M-61.336,216.984l452.126,-172.218l29.793,75.107l-452.126,172.218z"
android:strokeAlpha="0.2"
android:fillColor="#3A3A3C"
android:fillAlpha="0.2"/>
</vector>

View file

@ -0,0 +1,25 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="375dp"
android:height="471dp"
android:viewportWidth="375"
android:viewportHeight="471">
<path
android:pathData="M-65.241,418.887l452.126,-172.218l26.339,66.399l-452.126,172.218z"
android:fillColor="@color/warning"
android:fillAlpha="0.25"/>
<path
android:pathData="M-91.81,315l474.237,-180.641l39.37,99.249l-474.237,180.641z"
android:strokeAlpha="0.2"
android:fillColor="@color/warning"
android:fillAlpha="0.35"/>
<path
android:pathData="M-114,31.889l452.126,-172.218l70.731,178.307l-452.126,172.218z"
android:strokeAlpha="0.22"
android:fillColor="@color/warning"
android:fillAlpha="0.55"/>
<path
android:pathData="M-61.336,216.984l452.126,-172.218l29.793,75.107l-452.126,172.218z"
android:strokeAlpha="0.2"
android:fillColor="@color/warning"
android:fillAlpha="0.45"/>
</vector>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"
>
<size
android:width="484dp"
android:height="192dp" />
<gradient
android:endColor="@color/darkGray6"
android:startColor="@color/darkGray6" />
</shape>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape android:shape="rectangle"
xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/twins_dark" />
<corners
android:bottomLeftRadius="30dp"
android:bottomRightRadius="30dp"
android:topLeftRadius="30dp"
android:topRightRadius="30dp" />
</shape>

View file

@ -0,0 +1,160 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/coordinator_details_confirm"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/backgroundLightGray"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
style="@style/Widget.MaterialComponents.Toolbar.Surface"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/backgroundLightGray"
android:fitsSystemWindows="true"
app:liftOnScroll="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationIcon="@drawable/ic_baseline_arrow_back_24"
app:title="Re-create wallet" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_details_confirm"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="16dp"
android:layout_marginBottom="33dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<TextView
android:id="@+id/tv_step_number"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
android:text="@string/details_twins_recreate_step_format"
android:textColor="@color/colorSecondary"
android:textSize="20sp"
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
android:id="@+id/v_step_1"
android:layout_width="0dp"
android:layout_height="3dp"
android:layout_marginStart="24dp"
android:layout_marginTop="14dp"
android:layout_marginEnd="4dp"
android:background="@color/colorSecondary"
app:layout_constraintEnd_toStartOf="@+id/v_step_2"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_step_number" />
<View
android:id="@+id/v_step_2"
android:layout_width="0dp"
android:layout_height="3dp"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:background="@color/colorSecondary"
app:layout_constraintEnd_toStartOf="@+id/v_step_3"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/v_step_1"
app:layout_constraintTop_toTopOf="@id/v_step_1" />
<View
android:id="@+id/v_step_3"
android:layout_width="0dp"
android:layout_height="3dp"
android:layout_marginStart="4dp"
android:layout_marginEnd="24dp"
android:background="@color/colorSecondary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/v_step_2"
app:layout_constraintTop_toTopOf="@id/v_step_1" />
<TextView
android:id="@+id/tv_twin_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:layout_marginBottom="8dp"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:text="@string/details_twins_recreate_title_format"
android:textColor="@color/darkGray6"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/v_step_1" />
<TextView
android:id="@+id/tv_twin_cards_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:text="@string/details_twins_recreate_subtitle"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_twin_title" />
<ImageView
android:id="@+id/iv_card_1"
android:layout_width="130dp"
android:layout_height="70dp"
android:layout_marginStart="47dp"
android:layout_marginTop="16dp"
android:src="@drawable/card_placeholder"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_twin_cards_description" />
<ImageView
android:id="@+id/iv_card_2"
android:layout_width="130dp"
android:layout_height="70dp"
android:layout_marginStart="24dp"
android:layout_marginTop="20dp"
android:src="@drawable/card_placeholder"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/iv_card_1" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintGuide_percent="0.37" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_tap"
style="@style/TapBlackButton"
android:layout_width="0dp"
android:layout_marginStart="7dp"
android:layout_marginTop="30dp"
android:layout_marginEnd="16dp"
app:icon="@drawable/ic_scan"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/guideline"
tools:text="@string/details_twins_recreate_button_format" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/coordinator_details_confirm"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/backgroundLightGray"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_details_confirm"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="33dp">
<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"
app:layout_constraintBottom_toTopOf="@id/tv_twin_title"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0" />
<TextView
android:id="@+id/tv_twin_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:drawablePadding="4dp"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:text="@string/common_warning"
android:textColor="@color/darkGray6"
android:textSize="20sp"
android:textStyle="bold"
app:drawableStartCompat="@drawable/ic_warning"
app:drawableTint="@color/darkGray6"
app:layout_constraintBottom_toTopOf="@id/tv_twin_cards_description"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/tv_twin_cards_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:text="@string/details_twins_recreate_warning"
android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@id/btn_start"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_cancel"
style="@style/TapBlackButton"
android:layout_width="0dp"
android:layout_marginStart="16dp"
android:layout_marginTop="30dp"
android:gravity="center"
android:text="@string/common_back"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintVertical_bias="1" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintGuide_percent="0.37" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_start"
style="@style/TapButtonWithIcon"
android:layout_width="0dp"
android:layout_marginStart="7dp"
android:layout_marginTop="30dp"
android:layout_marginEnd="16dp"
android:text="@string/common_start"
app:icon="@drawable/ic_send"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/guideline" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -28,7 +28,6 @@
android:id="@+id/cl_details_confirm"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="33dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
@ -36,7 +35,7 @@
android:id="@+id/sv_wallet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layout_marginBottom="8dp"
android:fillViewport="true"
android:overScrollMode="never"
app:layout_constraintBottom_toTopOf="@id/btn_accept">
@ -67,7 +66,6 @@
android:id="@+id/btn_accept"
style="@style/TapButtonWithIcon"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_marginStart="7dp"
android:layout_marginTop="30dp"
android:layout_marginEnd="16dp"

View file

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/backgroundLightGray"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_details_confirm"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="33dp">
<include
android:id="@+id/l_twin_cards"
layout="@layout/layout_twin_cards"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="-40dp"
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
android:id="@+id/tv_twin_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24sp"
android:layout_marginBottom="8dp"
android:text="@string/twins_onboarding_title"
android:textColor="@color/darkGray6"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@id/tv_twin_subtitle"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/tv_twin_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginBottom="16dp"
android:text="@string/twins_onboarding_subtitle"
android:textColor="@color/darkGray6"
android:textSize="17sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@id/tv_twin_cards_description_1"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/tv_twin_cards_description_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:text="@string/twins_onboarding_description_format"
android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@id/btn_continue"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintGuide_percent="0.37" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_continue"
style="@style/TapBlackButton"
android:layout_width="0dp"
android:layout_marginStart="7dp"
android:layout_marginTop="30dp"
android:layout_marginEnd="16dp"
android:text="@string/common_continue"
app:icon="@drawable/ic_send"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/guideline" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/coordinator_wallet"
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -61,6 +62,40 @@
android:src="@drawable/card_placeholder"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/iv_twin_card"
android:layout_width="100dp"
android:layout_height="30dp"
android:src="@drawable/shape_chip"
android:elevation="3dp"
android:layout_marginStart="25dp"
app:layout_constraintStart_toStartOf="@id/iv_card"
app:layout_constraintTop_toBottomOf="@id/iv_card"
app:layout_constraintBottom_toBottomOf="@id/iv_card"/>
<TextView
android:id="@+id/tv_twin_card_number"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="@id/iv_twin_card"
app:layout_constraintEnd_toEndOf="@id/iv_twin_card"
app:layout_constraintTop_toTopOf="@id/iv_twin_card"
app:layout_constraintBottom_toBottomOf="@id/iv_twin_card"
tools:text="Card 2 of 2"
android:elevation="3dp"
android:gravity="center"
android:textColor="@color/buttonGray"
android:textStyle="bold"
android:textSize="14sp"/>
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier"
app:barrierDirection="bottom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="iv_card,iv_twin_card"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_pending_transaction"
android:layout_width="match_parent"
@ -68,7 +103,7 @@
android:layout_marginTop="12dp"
android:nestedScrollingEnabled="false"
android:overScrollMode="never"
app:layout_constraintTop_toBottomOf="@id/iv_card" />
app:layout_constraintTop_toBottomOf="@id/barrier" />
<include
android:id="@+id/l_card_balance"

View file

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="-40dp">
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="40dp"
android:layout_marginTop="60dp"
android:scaleType="fitXY"
android:src="@drawable/ic_group_1258"
app:layout_constraintBottom_toBottomOf="@id/iv_twin_card_2"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/iv_twin_card_1"
android:layout_width="wrap_content"
android:layout_height="170dp"
android:layout_marginTop="50dp"
android:rotation="-19.5"
android:src="@drawable/card_placeholder"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/iv_twin_card_2"
android:layout_width="wrap_content"
android:layout_height="170dp"
android:layout_marginTop="20dp"
android:rotation="-19.5"
android:src="@drawable/card_placeholder"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/iv_twin_card_1" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="-40dp">
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="40dp"
android:layout_marginTop="60dp"
android:scaleType="fitXY"
android:src="@drawable/ic_twin_cards_background_warning"
app:layout_constraintBottom_toBottomOf="@id/iv_twin_card_2"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/iv_twin_card_1"
android:layout_width="wrap_content"
android:layout_height="170dp"
android:layout_marginTop="50dp"
android:rotation="-19.5"
android:src="@drawable/card_placeholder"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/iv_twin_card_2"
android:layout_width="wrap_content"
android:layout_height="170dp"
android:layout_marginTop="20dp"
android:rotation="-19.5"
android:src="@drawable/card_placeholder"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/iv_twin_card_1" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -26,6 +26,8 @@
<color name="buttonGray">#F4F5F6</color>
<color name="textGray">#DE000000</color>
<color name="twins_dark">#14181D</color>
<color name="lightGray1">#F8F8FB</color>
<color name="lightGray4">#C7C7CC</color>
@ -39,5 +41,7 @@
<color name="blue1">#0029FF</color>
<color name="blue2">#1F50FF</color>
<color name="blue3">#CBE4FF</color>
<color name="blue_pale">#E0E6FA</color>
</resources>

View file

@ -204,8 +204,4 @@
<string name="xtz_withdrawal_message_warning">To avoid paying an increased commission the next time you top up your wallet, reduce the amount by %s XTZ</string>
<string name="xtz_withdrawal_message_reduce">Reduce by %s XTZ</string>
<string name="xtz_withdrawal_message_ignore">No, send all</string>
</resources>

View file

@ -3,4 +3,28 @@
<string name="wallet_address_chip_default" translatable="false">Default</string>
<string name="wallet_address_chip_compatibility" translatable="false">Compatibility</string>
<string name="wallet_address_chip_legacy" translatable="false">Legacy</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
and the other one with number %s.\n\nBoth cards can be used to extract funds from
this wallet.
</string>
<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_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_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>
<string name="notification_twins_recreate_success" translatable="false">The twin address was successfully created</string>
<string name="common_start" translatable="false">Start</string>
<string name="common_back" translatable="false">Back</string>
</resources>

View file

@ -1,4 +1,4 @@
ext.versions = [
kotlin : '1.4.0',
build_gradle: '4.1.0',
kotlin : '1.4.10',
build_gradle: '4.1.1',
]