Updated on 2026-08-14

This commit is contained in:
Tangem 2021-12-09 17:53:02 +03:00
commit 0725a039b4
61 changed files with 914 additions and 556 deletions

View file

@ -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-48'
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-99'
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-99'
implementation 'com.tangem:blockchain:develop-51'
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-106'
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-106'
// WebView
implementation "androidx.browser:browser:1.3.0"

View file

@ -96,8 +96,8 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
val backStackIsEmpty = supportFragmentManager.backStackEntryCount == 0
val isScannedBefore = store.state.globalState.scanResponse != null
val isOnboardingServiceActive = store.state.globalState.onboardingManager != null
if (backStackIsEmpty && (!isOnboardingServiceActive && !isScannedBefore)) {
val isOnboardingServiceActive = store.state.globalState.onboardingState.onboardingStarted
if (backStackIsEmpty || (!isOnboardingServiceActive && !isScannedBefore)) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Home))
}
intentHandler.handleIntent(intent)

View file

@ -10,8 +10,8 @@ import com.tangem.tap.features.onboarding.AddressInfoBottomSheetDialog
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.ui.dialog.CreateWalletInterruptDialog
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.AddMoreBackupCardsDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.BackupInProgressDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.BuyMoreBackupCardsDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.ConfirmDiscardingBackupDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.UnfinishedBackupFoundDialog
import com.tangem.tap.features.wallet.ui.dialogs.ScanFailsDialog
@ -79,7 +79,7 @@ class DialogManager : StoreSubscriber<GlobalState> {
TransactionDialog.create(state.dialog.dialogData, context)
is WalletConnectDialog.PersonalSign ->
PersonalSignDialog.create(state.dialog.data, context)
is BackupDialog.BuyMoreBackupCards -> BuyMoreBackupCardsDialog.create(context)
is BackupDialog.AddMoreBackupCards -> AddMoreBackupCardsDialog.create(context)
is BackupDialog.BackupInProgress -> BackupInProgressDialog.create(context)
is BackupDialog.UnfinishedBackupFound -> UnfinishedBackupFoundDialog.create(context)
is BackupDialog.ConfirmDiscardingBackup -> ConfirmDiscardingBackupDialog.create(context)

View file

@ -103,6 +103,8 @@ object TangemSdk {
is TangemSdkError.FilesDisabled -> TangemSdkError.FilesDisabled()
is TangemSdkError.HDWalletDisabled -> TangemSdkError.HDWalletDisabled()
is TangemSdkError.WrongInteractionMode -> TangemSdkError.WrongInteractionMode()
is TangemSdkError.IssuerSignatureLoadingFailed -> TangemSdkError.IssuerSignatureLoadingFailed()
is TangemSdkError.BackupFailedFirmware -> TangemSdkError.BackupFailedFirmware()
}
}

View file

@ -0,0 +1,17 @@
package com.tangem.tap.common.extensions
/**
[REDACTED_AUTHOR]
*/
data class ByteArrayKey(val bytes: ByteArray) {
override fun equals(other: Any?): Boolean {
return this === other || other is ByteArrayKey && this.bytes.contentEquals(other.bytes)
}
override fun hashCode(): Int = bytes.contentHashCode()
}
fun ByteArray.toMapKey(): ByteArrayKey = ByteArrayKey(this)
fun ByteArrayKey.bytes(): ByteArray = this.bytes

View file

@ -68,7 +68,6 @@ fun FragmentActivity.addOnBackPressedDispatcher(
private fun fragmentFactory(screen: AppScreen): Fragment {
return when (screen) {
// AppScreen.Home -> TestLeapfrogFragment()
AppScreen.Home -> HomeFragment()
AppScreen.OnboardingNote -> OnboardingNoteFragment()
AppScreen.OnboardingWallet -> OnboardingWalletFragment()

View file

@ -31,11 +31,12 @@ sealed class GlobalAction : Action {
object HideDialog : GlobalAction()
sealed class Onboarding {
data class Start(val scanResponse: ScanResponse, val fromHomeScreen: Boolean = true) : GlobalAction()
data class Start(val scanResponse: ScanResponse?, val fromHomeScreen: Boolean = true) : GlobalAction()
object Stop : GlobalAction()
}
data class ScanCard(
val shouldDeriveWC: Boolean,
val onSuccess: ((ScanResponse) -> Unit)? = null,
val onFailure: ((TangemError) -> Unit)? = null,
val messageResId: Int? = null,

View file

@ -3,6 +3,7 @@ package com.tangem.tap.common.redux.global
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.services.Result
import com.tangem.tap.*
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
@ -13,10 +14,6 @@ import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.moonpay.MoonpayService
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
import timber.log.Timber
@ -91,7 +88,12 @@ private val globalMiddlewareHandler: Middleware<AppState> = { dispatch, appState
}
is GlobalAction.ScanCard -> {
scope.launch {
val result = tangemSdkManager.scanProduct(FirebaseAnalyticsHandler, action.messageResId)
val result = tangemSdkManager.scanProduct(
FirebaseAnalyticsHandler,
currenciesRepository,
action.shouldDeriveWC,
action.messageResId
)
store.dispatch(GlobalAction.ScanFailsCounter.ChooseBehavior(result))
withMainContext {
when (result) {

View file

@ -17,12 +17,16 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
globalState.copy(resources = action.resources)
}
is GlobalAction.Onboarding.Start -> {
val usedCardsPrefStorage = preferencesStorage.usedCardsPrefStorage
val onboardingState = OnboardingManager(action.scanResponse, usedCardsPrefStorage)
globalState.copy(onboardingManager = onboardingState)
val onboardingManager = if (action.scanResponse != null) {
val usedCardsPrefStorage = preferencesStorage.usedCardsPrefStorage
OnboardingManager(action.scanResponse, usedCardsPrefStorage)
} else {
null
}
globalState.copy(onboardingState = OnboardingState(true, onboardingManager))
}
is GlobalAction.Onboarding.Stop -> {
globalState.copy(onboardingManager = null)
globalState.copy(onboardingState = OnboardingState(false))
}
is GlobalAction.ScanFailsCounter.Increment -> {
globalState.copy(scanCardFailsCounter = globalState.scanCardFailsCounter + 1)

View file

@ -15,7 +15,7 @@ import org.rekotlin.StateType
data class GlobalState(
val scanResponse: ScanResponse? = null,
val onboardingManager: OnboardingManager? = null,
val onboardingState: OnboardingState = OnboardingState(),
val cardVerifiedOnline: Boolean = false,
val tapWalletManager: TapWalletManager = TapWalletManager(),
val payIdManager: PayIdManager = PayIdManager(),
@ -32,12 +32,17 @@ data class GlobalState(
data class AndroidResources(
val strings: RString = RString()
val strings: RString = RString(),
) {
data class RString(
val addressWasCopied: Int = -1,
val walletIsNotEmpty: Int = -1
val walletIsNotEmpty: Int = -1,
)
}
typealias CryptoCurrencyName = String
typealias FiatCurrencyName = String
typealias FiatCurrencyName = String
data class OnboardingState(
val onboardingStarted: Boolean = false,
val onboardingManager: OnboardingManager? = null,
)

View file

@ -1,6 +1,6 @@
package com.tangem.tap.domain
import CreateProductWalletAndRescanTask
import CreateProductWalletTask
import android.content.Context
import com.tangem.Message
import com.tangem.TangemSdk
@ -12,7 +12,10 @@ import com.tangem.common.card.FirmwareVersion
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
@ -23,6 +26,7 @@ import com.tangem.tap.domain.tasks.CreateWalletAndRescanTask
import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask
import com.tangem.tap.domain.tasks.product.ScanProductTask
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.domain.tokens.CurrenciesRepository
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -33,18 +37,20 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
suspend fun scanProduct(
analyticsHandler: AnalyticsHandler,
currenciesRepository: CurrenciesRepository,
shouldDeriveWC: Boolean,
messageRes: Int? = null,
): CompletionResult<ScanResponse> {
analyticsHandler.triggerEvent(AnalyticsEvent.READY_TO_SCAN, null)
val message = Message(context.getString(messageRes ?: R.string.initial_message_scan_header))
return runTaskAsyncReturnOnMain(ScanProductTask(), null, message)
.also { sendScanFailuresToAnalytics(analyticsHandler, it) }
return runTaskAsyncReturnOnMain(ScanProductTask(null, currenciesRepository, shouldDeriveWC), null, message)
.also { sendScanFailuresToAnalytics(analyticsHandler, it) }
}
suspend fun createProductWallet(scanResponse: ScanResponse): CompletionResult<Card> {
return runTaskAsync(
CreateProductWalletAndRescanTask(scanResponse.productType),
CreateProductWalletTask(scanResponse.productType),
scanResponse.card.cardId,
Message(context.getString(R.string.initial_message_create_wallet_body))
)
@ -66,6 +72,14 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
initialMessage = Message(context.getString(R.string.initial_message_create_wallet_body)))
}
suspend fun derivePublicKeys(
cardId: String,
walletPublicKey: ByteArray,
derivationPaths: List<DerivationPath>
): CompletionResult<ExtendedPublicKeyList> {
return runTaskAsyncReturnOnMain(DeriveWalletPublicKeysTask(walletPublicKey, derivationPaths), cardId)
}
suspend fun resetToFactorySettings(card: Card): CompletionResult<Card> {
return runTaskAsyncReturnOnMain(
ResetToFactorySettingsTask(),
@ -104,13 +118,13 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
suspend fun <T : CommandResponse> runTaskAsync(
runnable: CardSessionRunnable<T>, cardId: String? = null, initialMessage: Message? = null,
): CompletionResult<T> =
withContext(Dispatchers.Main) {
suspendCoroutine { continuation ->
tangemSdk.startSessionWithRunnable(runnable, cardId, initialMessage) { result ->
continuation.resume(result)
}
withContext(Dispatchers.Main) {
suspendCoroutine { continuation ->
tangemSdk.startSessionWithRunnable(runnable, cardId, initialMessage) { result ->
continuation.resume(result)
}
}
}
private suspend fun <T : CommandResponse> runTaskAsyncReturnOnMain(
runnable: CardSessionRunnable<T>, cardId: String? = null, initialMessage: Message? = null,

View file

@ -3,6 +3,7 @@ package com.tangem.tap.domain
import com.tangem.Message
import com.tangem.TangemSdk
import com.tangem.blockchain.common.TransactionSigner
import com.tangem.blockchain.common.Wallet
import com.tangem.common.CompletionResult
import com.tangem.tap.domain.tasks.SignHashTask
import com.tangem.tap.domain.tasks.SignHashesTask
@ -15,13 +16,9 @@ class TangemSigner(
private val signerCallback: (TangemSignerResponse) -> Unit,
) : TransactionSigner {
override suspend fun sign(
hash: ByteArray,
cardId: String,
walletPublicKey: ByteArray,
): CompletionResult<ByteArray> =
suspendCoroutine { continuation ->
val command = SignHashTask(hash, walletPublicKey)
override suspend fun sign(hash: ByteArray, cardId: String, publicKey: Wallet.PublicKey): CompletionResult<ByteArray> {
return suspendCoroutine { continuation ->
val command = SignHashTask(hash, publicKey)
tangemSdk.startSessionWithRunnable(
runnable = command,
cardId = cardId,
@ -42,15 +39,11 @@ class TangemSigner(
}
}
}
}
override suspend fun sign(
hashes: List<ByteArray>,
cardId: String,
walletPublicKey: ByteArray,
): CompletionResult<List<ByteArray>> =
suspendCoroutine { continuation ->
val task = SignHashesTask(hashes, walletPublicKey)
override suspend fun sign(hashes: List<ByteArray>, cardId: String, publicKey: Wallet.PublicKey): CompletionResult<List<ByteArray>> {
return suspendCoroutine { continuation ->
val task = SignHashesTask(hashes, publicKey)
tangemSdk.startSessionWithRunnable(
runnable = task,
cardId = cardId,
@ -71,6 +64,7 @@ class TangemSigner(
}
}
}
}
}
data class TangemSignerResponse(

View file

@ -1,7 +1,6 @@
package com.tangem.tap.domain
import com.tangem.blockchain.common.*
import com.tangem.common.card.Card
import com.tangem.common.services.Result
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
@ -16,7 +15,6 @@ import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.extensions.*
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.domain.tokens.CardCurrencies
import com.tangem.tap.features.tokens.redux.TokensAction
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.NetworkConnectivity
@ -85,12 +83,6 @@ class TapWalletManager {
store.dispatch(GlobalAction.SaveScanNoteResponse(data))
store.dispatch(WalletAction.SetIfTestnetCard(data.card.isTestCard))
store.dispatch(WalletAction.MultiWallet.SetIsMultiwalletAllowed(data.card.isMultiwalletAllowed))
val blockchain = data.getBlockchain()
if (blockchain == Blockchain.Ethereum ||
blockchain == Blockchain.EthereumTestnet) {
store.dispatch(TokensAction.LoadCardTokens)
}
loadData(data)
}
}
@ -131,7 +123,7 @@ class TapWalletManager {
store.dispatch(WalletAction.MultiWallet.SetPrimaryToken(primaryToken))
}
if (data.card.isMultiwalletAllowed) {
loadMultiWalletData(data.card, blockchain, primaryWalletManager)
loadMultiWalletData(data, blockchain, primaryWalletManager)
} else {
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(primaryWalletManager))
store.dispatch(WalletAction.MultiWallet.AddBlockchains(listOf(blockchain)))
@ -139,7 +131,7 @@ class TapWalletManager {
} else {
if (data.card.isMultiwalletAllowed) {
loadMultiWalletData(data.card, blockchain, null)
loadMultiWalletData(data, blockchain, null)
}
}
val moonPayStatus = store.state.globalState.moonpayStatus
@ -149,10 +141,10 @@ class TapWalletManager {
}
private fun loadMultiWalletData(
card: Card, primaryBlockchain: Blockchain?, primaryWalletManager: WalletManager?
scanResponse: ScanResponse, primaryBlockchain: Blockchain?, primaryWalletManager: WalletManager?
) {
val primaryTokens = primaryWalletManager?.cardTokens?.toList() ?: emptyList()
val savedCurrencies = currenciesRepository.loadCardCurrencies(card.cardId)
val savedCurrencies = currenciesRepository.loadCardCurrencies(scanResponse.card.cardId)
if (savedCurrencies == null) {
if (primaryBlockchain != null && primaryWalletManager != null) {
@ -169,11 +161,11 @@ class TapWalletManager {
CardCurrencies(blockchains = blockchains, tokens = emptyList())
))
val walletManagers =
walletManagerFactory.makeWalletManagersForApp(card, blockchains.toList())
walletManagerFactory.makeWalletManagersForApp(scanResponse, blockchains.toList())
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(walletManagers))
store.dispatch(WalletAction.MultiWallet.AddBlockchains(blockchains.toList()))
}
store.dispatch(WalletAction.MultiWallet.FindBlockchainsInUse(card, walletManagerFactory))
store.dispatch(WalletAction.MultiWallet.FindBlockchainsInUse)
store.dispatch(WalletAction.MultiWallet.FindTokensInUse)
} else {
val blockchains = savedCurrencies.blockchains.toList()
@ -182,10 +174,10 @@ class TapWalletManager {
primaryWalletManager != null && primaryBlockchain != null
) {
val blockchainsWithoutPrimary = blockchains.filterNot { it == primaryBlockchain }
walletManagerFactory.makeWalletManagersForApp(card, blockchainsWithoutPrimary)
walletManagerFactory.makeWalletManagersForApp(scanResponse, blockchainsWithoutPrimary)
.plus(primaryWalletManager)
} else {
walletManagerFactory.makeWalletManagersForApp(card, blockchains)
walletManagerFactory.makeWalletManagersForApp(scanResponse, blockchains)
}
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(walletManagers))

View file

@ -3,74 +3,70 @@ package com.tangem.tap.domain.extensions
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.common.card.Card
import com.tangem.common.card.CardWallet
import com.tangem.common.card.EllipticCurve
import com.tangem.common.extensions.hexToBytes
import com.tangem.tap.common.extensions.toMapKey
import com.tangem.tap.domain.TapWorkarounds.isTestCard
import com.tangem.tap.domain.tasks.product.ScanResponse
fun WalletManagerFactory.makeWalletManagerForApp(
card: Card,
blockchain: Blockchain,
scanResponse: ScanResponse, blockchain: Blockchain
): WalletManager? {
val card = scanResponse.card
if (card.isTestCard && blockchain.getTestnetVersion() == null) return null
val supportedCurves = blockchain.getSupportedCurves() ?: return null
val wallets = card.wallets.filter { wallet -> supportedCurves.contains(wallet.curve) }
val wallet = selectWallet(wallets)
val publicKey = wallet?.publicKey ?: return null
val curveToUse = wallet.curve ?: return null
return if (card.isTestCard) {
blockchain.getTestnetVersion()?.let {
makeWalletManager(card.cardId, publicKey, it, curveToUse)
val wallet = selectWallet(wallets) ?: return null
val environmentBlockchain = if (card.isTestCard) blockchain.getTestnetVersion()!! else blockchain
val seedKey = wallet.extendedPublicKey
return when {
scanResponse.isTangemTwins() && scanResponse.secondTwinPublicKey != null -> {
makeTwinWalletManager(
card.cardId,
wallet.publicKey, scanResponse.secondTwinPublicKey.hexToBytes(),
environmentBlockchain, wallet.curve
)
}
seedKey != null -> {
val derivedKeys = scanResponse.derivedKeys[wallet.publicKey.toMapKey()]
val derivedKey = derivedKeys?.firstOrNull { it.derivationPath == blockchain.derivationPath() }
?: return null
makeWalletManager(card.cardId, environmentBlockchain, seedKey, derivedKey)
}
else -> {
makeWalletManager(card.cardId, environmentBlockchain, wallet.publicKey, wallet.curve)
}
} else {
makeWalletManager(card.cardId, publicKey, blockchain, curveToUse)
}
}
fun WalletManagerFactory.makeWalletManagersForApp(
scanResponse: ScanResponse, blockchains: List<Blockchain>,
): List<WalletManager> {
val isTestCard = scanResponse.card.isTestCard
val filteredBlockchains = blockchains.mapNotNull { if (isTestCard) it.getTestnetVersion() else it }
return filteredBlockchains.mapNotNull { makeWalletManagerForApp(scanResponse, it) }
}
fun WalletManagerFactory.makePrimaryWalletManager(
scanResponse: ScanResponse,
): WalletManager? {
val blockchain = if (scanResponse.card.isTestCard) {
scanResponse.getBlockchain().getTestnetVersion() ?: return null
} else {
scanResponse.getBlockchain()
}
return makeWalletManagerForApp(scanResponse, blockchain)
}
private fun selectWallet(wallets: List<CardWallet>): CardWallet? {
return when (wallets.size) {
0 -> null
1 -> wallets[0]
else -> wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: wallets[0]
}
}
fun WalletManagerFactory.makeWalletManagersForApp(
card: Card, blockchains: List<Blockchain>,
): List<WalletManager> {
return if (card.isTestCard) {
blockchains.mapNotNull { blockchain ->
blockchain.getTestnetVersion()?.let { makeWalletManagerForApp(card, it) }
}
} else {
blockchains.mapNotNull { blockchain -> makeWalletManagerForApp(card, blockchain) }
}
}
fun WalletManagerFactory.makePrimaryWalletManager(
data: ScanResponse,
): WalletManager? {
val card = data.card
val blockchain = if (card.isTestCard) {
data.getBlockchain().getTestnetVersion()
} else {
data.getBlockchain()
}
val supportedCurves = blockchain?.getSupportedCurves() ?: return null
val wallets = card.wallets.filter { wallet -> supportedCurves.contains(wallet.curve) }
val wallet = selectWallet(wallets)
val publicKey = wallet?.publicKey ?: return null
val curveToUse = wallet.curve
return if (data.isTangemTwins() && data.secondTwinPublicKey != null) {
makeMultisigWalletManager(
cardId = card.cardId,
walletPublicKey = publicKey, pairPublicKey = data.secondTwinPublicKey.hexToBytes(),
blockchain = blockchain, curve = curveToUse
)
} else {
makeWalletManager(card.cardId, publicKey, blockchain, curveToUse)
}
}

View file

@ -9,7 +9,7 @@ import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.operations.PreflightReadMode
import com.tangem.operations.PreflightReadTask
import com.tangem.operations.wallet.CreateWalletCommand
import com.tangem.operations.wallet.CreateWalletTask
@Deprecated("Use CreateProductWalletAndRescanTask instead")
class CreateWalletAndRescanTask : CardSessionRunnable<Card> {
@ -22,7 +22,7 @@ class CreateWalletAndRescanTask : CardSessionRunnable<Card> {
val firmwareVersion = card.firmwareVersion
val task = if (firmwareVersion < FirmwareVersion.MultiWalletAvailable) {
CreateWalletCommand(card.supportedCurves.first())
CreateWalletTask(card.supportedCurves.first())
} else {
CreateWalletsTask()
}

View file

@ -7,9 +7,9 @@ import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.operations.PreflightReadMode
import com.tangem.operations.PreflightReadTask
import com.tangem.operations.wallet.CreateWalletCommand
import com.tangem.operations.wallet.CreateWalletTask
@Deprecated("Use CreateProductWalletsResponse instead")
@Deprecated("Use CreateProductWalletTask instead")
class CreateWalletsTask(curves: List<EllipticCurve>? = null) : CardSessionRunnable<Card> {
private val curves = curves ?: listOf(
@ -30,7 +30,7 @@ class CreateWalletsTask(curves: List<EllipticCurve>? = null) : CardSessionRunnab
callback: (result: CompletionResult<Card>) -> Unit
) {
CreateWalletCommand(curve).run(session) { result ->
CreateWalletTask(curve).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
if (index == curves.lastIndex) {

View file

@ -1,5 +1,6 @@
package com.tangem.tap.domain.tasks
import com.tangem.blockchain.common.Wallet
import com.tangem.common.CompletionResult
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
@ -15,17 +16,17 @@ class TangemSignHashResponse(
class SignHashTask(
private val hash: ByteArray,
private val walletPublicKey: ByteArray,
private val publicKey: Wallet.PublicKey,
) : CardSessionRunnable<TangemSignHashResponse> {
override fun run(session: CardSession, callback: CompletionCallback<TangemSignHashResponse>) {
SignHashCommand(hash, walletPublicKey).run(session) { response ->
SignHashCommand(hash, publicKey.seedKey, publicKey.derivationPath).run(session) { response ->
when (response) {
is CompletionResult.Success -> {
callback(CompletionResult.Success(TangemSignHashResponse(
response.data.signature,
response.data.totalSignedHashes,
session.environment.card?.wallet(walletPublicKey)?.remainingSignatures
session.environment.card?.wallet(publicKey.seedKey)?.remainingSignatures
)))
}
is CompletionResult.Failure ->

View file

@ -1,5 +1,6 @@
package com.tangem.tap.domain.tasks
import com.tangem.blockchain.common.Wallet
import com.tangem.common.CompletionResult
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
@ -15,16 +16,16 @@ class TangemSignHashesResponse(
class SignHashesTask(
private val hashes: Collection<ByteArray>,
private val walletPublicKey: ByteArray,
private val publicKey: Wallet.PublicKey,
) : CardSessionRunnable<TangemSignHashesResponse> {
override fun run(session: CardSession, callback: CompletionCallback<TangemSignHashesResponse>) {
SignHashesCommand(hashes.toTypedArray(), walletPublicKey).run(session) { response ->
SignHashesCommand(hashes.toTypedArray(), publicKey.seedKey, publicKey.derivationPath).run(session) { response ->
when (response) {
is CompletionResult.Success -> {
callback(CompletionResult.Success(TangemSignHashesResponse(
response.data.signatures,
response.data.totalSignedHashes,
session.environment.card?.wallet(walletPublicKey)?.remainingSignatures
session.environment.card?.wallet(publicKey.seedKey)?.remainingSignatures
)))
}
is CompletionResult.Failure ->

View file

@ -1,25 +1,29 @@
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.PreflightReadMode
import com.tangem.operations.PreflightReadTask
import com.tangem.operations.wallet.CreateWalletCommand
import com.tangem.operations.derivation.DeriveWalletPublicKeysTask
import com.tangem.operations.derivation.ExtendedPublicKeyList
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.product.CreateProductWalletsTask
import com.tangem.tap.domain.tasks.product.CreateWalletsTask
import com.tangem.tap.domain.tasks.product.ProductCommandProcessor
import com.tangem.tap.domain.tasks.product.getCurvesForNonCreatedWallets
import com.tangem.tap.store
/**
[REDACTED_AUTHOR]
*/
class CreateProductWalletAndRescanTask(
class CreateProductWalletTask(
private val type: ProductType,
) : CardSessionRunnable<Card> {
@ -28,33 +32,19 @@ class CreateProductWalletAndRescanTask(
callback(CompletionResult.Failure(TangemSdkError.CardError()))
return
}
CreateProductWalletTask(card, type).run(session) { result ->
when (result) {
is CompletionResult.Success -> PreflightReadTask(PreflightReadMode.FullCardRead).run(
session,
callback)
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
}
private class CreateProductWalletTask(
private val card: Card,
private val type: ProductType,
) : CardSessionRunnable<CreateWalletResponse> {
override fun run(
session: CardSession,
callback: (result: CompletionResult<CreateWalletResponse>) -> Unit,
) {
val commandProcessor = when (type) {
ProductType.Note -> CreateWalletTangemNote()
ProductType.Twins -> throw UnsupportedOperationException("Use the TwinCardsManager to create a wallet")
ProductType.Wallet -> CreateWalletTangemWallet()
else -> CreateWalletOtherCards()
}
commandProcessor.proceed(card, session, callback)
commandProcessor.proceed(card, session) {
when (it) {
is CompletionResult.Success -> callback(CompletionResult.Success(session.environment.card!!))
is CompletionResult.Failure -> callback(CompletionResult.Failure(it.error))
}
}
}
}
@ -84,13 +74,13 @@ private class CreateWalletTangemNote : ProductCommandProcessor<CreateWalletRespo
} else {
intersectCurves[0]
}
CreateWalletCommand(curve).run(session, callback)
CreateWalletTask(curve).run(session, callback)
}
}
}
private class CreateWalletTangemWallet : ProductCommandProcessor<CreateWalletResponse> {
override fun proceed(
card: Card,
session: CardSession,
@ -98,16 +88,37 @@ private class CreateWalletTangemWallet : ProductCommandProcessor<CreateWalletRes
) {
val supportedCurves = setOf(EllipticCurve.Secp256k1, EllipticCurve.Ed25519)
val curves = card.getCurvesForNonCreatedWallets().intersect(supportedCurves).toList()
CreateProductWalletsTask(curves).run(session) { result ->
CreateWalletsTask(curves).run(session) { result ->
when (result) {
is CompletionResult.Success ->
callback(CompletionResult.Success(result.data.createWalletResponses[0]))
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))
}
}
}
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<CreateWalletResponse> {
override fun proceed(
@ -117,9 +128,9 @@ private class CreateWalletOtherCards : ProductCommandProcessor<CreateWalletRespo
) {
val firmwareVersion = card.firmwareVersion
val task = if (firmwareVersion < FirmwareVersion.MultiWalletAvailable) {
CreateProductWalletsTask(listOf(card.supportedCurves.first()))
CreateWalletsTask(listOf(card.supportedCurves.first()))
} else {
CreateProductWalletsTask(card.getCurvesForNonCreatedWallets())
CreateWalletsTask(card.getCurvesForNonCreatedWallets())
}
task.run(session) { result ->

View file

@ -5,23 +5,23 @@ import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.operations.CommandResponse
import com.tangem.operations.wallet.CreateWalletCommand
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.operations.wallet.CreateWalletTask
/**
[REDACTED_AUTHOR]
*/
class CreateProductWalletsResponse(
class CreateWalletsResponse(
val createWalletResponses: List<CreateWalletResponse>
) : CommandResponse
class CreateProductWalletsTask(
class CreateWalletsTask(
private val curves: List<EllipticCurve>,
) : CardSessionRunnable<CreateProductWalletsResponse> {
) : CardSessionRunnable<CreateWalletsResponse> {
private val createdWalletsResponses = mutableListOf<CreateWalletResponse>()
override fun run(session: CardSession, callback: (result: CompletionResult<CreateProductWalletsResponse>) -> Unit) {
override fun run(session: CardSession, callback: (result: CompletionResult<CreateWalletsResponse>) -> Unit) {
val curve = curves[createdWalletsResponses.size]
createWallet(curve, session, callback)
}
@ -29,15 +29,14 @@ class CreateProductWalletsTask(
private fun createWallet(
curve: EllipticCurve,
session: CardSession,
callback: (result: CompletionResult<CreateProductWalletsResponse>) -> Unit
callback: (result: CompletionResult<CreateWalletsResponse>) -> Unit
) {
CreateWalletCommand(curve).run(session) { result ->
CreateWalletTask(curve).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
createdWalletsResponses.add(result.data)
if (createdWalletsResponses.size == curves.size) {
callback(CompletionResult.Success(CreateProductWalletsResponse(createdWalletsResponses)))
callback(CompletionResult.Success(CreateWalletsResponse(createdWalletsResponses)))
return@run
}
createWallet(curves[createdWalletsResponses.size], session, callback)

View file

@ -40,7 +40,7 @@ class ResetToFactorySettingsTask() : CardSessionRunnable<Card> {
) {
val backupStatus = session.environment.card?.backupStatus
if (backupStatus == null && backupStatus == Card.BackupStatus.NoBackup) {
if (backupStatus == null || backupStatus == Card.BackupStatus.NoBackup) {
callback(CompletionResult.Success(session.environment.card!!))
return
}

View file

@ -14,11 +14,16 @@ import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.toHexString
import com.tangem.common.hdWallet.DerivationPath
import com.tangem.common.hdWallet.ExtendedPublicKey
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.issuerAndUserData.ReadIssuerDataCommand
import com.tangem.tap.common.extensions.ByteArrayKey
import com.tangem.tap.common.extensions.toMapKey
import com.tangem.tap.domain.ProductType
import com.tangem.tap.domain.TapSdkError
import com.tangem.tap.domain.TapWorkarounds
@ -26,6 +31,7 @@ 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.getSingleWallet
import com.tangem.tap.domain.tokens.CurrenciesRepository
import com.tangem.tap.domain.twins.TwinsHelper
data class ScanResponse(
@ -33,6 +39,7 @@ data class ScanResponse(
val productType: ProductType,
val walletData: WalletData?,
val secondTwinPublicKey: String? = null,
val derivedKeys: Map<KeyWalletPublicKey, List<ExtendedPublicKey>> = mapOf()
) : CommandResponse {
fun getBlockchain(): Blockchain {
@ -60,7 +67,13 @@ data class ScanResponse(
fun twinsIsTwinned(): Boolean = card.isTangemTwins() && walletData != null && secondTwinPublicKey != null
}
class ScanProductTask(val card: Card? = null) : CardSessionRunnable<ScanResponse> {
typealias KeyWalletPublicKey = ByteArrayKey
class ScanProductTask(
val card: Card? = null,
private val currenciesRepository: CurrenciesRepository?,
private val shouldDeriveWC: Boolean
) : CardSessionRunnable<ScanResponse> {
override fun run(
session: CardSession,
@ -80,7 +93,7 @@ class ScanProductTask(val card: Card? = null) : CardSessionRunnable<ScanResponse
val commandProcessor = when {
TapWorkarounds.isTangemNote(card) -> ScanNoteProcessor()
card.isTangemTwins() -> ScanTwinProcessor()
TapWorkarounds.isTangemWallet(card) -> ScanWalletProcessor()
TapWorkarounds.isTangemWallet(card) -> ScanWalletProcessor(currenciesRepository, shouldDeriveWC)
else -> ScanOtherCardsProcessor()
}
commandProcessor.proceed(card, session) { processorResult ->
@ -116,13 +129,60 @@ private class ScanNoteProcessor : ProductCommandProcessor<ScanResponse> {
}
}
private class ScanWalletProcessor : ProductCommandProcessor<ScanResponse> {
private class ScanWalletProcessor(
private val currenciesRepository: CurrenciesRepository?,
private val shouldDeriveWC: Boolean
) : ProductCommandProcessor<ScanResponse> {
override fun proceed(
card: Card,
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit
) {
callback(CompletionResult.Success(ScanResponse(card, ProductType.Wallet, session.environment.walletData)))
val derivationPaths = collectDerivationPaths(card)?.distinct()
val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 }
if (derivationPaths.isNullOrEmpty() || wallet == null || wallet.chainCode == null) {
callback(CompletionResult.Success(ScanResponse(card, ProductType.Wallet, session.environment.walletData)))
return
}
DeriveWalletPublicKeysTask(wallet.publicKey, derivationPaths).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
)
callback(CompletionResult.Success(response))
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
private fun collectDerivationPaths(card: Card): List<DerivationPath>? {
val currenciesRepository = currenciesRepository ?: return null
val cardCurrencies = currenciesRepository.loadCardCurrencies(card.cardId)
val blockchainsToDerive = if (cardCurrencies == null) {
mutableListOf(Blockchain.Bitcoin, Blockchain.Ethereum)
} else {
val tokenBlockchains = cardCurrencies.tokens.map { it.blockchain }
(cardCurrencies.blockchains + tokenBlockchains).toMutableList()
}
if (shouldDeriveWC) {
blockchainsToDerive.addAll(listOf(Blockchain.Ethereum, Blockchain.Binance, Blockchain.EthereumTestnet))
}
return blockchainsToDerive.toSet()
.filter { it.getSupportedCurves()?.contains(EllipticCurve.Secp256k1) == true }
.mapNotNull { it.derivationPath() }
}
}
@ -174,7 +234,7 @@ private class ScanOtherCardsProcessor : ProductCommandProcessor<ScanResponse> {
return
}
CreateProductWalletsTask(curvesToCreate).run(session) { result ->
CreateWalletsTask(curvesToCreate).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
PreflightReadTask(PreflightReadMode.FullCardRead, card.cardId).run(session) { readResult ->

View file

@ -35,7 +35,7 @@ class TopUpManager {
store.dispatch(
GlobalAction.UpdateWalletSignedHashes(
walletSignedHashes = signResponse.totalSignedHashes,
walletPublicKey = walletManager.wallet.publicKey,
walletPublicKey = walletManager.wallet.publicKey.seedKey,
remainingSignatures = signResponse.remainingSignatures
)
)

View file

@ -4,8 +4,8 @@ import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.operations.wallet.CreateWalletCommand
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.operations.wallet.CreateWalletTask
import com.tangem.operations.wallet.PurgeWalletCommand
import com.tangem.tap.domain.extensions.getSingleWallet
@ -20,15 +20,13 @@ class CreateFirstTwinWalletTask : CardSessionRunnable<CreateWalletResponse> {
when (response) {
is CompletionResult.Success -> {
session.environment.card = session.environment.card?.setWallets(emptyList())
CreateWalletCommand(EllipticCurve.Secp256k1)
.run(session) { callback(it) }
CreateWalletTask(EllipticCurve.Secp256k1).run(session) { callback(it) }
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(response.error))
}
}
} else {
CreateWalletCommand(EllipticCurve.Secp256k1)
.run(session) { callback(it) }
CreateWalletTask(EllipticCurve.Secp256k1).run(session) { callback(it) }
}
}
}

View file

@ -8,8 +8,8 @@ import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.TangemError
import com.tangem.common.extensions.hexToBytes
import com.tangem.operations.wallet.CreateWalletCommand
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.operations.wallet.CreateWalletTask
import com.tangem.operations.wallet.PurgeWalletCommand
import com.tangem.tap.domain.extensions.getSingleWallet
import com.tangem.wallet.R
@ -47,7 +47,7 @@ class CreateSecondTwinWalletTask(
private fun finishTask(session: CardSession, callback: (result: CompletionResult<CreateWalletResponse>) -> Unit) {
session.setMessage(creatingWalletMessage)
CreateWalletCommand(EllipticCurve.Secp256k1).run(session) { result ->
CreateWalletTask(EllipticCurve.Secp256k1).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
session.environment.card = session.environment.card?.updateWallet(result.data.wallet)

View file

@ -23,7 +23,7 @@ class FinalizeTwinTask(
PreflightReadTask(PreflightReadMode.FullCardRead).run(session) { readResult ->
when (readResult) {
is CompletionResult.Success ->
ScanProductTask(readResult.data).run(session, callback)
ScanProductTask(readResult.data, null, false).run(session, callback)
is CompletionResult.Failure ->
callback(CompletionResult.Failure(readResult.error))
}

View file

@ -2,7 +2,6 @@ package com.tangem.tap.domain.walletconnect
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.hexToBytes
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.global.GlobalAction
@ -97,10 +96,10 @@ class WalletConnectManager {
val activeData = sessions[session] ?: return
removeSimilarSessions(activeData)
val key = activeData.wallet.derivedPublicKey ?: activeData.wallet.walletPublicKey
val accounts = listOf(Blockchain.Ethereum.makeAddresses(key).first().value)
val approved = activeData.client.approveSession(
accounts = listOf(Blockchain.Ethereum.makeAddresses(
activeData.wallet.walletPublicKey.hexToBytes()).first().value
),
accounts = accounts,
chainId = activeData.wallet.chainId
)
if (approved) {

View file

@ -5,14 +5,12 @@ import com.tangem.blockchain.blockchains.ethereum.EthereumGasLoader
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.Companion.toKeccak
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.TransactionSender
import com.tangem.blockchain.common.*
import com.tangem.blockchain.extensions.*
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.hexToBytes
import com.tangem.crypto.CryptoUtils
import com.tangem.operations.sign.SignHashCommand
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.common.extensions.toFormattedString
@ -36,9 +34,14 @@ class WalletConnectSdkHelper {
type: WcTransactionType,
): WcTransactionData? {
val factory = store.state.globalState.tapWalletManager.walletManagerFactory
val publicKey = Wallet.PublicKey(
session.wallet.walletPublicKey,
session.wallet.derivedPublicKey,
session.wallet.derivationPath,
)
val walletManager = factory.makeEthereumWalletManager(
session.wallet.cardId,
session.wallet.walletPublicKey.hexToBytes(),
publicKey,
emptyList(),
isTestNet = session.wallet.isTestNet
) ?: return null
@ -151,7 +154,8 @@ class WalletConnectSdkHelper {
val command = SignHashCommand(
hash = dataToSign.hash,
walletPublicKey = data.walletManager.wallet.publicKey
walletPublicKey = data.walletManager.wallet.publicKey.seedKey,
derivationPath = data.walletManager.wallet.publicKey.derivationPath
)
val result = tangemSdkManager.runTaskAsync(command, initialMessage = Message())
return when (result) {
@ -208,13 +212,13 @@ class WalletConnectSdkHelper {
}
suspend fun signPersonalMessage(hashToSign: ByteArray, wallet: WalletForSession): String? {
val publicKey = wallet.walletPublicKey.hexToBytes()
val command = SignHashCommand(hashToSign, publicKey)
val key = wallet.derivedPublicKey ?: wallet.walletPublicKey
val command = SignHashCommand(hashToSign, wallet.walletPublicKey, wallet.derivationPath)
return when (val result = tangemSdkManager.runTaskAsync(command, wallet.cardId)) {
is CompletionResult.Success -> {
val hash = result.data.signature
return EthereumUtils.prepareSignedMessageData(
hash, hashToSign, publicKey
hash, hashToSign, CryptoUtils.decompressPublicKey(key)
)
}
is CompletionResult.Failure -> {

View file

@ -184,16 +184,11 @@ private fun handleSecurityAction(
private fun prepareAllowedSecurityOptions(
card: Card?, currentSecurityOption: SecurityOption?,
): EnumSet<SecurityOption> {
val prohibitDefaultPin = card?.settings?.isResettingUserCodesAllowed != true
val allowedSecurityOptions = EnumSet.noneOf(SecurityOption::class.java)
val allowedSecurityOptions = EnumSet.of(SecurityOption.LongTap)
if (card?.isTangemTwin() == true) {
allowedSecurityOptions.add(SecurityOption.PassCode)
}
if ((currentSecurityOption == SecurityOption.LongTap) || !prohibitDefaultPin) {
allowedSecurityOptions.add(SecurityOption.LongTap)
}
if (currentSecurityOption == SecurityOption.AccessCode) {
allowedSecurityOptions.add(SecurityOption.AccessCode)
}

View file

@ -1,9 +1,7 @@
package com.tangem.tap.features.details.redux.walletconnect
import com.tangem.blockchain.common.*
import com.tangem.common.card.Card
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.toHexString
import com.tangem.tap.*
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.getFromClipboard
@ -14,6 +12,7 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.TapWorkarounds.isTestCard
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
import com.tangem.tap.domain.isMultiwalletAllowed
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.domain.walletconnect.WalletConnectManager
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.wallet.R
@ -128,24 +127,32 @@ class WalletConnectMiddleware {
}
private fun handleScanCard(wcUri: String) {
store.dispatch(GlobalAction.ScanCard({ scanResponse ->
store.dispatch(GlobalAction.ScanCard(true, { scanResponse ->
val card = scanResponse.card
if (!card.isMultiwalletAllowed) {
store.dispatchOnMain(WalletConnectAction.UnsupportedCard)
return@ScanCard
}
val walletManager = getWalletManager(card).guard {
val walletManager = getWalletManager(scanResponse).guard {
store.dispatchOnMain(WalletConnectAction.UnsupportedCard)
return@ScanCard
}
val key = walletManager.wallet.publicKey
val wallet = walletManager.wallet
val derivedKey = if (wallet.publicKey.blockchainKey.contentEquals(wallet.publicKey.seedKey)) {
null
} else {
walletManager.wallet.publicKey.blockchainKey
}
store.dispatchOnMain(WalletConnectAction.OpenSession(
wcUri = wcUri,
wallet = WalletForSession(
card.cardId, key.toHexString(),
isTestNet = card.isTestCard
card.cardId,
wallet.publicKey.seedKey,
derivedKey,
wallet.publicKey.derivationPath,
card.isTestCard
),
))
}, {
@ -153,26 +160,32 @@ class WalletConnectMiddleware {
}, R.string.wallet_connect_scan_card_message))
}
private fun getWalletManager(card: Card): WalletManager? {
private fun getWalletManager(scanResponse: ScanResponse): WalletManager? {
val card = scanResponse.card
val factory = store.state.globalState.tapWalletManager.walletManagerFactory
val wcBlockchain = if (scanResponse.card.isTestCard) {
Blockchain.EthereumTestnet
} else {
Blockchain.Ethereum
}
return if (store.state.globalState.scanResponse?.card?.cardId == card.cardId) {
store.state.walletState.getWalletManager(Blockchain.Ethereum)
?: factory.makeWalletManagerForApp(card, Blockchain.Ethereum)
store.state.walletState.getWalletManager(wcBlockchain)
?: factory.makeWalletManagerForApp(scanResponse, wcBlockchain)
?.also { walletManager ->
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(walletManager))
store.dispatch(WalletAction.MultiWallet.AddBlockchain(walletManager.wallet.blockchain))
}
} else {
if (currenciesRepository.loadCardCurrencies(card.cardId)?.blockchains?.contains(
Blockchain.Ethereum) == true
wcBlockchain) == true
) {
factory.makeWalletManagerForApp(card, Blockchain.Ethereum)
factory.makeWalletManagerForApp(scanResponse, wcBlockchain)
} else {
factory.makeWalletManagerForApp(card,
Blockchain.Ethereum)
factory.makeWalletManagerForApp(scanResponse, wcBlockchain)
?.also {
currenciesRepository.saveAddedBlockchain(card.cardId, Blockchain.Ethereum)
currenciesRepository.saveAddedBlockchain(card.cardId, wcBlockchain)
}
}
}

View file

@ -1,7 +1,9 @@
package com.tangem.tap.features.details.redux.walletconnect
import com.squareup.moshi.JsonClass
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.hdWallet.DerivationPath
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialogData
import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionRequestDialogData
@ -21,9 +23,12 @@ data class WalletConnectSession(
val peerMeta: WCPeerMeta,
)
@JsonClass(generateAdapter = true)
data class WalletForSession(
val cardId: String,
val walletPublicKey: String,
val walletPublicKey: ByteArray,
val derivedPublicKey: ByteArray?,
val derivationPath: DerivationPath?,
val isTestNet: Boolean = false,
) {
val chainId

View file

@ -27,8 +27,7 @@ class HomeMiddleware {
companion object {
val handler = homeMiddleware
const val CARD_SHOP_URI =
"https://shop.tangem.com/?afmc=1i&utm_campaign=1i&utm_source=leaddyno&utm_medium=affiliate"
const val CARD_SHOP_URI = "http://cards.tangem.com/"
}
}
@ -61,7 +60,7 @@ private fun handleReadCard() {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer))
} else {
changeButtonState(ButtonState.PROGRESS)
store.dispatch(GlobalAction.ScanCard({ scanResponse ->
store.dispatch(GlobalAction.ScanCard(false, { scanResponse ->
store.state.globalState.tapWalletManager.updateConfigManager(scanResponse)
store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))

View file

@ -42,7 +42,7 @@ private val onboardingNoteMiddleware: Middleware<AppState> = { dispatch, state -
private fun handleNoteAction(action: Action, dispatch: DispatchFunction) {
if (action !is OnboardingNoteAction) return
val globalState = store.state.globalState
val onboardingManager = globalState.onboardingManager ?: return
val onboardingManager = globalState.onboardingState.onboardingManager ?: return
val scanResponse = onboardingManager.scanResponse
val card = onboardingManager.scanResponse.card

View file

@ -37,7 +37,7 @@ private val onboardingOtherCardsMiddleware: Middleware<AppState> = { dispatch, s
private fun handleOtherCardsAction(action: Action, dispatch: DispatchFunction) {
if (action !is OnboardingOtherCardsAction) return
val globalState = store.state.globalState
val onboardingManager = globalState.onboardingManager ?: return
val onboardingManager = globalState.onboardingState.onboardingManager ?: return
val card = onboardingManager.scanResponse.card

View file

@ -45,7 +45,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
val action = action as? TwinCardsAction ?: return
val globalState = store.state.globalState
val onboardingManager = globalState.onboardingManager
val onboardingManager = globalState.onboardingState.onboardingManager
val twinCardsState = store.state.twinCardsState
fun getScanResponse(): ScanResponse {

View file

@ -51,6 +51,7 @@ sealed class BackupAction : Action {
data class SaveFirstAccessCode(val accessCode: String) : BackupAction()
object ShowReenterAccessCodeScreen : BackupAction()
data class SaveAccessCodeConfirmation(val accessCodeConfirmation: String) : BackupAction()
object OnAccessCodeDialogClosed : BackupAction()
data class PrepareToWriteBackupCard(val cardNumber: Int) : BackupAction()
data class WriteBackupCard(val cardNumber: Int) : BackupAction()
@ -60,6 +61,7 @@ sealed class BackupAction : Action {
object FinishBackup : BackupAction()
object DiscardBackup : BackupAction()
object DiscardSavedBackup : BackupAction()
object ResumeBackup : BackupAction()
}

View file

@ -12,9 +12,10 @@ import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.extensions.hasWallets
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.home.redux.HomeMiddleware
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteAction
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletMiddleware.Companion.BUY_WALLET_URL
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
@ -22,6 +23,8 @@ import org.rekotlin.Middleware
class OnboardingWalletMiddleware {
companion object {
val handler = onboardingWalletMiddleware
const val BUY_WALLET_URL = "https://wallet.tangem.com/"
}
}
@ -37,7 +40,7 @@ private val onboardingWalletMiddleware: Middleware<AppState> = { dispatch, state
private fun handleWalletAction(action: Action) {
if (action !is OnboardingWalletAction) return
val globalState = store.state.globalState
val onboardingManager = globalState.onboardingManager
val onboardingManager = globalState.onboardingState.onboardingManager
val scanResponse = onboardingManager?.scanResponse
val card = scanResponse?.card
@ -71,7 +74,9 @@ private fun handleWalletAction(action: Action) {
withMainContext {
when (result) {
is CompletionResult.Success -> {
val updatedResponse = scanResponse.copy(card = result.data)
//here we must use updated scanResponse after createWallet & derivation
val updatedResponse =
globalState.onboardingState.onboardingManager.scanResponse.copy(card = result.data)
onboardingManager.scanResponse = updatedResponse
onboardingManager.activationStarted(updatedResponse.card.cardId)
store.dispatch(OnboardingWalletAction.ProceedBackup)
@ -86,157 +91,192 @@ private fun handleWalletAction(action: Action) {
OnboardingWalletAction.FinishOnboarding -> {
store.dispatch(GlobalAction.Onboarding.Stop)
(listOf(walletState.backupState.primaryCardId) +
walletState.backupState.backupCardIds + scanResponse?.card?.cardId)
.distinct().filterNotNull()
.forEach { cardId ->
preferencesStorage.usedCardsPrefStorage.activationFinished(cardId)
}
if (scanResponse == null) {
store.dispatch(NavigationAction.PopBackTo())
store.dispatch(HomeAction.ReadCard)
} else {
scope.launch { globalState.tapWalletManager.onCardScanned(scanResponse) }
val backupState = store.state.onboardingWalletState.backupState
val updatedScanResponse = updateScanResponseAfterBackup(scanResponse, backupState)
scope.launch { globalState.tapWalletManager.onCardScanned(updatedScanResponse) }
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
}
}
OnboardingWalletAction.ProceedBackup -> {
val newAction = when (val backupState = backupService.currentState) {
BackupService.State.Preparing -> BackupAction.IntroduceBackup
BackupService.State.FinalizingPrimaryCard -> BackupAction.PrepareToWritePrimaryCard
is BackupService.State.FinalizingBackupCard ->
BackupAction.PrepareToWriteBackupCard(backupState.index)
BackupService.State.Finished -> BackupAction.FinishBackup
else -> BackupAction.IntroduceBackup
}
store.dispatch(newAction)
}
OnboardingWalletAction.OnBackPressed -> {
if (walletState.backupState.backupStep is BackupStep.WriteBackupCard ||
walletState.backupState.backupStep is BackupStep.WritePrimaryCard) {
store.dispatch(GlobalAction.ShowDialog(BackupDialog.BackupInProgress))
} else {
store.dispatch(NavigationAction.PopBackTo())
when (walletState.backupState.backupStep) {
BackupStep.InitBackup, BackupStep.Finished -> store.dispatch(NavigationAction.PopBackTo())
BackupStep.ScanOriginCard, BackupStep.AddBackupCards, BackupStep.EnterAccessCode,
BackupStep.ReenterAccessCode, BackupStep.SetAccessCode, BackupStep.WritePrimaryCard,
-> {
store.dispatch(BackupAction.DiscardBackup)
store.dispatch(NavigationAction.PopBackTo())
}
is BackupStep.WriteBackupCard ->
store.dispatch(GlobalAction.ShowDialog(BackupDialog.BackupInProgress))
}
}
}
}
private fun updateScanResponseAfterBackup(
scanResponse: ScanResponse, backupState: BackupState,
): ScanResponse {
val card = if (backupState.backupCardsNumber > 0) {
val cardsCount = backupState.backupCardsNumber
scanResponse.card.copy(
backupStatus = Card.BackupStatus.Active(cardCount = cardsCount),
isAccessCodeSet = true
)
} else {
scanResponse.card
}
return scanResponse.copy(card = card)
}
class BackupMiddleware {
val backupMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
val backupState = state()?.onboardingWalletState?.backupState
when (action) {
is BackupAction.StartBackup -> {
backupService.discardSavedBackup()
}
is BackupAction.ScanPrimaryCard -> {
backupService.readPrimaryCard { result ->
when (result) {
is CompletionResult.Success -> {
store.dispatchOnMain(BackupAction.StartAddingBackupCards)
}
is CompletionResult.Failure -> {
}
}
}
}
is BackupAction.AddBackupCard -> {
backupService.addBackupCard { result ->
when (result) {
is CompletionResult.Success -> {
store.dispatchOnMain(BackupAction.AddBackupCard.Success)
}
is CompletionResult.Failure -> {
}
}
}
}
is BackupAction.GoToShop -> {
store.dispatchOpenUrl(HomeMiddleware.CARD_SHOP_URI)
}
is BackupAction.FinishAddingBackupCards -> {
if (backupService.addedBackupCardsCount == 1) {
store.dispatchOnMain(GlobalAction.ShowDialog(BackupDialog.BuyMoreBackupCards))
}
if (backupService.addedBackupCardsCount == 2) {
store.dispatchOnMain(BackupAction.ShowAccessCodeInfoScreen)
}
}
is BackupAction.CheckAccessCode -> {
if (action.accessCode.length < 4) {
store.dispatch(BackupAction.SetAccessCodeError(
AccessCodeError.CodeTooShort
))
} else {
store.dispatch(BackupAction.SaveFirstAccessCode(action.accessCode))
}
}
is BackupAction.SaveAccessCodeConfirmation -> {
if (action.accessCodeConfirmation == backupState?.accessCode) {
backupService.setAccessCode(action.accessCodeConfirmation)
store.dispatch(BackupAction.PrepareToWritePrimaryCard)
} else {
store.dispatch(BackupAction.SetAccessCodeError(
AccessCodeError.CodesDoNotMatch
))
}
}
is BackupAction.WritePrimaryCard -> {
backupService.proceedBackup { result ->
when (result) {
is CompletionResult.Success -> {
store.dispatchOnMain(
BackupAction.PrepareToWriteBackupCard(1)
)
}
is CompletionResult.Failure -> {
}
}
}
}
is BackupAction.WriteBackupCard -> {
backupService.proceedBackup { result ->
when (result) {
is CompletionResult.Success -> {
if (backupService.currentState == BackupService.State.Finished) {
store.dispatchOnMain(BackupAction.FinishBackup)
} else {
store.dispatchOnMain(
BackupAction.PrepareToWriteBackupCard(action.cardNumber + 1)
)
}
}
is CompletionResult.Failure -> {
}
}
}
}
is BackupAction.FinishBackup -> {
// store.dispatch(OnboardingWalletAction.Done)
}
is BackupAction.DiscardBackup -> {
backupService.discardSavedBackup()
}
is BackupAction.CheckForUnfinishedBackup -> {
if (backupService.hasIncompletedBackup) {
store.dispatch(GlobalAction.ShowDialog(BackupDialog.UnfinishedBackupFound))
}
}
is BackupAction.ResumeBackup -> {
store.dispatch(OnboardingWalletAction.ProceedBackup)
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
}
is BackupAction.DismissBackup -> {
store.dispatch(BackupAction.FinishBackup)
}
}
if (action is BackupAction) handleBackupAction(action)
next(action)
}
}
}
}
private fun handleBackupAction(action: BackupAction) {
val backupState = store.state.onboardingWalletState.backupState
val globalState = store.state.globalState
val onboardingManager = globalState.onboardingState.onboardingManager
val scanResponse = onboardingManager?.scanResponse
val card = scanResponse?.card
when (action) {
is BackupAction.StartBackup -> {
backupService.discardSavedBackup()
}
is BackupAction.ScanPrimaryCard -> {
backupService.readPrimaryCard(cardId = card?.cardId) { result ->
when (result) {
is CompletionResult.Success -> {
store.dispatchOnMain(BackupAction.StartAddingBackupCards)
}
is CompletionResult.Failure -> {
}
}
}
}
is BackupAction.AddBackupCard -> {
backupService.addBackupCard { result ->
when (result) {
is CompletionResult.Success -> {
store.dispatchOnMain(BackupAction.AddBackupCard.Success)
}
is CompletionResult.Failure -> {
}
}
}
}
is BackupAction.GoToShop -> {
store.dispatchOpenUrl(BUY_WALLET_URL)
}
is BackupAction.FinishAddingBackupCards -> {
if (backupService.addedBackupCardsCount == 1) {
store.dispatchOnMain(GlobalAction.ShowDialog(BackupDialog.AddMoreBackupCards))
}
if (backupService.addedBackupCardsCount == 2) {
store.dispatchOnMain(BackupAction.ShowAccessCodeInfoScreen)
}
}
is BackupAction.CheckAccessCode -> {
if (action.accessCode.length < 4) {
store.dispatch(BackupAction.SetAccessCodeError(
AccessCodeError.CodeTooShort
))
} else {
store.dispatch(BackupAction.SaveFirstAccessCode(action.accessCode))
}
}
is BackupAction.SaveAccessCodeConfirmation -> {
if (action.accessCodeConfirmation == backupState.accessCode) {
backupService.setAccessCode(action.accessCodeConfirmation)
store.dispatch(BackupAction.PrepareToWritePrimaryCard)
} else {
store.dispatch(BackupAction.SetAccessCodeError(
AccessCodeError.CodesDoNotMatch
))
}
}
is BackupAction.WritePrimaryCard -> {
backupService.proceedBackup { result ->
when (result) {
is CompletionResult.Success -> {
store.dispatchOnMain(
BackupAction.PrepareToWriteBackupCard(1)
)
}
is CompletionResult.Failure -> {
}
}
}
}
is BackupAction.WriteBackupCard -> {
backupService.proceedBackup { result ->
when (result) {
is CompletionResult.Success -> {
if (backupService.currentState == BackupService.State.Finished) {
store.dispatchOnMain(BackupAction.FinishBackup)
} else {
store.dispatchOnMain(
BackupAction.PrepareToWriteBackupCard(action.cardNumber + 1)
)
}
}
is CompletionResult.Failure -> {
}
}
}
}
is BackupAction.FinishBackup -> {
(listOf(backupState.primaryCardId, card?.cardId) + backupState.backupCardIds)
.distinct().filterNotNull()
.forEach { cardId ->
preferencesStorage.usedCardsPrefStorage.activationFinished(cardId)
}
}
is BackupAction.DiscardBackup -> {
backupService.discardSavedBackup()
}
is BackupAction.DiscardSavedBackup -> {
backupService.primaryCardId?.let {
preferencesStorage.usedCardsPrefStorage.activationFinished(it)
}
backupService.discardSavedBackup()
}
is BackupAction.CheckForUnfinishedBackup -> {
if (backupService.hasIncompletedBackup) {
store.dispatch(GlobalAction.ShowDialog(BackupDialog.UnfinishedBackupFound))
}
}
is BackupAction.ResumeBackup -> {
store.dispatch(GlobalAction.Onboarding.Start(null, true))
store.dispatch(OnboardingWalletAction.ProceedBackup)
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
}
is BackupAction.DismissBackup -> {
store.dispatch(BackupAction.FinishBackup)
}
}
}

View file

@ -39,7 +39,10 @@ class BackupReducer {
): BackupState {
return when (action) {
BackupAction.IntroduceBackup -> state.copy(backupStep = BackupStep.InitBackup)
BackupAction.IntroduceBackup -> BackupState(
backupStep = BackupStep.InitBackup,
canSkipBackup = state.canSkipBackup
)
BackupAction.StartBackup -> state.copy(backupStep = BackupStep.ScanOriginCard)
@ -104,6 +107,10 @@ class BackupReducer {
)
}
BackupAction.OnAccessCodeDialogClosed -> {
state.copy(backupStep = BackupStep.AddBackupCards)
}
BackupAction.WritePrimaryCard -> state
is BackupAction.WriteBackupCard -> state
is BackupAction.SaveAccessCodeConfirmation -> state
@ -119,6 +126,7 @@ class BackupReducer {
BackupAction.CheckForUnfinishedBackup -> state
BackupAction.DiscardBackup -> state
BackupAction.ResumeBackup -> state
BackupAction.DiscardSavedBackup -> state
}
}

View file

@ -74,7 +74,7 @@ sealed class BackupStep {
}
sealed class BackupDialog: StateDialog {
object BuyMoreBackupCards : StateDialog
object AddMoreBackupCards : StateDialog
object BackupInProgress : StateDialog
object UnfinishedBackupFound : StateDialog
object ConfirmDiscardingBackup : StateDialog

View file

@ -10,20 +10,19 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.tabs.TabLayoutMediator
import com.tangem.common.CardIdFormatter
import com.tangem.common.core.CardIdDisplayFormat
import com.tangem.tap.common.extensions.addOnBackPressedDispatcher
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.leapfrogWidget.LeapfrogWidget
import com.tangem.tap.common.postUi
import com.tangem.tap.features.home.redux.HomeMiddleware
import com.tangem.tap.features.FragmentOnBackPressedHandler
import com.tangem.tap.features.addBackPressHandler
import com.tangem.tap.features.onboarding.products.wallet.redux.*
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletMiddleware.Companion.BUY_WALLET_URL
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.AccessCodeDialog
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_onboarding_wallet.*
import kotlinx.android.synthetic.main.fragment_onboarding_wallet.toolbar
import kotlinx.android.synthetic.main.fragment_wallet_details.*
import kotlinx.android.synthetic.main.layout_onboarding_buttons_add_cards.*
import kotlinx.android.synthetic.main.layout_onboarding_buttons_common.*
import kotlinx.android.synthetic.main.view_confetti.*
@ -31,7 +30,7 @@ import kotlinx.android.synthetic.main.view_onboarding_progress.*
import org.rekotlin.StoreSubscriber
class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
StoreSubscriber<OnboardingWalletState> {
StoreSubscriber<OnboardingWalletState>, FragmentOnBackPressedHandler {
private var accessCodeDialog: AccessCodeDialog? = null
private lateinit var cardsWidget: BackupCardsWidget
@ -41,11 +40,6 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
postponeEnterTransition()
setHasOptionsMenu(true)
activity?.addOnBackPressedDispatcher {
store.dispatch(OnboardingWalletAction.OnBackPressed)
}
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.fade)
exitTransition = inflater.inflateTransition(R.transition.fade)
@ -68,7 +62,7 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
store.dispatch(OnboardingWalletAction.Init)
toolbar.setNavigationOnClickListener { activity?.onBackPressed() }
// store.dispatch(OnboardingWalletAction.LoadArtwork)
addBackPressHandler(this)
}
override fun onStart() {
@ -181,6 +175,9 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
imv_first_backup_card.show()
imv_second_backup_card.show()
accessCodeDialog?.dismiss()
accessCodeDialog = null
layout_buttons_add_cards.show()
layout_buttons_common.hide()
if (state.backupCardsNumber < state.maxBackupCards) {
@ -224,7 +221,7 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
dismissWithAnimation = true
create()
setOnCancelListener {
store.dispatch(BackupAction.OnAccessCodeDialogClosed)
}
show()
showInfoScreen()
@ -366,7 +363,7 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.shop_menu -> {
store.dispatchOpenUrl(HomeMiddleware.CARD_SHOP_URI)
store.dispatchOpenUrl(BUY_WALLET_URL)
true
}
else -> super.onOptionsItemSelected(item)
@ -381,4 +378,8 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
backupStep == BackupStep.ScanOriginCard || backupStep == BackupStep.AddBackupCards
menu.getItem(0).isVisible = shopMenuShouldBeVisible
}
override fun handleOnBackPressed() {
store.dispatch(OnboardingWalletAction.OnBackPressed)
}
}

View file

@ -33,6 +33,7 @@ class AccessCodeDialog(context: Context) : BottomSheetDialog(context) {
fun showInfoScreen() {
behavior.state = BottomSheetBehavior.STATE_EXPANDED
access_code_title.text = context.getText(R.string.onboarding_access_code_intro_title)
layout_backup_access_code_info.show()
layout_backup_access_code_submit.hide()
btn_access_code_create.setOnClickListener {
@ -43,6 +44,7 @@ class AccessCodeDialog(context: Context) : BottomSheetDialog(context) {
fun showEnterAccessCode() {
layout_backup_access_code_info.hide()
layout_backup_access_code_submit.show()
access_code_title.text = context.getText(R.string.onboarding_access_code_intro_title)
btn_access_code_submit.text = context.getText(R.string.common_continue)
btn_access_code_submit.setOnClickListener {
store.dispatch(BackupAction.CheckAccessCode(et_access_code.text.toString()))
@ -52,6 +54,7 @@ class AccessCodeDialog(context: Context) : BottomSheetDialog(context) {
fun showReenterAccessCode() {
layout_backup_access_code_info.hide()
layout_backup_access_code_submit.show()
access_code_title.text = context.getText(R.string.onboarding_access_code_repeat_code_title)
et_access_code.setText("")
btn_access_code_submit.text = context.getText(R.string.common_submit)
btn_access_code_submit.setOnClickListener {

View file

@ -7,7 +7,7 @@ import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.store
import com.tangem.wallet.R
class BuyMoreBackupCardsDialog {
class AddMoreBackupCardsDialog {
companion object {
fun create(context: Context): AlertDialog {
return AlertDialog.Builder(context).apply {
@ -16,8 +16,8 @@ class BuyMoreBackupCardsDialog {
setPositiveButton(R.string.common_continue) { _, _ ->
store.dispatch(BackupAction.ShowAccessCodeInfoScreen)
}
setNegativeButton(R.string.onboarding_button_buy_more_cards) { _, _ ->
store.dispatch(BackupAction.GoToShop)
setNegativeButton(R.string.onboarding_button_add_more_cards) { _, _ ->
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)

View file

@ -17,7 +17,7 @@ class ConfirmDiscardingBackupDialog {
store.dispatch(BackupAction.ResumeBackup)
}
setNegativeButton(R.string.welcome_interrupted_backup_discard_discard) { _, _ ->
store.dispatch(BackupAction.DiscardBackup)
store.dispatch(BackupAction.DiscardSavedBackup)
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)

View file

@ -15,6 +15,7 @@ import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.domain.TangemSigner
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.TapWorkarounds.isStart2Coin
@ -152,21 +153,32 @@ private fun sendTransaction(
if (card.isStart2Coin) {
tangemSdk.config.linkedTerminal = false
}
val signer = TangemSigner(
tangemSdk = tangemSdk, initialMessage = action.messageForSigner
) { signResponse ->
val signer = TangemSigner(tangemSdk, action.messageForSigner) { signResponse ->
store.dispatch(
GlobalAction.UpdateWalletSignedHashes(
walletSignedHashes = signResponse.totalSignedHashes,
walletPublicKey = walletManager.wallet.publicKey,
walletPublicKey = walletManager.wallet.publicKey.seedKey,
remainingSignatures = signResponse.remainingSignatures
)
)
}
val result = (walletManager as TransactionSender).send(txData, signer)
val sendResult = try {
(walletManager as TransactionSender).send(txData, signer)
} catch (ex: Exception) {
FirebaseCrashlytics.getInstance().recordException(ex)
delay(DELAY_SDK_DIALOG_CLOSE)
withMainContext {
dispatch(SendAction.ChangeSendButtonState(ButtonState.ENABLED))
store.dispatchErrorNotification(TapError.CustomError(ex.localizedMessage ?: "Unknown error"))
}
return@launch
}
withContext(Dispatchers.Main) {
tangemSdk.config.linkedTerminal = isLinkedTerminal
when (result) {
when (sendResult) {
is SimpleResult.Success -> {
FirebaseAnalyticsHandler.triggerEvent(
event = AnalyticsEvent.TRANSACTION_IS_SENT,
@ -191,20 +203,18 @@ private fun sendTransaction(
}
}
is SimpleResult.Failure -> {
when (result.error) {
when (sendResult.error) {
is CreateAccountUnderfunded -> {
val error = result.error as CreateAccountUnderfunded
val error = sendResult.error as CreateAccountUnderfunded
val reserve = error.minReserve.value?.stripZeroPlainString() ?: "0"
val symbol = error.minReserve.currencySymbol
dispatch(SendAction.SendError(TapError.CreateAccountUnderfunded(listOf(reserve, symbol))))
}
is SendException -> {
result.error?.let {
FirebaseCrashlytics.getInstance().recordException(it)
}
sendResult.error?.let { FirebaseCrashlytics.getInstance().recordException(it) }
}
is Throwable -> {
val throwable = result.error as Throwable
val throwable = sendResult.error as Throwable
val message = throwable.message
val infoHolder = store.state.globalState.feedbackManager?.infoHolder
when {
@ -228,13 +238,13 @@ private fun sendTransaction(
dispatch(SendAction.SendError(TapError.XmlError.AssetAccountNotCreated))
}
else -> {
(result.error as? TangemSdkError)?.let { error ->
(sendResult.error as? TangemSdkError)?.let { error ->
FirebaseAnalyticsHandler.logCardSdkError(
error,
FirebaseAnalyticsHandler.ActionToLog.SendTransaction,
mapOf(
FirebaseAnalyticsHandler.AnalyticsParam.BLOCKCHAIN
to walletManager.wallet.blockchain.currency),
to walletManager.wallet.blockchain.currency),
card = card,
)
}
@ -303,7 +313,7 @@ fun createValidateTransactionError(errorList: EnumSet<TransactionError>, walletM
private fun setIfSendingToPayIdEnabled(appState: AppState?, dispatch: (Action) -> Unit) {
val isSendingToPayIdEnabled =
appState?.globalState?.configManager?.config?.isSendingToPayIdEnabled ?: false
appState?.globalState?.configManager?.config?.isSendingToPayIdEnabled ?: false
dispatch(AddressPayIdActionUi.ChangePayIdState(isSendingToPayIdEnabled))
}

View file

@ -6,8 +6,12 @@ import com.tangem.blockchain.common.Token
import com.tangem.wallet.R
sealed class CurrencyListItem {
var isAdded: Boolean = false
var isLock: Boolean = false
data class TokenListItem(val token: Token) : CurrencyListItem()
data class BlockchainListItem(val blockchain: Blockchain) : CurrencyListItem()
data class TitleListItem(
@StringRes val titleResId: Int,
var isContentShown: Boolean = true,

View file

@ -1,22 +1,20 @@
package com.tangem.tap.features.tokens.redux
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.tap.features.wallet.redux.WalletData
import org.rekotlin.Action
sealed class TokensAction : Action {
object ResetState : TokensAction()
object LoadCurrencies : TokensAction() {
data class Success(val currencies: List<CurrencyListItem>) : TokensAction()
}
object LoadCardTokens : TokensAction() {
data class Success(val tokens: List<Token>) : TokensAction()
}
data class SetAddedCurrencies(val wallets: List<WalletData>) : TokensAction()
data class ToggleShowTokensForBlockchain(val isShown: Boolean, val blockchain: Blockchain) : TokensAction()
data class SaveChanges(val addedItems: List<CurrencyListItem>) : TokensAction()
}

View file

@ -1,9 +1,25 @@
package com.tangem.tap.features.tokens.redux
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.tap.common.extensions.dispatchErrorNotification
import com.tangem.tap.common.extensions.toMapKey
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.NavigationAction
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.ScanResponse
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
class TokensMiddleware {
@ -12,22 +28,111 @@ class TokensMiddleware {
{ next ->
{ action ->
when (action) {
is TokensAction.LoadCurrencies -> {
val card = state()?.globalState?.scanResponse?.card
val isTestcard = card?.isTestCard ?: false
val tokens = currenciesRepository.getPopularTokens(isTestcard)
val blockchains = currenciesRepository.getBlockchains(
cardFirmware = card?.firmwareVersion,
isTestNet = isTestcard
)
val currencies = CurrencyListItem.createListOfCurrencies(
blockchains, tokens
)
store.dispatch(TokensAction.LoadCurrencies.Success(currencies))
}
is TokensAction.LoadCurrencies -> handleLoadCurrencies(action)
is TokensAction.SaveChanges -> handleSaveChanges(action)
}
next(action)
}
}
}
private fun handleLoadCurrencies(action: TokensAction.LoadCurrencies) {
val scanResponse = store.state.globalState.scanResponse ?: return
val isTestcard = scanResponse.card.isTestCard
val tokens = currenciesRepository.getPopularTokens(isTestcard)
val blockchains = currenciesRepository.getBlockchains(
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
}
}
}
}
}
store.dispatch(TokensAction.LoadCurrencies.Success(currencies))
}
private fun handleSaveChanges(action: TokensAction.SaveChanges) {
val scanResponse = store.state.globalState.scanResponse ?: return
val candidatesToAdd = action.addedItems
if (candidatesToAdd.isEmpty()) return
val blockchains = candidatesToAdd.filterIsInstance<CurrencyListItem.BlockchainListItem>()
.map { it.blockchain }
val tokens = candidatesToAdd.filterIsInstance<CurrencyListItem.TokenListItem>()
.map { it.token }
if (blockchains.isEmpty() && tokens.isEmpty()) return
if (scanResponse.isTangemWallet()) {
deriveMissingBlockchains(scanResponse, blockchains, tokens)
} else {
submitAdd(blockchains, tokens)
store.dispatch(NavigationAction.PopBackTo())
}
}
private fun deriveMissingBlockchains(
scanResponse: ScanResponse,
blockchains: List<Blockchain>,
tokens: List<Token>
) {
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) }
scope.launch {
val result = tangemSdkManager.derivePublicKeys(scanResponse.card.cardId, wallet.publicKey, toDerive)
when (result) {
is CompletionResult.Success -> {
val newDerivedKeys = result.data
alreadyDerivedKeys.addAll(newDerivedKeys)
val updatedScanResponse = scanResponse.copy(
derivedKeys = mapOf(mapKeyOfWalletPublicKey to alreadyDerivedKeys.toList())
)
store.dispatch(GlobalAction.SaveScanNoteResponse(updatedScanResponse))
submitAdd(blockchains, tokens)
delay(DELAY_SDK_DIALOG_CLOSE)
store.dispatch(NavigationAction.PopBackTo())
}
is CompletionResult.Failure -> {
store.dispatchErrorNotification(TapError.CustomError("Error adding tokens"))
}
}
}
}
private fun submitAdd(blockchains: List<Blockchain>, tokens: List<Token>) {
(blockchains.map {
WalletAction.MultiWallet.AddBlockchain(it)
} + tokens.map {
WalletAction.MultiWallet.AddToken(it)
}).forEach { store.dispatch(it) }
}
}

View file

@ -17,28 +17,23 @@ private fun internalReduce(action: Action, state: AppState): TokensState {
val tokensState = state.tokensState
return when (action) {
is TokensAction.ResetState -> TokensState()
is TokensAction.LoadCurrencies.Success -> {
tokensState.copy(currencies = action.currencies, shownCurrencies = action.currencies)
}
is TokensAction.SetAddedCurrencies -> {
tokensState.copy(addedCurrencies = action.wallets.toCardCurrencies())
}
is TokensAction.LoadCardTokens.Success -> {
tokensState.copy(addedTokens = LinkedHashSet(
action.tokens.map { TokenWithAmount(it, null) }
))
}
is TokensAction.ToggleShowTokensForBlockchain -> {
if (action.isShown) {
val shownCurrencies = tokensState.shownCurrencies
.removeTokensForBlockchain(action.blockchain)
shownCurrencies.toggleHeaderContentShownValue(action.blockchain)
shownCurrencies.toggleHeaderContentShownValue(action.blockchain)
tokensState.copy(shownCurrencies = shownCurrencies)
} else {
val shownCurrencies = tokensState.shownCurrencies
.addTokensForBlockchain(action.blockchain, tokensState.currencies)
shownCurrencies.toggleHeaderContentShownValue(action.blockchain)
shownCurrencies.toggleHeaderContentShownValue(action.blockchain)
tokensState.copy(shownCurrencies = shownCurrencies)
}
}

View file

@ -12,6 +12,7 @@ import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.transition.TransitionInflater
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.tokens.redux.TokensAction
import com.tangem.tap.features.tokens.redux.TokensState
import com.tangem.tap.features.tokens.ui.adapters.CurrenciesAdapter
import com.tangem.tap.mainScope
@ -57,30 +58,35 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens),
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(activity as? AppCompatActivity)?.setSupportActionBar(toolbar)
toolbar.setNavigationOnClickListener { activity?.onBackPressed() }
setupPopularTokensRecyclerView()
btn_tokens_save_changes.setOnClickListener {
store.dispatch(TokensAction.SaveChanges(viewAdapter.getAddedItems()))
}
}
private fun setupPopularTokensRecyclerView() {
viewAdapter = CurrenciesAdapter()
viewAdapter.setOnItemAddListener {
btn_tokens_save_changes.isEnabled = viewAdapter.getAddedItems().isNotEmpty()
}
rv_popular_tokens.layoutManager = LinearLayoutManager(context)
rv_popular_tokens.adapter = viewAdapter
}
override fun newState(state: TokensState) {
if (activity == null) return
viewAdapter.addedCurrencies = state.addedCurrencies
viewAdapter.submitUnfilteredList(state.shownCurrencies)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.menu_search -> {
true
}
R.id.menu_search -> true
else -> super.onOptionsItemSelected(item)
}
}
@ -101,6 +107,11 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens),
.launchIn(mainScope)
return super.onCreateOptionsMenu(menu, inflater);
}
override fun onDestroy() {
store.dispatch(TokensAction.ResetState)
super.onDestroy()
}
}
fun SearchView.inputtedTextAsFlow(): Flow<String> = callbackFlow {

View file

@ -7,6 +7,9 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.squareup.picasso.Picasso
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.common.extensions.getString
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.loadCurrenciesIcon
@ -14,7 +17,6 @@ import com.tangem.tap.common.extensions.show
import com.tangem.tap.domain.tokens.CardCurrencies
import com.tangem.tap.features.tokens.redux.CurrencyListItem
import com.tangem.tap.features.tokens.redux.TokensAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.item_currency_subtitle.view.*
@ -27,11 +29,28 @@ class CurrenciesAdapter : ListAdapter<CurrencyListItem, RecyclerView.ViewHolder>
private var unfilteredList = listOf<CurrencyListItem>()
private val currentlyAddedItems = mutableListOf<CurrencyListItem>()
private var vhOnItemAddListener: ((CurrencyListItem) -> Unit) = {
currentlyAddedItems.add(it)
onItemAddListener?.invoke()
}
private var onItemAddListener: VoidCallback? = null
fun submitUnfilteredList(list: List<CurrencyListItem>) {
unfilteredList = list
submitList(list)
}
fun setOnItemAddListener(itemAddListener: VoidCallback) {
onItemAddListener = itemAddListener
if (currentlyAddedItems.isNotEmpty()) itemAddListener()
}
fun getAddedItems(): List<CurrencyListItem> {
return currentlyAddedItems.toList()
}
override fun getItemViewType(position: Int): Int {
return when (currentList[position]) {
is CurrencyListItem.TitleListItem -> 0
@ -47,11 +66,13 @@ class CurrenciesAdapter : ListAdapter<CurrencyListItem, RecyclerView.ViewHolder>
)
1 -> CurrenciesViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.item_popular_token, parent, false)
.inflate(R.layout.item_popular_token, parent, false),
vhOnItemAddListener
)
else -> CurrenciesViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.item_popular_token, parent, false)
.inflate(R.layout.item_popular_token, parent, false),
vhOnItemAddListener
)
}
}
@ -61,7 +82,7 @@ class CurrenciesAdapter : ListAdapter<CurrencyListItem, RecyclerView.ViewHolder>
if (holder is TitleViewHolder && listItem is CurrencyListItem.TitleListItem) {
holder.bind(listItem)
} else if (holder is CurrenciesViewHolder) {
holder.bind(listItem, addedCurrencies)
holder.bind(listItem, addedCurrencies, currentlyAddedItems)
}
}
@ -86,15 +107,15 @@ class CurrenciesAdapter : ListAdapter<CurrencyListItem, RecyclerView.ViewHolder>
is CurrencyListItem.BlockchainListItem -> {
element.blockchain.currency.toLowerCase(Locale.US)
.contains(queryNormalized) ||
element.blockchain.fullName.toLowerCase(Locale.US)
.contains(queryNormalized)
element.blockchain.fullName.toLowerCase(Locale.US)
.contains(queryNormalized)
}
is CurrencyListItem.TokenListItem -> {
element.token.name.toLowerCase(Locale.US)
.contains(queryNormalized) ||
element.token.symbol.toLowerCase(Locale.US)
.contains(queryNormalized)
element.token.symbol.toLowerCase(Locale.US)
.contains(queryNormalized)
}
is CurrencyListItem.TitleListItem -> true
}
@ -105,56 +126,85 @@ class CurrenciesAdapter : ListAdapter<CurrencyListItem, RecyclerView.ViewHolder>
submitList(list)
}
class CurrenciesViewHolder(val view: View) :
RecyclerView.ViewHolder(view) {
fun bind(currency: CurrencyListItem, addedCurrencies: CardCurrencies?) {
class CurrenciesViewHolder(
private val view: View,
private val onItemAddListener: ((CurrencyListItem) -> Unit),
) : RecyclerView.ViewHolder(view) {
fun bind(
currency: CurrencyListItem,
addedCurrencies: CardCurrencies?,
currentlyAddedItems: MutableList<CurrencyListItem>
) {
when (currency) {
is CurrencyListItem.BlockchainListItem -> {
val blockchain = currency.blockchain
view.tv_currency_name.text = blockchain.fullName
view.tv_currency_symbol.text = blockchain.currency
val isAdded = addedCurrencies?.blockchains?.contains(blockchain) == true
view.btn_add_token.show(!isAdded)
view.btn_token_added.show(isAdded)
Picasso.get().loadCurrenciesIcon(
imageView = view.iv_currency,
textView = view.tv_token_letter,
blockchain = blockchain, token = null
)
view.btn_add_token.setOnClickListener {
store.dispatch(WalletAction.MultiWallet.AddBlockchain(blockchain))
view.btn_add_token.hide()
view.btn_token_added.show()
}
val addedBlockchains = addedCurrencies?.blockchains ?: listOf()
val currentlyAdded = currentlyAddedItems.filterIsInstance<CurrencyListItem.BlockchainListItem>()
bindBlockchain(currency, addedBlockchains, currentlyAdded)
}
is CurrencyListItem.TokenListItem -> {
val token = currency.token
view.tv_currency_name.text = token.name
view.tv_currency_symbol.text = token.symbol
val isAdded = addedCurrencies?.tokens
?.any {
it.symbol == token.symbol && it.contractAddress == token.contractAddress
} == true
view.btn_add_token.show(!isAdded)
view.btn_token_added.show(isAdded)
Picasso.get().loadCurrenciesIcon(
imageView = view.iv_currency,
textView = view.tv_token_letter,
token = token, blockchain = token.blockchain
)
view.btn_add_token.setOnClickListener {
store.dispatch(WalletAction.MultiWallet.AddToken(token))
view.btn_add_token.hide()
view.btn_token_added.show()
}
val addedTokens = addedCurrencies?.tokens ?: listOf()
val currentlyAdded = currentlyAddedItems.filterIsInstance<CurrencyListItem.TokenListItem>()
bindToken(currency, addedTokens, currentlyAdded)
}
}
}
private fun bindBlockchain(
currency: CurrencyListItem.BlockchainListItem,
addedBlockchains: List<Blockchain>,
currentlyAdded: List<CurrencyListItem.BlockchainListItem>
) {
val blockchain = currency.blockchain
Picasso.get().loadCurrenciesIcon(view.iv_currency, view.tv_token_letter, null, blockchain)
view.tv_currency_name.text = blockchain.fullName
view.tv_currency_symbol.text = blockchain.currency
view.btn_add_token.setOnClickListener {
onItemAddListener.invoke(currency)
currency.isAdded = true
modifyAddTokenButton(currency)
}
val isAddedBefore = addedBlockchains.contains(blockchain)
val isCurrentlyAdded = currentlyAdded.any { it.blockchain == blockchain }
currency.isAdded = isAddedBefore || isCurrentlyAdded
modifyAddTokenButton(currency)
}
private fun bindToken(
currency: CurrencyListItem.TokenListItem,
addedTokens: List<Token>,
currentlyAdded: List<CurrencyListItem.TokenListItem>
) {
val token = currency.token
Picasso.get().loadCurrenciesIcon(view.iv_currency, view.tv_token_letter, token, token.blockchain)
view.tv_currency_name.text = token.name
view.tv_currency_symbol.text = token.symbol
view.btn_add_token.setOnClickListener {
onItemAddListener.invoke(currency)
currency.isAdded = true
modifyAddTokenButton(currency)
}
val isAddedBefore = addedTokens.any { it == token }
val isCurrentlyAdded = currentlyAdded.any { it.token == token }
currency.isAdded = isAddedBefore || isCurrentlyAdded
modifyAddTokenButton(currency)
}
private fun modifyAddTokenButton(currency: CurrencyListItem) {
if (currency.isLock) {
view.btn_add_token.setText(R.string.common_add)
view.btn_add_token.isEnabled = false
} else {
val text = if (currency.isAdded) R.string.add_token_added else R.string.common_add
view.btn_add_token.setText(text)
view.btn_add_token.isEnabled = !currency.isAdded
}
}
}
class TitleViewHolder(val view: View) :

View file

@ -50,8 +50,7 @@ sealed class WalletAction : Action {
data class AddToken(val token: Token) : MultiWallet()
data class SaveCurrencies(val cardCurrencies: CardCurrencies) : MultiWallet()
object FindTokensInUse : MultiWallet()
data class FindBlockchainsInUse(val card: Card, val factory: WalletManagerFactory) :
MultiWallet()
object FindBlockchainsInUse : MultiWallet()
data class TokenLoaded(val amount: Amount, val token: Token) : MultiWallet()
data class SelectWallet(val walletData: WalletData?) : MultiWallet()

View file

@ -4,6 +4,7 @@ import com.tangem.blockchain.common.*
import com.tangem.blockchain.extensions.Result
import com.tangem.common.extensions.isZero
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
@ -24,10 +25,12 @@ class MultiWalletMiddleware {
fun handle(
action: WalletAction.MultiWallet, walletState: WalletState?, globalState: GlobalState?,
) {
val globalState = globalState ?: return
val tapWalletManager = globalState.tapWalletManager
when (action) {
is WalletAction.MultiWallet.AddWalletManagers -> {
store.state.globalState.feedbackManager?.infoHolder
?.setWalletsInfo(action.walletManagers)
globalState.feedbackManager?.infoHolder?.setWalletsInfo(action.walletManagers)
}
is WalletAction.MultiWallet.SelectWallet -> {
if (action.walletData != null) {
@ -35,7 +38,7 @@ class MultiWalletMiddleware {
}
}
is WalletAction.MultiWallet.AddToken -> {
globalState?.scanResponse?.card?.cardId?.let {
globalState.scanResponse?.card?.cardId?.let {
currenciesRepository.saveAddedToken(it, action.token)
}
addToken(action.token, walletState, globalState)
@ -44,35 +47,33 @@ class MultiWalletMiddleware {
addTokens(action.tokens, walletState, globalState)
}
is WalletAction.MultiWallet.AddBlockchain -> {
globalState?.scanResponse?.card?.let { card ->
currenciesRepository.saveAddedBlockchain(card.cardId, action.blockchain)
globalState.scanResponse?.let {
currenciesRepository.saveAddedBlockchain(it.card.cardId, action.blockchain)
if (walletState?.blockchains?.contains(action.blockchain) != true) {
globalState.tapWalletManager.walletManagerFactory
.makeWalletManagerForApp(card, action.blockchain)?.let {
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(it))
tapWalletManager.walletManagerFactory
.makeWalletManagerForApp(it, action.blockchain)?.let { walletManager ->
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(walletManager))
}
}
}
store.dispatch(WalletAction.LoadFiatRate(currency = Currency.Blockchain(action.blockchain)))
store.dispatch(WalletAction.LoadWallet(
moonpayStatus = globalState?.moonpayStatus,
moonpayStatus = globalState.moonpayStatus,
blockchain = action.blockchain
)
)
)
}
is WalletAction.MultiWallet.SaveCurrencies -> {
val cardId = globalState?.scanResponse?.card?.cardId
cardId?.let { currenciesRepository.saveCardCurrencies(it, action.cardCurrencies) }
globalState.scanResponse?.card?.cardId?.let {
currenciesRepository.saveCardCurrencies(it, action.cardCurrencies)
}
}
is WalletAction.MultiWallet.RemoveWallet -> {
val cardId = globalState?.scanResponse?.card?.cardId
val cardId = globalState.scanResponse?.card?.cardId
when (val currency = action.walletData.currency) {
is Currency.Blockchain -> {
cardId?.let {
currenciesRepository.removeBlockchain(
it,
currency.blockchain
)
currenciesRepository.removeBlockchain(it, currency.blockchain)
}
}
is Currency.Token -> {
@ -83,11 +84,14 @@ class MultiWalletMiddleware {
}
}
is WalletAction.MultiWallet.FindBlockchainsInUse -> {
val cardFirmware = globalState?.scanResponse?.card?.firmwareVersion
val scanResponse = globalState.scanResponse ?: return
if (scanResponse.isTangemWallet()) return
val cardFirmware = scanResponse.card.firmwareVersion
val blockchains = currenciesRepository.getBlockchains(cardFirmware)
.filterNot { walletState?.blockchains?.contains(it) == true }
val walletManagers =
action.factory.makeWalletManagersForApp(action.card, blockchains)
tapWalletManager.walletManagerFactory.makeWalletManagersForApp(scanResponse, blockchains)
scope.launch {
walletManagers.map { walletManager ->
@ -120,12 +124,14 @@ class MultiWalletMiddleware {
}
}
is WalletAction.MultiWallet.FindTokensInUse -> {
val card = globalState?.scanResponse?.card ?: return
val scanResponse = globalState.scanResponse ?: return
if (scanResponse.isTangemWallet()) return
val walletFactory = tapWalletManager.walletManagerFactory
val card = scanResponse.card
val walletManager = walletState?.getWalletManager(Blockchain.Ethereum)
?: globalState.tapWalletManager.walletManagerFactory.makeWalletManagerForApp(
card = card,
blockchain = Blockchain.Ethereum
)
?: walletFactory.makeWalletManagerForApp(scanResponse, Blockchain.Ethereum)
val tokenFinder = walletManager as TokenFinder
scope.launch {
val result = tokenFinder.findTokens()
@ -159,26 +165,33 @@ class MultiWalletMiddleware {
}
private fun addToken(token: Token, walletState: WalletState?, globalState: GlobalState?) {
val card = globalState?.scanResponse?.card ?: return
val scanResponse = globalState?.scanResponse ?: return
val walletManager = walletState?.getWalletManager(token)
?: globalState.tapWalletManager.walletManagerFactory.makeWalletManagerForApp(
card = card,
blockchain = token.blockchain
scanResponse,
token.blockchain
)?.also { walletManager ->
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(walletManager))
store.dispatch(WalletAction.MultiWallet.AddBlockchain(walletManager.wallet.blockchain))
}
store.dispatch(
WalletAction.LoadFiatRate(currency = Currency.Token(token))
)
store.dispatch(WalletAction.LoadFiatRate(currency = Currency.Token(token)))
scope.launch {
when (val result = walletManager?.addToken(token)) {
is Result.Success -> {
store.dispatchOnMain(
WalletAction.MultiWallet.TokenLoaded(amount = result.data, token = token)
)
store.dispatchOnMain(WalletAction.MultiWallet.TokenLoaded(result.data, token))
}
is Result.Failure -> {
when (val result = walletManager.safeUpdate()) {
is com.tangem.common.services.Result.Success -> {
val tokenAmount = result.data.getTokenAmount(token) ?: return@launch
store.dispatchOnMain(WalletAction.MultiWallet.TokenLoaded(tokenAmount, token))
}
is com.tangem.common.services.Result.Failure -> {
}
}
}
}
}
@ -189,12 +202,13 @@ class MultiWalletMiddleware {
walletState: WalletState?,
globalState: GlobalState?,
) {
val card = globalState?.scanResponse?.card ?: return
val scanResponse = globalState?.scanResponse ?: return
val tokensWithManagers = tokens.map { token ->
val walletManager = walletState?.getWalletManager(token)
?: globalState.tapWalletManager.walletManagerFactory.makeWalletManagerForApp(
card = card,
blockchain = token.blockchain
scanResponse,
token.blockchain
)?.also { walletManager ->
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(walletManager))
store.dispatch(WalletAction.MultiWallet.AddBlockchain(walletManager.wallet.blockchain))
@ -208,11 +222,17 @@ class MultiWalletMiddleware {
tokensWithManagers.forEach {
when (val result = it.walletManager?.addToken(it.token)) {
is Result.Success -> {
store.dispatchOnMain(
WalletAction.MultiWallet.TokenLoaded(
amount = result.data, token = it.token
)
)
store.dispatchOnMain(WalletAction.MultiWallet.TokenLoaded(result.data, it.token))
}
is Result.Failure -> {
when (val result = it.walletManager.safeUpdate()) {
is com.tangem.common.services.Result.Success -> {
val tokenAmount = result.data.getTokenAmount(it.token) ?: return@launch
store.dispatchOnMain(WalletAction.MultiWallet.TokenLoaded(tokenAmount, it.token))
}
is com.tangem.common.services.Result.Failure -> {
}
}
}
}
}

View file

@ -55,7 +55,7 @@ class MultiWalletView : WalletView {
private fun setupWalletCardNumber(fragment: WalletFragment) = with(fragment) {
val card = store.state.globalState.scanResponse?.card
if (card?.backupStatus?.isActive == true) {
if (card?.backupStatus is Card.BackupStatus.Active) {
val cardCount = (card.backupStatus as Card.BackupStatus.Active).cardCount + 1
tv_twin_card_number.show()
tv_twin_card_number.text =

View file

@ -4,6 +4,7 @@ import com.squareup.moshi.FromJson
import com.squareup.moshi.Moshi
import com.squareup.moshi.ToJson
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.tangem.common.json.TangemSdkAdapter
import com.tangem.wallet.BuildConfig
import okhttp3.Interceptor
import okhttp3.OkHttpClient
@ -41,7 +42,10 @@ fun createMoshiConverterFactory(): Converter.Factory = MoshiConverterFactory.cre
fun createMoshi(): Moshi = Moshi.Builder()
.add(BigDecimalAdapter)
.add(KotlinJsonAdapterFactory()).build()
.add(KotlinJsonAdapterFactory())
.add(TangemSdkAdapter.DerivationPathAdapter())
.add(TangemSdkAdapter.DerivationNodeAdapter())
.build()
private fun createHttpLoggingInterceptor(): HttpLoggingInterceptor {
val logging = HttpLoggingInterceptor()

View file

@ -45,7 +45,7 @@ class UsedCardsPrefStorage(
}
fun activationIsStarted(cardId: String): Boolean {
return findCardInfo(cardId)?.isActivationStarted ?: true
return findCardInfo(cardId)?.isActivationStarted ?: false
}
fun activationFinished(cardId: String) {

View file

@ -1,7 +1,7 @@
<?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" />
<solid android:color="@color/darkGray1" />
<corners
android:bottomLeftRadius="30dp"
android:bottomRightRadius="30dp"

View file

@ -13,8 +13,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/backgroundLightGray"
android:fitsSystemWindows="true"
>
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
@ -30,14 +29,34 @@
android:id="@+id/cl_details_confirm"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_popular_tokens"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="0dp"
android:clipToPadding="false"
android:paddingBottom="80dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_tokens_save_changes"
style="@style/TapBlackButton"
android:layout_width="204dp"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:enabled="false"
android:gravity="center"
android:text="Save changes"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -78,30 +78,27 @@
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_second_backup_card"
android:layout_width="296dp"
android:layout_height="186dp"
android:scaleType="fitXY"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/card_placeholder_black"
android:src="@drawable/card_placeholder_new"
android:visibility="gone"
/>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_first_backup_card"
android:layout_width="296dp"
android:layout_height="186dp"
android:scaleType="fitXY"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/card_placeholder_black"
android:src="@drawable/card_placeholder_new"
android:visibility="gone"/>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_front_card"
android:layout_width="296dp"
android:layout_height="186dp"
android:scaleType="fitXY"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/card_placeholder_black"
android:src="@drawable/card_placeholder_new"
android:transitionName="imv_front_card"
/>
@ -168,15 +165,14 @@
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintHorizontal_bias="0.504"
app:layout_constraintBottom_toTopOf="@id/layout_buttons_common"
app:flow_verticalBias="0"
app:layout_constraintTop_toTopOf="@+id/guideline2"
android:layout_marginBottom="12dp"/>
android:layout_marginBottom="8dp"/>
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout_backup_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="10dp"
android:background="@color/backgroundLightGray"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
@ -195,7 +191,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
/>
<include

View file

@ -74,33 +74,17 @@
app:layout_constraintVertical_bias="0"
tools:text="NODLE" />
<com.google.android.material.chip.Chip
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_add_token"
style="@style/Widget.MaterialComponents.Chip.Action"
android:layout_width="102dp"
android:layout_height="wrap_content"
android:layout_height="43dp"
android:layout_marginEnd="24dp"
android:src="@drawable/ic_inactive"
android:backgroundTint="@color/selector_btn_green"
android:elevation="0dp"
android:text="@string/common_add"
android:textAlignment="center"
android:textColor="@android:color/white"
app:chipBackgroundColor="@color/accent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.chip.Chip
android:id="@+id/btn_token_added"
style="@style/Widget.MaterialComponents.Chip.Action"
android:layout_width="102dp"
android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:enabled="false"
android:src="@drawable/ic_inactive"
android:text="@string/add_token_added"
android:textAlignment="center"
android:textColor="@android:color/white"
app:chipBackgroundColor="@color/tapButtonDisabled"
android:textAllCaps="false"
android:textColor="@color/btn_tap_text_color"
app:cornerRadius="20dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
@ -110,7 +94,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="start"
app:constraint_referenced_ids="btn_add_token, btn_token_added" />
app:constraint_referenced_ids="btn_add_token" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -10,7 +10,7 @@
<color name="tapButtonColor">@color/accent</color>
<color name="tapButtonDisabled">#661ACE80</color>
<color name="tapButtonColorBlack">@color/darkGray6</color>
<color name="tapButtonBlackDisabled">#661C1C1E</color>
<color name="tapButtonBlackDisabled">#6A6A6A</color>
<color name="success">#5AC461</color>
<color name="error">#C4291C</color>

View file

@ -218,7 +218,7 @@
<string name="onboarding_button_add_backup_card" translatable="false">+ Add backup card</string>
<string name="onboarding_button_backup_card_format" translatable="false">Scan the card #%d</string>
<string name="onboarding_button_scan_origin_card" translatable="false">Scan primary card</string>
<string name="onboarding_button_buy_more_cards" translatable="false">Buy more cards</string>
<string name="onboarding_button_add_more_cards" translatable="false">Add more cards</string>
<string name="onboarding_title_backup_card" translatable="false">Backup your wallet</string>
<string name="onboarding_title_no_backup_cards" translatable="false">No backup cards</string>