diff --git a/app/build.gradle b/app/build.gradle index c8e2711c22..ff53e7b789 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -77,9 +77,9 @@ dependencies { implementation 'com.google.android.play:core-ktx:1.8.1' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' - implementation 'com.tangem:blockchain:develop-51' - implementation 'com.tangem.tangem-sdk-kotlin:core:develop-106' - implementation 'com.tangem.tangem-sdk-kotlin:android:develop-106' + implementation 'com.tangem:blockchain:develop-52' + implementation 'com.tangem.tangem-sdk-kotlin:core:develop-115' + implementation 'com.tangem.tangem-sdk-kotlin:android:develop-115' // WebView implementation "androidx.browser:browser:1.3.0" diff --git a/app/src/main/assets/bsc_tokens.json b/app/src/main/assets/bsc_tokens.json index 712d35c6dd..9859e13f27 100644 --- a/app/src/main/assets/bsc_tokens.json +++ b/app/src/main/assets/bsc_tokens.json @@ -778,5 +778,16 @@ "curve" : "secp256k1", "testnet" : false } + }, + { + "decimalCount" : 18, + "symbol" : "VOYRME", + "name" : "VOYR", + "contractAddress" : "0x33a887Cf76383be39DC51786e8f641c9D6665D84", + "blockchain" : { + "key" : "bsc", + "curve" : "secp256k1", + "testnet" : false + } } ] diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Picasso.kt b/app/src/main/java/com/tangem/tap/common/extensions/Picasso.kt index be71883ecc..60c8a1ec9d 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Picasso.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Picasso.kt @@ -32,6 +32,9 @@ fun Picasso.loadCurrenciesIcon( token?.symbol == QCX -> { this.load(R.drawable.ic_qcx)?.into(imageView) } + token?.symbol == VOYR -> { + this.load(R.drawable.ic_voyr)?.into(imageView) + } url != null -> { if (token != null) { setTokenImage(imageView, textView, token) @@ -60,6 +63,7 @@ fun Picasso.loadCurrenciesIcon( } private const val QCX = "QCX" +private const val VOYR = "VOYRME" private fun setOfflineCurrencyImage( imageView: ImageView, diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt index 258131199b..b16fbe21c7 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt @@ -94,8 +94,8 @@ private val globalMiddlewareHandler: Middleware = { dispatch, appState action.shouldDeriveWC, action.messageResId ) - store.dispatch(GlobalAction.ScanFailsCounter.ChooseBehavior(result)) withMainContext { + store.dispatch(GlobalAction.ScanFailsCounter.ChooseBehavior(result)) when (result) { is CompletionResult.Success -> { tangemSdkManager.changeDisplayedCardIdNumbersCount(result.data) diff --git a/app/src/main/java/com/tangem/tap/domain/ProductType.kt b/app/src/main/java/com/tangem/tap/domain/ProductType.kt index 07cbea8905..4f57e8949d 100644 --- a/app/src/main/java/com/tangem/tap/domain/ProductType.kt +++ b/app/src/main/java/com/tangem/tap/domain/ProductType.kt @@ -4,5 +4,6 @@ package com.tangem.tap.domain [REDACTED_AUTHOR] */ enum class ProductType { - Note, Wallet, Twins, Other + Note, Twins, Wallet + } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt index 7131feb7b7..4c3f0c78d4 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -1,6 +1,7 @@ package com.tangem.tap.domain import CreateProductWalletTask +import CreateProductWalletTaskResponse import android.content.Context import com.tangem.Message import com.tangem.TangemSdk @@ -9,20 +10,22 @@ import com.tangem.common.CompletionResult import com.tangem.common.SuccessResponse import com.tangem.common.card.Card import com.tangem.common.card.FirmwareVersion +import com.tangem.common.core.CardIdDisplayFormat import com.tangem.common.core.CardSessionRunnable import com.tangem.common.core.Config import com.tangem.common.core.TangemSdkError import com.tangem.common.hdWallet.DerivationPath import com.tangem.operations.CommandResponse -import com.tangem.operations.derivation.DeriveWalletPublicKeysTask -import com.tangem.operations.derivation.ExtendedPublicKeyList import com.tangem.operations.pins.CheckUserCodesCommand import com.tangem.operations.pins.CheckUserCodesResponse import com.tangem.operations.pins.SetUserCodeCommand import com.tangem.tap.common.analytics.AnalyticsEvent import com.tangem.tap.common.analytics.AnalyticsHandler import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler +import com.tangem.tap.common.extensions.ByteArrayKey import com.tangem.tap.domain.tasks.CreateWalletAndRescanTask +import com.tangem.tap.domain.tasks.DerivationTask +import com.tangem.tap.domain.tasks.DerivationTaskResponse import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask import com.tangem.tap.domain.tasks.product.ScanProductTask import com.tangem.tap.domain.tasks.product.ScanResponse @@ -44,11 +47,13 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co analyticsHandler.triggerEvent(AnalyticsEvent.READY_TO_SCAN, null) val message = Message(context.getString(messageRes ?: R.string.initial_message_scan_header)) - return runTaskAsyncReturnOnMain(ScanProductTask(null, currenciesRepository, shouldDeriveWC), null, message) + return runTaskAsyncReturnOnMain(ScanProductTask(null, currenciesRepository), null, message) .also { sendScanFailuresToAnalytics(analyticsHandler, it) } } - suspend fun createProductWallet(scanResponse: ScanResponse): CompletionResult { + suspend fun createProductWallet( + scanResponse: ScanResponse + ): CompletionResult { return runTaskAsync( CreateProductWalletTask(scanResponse.productType), scanResponse.card.cardId, @@ -74,10 +79,9 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co suspend fun derivePublicKeys( cardId: String, - walletPublicKey: ByteArray, - derivationPaths: List - ): CompletionResult { - return runTaskAsyncReturnOnMain(DeriveWalletPublicKeysTask(walletPublicKey, derivationPaths), cardId) + derivations: Map> + ): CompletionResult { + return runTaskAsyncReturnOnMain(DerivationTask(derivations), cardId) } suspend fun resetToFactorySettings(card: Card): CompletionResult { @@ -134,7 +138,11 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co } fun changeDisplayedCardIdNumbersCount(scanResponse: ScanResponse) { - tangemSdk.config.cardIdDisplayedNumbersCount = if (scanResponse.isTangemTwins()) 4 else null + tangemSdk.config.cardIdDisplayFormat = if (scanResponse.isTangemTwins()) { + CardIdDisplayFormat.LastLunh(4) + } else { + CardIdDisplayFormat.Full + } } companion object { diff --git a/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt b/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt index 45e19eb81a..ffc8df824c 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt @@ -35,8 +35,7 @@ object TapWorkarounds { @Deprecated("Use ScanResponse.isTangemNote") fun isTangemNote(card: Card): Boolean = tangemNoteBatches.contains(card.batchId) - @Deprecated("Use ScanResponse.isTangemWallet") - fun isTangemWallet(card: Card): Boolean = tangemWalletBatches.contains(card.batchId) + fun isTangemWalletBatch(card: Card): Boolean = tangemWalletBatches.contains(card.batchId) fun getTangemNoteBlockchain(card: Card): Blockchain? = tangemNoteBatches[card.batchId] diff --git a/app/src/main/java/com/tangem/tap/domain/extensions/Blockchain.kt b/app/src/main/java/com/tangem/tap/domain/extensions/Blockchain.kt index 5ec3ea7672..0563eb8482 100644 --- a/app/src/main/java/com/tangem/tap/domain/extensions/Blockchain.kt +++ b/app/src/main/java/com/tangem/tap/domain/extensions/Blockchain.kt @@ -2,6 +2,7 @@ package com.tangem.tap.domain.extensions import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token +import com.tangem.common.card.EllipticCurve import java.math.BigDecimal @@ -25,4 +26,18 @@ fun Blockchain.minimalAmount(): BigDecimal { return 1.toBigDecimal().movePointLeft(decimals()) } +fun Blockchain.getPrimaryCurve(): EllipticCurve? { + return when { + getSupportedCurves().contains(EllipticCurve.Secp256k1) -> { + EllipticCurve.Secp256k1 + } + getSupportedCurves().contains(EllipticCurve.Ed25519) -> { + EllipticCurve.Ed25519 + } + else -> { + null + } + } +} + private const val NODL = "NODL" \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/DerivationTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/DerivationTask.kt new file mode 100644 index 0000000000..8f03e068b5 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/tasks/DerivationTask.kt @@ -0,0 +1,50 @@ +package com.tangem.tap.domain.tasks + +import com.tangem.common.CompletionResult +import com.tangem.common.core.CardSession +import com.tangem.common.core.CardSessionRunnable +import com.tangem.common.core.CompletionCallback +import com.tangem.common.hdWallet.DerivationPath +import com.tangem.operations.CommandResponse +import com.tangem.operations.derivation.DeriveWalletPublicKeysTask +import com.tangem.operations.derivation.ExtendedPublicKeyList +import com.tangem.tap.common.extensions.ByteArrayKey + +class DerivationTaskResponse( + val entries: Map +): CommandResponse + +class DerivationTask( + private val derivations: Map> +) : CardSessionRunnable { + + val response: MutableMap = mutableMapOf() + + override fun run(session: CardSession, callback: CompletionCallback) { + derive(keys = derivations.keys.toList(), index = 0, session = session, callback = callback) + } + + private fun derive( + keys: List, + index: Int, + session: CardSession, + callback: CompletionCallback + ) { + if (index == keys.count()) { + callback(CompletionResult.Success(DerivationTaskResponse(response.toMap()))) + return + } + + val key = keys[index] + val paths = derivations[key]!! + DeriveWalletPublicKeysTask(key.bytes, paths).run(session) { result -> + when (result) { + is CompletionResult.Success -> { + response[key] = result.data + derive(keys = keys, index = index + 1, session = session, callback = callback) + } + is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error)) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt index 530b53ac4c..89bdb12dd2 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt @@ -1,33 +1,41 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.common.CompletionResult import com.tangem.common.card.Card -import com.tangem.common.card.CardWallet import com.tangem.common.card.EllipticCurve -import com.tangem.common.card.FirmwareVersion import com.tangem.common.core.CardSession import com.tangem.common.core.CardSessionRunnable import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.guard -import com.tangem.operations.derivation.DeriveWalletPublicKeysTask -import com.tangem.operations.derivation.ExtendedPublicKeyList +import com.tangem.common.hdWallet.ExtendedPublicKey +import com.tangem.operations.CommandResponse +import com.tangem.operations.backup.PrimaryCard +import com.tangem.operations.backup.StartPrimaryCardLinkingTask import com.tangem.operations.wallet.CreateWalletResponse import com.tangem.operations.wallet.CreateWalletTask import com.tangem.tap.common.extensions.toMapKey import com.tangem.tap.domain.ProductType import com.tangem.tap.domain.TapWorkarounds.getTangemNoteBlockchain +import com.tangem.tap.domain.tasks.DerivationTask import com.tangem.tap.domain.tasks.product.CreateWalletsTask +import com.tangem.tap.domain.tasks.product.KeyWalletPublicKey import com.tangem.tap.domain.tasks.product.ProductCommandProcessor import com.tangem.tap.domain.tasks.product.getCurvesForNonCreatedWallets -import com.tangem.tap.store -/** -[REDACTED_AUTHOR] - */ + +data class CreateProductWalletTaskResponse( + val card: Card, + val derivedKeys: Map> = mapOf(), + val primaryCard: PrimaryCard? = null +) : CommandResponse + class CreateProductWalletTask( private val type: ProductType, -) : CardSessionRunnable { +) : CardSessionRunnable { - override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { + override fun run( + session: CardSession, + callback: (result: CompletionResult) -> Unit + ) { val card = session.environment.card.guard { callback(CompletionResult.Failure(TangemSdkError.CardError())) return @@ -36,12 +44,19 @@ class CreateProductWalletTask( val commandProcessor = when (type) { ProductType.Note -> CreateWalletTangemNote() ProductType.Twins -> throw UnsupportedOperationException("Use the TwinCardsManager to create a wallet") - ProductType.Wallet -> CreateWalletTangemWallet() - else -> CreateWalletOtherCards() + else -> CreateWalletTangemWallet() } commandProcessor.proceed(card, session) { when (it) { - is CompletionResult.Success -> callback(CompletionResult.Success(session.environment.card!!)) + is CompletionResult.Success -> { + val result = when (commandProcessor) { + is CreateWalletTangemWallet -> { + it.data as CreateProductWalletTaskResponse + } + else -> CreateProductWalletTaskResponse(card = session.environment.card!!) + } + callback(CompletionResult.Success(result)) + } is CompletionResult.Failure -> callback(CompletionResult.Failure(it.error)) } } @@ -79,65 +94,98 @@ private class CreateWalletTangemNote : ProductCommandProcessor { +private class CreateWalletTangemWallet : ProductCommandProcessor { + + private var primaryCard: PrimaryCard? = null + private var createWalletResponse: CreateWalletResponse? = null override fun proceed( card: Card, session: CardSession, - callback: (result: CompletionResult) -> Unit, + callback: (result: CompletionResult) -> Unit, ) { - val supportedCurves = setOf(EllipticCurve.Secp256k1, EllipticCurve.Ed25519) - val curves = card.getCurvesForNonCreatedWallets().intersect(supportedCurves).toList() + val curves = card.getCurvesForNonCreatedWallets() CreateWalletsTask(curves).run(session) { result -> when (result) { is CompletionResult.Success -> { - val response = result.data.createWalletResponses[0] - val derivationPaths = listOf(Blockchain.Bitcoin, Blockchain.Ethereum) - .mapNotNull { it.derivationPath() } - - DeriveWalletPublicKeysTask(response.wallet.publicKey, derivationPaths).run(session) { - when (it) { - is CompletionResult.Success -> { - updateDerivedKeys(response.wallet, it.data) - callback(CompletionResult.Success(response)) - } - is CompletionResult.Failure -> callback(CompletionResult.Failure(it.error)) + createWalletResponse = result.data.createWalletResponses[0] + when { + card.settings.isBackupAllowed -> { + linkPrimaryCard(session, callback) + } + card.settings.isHDWalletAllowed -> { + deriveKeys(session, callback) + } + else -> { + callback( + CompletionResult.Success( + CreateProductWalletTaskResponse(card = session.environment.card!!) + ) + ) } } + } is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error)) } } } -} -//TODO: updating derived wallet public keys - make it better later -private fun updateDerivedKeys(wallet: CardWallet, derivedKeys: ExtendedPublicKeyList) { - val onboardingManager = store.state.globalState.onboardingState.onboardingManager ?: return - onboardingManager.scanResponse = onboardingManager.scanResponse.copy( - derivedKeys = mapOf(wallet.publicKey.toMapKey() to derivedKeys) - ) -} - -private class CreateWalletOtherCards : ProductCommandProcessor { - - override fun proceed( - card: Card, + private fun linkPrimaryCard( session: CardSession, - callback: (result: CompletionResult) -> Unit, + callback: (result: CompletionResult) -> Unit, ) { - val firmwareVersion = card.firmwareVersion - val task = if (firmwareVersion < FirmwareVersion.MultiWalletAvailable) { - CreateWalletsTask(listOf(card.supportedCurves.first())) - } else { - CreateWalletsTask(card.getCurvesForNonCreatedWallets()) - } - - task.run(session) { result -> + StartPrimaryCardLinkingTask().run(session) { result -> when (result) { - is CompletionResult.Success -> callback(CompletionResult.Success(result.data.createWalletResponses[0])) - is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error)) + is CompletionResult.Success -> { + primaryCard = result.data + deriveKeys(session, callback) + } + is CompletionResult.Failure -> { + callback(CompletionResult.Failure(result.error)) + } } } } + + private fun deriveKeys( + session: CardSession, + callback: (result: CompletionResult) -> Unit, + ) { + val derivationPaths = listOf(Blockchain.Bitcoin, Blockchain.Ethereum) + .mapNotNull { it.derivationPath() } + val response = createWalletResponse.guard { + callback(CompletionResult.Failure(TangemSdkError.UnknownError())) + return + } + + if (derivationPaths.isNullOrEmpty()) { + callback( + CompletionResult.Success( + CreateProductWalletTaskResponse( + card = session.environment.card!!, primaryCard = primaryCard + ) + ) + ) + return + } + + DerivationTask(mapOf(response.wallet.publicKey.toMapKey() to derivationPaths)) + .run(session) { result -> + when (result) { + is CompletionResult.Success -> { + callback( + CompletionResult.Success( + CreateProductWalletTaskResponse( + card = session.environment.card!!, + derivedKeys = result.data.entries, + primaryCard = primaryCard + ) + ) + ) + } + is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error)) + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt index 4aa99403d2..506f5aa9c3 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt @@ -20,7 +20,8 @@ import com.tangem.operations.CommandResponse import com.tangem.operations.PreflightReadMode import com.tangem.operations.PreflightReadTask import com.tangem.operations.ScanTask -import com.tangem.operations.derivation.DeriveWalletPublicKeysTask +import com.tangem.operations.backup.PrimaryCard +import com.tangem.operations.backup.StartPrimaryCardLinkingTask import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand import com.tangem.tap.common.extensions.ByteArrayKey import com.tangem.tap.common.extensions.toMapKey @@ -30,20 +31,25 @@ import com.tangem.tap.domain.TapWorkarounds import com.tangem.tap.domain.TapWorkarounds.getTangemNoteBlockchain import com.tangem.tap.domain.TapWorkarounds.isExcluded import com.tangem.tap.domain.TapWorkarounds.isNotSupportedInThatRelease +import com.tangem.tap.domain.extensions.getPrimaryCurve import com.tangem.tap.domain.extensions.getSingleWallet +import com.tangem.tap.domain.tasks.DerivationTask import com.tangem.tap.domain.tokens.CurrenciesRepository import com.tangem.tap.domain.twins.TwinsHelper +import com.tangem.tap.preferencesStorage data class ScanResponse( val card: Card, val productType: ProductType, val walletData: WalletData?, val secondTwinPublicKey: String? = null, - val derivedKeys: Map> = mapOf() + val derivedKeys: Map> = mapOf(), + val primaryCard: PrimaryCard? = null ) : CommandResponse { fun getBlockchain(): Blockchain { - if (productType == ProductType.Note) return getTangemNoteBlockchain(card) ?: return Blockchain.Unknown + if (productType == ProductType.Note) return getTangemNoteBlockchain(card) + ?: return Blockchain.Unknown val blockchainName: String = walletData?.blockchain ?: return Blockchain.Unknown return Blockchain.fromId(blockchainName) } @@ -62,17 +68,19 @@ data class ScanResponse( fun isTangemNote(): Boolean = productType == ProductType.Note fun isTangemWallet(): Boolean = productType == ProductType.Wallet fun isTangemTwins(): Boolean = productType == ProductType.Twins - fun isTangemOtherCards(): Boolean = productType == ProductType.Other - fun twinsIsTwinned(): Boolean = card.isTangemTwins() && walletData != null && secondTwinPublicKey != null + fun supportsHdWallet(): Boolean = card.settings.isHDWalletAllowed + fun supportsBackup(): Boolean = card.settings.isBackupAllowed + + fun twinsIsTwinned(): Boolean = + card.isTangemTwins() && walletData != null && secondTwinPublicKey != null } typealias KeyWalletPublicKey = ByteArrayKey class ScanProductTask( val card: Card? = null, - private val currenciesRepository: CurrenciesRepository?, - private val shouldDeriveWC: Boolean + private val currenciesRepository: CurrenciesRepository? ) : CardSessionRunnable { override fun run( @@ -93,15 +101,22 @@ class ScanProductTask( val commandProcessor = when { TapWorkarounds.isTangemNote(card) -> ScanNoteProcessor() card.isTangemTwins() -> ScanTwinProcessor() - TapWorkarounds.isTangemWallet(card) -> ScanWalletProcessor(currenciesRepository, shouldDeriveWC) - else -> ScanOtherCardsProcessor() + else -> ScanWalletProcessor(currenciesRepository) } commandProcessor.proceed(card, session) { processorResult -> when (processorResult) { is CompletionResult.Success -> ScanTask().run(session) { scanTaskResult -> when (scanTaskResult) { - is CompletionResult.Success -> callback(CompletionResult.Success(processorResult.data)) - is CompletionResult.Failure -> callback(CompletionResult.Failure(scanTaskResult.error)) + is CompletionResult.Success -> callback( + CompletionResult.Success( + processorResult.data + ) + ) + is CompletionResult.Failure -> callback( + CompletionResult.Failure( + scanTaskResult.error + ) + ) } } is CompletionResult.Failure -> callback(CompletionResult.Failure(processorResult.error)) @@ -125,37 +140,128 @@ private class ScanNoteProcessor : ProductCommandProcessor { session: CardSession, callback: (result: CompletionResult) -> Unit ) { - callback(CompletionResult.Success(ScanResponse(card, ProductType.Note, session.environment.walletData))) + callback( + CompletionResult.Success( + ScanResponse( + card, + ProductType.Note, + session.environment.walletData + ) + ) + ) } } private class ScanWalletProcessor( - private val currenciesRepository: CurrenciesRepository?, - private val shouldDeriveWC: Boolean + private val currenciesRepository: CurrenciesRepository? ) : ProductCommandProcessor { + var primaryCard: PrimaryCard? = null + override fun proceed( card: Card, session: CardSession, callback: (result: CompletionResult) -> Unit ) { - val derivationPaths = collectDerivationPaths(card)?.distinct() - val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } + createMissingWalletsIfNeeded(card, session, callback) + } - if (derivationPaths.isNullOrEmpty() || wallet == null || wallet.chainCode == null) { - callback(CompletionResult.Success(ScanResponse(card, ProductType.Wallet, session.environment.walletData))) + private fun createMissingWalletsIfNeeded( + card: Card, + session: CardSession, + callback: (result: CompletionResult) -> Unit + ) { + if (card.wallets.isEmpty() || card.firmwareVersion < FirmwareVersion.MultiWalletAvailable) { + startLinkingForBackupIfNeeded(card, session, callback) return } - DeriveWalletPublicKeysTask(wallet.publicKey, derivationPaths).run(session) { result -> + val curvesToCreate = card.getCurvesForNonCreatedWallets() + if (curvesToCreate.isEmpty()) { + startLinkingForBackupIfNeeded(card, session, callback) + return + } + + CreateWalletsTask(curvesToCreate).run(session) { result -> + when (result) { + is CompletionResult.Success -> { + PreflightReadTask( + PreflightReadMode.FullCardRead, + card.cardId + ).run(session) { readResult -> + when (readResult) { + is CompletionResult.Success -> { + startLinkingForBackupIfNeeded(card, session, callback) + } + is CompletionResult.Failure -> callback( + CompletionResult.Failure( + readResult.error + ) + ) + } + } + } + is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error)) + } + } + } + + private fun startLinkingForBackupIfNeeded( + card: Card, + session: CardSession, + callback: (result: CompletionResult) -> Unit + ) { + val activationIsFinished = + preferencesStorage.usedCardsPrefStorage.isActivationFinished(card.cardId) + + if (card.backupStatus == Card.BackupStatus.NoBackup && + !activationIsFinished && card.wallets.isNotEmpty() + ) { + StartPrimaryCardLinkingTask().run(session) { linkingResult -> + when (linkingResult) { + is CompletionResult.Success -> { + primaryCard = linkingResult.data + deriveKeysIfNeeded(card, session, callback) + } + is CompletionResult.Failure -> { + deriveKeysIfNeeded(card, session, callback) + } + } + } + } else { + deriveKeysIfNeeded(card, session, callback) + } + } + + private fun deriveKeysIfNeeded( + card: Card, + session: CardSession, + callback: (result: CompletionResult) -> Unit + ) { + val derivations = collectDerivations(card) + if (derivations.isEmpty()) { + callback( + CompletionResult.Success( + ScanResponse( + card = card, + productType = ProductType.Wallet, + walletData = session.environment.walletData, + primaryCard = primaryCard + ) + ) + ) + return + } + + DerivationTask(derivations).run(session) { result -> when (result) { is CompletionResult.Success -> { - val derivedKeys = mapOf(wallet.publicKey.toMapKey() to result.data) val response = ScanResponse( - card, - ProductType.Wallet, - session.environment.walletData, - derivedKeys = derivedKeys + card = card, + productType = ProductType.Wallet, + walletData = session.environment.walletData, + derivedKeys = result.data.entries, + primaryCard = primaryCard ) callback(CompletionResult.Success(response)) @@ -165,8 +271,8 @@ private class ScanWalletProcessor( } } - private fun collectDerivationPaths(card: Card): List? { - val currenciesRepository = currenciesRepository ?: return null + private fun getBlockchainsToDerive(card: Card): List { + val currenciesRepository = currenciesRepository ?: return emptyList() val cardCurrencies = currenciesRepository.loadCardCurrencies(card.cardId) val blockchainsToDerive = if (cardCurrencies == null) { @@ -176,13 +282,40 @@ private class ScanWalletProcessor( (cardCurrencies.blockchains + tokenBlockchains).toMutableList() } - if (shouldDeriveWC) { - blockchainsToDerive.addAll(listOf(Blockchain.Ethereum, Blockchain.Binance, Blockchain.EthereumTestnet)) + if (card.settings.isHDWalletAllowed) { + blockchainsToDerive.addAll( + listOf( + Blockchain.Ethereum, + Blockchain.Binance, + Blockchain.EthereumTestnet + ) + ) } + return blockchainsToDerive.distinct() + } - return blockchainsToDerive.toSet() - .filter { it.getSupportedCurves()?.contains(EllipticCurve.Secp256k1) == true } - .mapNotNull { it.derivationPath() } + private fun collectDerivations(card: Card): Map> { + val blockchains = getBlockchainsToDerive(card) + val derivations = mutableMapOf>() + + blockchains.forEach { blockchain -> + val curve = blockchain.getPrimaryCurve() + + val wallet = card.wallets.firstOrNull { it.curve == curve } ?: return@forEach + if (wallet.chainCode == null) return@forEach + + val key = wallet.publicKey.toMapKey() + val path = blockchain.derivationPath() + if (path != null) { + val addedDerivations = derivations[key] + if (addedDerivations != null) { + derivations[key] = addedDerivations + path + } else { + derivations[key] = listOf(path) + } + } + } + return derivations } } @@ -197,17 +330,39 @@ private class ScanTwinProcessor : ProductCommandProcessor { is CompletionResult.Success -> { val publicKey = card.getSingleWallet()?.publicKey if (publicKey == null) { - callback(CompletionResult.Success(ScanResponse(card, ProductType.Twins, null))) + callback( + CompletionResult.Success( + ScanResponse( + card, + ProductType.Twins, + null + ) + ) + ) return@run } - val verified = TwinsHelper.verifyTwinPublicKey(readDataResult.data.issuerData, publicKey) + val verified = + TwinsHelper.verifyTwinPublicKey(readDataResult.data.issuerData, publicKey) if (verified) { val twinPublicKey = readDataResult.data.issuerData.sliceArray(0 until 65) val walletData = session.environment.walletData - val response = ScanResponse(card, ProductType.Twins, walletData, twinPublicKey.toHexString()) + val response = ScanResponse( + card, + ProductType.Twins, + walletData, + twinPublicKey.toHexString() + ) callback(CompletionResult.Success(response)) } else { - callback(CompletionResult.Success(ScanResponse(card, ProductType.Twins, null))) + callback( + CompletionResult.Success( + ScanResponse( + card, + ProductType.Twins, + null + ) + ) + ) } } is CompletionResult.Failure -> @@ -218,43 +373,9 @@ private class ScanTwinProcessor : ProductCommandProcessor { } -private class ScanOtherCardsProcessor : ProductCommandProcessor { - - override fun proceed(card: Card, session: CardSession, callback: (result: CompletionResult) -> Unit) { - val walletData = session.environment.walletData - - if (card.wallets.isEmpty() || card.firmwareVersion < FirmwareVersion.MultiWalletAvailable) { - callback(CompletionResult.Success(ScanResponse(card, ProductType.Other, walletData))) - return - } - - val curvesToCreate = card.getCurvesForNonCreatedWallets() - if (curvesToCreate.isEmpty()) { - callback(CompletionResult.Success(ScanResponse(card, ProductType.Other, walletData))) - return - } - - CreateWalletsTask(curvesToCreate).run(session) { result -> - when (result) { - is CompletionResult.Success -> { - PreflightReadTask(PreflightReadMode.FullCardRead, card.cardId).run(session) { readResult -> - when (readResult) { - is CompletionResult.Success -> { - val response = ScanResponse(readResult.data, ProductType.Other, walletData) - callback(CompletionResult.Success(response)) - } - is CompletionResult.Failure -> callback(CompletionResult.Failure(readResult.error)) - } - } - } - is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error)) - } - } - } -} - fun Card.getCurvesForNonCreatedWallets(): List { - val curvesPresent = wallets.map { it.curve } - val curvesForNonCreatedWallets = supportedCurves.subtract(curvesPresent) + val curvesPresent = wallets.map { it.curve }.toSet() + val curvesForNonCreatedWallets = supportedCurves + .subtract(curvesPresent + EllipticCurve.Secp256r1) return curvesForNonCreatedWallets.toList() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/topup/TradeCryptoHelper.kt b/app/src/main/java/com/tangem/tap/domain/topup/TradeCryptoHelper.kt index c8599f6d4c..ec92f9f0fe 100644 --- a/app/src/main/java/com/tangem/tap/domain/topup/TradeCryptoHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/topup/TradeCryptoHelper.kt @@ -38,6 +38,8 @@ class TradeCryptoHelper { private const val TRANSACTION_RECEIPT_PATH = "transaction_receipt?transactionId=" + private const val BASE_CURRENCY_USD = "USD" + fun getUrl( action: Action, blockchain: Blockchain?, @@ -56,6 +58,7 @@ class TradeCryptoHelper { baseUrl = BASE_URL_BUY originalQuery = API_KEY_PATH + apiKey.urlEncode() + CURRENCY_PATH + cryptoCurrencyName.urlEncode() + + BASE_CURRENCY_PATH + BASE_CURRENCY_USD.urlEncode() + WALLET_ADDRESS_PATH + walletAddress.urlEncode() + REDIRECT_URL_PATH + REDIRECT_URL_BUY.urlEncode() } diff --git a/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt index 6fb4bb28c6..51ade7b4c3 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt @@ -23,7 +23,8 @@ class FinalizeTwinTask( PreflightReadTask(PreflightReadMode.FullCardRead).run(session) { readResult -> when (readResult) { is CompletionResult.Success -> - ScanProductTask(readResult.data, null, false).run(session, callback) + ScanProductTask(readResult.data, null) + .run(session, callback) is CompletionResult.Failure -> callback(CompletionResult.Failure(readResult.error)) } diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index 71f4bd5fd7..7ea35d3284 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -51,7 +51,7 @@ private fun handlePrepareScreen( cardInfo = action.scanResponse.card.toCardInfo(), appCurrencyState = AppCurrencyState(action.fiatCurrencyName), cardTermsOfUseUrl = action.cardTou.getUrl(action.scanResponse.card), - createBackupAllowed = action.scanResponse.isTangemWallet() && !backupIsActive, + createBackupAllowed = action.scanResponse.card.backupStatus == Card.BackupStatus.NoBackup, ) } @@ -65,7 +65,7 @@ private fun handleEraseWallet( val notAllowedByAnyWallet = card?.wallets?.any { it.settings.isPermanent } ?: false val notAllowedByCard = notAllowedByAnyWallet || (card?.isWalletDataSupported == true && - (!state.scanResponse.isTangemNote() && !state.scanResponse.isTangemWallet())) + (!state.scanResponse.isTangemNote() && !state.scanResponse.supportsBackup())) val notEmpty = state.wallets.any { it.hasPendingTransactions() || it.amounts.toSendableAmounts().isNotEmpty() @@ -139,7 +139,7 @@ private fun handleSecurityAction( state.scanResponse?.isTangemNote() == true -> { EnumSet.of(SecurityOption.LongTap) } - state.scanResponse?.isTangemWallet() == true -> { + state.scanResponse?.supportsBackup() == true -> { EnumSet.of(state.securityScreenState?.currentOption) } else -> prepareAllowedSecurityOptions( diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/DetailsSecurityFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/DetailsSecurityFragment.kt index fe017dfec7..8ef33711e7 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/DetailsSecurityFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/DetailsSecurityFragment.kt @@ -5,6 +5,7 @@ import android.view.View import androidx.activity.OnBackPressedCallback import androidx.fragment.app.Fragment import androidx.transition.TransitionInflater +import com.tangem.common.card.Card import com.tangem.tap.common.extensions.show import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.features.details.redux.DetailsAction @@ -72,6 +73,9 @@ class DetailsSecurityFragment : Fragment(R.layout.fragment_details_security), ?: EnumSet.noneOf(SecurityOption::class.java) ) } + tv_access_code_unavailable_disclaimer.show( + state.scanResponse?.card?.backupStatus == Card.BackupStatus.NoBackup + ) } private fun selectSecurityOption(securityOption: SecurityOption?) { diff --git a/app/src/main/java/com/tangem/tap/features/feedback/FeedbackManager.kt b/app/src/main/java/com/tangem/tap/features/feedback/FeedbackManager.kt index 0e4fe0a875..6da60e3904 100644 --- a/app/src/main/java/com/tangem/tap/features/feedback/FeedbackManager.kt +++ b/app/src/main/java/com/tangem/tap/features/feedback/FeedbackManager.kt @@ -40,7 +40,7 @@ class FeedbackManager( val fileLog = if (emailData is ScanFailsEmail) createLogFile() else null activity.sendEmail( email = getSupportEmail(), - subject = emailData.subject, message = emailData.joinTogether(infoHolder), + subject = emailData.subject, message = emailData.joinTogether(infoHolder, emailData !is ScanFailsEmail), file = fileLog, onFail = onFail ) @@ -216,9 +216,14 @@ interface EmailData { fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String - fun joinTogether(infoHolder: AdditionalEmailInfo): String { + fun joinTogether(infoHolder: AdditionalEmailInfo, allowToErasePhoneInfo: Boolean): String { + val allowToErasePhoneInfoDisclaimer = if (allowToErasePhoneInfo) { + "Following information is optional. You can erase it if you don’t want to share it.\n\n" + } else { + "" + } return "$mainMessage\n\n\n\n" + - "Following information is optional. You can erase it if you don’t want to share it.\n" + + allowToErasePhoneInfoDisclaimer + createOptionalMessage(infoHolder) } } diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt index f28523564f..fae5ee9060 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt @@ -13,6 +13,7 @@ import com.tangem.tap.common.transitions.FrontCardExitTransition import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.features.home.redux.HomeState import com.tangem.tap.features.onboarding.products.BaseOnboardingFragment +import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction import com.tangem.tap.store import com.tangem.wallet.R import kotlinx.android.synthetic.main.fragment_onboarding_main.* @@ -36,6 +37,8 @@ class HomeFragment : BaseOnboardingFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + store.dispatch(BackupAction.CheckForUnfinishedBackup) + toolbar.hide() val shareTransition = FragmentShareTransition( listOf( diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt index b93b3a2e7e..341aa3e64b 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt @@ -15,7 +15,6 @@ import com.tangem.tap.domain.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.features.onboarding.OnboardingHelper import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep -import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction import com.tangem.tap.features.send.redux.states.ButtonState import com.tangem.tap.preferencesStorage import com.tangem.tap.scope @@ -39,7 +38,6 @@ private val homeMiddleware: Middleware = { dispatch, state -> store.dispatch(GlobalAction.RestoreAppCurrency) store.dispatch(GlobalAction.GetMoonPayStatus) store.dispatch(HomeAction.SetTermsOfUseState(preferencesStorage.wasDisclaimerAccepted())) - store.dispatch(BackupAction.CheckForUnfinishedBackup) } is HomeAction.ShouldScanCardOnResume -> { if (action.shouldScanCard) { diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index 6a097f6511..258701cabc 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt @@ -32,9 +32,12 @@ class OnboardingHelper { fun whereToNavigate(scanResponse: ScanResponse): AppScreen { return when (scanResponse.productType) { ProductType.Note -> AppScreen.OnboardingNote - ProductType.Wallet -> AppScreen.OnboardingWallet + ProductType.Wallet -> if (scanResponse.card.settings.isBackupAllowed) { + AppScreen.OnboardingWallet + } else { + AppScreen.OnboardingOther + } ProductType.Twins -> AppScreen.OnboardingTwins - ProductType.Other -> AppScreen.OnboardingOther } } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt index 36e9c93cc1..6a8cc146b7 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt @@ -80,7 +80,7 @@ private fun handleNoteAction(action: Action, dispatch: DispatchFunction) { withMainContext { when (result) { is CompletionResult.Success -> { - val updatedResponse = scanResponse.copy(card = result.data) + val updatedResponse = scanResponse.copy(card = result.data.card) onboardingManager.scanResponse = updatedResponse onboardingManager.activationStarted(updatedResponse.card.cardId) store.dispatch(OnboardingNoteAction.SetStepOfScreen(OnboardingNoteStep.TopUpWallet)) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt index f3419e67fa..29ca1fbb3b 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt @@ -67,7 +67,9 @@ private fun handleOtherCardsAction(action: Action, dispatch: DispatchFunction) { withMainContext { when (result) { is CompletionResult.Success -> { - val updatedResponse = onboardingManager.scanResponse.copy(card = result.data) + val updatedResponse = onboardingManager.scanResponse.copy( + card = result.data.card + ) onboardingManager.scanResponse = updatedResponse onboardingManager.activationStarted(updatedResponse.card.cardId) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt index b3056d7bdd..9bfdaaf381 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt @@ -22,9 +22,11 @@ sealed class OnboardingWalletAction : Action { sealed class BackupAction : Action { object DetermineBackupStep : BackupAction() - object IntroduceBackup : BackupAction() + data class IntroduceBackup(val buyCardsUrl: String? = null) : BackupAction() object StartBackup : BackupAction() object DismissBackup : BackupAction() + + object StartAddingPrimaryCard : BackupAction() object ScanPrimaryCard : BackupAction() object CheckForUnfinishedBackup : BackupAction() diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index f9465751c3..1816465f6e 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -76,7 +76,11 @@ private fun handleWalletAction(action: Action) { is CompletionResult.Success -> { //here we must use updated scanResponse after createWallet & derivation val updatedResponse = - globalState.onboardingState.onboardingManager.scanResponse.copy(card = result.data) + globalState.onboardingState.onboardingManager.scanResponse.copy( + card = result.data.card, + derivedKeys = result.data.derivedKeys, + primaryCard = result.data.primaryCard + ) onboardingManager.scanResponse = updatedResponse onboardingManager.activationStarted(updatedResponse.card.cardId) store.dispatch(OnboardingWalletAction.ProceedBackup) @@ -106,7 +110,14 @@ private fun handleWalletAction(action: Action) { BackupService.State.FinalizingPrimaryCard -> BackupAction.PrepareToWritePrimaryCard is BackupService.State.FinalizingBackupCard -> BackupAction.PrepareToWriteBackupCard(backupState.index) - else -> BackupAction.IntroduceBackup + else -> { + val url = if (card?.issuer?.name?.lowercase()?.contains("tangem") == true) { + BUY_WALLET_URL + } else { + null + } + BackupAction.IntroduceBackup(url) + } } store.dispatch(newAction) } @@ -164,6 +175,13 @@ private fun handleBackupAction(action: BackupAction) { when (action) { is BackupAction.StartBackup -> { backupService.discardSavedBackup() + val primaryCard = scanResponse?.primaryCard + if (primaryCard != null) { + backupService.setPrimaryCard(primaryCard) + store.dispatch(BackupAction.StartAddingBackupCards) + } else { + store.dispatch(BackupAction.StartAddingPrimaryCard) + } } is BackupAction.ScanPrimaryCard -> { backupService.readPrimaryCard(cardId = card?.cardId) { result -> diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt index 3947cd5d3f..89542c532e 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt @@ -39,12 +39,13 @@ class BackupReducer { ): BackupState { return when (action) { - BackupAction.IntroduceBackup -> BackupState( + is BackupAction.IntroduceBackup -> BackupState( backupStep = BackupStep.InitBackup, - canSkipBackup = state.canSkipBackup + canSkipBackup = state.canSkipBackup, + buyAdditionalCardsUrl = action.buyCardsUrl ) - BackupAction.StartBackup -> state.copy(backupStep = BackupStep.ScanOriginCard) + BackupAction.StartAddingPrimaryCard -> state.copy(backupStep = BackupStep.ScanOriginCard) BackupAction.StartAddingBackupCards -> { state.copy(backupStep = BackupStep.AddBackupCards) @@ -127,6 +128,7 @@ class BackupReducer { BackupAction.DiscardBackup -> state BackupAction.ResumeBackup -> state BackupAction.DiscardSavedBackup -> state + BackupAction.StartBackup -> state } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt index d8d0c687b7..a048261a6d 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt @@ -52,7 +52,8 @@ data class BackupState( val accessCodeError: AccessCodeError? = null, val backupStep: BackupStep = BackupStep.InitBackup, val maxBackupCards: Int = 2, - val canSkipBackup: Boolean = true + val canSkipBackup: Boolean = true, + val buyAdditionalCardsUrl: String? = null ) enum class AccessCodeError { diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupCardsWidget.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupCardsWidget.kt index d5b5065f36..e4de53c9a4 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupCardsWidget.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupCardsWidget.kt @@ -15,7 +15,13 @@ class BackupCardsWidget( val getTopOfAnchorViewForActivateState: () -> Float, ) { + var currentState: WidgetState? = null + fun toWelcome(animate: Boolean = true, onEnd: () -> Unit = {}) { + if (currentState == WidgetState.WELCOME) return + + currentState = WidgetState.WELCOME + val animator = createAnimator(animate, onEnd) animator.playTogether( createAnimator(BackupCardType.ORIGIN, createWelcomeProperties(BackupCardType.ORIGIN)), @@ -26,6 +32,10 @@ class BackupCardsWidget( } fun toFolded(animate: Boolean = true, onEnd: () -> Unit = {}) { + if (currentState == WidgetState.FOLDED) return + + currentState = WidgetState.FOLDED + val animator = createAnimator(animate, onEnd) animator.playTogether( createAnimator(BackupCardType.ORIGIN, createLeapfrogProperties(BackupCardType.ORIGIN)), @@ -36,6 +46,10 @@ class BackupCardsWidget( } fun toFan(animate: Boolean = true, onEnd: () -> Unit = {}) { + if (currentState == WidgetState.FAN) return + + currentState = WidgetState.FAN + val animator = createAnimator(animate, onEnd) animator.playTogether( createAnimator(BackupCardType.ORIGIN, createFanProperties(BackupCardType.ORIGIN)), @@ -46,6 +60,8 @@ class BackupCardsWidget( } fun toLeapfrog(animate: Boolean = true, onEnd: () -> Unit = {}) { + currentState = WidgetState.LEAPFROG + val animator = createAnimator(animate, onEnd) animator.playTogether( createAnimator(BackupCardType.ORIGIN, createLeapfrogProperties(BackupCardType.ORIGIN)), @@ -60,15 +76,6 @@ class BackupCardsWidget( animator.start() } } -// -// fun toActivate(animate: Boolean = true, onEnd: () -> Unit = {}) { -// val animator = createAnimator(animate, onEnd) -// animator.playTogether( -// createAnimator(TwinCardNumber.First, createActivateProperties(TwinCardNumber.First)), -// createAnimator(TwinCardNumber.Second, createActivateProperties(TwinCardNumber.Second)) -// ) -// animator.start() -// } private fun createAnimator(animate: Boolean, onEnd: () -> Unit): AnimatorSet { return AnimatorSet().apply { @@ -196,6 +203,8 @@ class BackupCardsWidget( fun getSecondBackupCardView(): ImageView { return getLeapViewByCardNumber(BackupCardType.SECOND_BACKUP).view as ImageView } + + enum class WidgetState { WELCOME, FOLDED, FAN, LEAPFROG } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt index f415e176b9..5a1ccf6684 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt @@ -148,19 +148,10 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), } private fun showScanOriginCard(state: BackupState) { - - toolbar.title = getText(R.string.onboarding_navbar_title_creating_backup) + prepareBackupView() cardsWidget.toFolded() - tv_header.show() - tv_body.show() - view_pager_backup_info.hide() - tab_layout_backup_info.hide() - - imv_first_backup_card.show() - imv_second_backup_card.show() - tv_header.text = getText(R.string.onboarding_title_scan_origin_card) tv_body.text = getString( R.string.onboarding_subtitle_scan_origin_card, @@ -171,9 +162,20 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), btn_main_action.setOnClickListener { store.dispatch(BackupAction.ScanPrimaryCard) } } - private fun showAddBackupCards(state: BackupState) { + private fun prepareBackupView() { + toolbar.title = getText(R.string.onboarding_navbar_title_creating_backup) + + tv_header.show() + tv_body.show() + view_pager_backup_info.hide() + tab_layout_backup_info.hide() + imv_first_backup_card.show() imv_second_backup_card.show() + } + + private fun showAddBackupCards(state: BackupState) { + prepareBackupView() accessCodeDialog?.dismiss() accessCodeDialog = null @@ -190,11 +192,12 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), when (state.backupCardsNumber) { 0 -> { - cardsWidget.toFan() + cardsWidget.toFan() { + cardsWidget.getFirstBackupCardView().animate().alpha(0.6f).setDuration(200) + cardsWidget.getSecondBackupCardView().animate().alpha(0.2f).setDuration(200) + } tv_header.text = getText(R.string.onboarding_title_no_backup_cards) tv_body.text = getText(R.string.onboarding_subtitle_no_backup_cards) - cardsWidget.getFirstBackupCardView().alpha = 0.6f - cardsWidget.getSecondBackupCardView().alpha = 0.2f } 1 -> { tv_header.text = getText(R.string.onboarding_title_one_backup_card) @@ -283,10 +286,6 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), cardsWidget.toLeapfrog() -// imv_front_card.alpha = 1f -// imv_first_backup_card.alpha = 1f -// imv_second_backup_card.alpha = 1f - btn_alternative_action.hide() } @@ -373,9 +372,12 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { inflater.inflate(R.menu.shop, menu) - val backupStep = store.state.onboardingWalletState.backupState.backupStep + val backupState = store.state.onboardingWalletState.backupState + val backupStep = backupState.backupStep + val shopMenuShouldBeVisible = - backupStep == BackupStep.ScanOriginCard || backupStep == BackupStep.AddBackupCards + (backupStep == BackupStep.ScanOriginCard || backupStep == BackupStep.AddBackupCards) && + backupState.buyAdditionalCardsUrl != null menu.getItem(0).isVisible = shopMenuShouldBeVisible } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/CurrencyListItem.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/CurrencyListItem.kt index bcf0bf5644..53acef169a 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/CurrencyListItem.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/CurrencyListItem.kt @@ -7,7 +7,7 @@ import com.tangem.wallet.R sealed class CurrencyListItem { var isAdded: Boolean = false - var isLock: Boolean = false + var isLocked: Boolean = false data class TokenListItem(val token: Token) : CurrencyListItem() data class BlockchainListItem(val blockchain: Blockchain) : CurrencyListItem() diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt index 2176e4ea51..30db14cfbe 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt @@ -4,6 +4,9 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token import com.tangem.common.CompletionResult import com.tangem.common.card.EllipticCurve +import com.tangem.common.hdWallet.DerivationPath +import com.tangem.common.hdWallet.ExtendedPublicKey +import com.tangem.tap.common.extensions.ByteArrayKey import com.tangem.tap.common.extensions.dispatchErrorNotification import com.tangem.tap.common.extensions.toMapKey import com.tangem.tap.common.redux.AppState @@ -13,6 +16,7 @@ import com.tangem.tap.currenciesRepository import com.tangem.tap.domain.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.domain.TapError import com.tangem.tap.domain.TapWorkarounds.isTestCard +import com.tangem.tap.domain.tasks.product.KeyWalletPublicKey import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.scope @@ -45,27 +49,8 @@ class TokensMiddleware { cardFirmware = scanResponse.card.firmwareVersion, isTestNet = isTestcard ) - val currencies = CurrencyListItem.createListOfCurrencies(blockchains, tokens).toMutableList() - if (scanResponse.isTangemWallet()) { - currencies.forEach { - when (it) { - is CurrencyListItem.TitleListItem -> { - } - is CurrencyListItem.BlockchainListItem -> { - val firstCurve = it.blockchain.getSupportedCurves()?.firstOrNull() - if (firstCurve == EllipticCurve.Ed25519) { - it.isLock = true - } - } - is CurrencyListItem.TokenListItem -> { - val firstCurve = it.token.blockchain.getSupportedCurves()?.firstOrNull() - if (firstCurve == EllipticCurve.Ed25519) { - it.isLock = true - } - } - } - } - } + val currencies = + CurrencyListItem.createListOfCurrencies(blockchains, tokens).toMutableList() store.dispatch(TokensAction.LoadCurrencies.Success(currencies)) } @@ -81,7 +66,7 @@ class TokensMiddleware { .map { it.token } if (blockchains.isEmpty() && tokens.isEmpty()) return - if (scanResponse.isTangemWallet()) { + if (scanResponse.supportsHdWallet()) { deriveMissingBlockchains(scanResponse, blockchains, tokens) } else { submitAdd(blockchains, tokens) @@ -94,26 +79,33 @@ class TokensMiddleware { blockchains: List, tokens: List ) { - val wallet = scanResponse.card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: return - - val tokenBlockchains = tokens.map { it.blockchain } - val derivationPathsCandidates = (blockchains + tokenBlockchains).distinct() - .mapNotNull { it.derivationPath() } - - val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey() - val alreadyDerivedKeys = scanResponse.derivedKeys[mapKeyOfWalletPublicKey]?.toMutableList() ?: mutableListOf() - val alreadyDerivedPaths = alreadyDerivedKeys.map { it.derivationPath } - - val toDerive = derivationPathsCandidates.filterNot { alreadyDerivedPaths.contains(it) } + val derivationDataList = listOfNotNull( + getDerivations(EllipticCurve.Secp256k1, scanResponse, blockchains, tokens), + getDerivations(EllipticCurve.Ed25519, scanResponse, blockchains, tokens) + ) + val derivations = derivationDataList.map { it.derivations }.toMap() scope.launch { - val result = tangemSdkManager.derivePublicKeys(scanResponse.card.cardId, wallet.publicKey, toDerive) + val result = tangemSdkManager.derivePublicKeys( + scanResponse.card.cardId, + derivations + ) when (result) { is CompletionResult.Success -> { - val newDerivedKeys = result.data - alreadyDerivedKeys.addAll(newDerivedKeys) + val newDerivedKeys = result.data.entries + val updatedDerivedKeys = + mutableMapOf>() + + newDerivedKeys.forEach { entry -> + val derivationData = derivationDataList.find { + it.mapKeyOfWalletPublicKey == entry.key + } ?: return@forEach + updatedDerivedKeys[entry.key] = + derivationData.alreadyDerivedKeys + entry.value.toList() + } + val updatedScanResponse = scanResponse.copy( - derivedKeys = mapOf(mapKeyOfWalletPublicKey to alreadyDerivedKeys.toList()) + derivedKeys = updatedDerivedKeys ) store.dispatch(GlobalAction.SaveScanNoteResponse(updatedScanResponse)) submitAdd(blockchains, tokens) @@ -128,6 +120,37 @@ class TokensMiddleware { } } + private fun getDerivations( + curve: EllipticCurve, + scanResponse: ScanResponse, + blockchains: List, + tokens: List + ): DerivationData? { + val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null + + val tokenBlockchains = tokens.map { it.blockchain } + val derivationPathsCandidates = (blockchains + tokenBlockchains).distinct() + .mapNotNull { it.derivationPath() } + + val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey() + val alreadyDerivedKeys = + scanResponse.derivedKeys[mapKeyOfWalletPublicKey]?.toMutableList() ?: mutableListOf() + val alreadyDerivedPaths = alreadyDerivedKeys.map { it.derivationPath } + + val toDerive = derivationPathsCandidates.filterNot { alreadyDerivedPaths.contains(it) } + return DerivationData( + derivations = mapKeyOfWalletPublicKey to toDerive, + alreadyDerivedKeys = alreadyDerivedKeys, + mapKeyOfWalletPublicKey = mapKeyOfWalletPublicKey + ) + } + + private class DerivationData( + val derivations: Pair>, + val alreadyDerivedKeys: List, + val mapKeyOfWalletPublicKey: ByteArrayKey + ) + private fun submitAdd(blockchains: List, tokens: List) { (blockchains.map { WalletAction.MultiWallet.AddBlockchain(it) diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/adapters/CurrenciesAdapter.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/adapters/CurrenciesAdapter.kt index b3d0ecdc04..f0046353d1 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/adapters/CurrenciesAdapter.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/adapters/CurrenciesAdapter.kt @@ -196,7 +196,7 @@ class CurrenciesAdapter : ListAdapter } private fun modifyAddTokenButton(currency: CurrencyListItem) { - if (currency.isLock) { + if (currency.isLocked) { view.btn_add_token.setText(R.string.common_add) view.btn_add_token.isEnabled = false } else { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt index 11d7232cd3..ace2084261 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt @@ -85,7 +85,7 @@ class MultiWalletMiddleware { } is WalletAction.MultiWallet.FindBlockchainsInUse -> { val scanResponse = globalState.scanResponse ?: return - if (scanResponse.isTangemWallet()) return + if (scanResponse.supportsHdWallet()) return val cardFirmware = scanResponse.card.firmwareVersion val blockchains = currenciesRepository.getBlockchains(cardFirmware) @@ -125,7 +125,7 @@ class MultiWalletMiddleware { } is WalletAction.MultiWallet.FindTokensInUse -> { val scanResponse = globalState.scanResponse ?: return - if (scanResponse.isTangemWallet()) return + if (scanResponse.supportsHdWallet()) return val walletFactory = tapWalletManager.walletManagerFactory val card = scanResponse.card diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index 2b2087dd25..2cd5627545 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -58,12 +58,12 @@ class TradeCryptoMiddleware { } val url = TradeCryptoHelper.getUrl( - exchangeAction, - currency?.blockchain, - currencySymbol, - defaultAddress, - config.moonPayApiKey, - config.moonPayApiSecretKey + action = exchangeAction, + blockchain = currency?.blockchain, + cryptoCurrencyName = currencySymbol, + walletAddress = defaultAddress, + apiKey = config.moonPayApiKey, + secretKey = config.moonPayApiSecretKey ) Timber.d("Moonpay $exchangeAction URL: $url") store.dispatchOnMain(NavigationAction.OpenUrl(url)) diff --git a/app/src/main/java/com/tangem/tap/persistence/UsedCardsPrefStorage.kt b/app/src/main/java/com/tangem/tap/persistence/UsedCardsPrefStorage.kt index 17238689f5..9d8d877f32 100644 --- a/app/src/main/java/com/tangem/tap/persistence/UsedCardsPrefStorage.kt +++ b/app/src/main/java/com/tangem/tap/persistence/UsedCardsPrefStorage.kt @@ -44,6 +44,10 @@ class UsedCardsPrefStorage( save(foundItem, restoredList) } + fun isActivationFinished(cardId: String): Boolean { + return !(findCardInfo(cardId)?.isActivationStarted ?: true) + } + fun activationIsStarted(cardId: String): Boolean { return findCardInfo(cardId)?.isActivationStarted ?: false } diff --git a/app/src/main/res/drawable/ic_voyr.png b/app/src/main/res/drawable/ic_voyr.png new file mode 100644 index 0000000000..545990d041 Binary files /dev/null and b/app/src/main/res/drawable/ic_voyr.png differ diff --git a/app/src/main/res/layout/fragment_details_security.xml b/app/src/main/res/layout/fragment_details_security.xml index 3e232e82d8..98f442ad29 100644 --- a/app/src/main/res/layout/fragment_details_security.xml +++ b/app/src/main/res/layout/fragment_details_security.xml @@ -183,6 +183,23 @@ app:layout_constraintBottom_toBottomOf="@id/tv_access_code_description" app:layout_constraintTop_toTopOf="@id/tv_access_code_title" /> + + + You may experience NFC problems with some iPhone 7/7+ during the extraction This card has been already topped up and signed transactions in the past. Consider immediate withdrawal of all funds if you have received this card from an untrusted source. Warning - This card it is not designed to work with Tangem + This card is not designed to work with this app The card you scanned is a development card. Don’t accept it as a payment Tangem cards manufactured before September 2019 cannot currently be extracted with an iPhone. We’re working hard with Apple to make it possible in future versions of iOS. diff --git a/app/src/main/res/values/strings_untranslated.xml b/app/src/main/res/values/strings_untranslated.xml index e81e6a07bf..faad406cb3 100644 --- a/app/src/main/res/values/strings_untranslated.xml +++ b/app/src/main/res/values/strings_untranslated.xml @@ -292,5 +292,6 @@ Reset to factory settings This action is irreversible. If, after resetting the card, someone sends funds to it, then you will not be able to withdraw them. + Access code is available only for cards with a backup.