Updated on 2026-08-14

This commit is contained in:
Tangem 2021-12-09 17:52:58 +03:00
commit 2126d9803e
106 changed files with 3186 additions and 516 deletions

View file

@ -72,13 +72,14 @@ dependencies {
implementation 'androidx.fragment:fragment-ktx:1.3.6'
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
implementation 'com.google.android.material:material:1.4.0'
implementation "androidx.core:core-ktx:1.6.0"
implementation 'com.google.android.play:core:1.10.2'
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-95'
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-95'
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

@ -8,6 +8,7 @@ import android.view.View
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.snackbar.Snackbar
import com.tangem.TangemSdk
import com.tangem.operations.backup.BackupService
import com.tangem.tangem_sdk_new.extensions.init
import com.tangem.tap.common.DialogManager
import com.tangem.tap.common.IntentHandler
@ -32,6 +33,7 @@ import kotlin.coroutines.CoroutineContext
lateinit var tangemSdk: TangemSdk
lateinit var tangemSdkManager: TangemSdkManager
lateinit var backupService: BackupService
var notificationsHandler: NotificationsHandler? = null
private val coroutineContext: CoroutineContext
@ -67,6 +69,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
tangemSdk = TangemSdk.init(this, TangemSdkManager.config)
tangemSdkManager = TangemSdkManager(tangemSdk, this)
backupService = BackupService.init(tangemSdk, this)
store.dispatch(GlobalAction.SetResources(getAndroidResources()))
store.dispatch(WalletConnectAction.RestoreSessions)
@ -93,7 +96,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
val backStackIsEmpty = supportFragmentManager.backStackEntryCount == 0
val isScannedBefore = store.state.globalState.scanResponse != null
val isOnboardingServiceActive = store.state.globalState.onboardingManager != null
val isOnboardingServiceActive = store.state.globalState.onboardingState.onboardingStarted
if (backStackIsEmpty || (!isOnboardingServiceActive && !isScannedBefore)) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Home))
}

View file

@ -9,6 +9,11 @@ import com.tangem.tap.features.details.ui.walletconnect.dialogs.*
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.ConfirmDiscardingBackupDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.UnfinishedBackupFoundDialog
import com.tangem.tap.features.wallet.ui.dialogs.ScanFailsDialog
import com.tangem.tap.store
import com.tangem.wallet.R
@ -74,6 +79,10 @@ class DialogManager : StoreSubscriber<GlobalState> {
TransactionDialog.create(state.dialog.dialogData, context)
is WalletConnectDialog.PersonalSign ->
PersonalSignDialog.create(state.dialog.data, 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)
else -> null
}
dialog?.show()

View file

@ -72,9 +72,39 @@ object TangemSdk {
is TangemSdkError.WalletCannotBeCreated -> TangemSdkError.WalletCannotBeCreated()
is TangemSdkError.UnsupportedCurve -> TangemSdkError.UnsupportedCurve()
is TangemSdkError.UnsupportedWalletConfig -> TangemSdkError.UnsupportedWalletConfig()
is TangemSdkError.CryptoUtilsError -> TangemSdkError.CryptoUtilsError()
is TangemSdkError.CryptoUtilsError -> TangemSdkError.CryptoUtilsError(error.customMessage)
is TangemSdkError.NetworkError -> TangemSdkError.NetworkError(error.customMessage)
is TangemSdkError.ExceptionError -> TangemSdkError.ExceptionError(error.throwable)
is TangemSdkError.TooMuchBackupCards -> TangemSdkError.TooMuchBackupCards()
is TangemSdkError.BackupCardRequired -> TangemSdkError.BackupCardRequired()
is TangemSdkError.CertificateSignatureRequired -> TangemSdkError.CertificateSignatureRequired()
is TangemSdkError.AccessCodeOrPasscodeRequired -> TangemSdkError.AccessCodeOrPasscodeRequired()
is TangemSdkError.ResetPinNoCardsToReset -> TangemSdkError.ResetPinNoCardsToReset()
is TangemSdkError.ResetPinWrongCard -> TangemSdkError.ResetPinWrongCard()
is TangemSdkError.BackupFailedCardNotLinked -> TangemSdkError.BackupFailedCardNotLinked()
is TangemSdkError.BackupNotAllowed -> TangemSdkError.BackupNotAllowed()
is TangemSdkError.BackupCardAlreadyAdded -> TangemSdkError.BackupCardAlreadyAdded()
is TangemSdkError.MissingPrimaryCard -> TangemSdkError.MissingPrimaryCard()
is TangemSdkError.MissingPrimaryAttestSignature -> TangemSdkError.MissingPrimaryAttestSignature()
is TangemSdkError.NoBackupDataForCard -> TangemSdkError.NoBackupDataForCard()
is TangemSdkError.BackupFailedEmptyWallets -> TangemSdkError.BackupFailedEmptyWallets()
is TangemSdkError.BackupFailedNotEmptyWallets -> TangemSdkError.BackupFailedNotEmptyWallets()
is TangemSdkError.NoActiveBackup -> TangemSdkError.NoActiveBackup()
is TangemSdkError.ResetBackupFailedHasBackupedWallets -> TangemSdkError.ResetBackupFailedHasBackupedWallets()
is TangemSdkError.BackupServiceInvalidState -> TangemSdkError.BackupServiceInvalidState()
is TangemSdkError.NoBackupCardForIndex -> TangemSdkError.NoBackupCardForIndex()
is TangemSdkError.EmptyBackupCards -> TangemSdkError.EmptyBackupCards()
is TangemSdkError.BackupFailedWrongIssuer -> TangemSdkError.BackupFailedWrongIssuer()
is TangemSdkError.BackupFailedHDWalletSettings -> TangemSdkError.BackupFailedHDWalletSettings()
is TangemSdkError.BackupFailedNotEnoughCurves -> TangemSdkError.BackupFailedNotEnoughCurves()
is TangemSdkError.BackupFailedNotEnoughWallets -> TangemSdkError.BackupFailedNotEnoughWallets()
is TangemSdkError.FileSettingsUnsupported -> TangemSdkError.FileSettingsUnsupported()
is TangemSdkError.FilesIsEmpty -> TangemSdkError.FilesIsEmpty()
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

@ -17,7 +17,7 @@ import com.tangem.tap.features.home.HomeFragment
import com.tangem.tap.features.onboarding.products.note.OnboardingNoteFragment
import com.tangem.tap.features.onboarding.products.otherCards.OnboardingOtherCardsFragment
import com.tangem.tap.features.onboarding.products.twins.ui.TwinsCardsFragment
import com.tangem.tap.features.onboarding.products.wallet.OnboardingWalletFragment
import com.tangem.tap.features.onboarding.products.wallet.ui.OnboardingWalletFragment
import com.tangem.tap.features.send.ui.SendFragment
import com.tangem.tap.features.tokens.ui.AddTokensFragment
import com.tangem.tap.features.wallet.ui.WalletDetailsFragment
@ -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

@ -18,6 +18,7 @@ import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOt
import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsState
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsMiddleware
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupMiddleware
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletMiddleware
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
import com.tangem.tap.features.send.redux.middlewares.SendMiddleware
@ -60,7 +61,8 @@ data class AppState(
DetailsMiddleware().detailsMiddleware,
DisclaimerMiddleware().disclaimerMiddleware,
TokensMiddleware().tokensMiddleware,
WalletConnectMiddleware().walletConnectMiddleware
WalletConnectMiddleware().walletConnectMiddleware,
BackupMiddleware().backupMiddleware
)
}
}

View file

@ -31,11 +31,12 @@ sealed class GlobalAction : Action {
object HideDialog : GlobalAction()
sealed class Onboarding {
data class Start(val scanResponse: ScanResponse) : 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,17 +12,21 @@ 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
import com.tangem.operations.wallet.PurgeWalletCommand
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.AnalyticsHandler
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.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,11 +72,19 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
initialMessage = Message(context.getString(R.string.initial_message_create_wallet_body)))
}
suspend fun eraseWallet(card: Card): CompletionResult<SuccessResponse> {
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(
PurgeWalletCommand(card.wallets.first().publicKey),
ResetToFactorySettingsTask(),
card.cardId,
initialMessage = Message(context.getString(R.string.initial_message_purge_wallet_body)))
initialMessage = Message(context.getString(R.string.details_row_title_reset_factory_settings)))
}
suspend fun setPasscode(cardId: String?): CompletionResult<SuccessResponse> {
@ -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

@ -29,8 +29,7 @@ object TapWorkarounds {
}
fun Card.isNotSupportedInThatRelease():Boolean {
return tangemWalletBatches.contains(batchId)
return false
}
@Deprecated("Use ScanResponse.isTangemNote")

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,28 +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))
}
}
}
}
@ -57,7 +52,7 @@ private class CreateWalletTangemNote : ProductCommandProcessor<CreateWalletRespo
override fun proceed(
card: Card,
session: CardSession,
callback: (result: CompletionResult<CreateWalletResponse>) -> Unit
callback: (result: CompletionResult<CreateWalletResponse>) -> Unit,
) {
if (card.supportedCurves.isEmpty()) {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
@ -79,34 +74,63 @@ 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,
callback: (result: CompletionResult<CreateWalletResponse>) -> Unit
callback: (result: CompletionResult<CreateWalletResponse>) -> Unit,
) {
//TODO:
val supportedCurves = setOf(EllipticCurve.Secp256k1, EllipticCurve.Ed25519)
val curves = card.getCurvesForNonCreatedWallets().intersect(supportedCurves).toList()
CreateWalletsTask(curves).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
val response = result.data.createWalletResponses[0]
val derivationPaths = listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
.mapNotNull { it.derivationPath() }
DeriveWalletPublicKeysTask(response.wallet.publicKey, derivationPaths).run(session) {
when (it) {
is CompletionResult.Success -> {
updateDerivedKeys(response.wallet, it.data)
callback(CompletionResult.Success(response))
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(it.error))
}
}
}
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(
card: Card,
session: CardSession,
callback: (result: CompletionResult<CreateWalletResponse>) -> Unit
callback: (result: CompletionResult<CreateWalletResponse>) -> Unit,
) {
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

@ -0,0 +1,57 @@
package com.tangem.tap.domain.tasks.product
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.extensions.guard
import com.tangem.operations.backup.ResetBackupCommand
import com.tangem.operations.wallet.PurgeWalletCommand
class ResetToFactorySettingsTask() : CardSessionRunnable<Card> {
override fun run(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit) {
deleteWallets(session, callback)
}
private fun deleteWallets(
session: CardSession,
callback: (result: CompletionResult<Card>) -> Unit,
) {
val wallet = session.environment.card?.wallets?.lastOrNull().guard {
resetBackup(session, callback)
return
}
PurgeWalletCommand(wallet.publicKey).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
deleteWallets(session, callback)
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
private fun resetBackup(
session: CardSession,
callback: (result: CompletionResult<Card>) -> Unit,
) {
val backupStatus = session.environment.card?.backupStatus
if (backupStatus == null || backupStatus == Card.BackupStatus.NoBackup) {
callback(CompletionResult.Success(session.environment.card!!))
return
}
ResetBackupCommand().run(session) { result ->
when (result) {
is CompletionResult.Success -> {
callback(CompletionResult.Success(session.environment.card!!))
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
}

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

@ -68,7 +68,7 @@ class CurrenciesRepository(val context: Application) {
}
return try {
tokensAdapter.fromJson(json)!!.map { it.toToken() }
tokensAdapter.fromJson(json)!!.map { it.toToken() }.distinct()
} catch (exception: Exception) {
try {
obsoleteTokensAdapter.fromJson(json)!!.map { it.toToken() }
@ -86,7 +86,7 @@ class CurrenciesRepository(val context: Application) {
private fun loadSavedBlockchains(cardId: String): List<Blockchain> {
return try {
val json = context.readFileText(getFileNameForBlockchains(cardId))
blockchainsAdapter.fromJson(json) ?: emptyList()
blockchainsAdapter.fromJson(json)?.distinct() ?: emptyList()
} catch (exception: Exception) {
emptyList()
}

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

@ -25,24 +25,26 @@ sealed class DetailsAction : Action {
object ShowDisclaimer : DetailsAction()
data class ReCreateTwinsWallet(val number: TwinCardNumber) : DetailsAction()
sealed class EraseWallet : DetailsAction() {
object Check : EraseWallet()
object Proceed : EraseWallet() {
object NotAllowedByCard : EraseWallet(), NotificationAction {
sealed class ResetToFactory : DetailsAction() {
object Check : ResetToFactory()
object Proceed : ResetToFactory() {
object NotAllowedByCard : ResetToFactory(), NotificationAction {
override val messageResource = R.string.error_purge_prohibited
}
object NotEmpty : EraseWallet(), NotificationAction {
object NotEmpty : ResetToFactory(), NotificationAction {
override val messageResource = R.string.details_notification_erase_wallet_not_possible
}
}
object Confirm : EraseWallet()
object Cancel : EraseWallet()
object Failure : EraseWallet()
object Success : EraseWallet()
object Confirm : ResetToFactory()
object Cancel : ResetToFactory()
object Failure : ResetToFactory()
object Success : ResetToFactory()
}
object CreateBackup : DetailsAction()
sealed class AppCurrencyAction : DetailsAction() {
data class SetCurrencies(val currencies: List<FiatCurrency>) : AppCurrencyAction()
object ChooseAppCurrency : AppCurrencyAction()

View file

@ -36,7 +36,7 @@ class DetailsMiddleware {
{ action ->
when (action) {
is DetailsAction.PrepareScreen -> prepareData()
is DetailsAction.EraseWallet -> eraseWalletMiddleware.handle(action)
is DetailsAction.ResetToFactory -> eraseWalletMiddleware.handle(action)
is DetailsAction.AppCurrencyAction -> appCurrencyMiddleware.handle(action)
is DetailsAction.ManageSecurity -> manageSecurityMiddleware.handle(action)
is DetailsAction.ShowDisclaimer -> {
@ -58,6 +58,12 @@ class DetailsMiddleware {
}
}
}
is DetailsAction.CreateBackup -> {
store.state.detailsState.scanResponse?.let {
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
store.dispatch(GlobalAction.Onboarding.Start(it, fromHomeScreen = false))
}
}
}
next(action)
}
@ -86,25 +92,25 @@ class DetailsMiddleware {
}
class EraseWalletMiddleware() {
fun handle(action: DetailsAction.EraseWallet) {
fun handle(action: DetailsAction.ResetToFactory) {
when (action) {
is DetailsAction.EraseWallet.Proceed -> {
is DetailsAction.ResetToFactory.Proceed -> {
when (store.state.detailsState.eraseWalletState) {
EraseWalletState.Allowed ->
store.dispatch(NavigationAction.NavigateTo(AppScreen.DetailsConfirm))
EraseWalletState.NotAllowedByCard ->
store.dispatch(DetailsAction.EraseWallet.Proceed.NotAllowedByCard)
store.dispatch(DetailsAction.ResetToFactory.Proceed.NotAllowedByCard)
EraseWalletState.NotEmpty ->
store.dispatch(DetailsAction.EraseWallet.Proceed.NotEmpty)
store.dispatch(DetailsAction.ResetToFactory.Proceed.NotEmpty)
}
}
is DetailsAction.EraseWallet.Cancel -> {
is DetailsAction.ResetToFactory.Cancel -> {
store.dispatch(NavigationAction.PopBackTo())
}
is DetailsAction.EraseWallet.Confirm -> {
is DetailsAction.ResetToFactory.Confirm -> {
val card = store.state.detailsState.scanResponse?.card ?: return
scope.launch {
val result = tangemSdkManager.eraseWallet(card)
val result = tangemSdkManager.resetToFactorySettings(card)
withContext(Dispatchers.Main) {
when (result) {
is CompletionResult.Success -> {

View file

@ -26,7 +26,7 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
is DetailsAction.PrepareScreen -> {
handlePrepareScreen(action, detailsState)
}
is DetailsAction.EraseWallet -> {
is DetailsAction.ResetToFactory -> {
handleEraseWallet(action, detailsState)
}
is DetailsAction.AppCurrencyAction -> {
@ -39,23 +39,34 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
}
}
private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: DetailsState): DetailsState {
private fun handlePrepareScreen(
action: DetailsAction.PrepareScreen,
state: DetailsState,
): DetailsState {
val backupIsActive = action.scanResponse.card.backupStatus?.isActive ?: false
return DetailsState(
scanResponse = action.scanResponse,
wallets = action.wallets,
cardInfo = action.scanResponse.card.toCardInfo(),
appCurrencyState = AppCurrencyState(action.fiatCurrencyName),
cardTermsOfUseUrl = action.cardTou.getUrl(action.scanResponse.card)
cardTermsOfUseUrl = action.cardTou.getUrl(action.scanResponse.card),
createBackupAllowed = action.scanResponse.isTangemWallet() && !backupIsActive,
)
}
private fun handleEraseWallet(action: DetailsAction.EraseWallet, state: DetailsState): DetailsState {
private fun handleEraseWallet(
action: DetailsAction.ResetToFactory,
state: DetailsState,
): DetailsState {
return when (action) {
DetailsAction.EraseWallet.Check -> {
DetailsAction.ResetToFactory.Check -> {
val card = state.scanResponse?.card
val notAllowedByAnyWallet = card?.wallets?.any { it.settings.isPermanent } ?: false
val notAllowedByCard = notAllowedByAnyWallet ||
(card?.isWalletDataSupported == true && !state.scanResponse.isTangemNote())
(card?.isWalletDataSupported == true &&
(!state.scanResponse.isTangemNote() && !state.scanResponse.isTangemWallet()))
val notEmpty = state.wallets.any {
it.hasPendingTransactions() || it.amounts.toSendableAmounts().isNotEmpty()
}
@ -66,22 +77,22 @@ private fun handleEraseWallet(action: DetailsAction.EraseWallet, state: DetailsS
}
state.copy(eraseWalletState = eraseWalletState)
}
DetailsAction.EraseWallet.Proceed -> {
DetailsAction.ResetToFactory.Proceed -> {
if (state.eraseWalletState == EraseWalletState.Allowed) {
state.copy(confirmScreenState = ConfirmScreenState.EraseWallet)
} else {
state
}
}
DetailsAction.EraseWallet.Cancel -> state.copy(eraseWalletState = null)
DetailsAction.EraseWallet.Failure -> state.copy(eraseWalletState = null)
DetailsAction.EraseWallet.Success -> state.copy(eraseWalletState = null)
DetailsAction.ResetToFactory.Cancel -> state.copy(eraseWalletState = null)
DetailsAction.ResetToFactory.Failure -> state.copy(eraseWalletState = null)
DetailsAction.ResetToFactory.Success -> state.copy(eraseWalletState = null)
else -> state
}
}
private fun handleAppCurrencyAction(
action: DetailsAction.AppCurrencyAction, state: DetailsState
action: DetailsAction.AppCurrencyAction, state: DetailsState,
): DetailsState {
return when (action) {
is DetailsAction.AppCurrencyAction.SetCurrencies -> {
@ -105,7 +116,7 @@ private fun handleAppCurrencyAction(
}
private fun handleSecurityAction(
action: DetailsAction.ManageSecurity, state: DetailsState
action: DetailsAction.ManageSecurity, state: DetailsState,
): DetailsState {
return when (action) {
is DetailsAction.ManageSecurity.SetCurrentOption -> {
@ -123,18 +134,18 @@ private fun handleSecurityAction(
state.copy(securityScreenState = SecurityScreenState(currentOption = securityOption))
}
is DetailsAction.ManageSecurity.OpenSecurity -> {
if (state.scanResponse?.card?.isStart2Coin == true ||
state.scanResponse?.isTangemNote() == true
) {
return state.copy(securityScreenState = state.securityScreenState?.copy(
allowedOptions = EnumSet.of(SecurityOption.LongTap),
selectedOption = state.securityScreenState.currentOption
))
val allowedSecurityOptions = when {
state.scanResponse?.card?.isStart2Coin == true ||
state.scanResponse?.isTangemNote() == true -> {
EnumSet.of(SecurityOption.LongTap)
}
state.scanResponse?.isTangemWallet() == true -> {
EnumSet.of(state.securityScreenState?.currentOption)
}
else -> prepareAllowedSecurityOptions(
state.scanResponse?.card, state.securityScreenState?.currentOption
)
}
val allowedSecurityOptions = prepareAllowedSecurityOptions(
state.scanResponse?.card, state.securityScreenState?.currentOption
)
state.copy(securityScreenState = state.securityScreenState?.copy(
allowedOptions = allowedSecurityOptions,
selectedOption = state.securityScreenState.currentOption
@ -171,18 +182,13 @@ private fun handleSecurityAction(
}
private fun prepareAllowedSecurityOptions(
card: Card?, currentSecurityOption: SecurityOption?
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

@ -22,6 +22,7 @@ data class DetailsState(
val confirmScreenState: ConfirmScreenState? = null,
val securityScreenState: SecurityScreenState? = null,
val cardTermsOfUseUrl: Uri? = null,
val createBackupAllowed: Boolean = false,
) : StateType {
// if you do not delegate - the application crashes on startup,

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

@ -58,13 +58,13 @@ class DetailsConfirmFragment : Fragment(R.layout.fragment_details_confirm),
when (state.confirmScreenState) {
ConfirmScreenState.EraseWallet -> {
toolbar.title = getString(R.string.details_row_title_erase_wallet)
tv_warning_description.text = getString(R.string.details_erase_wallet_warning)
btn_confirm.text = getString(R.string.details_row_title_erase_wallet)
toolbar.title = getString(R.string.details_row_title_reset_factory_settings)
tv_warning_description.text = getString(R.string.details_row_title_reset_factory_settings_warning)
btn_confirm.text = getString(R.string.details_row_title_reset_factory_settings)
btn_confirm.setCompoundDrawablesRelativeWithIntrinsicBounds(
null, null, getDrawable(R.drawable.ic_send), null
)
btn_confirm.setOnClickListener { store.dispatch(DetailsAction.EraseWallet.Confirm) }
btn_confirm.setOnClickListener { store.dispatch(DetailsAction.ResetToFactory.Confirm) }
}
ConfirmScreenState.LongTap, ConfirmScreenState.AccessCode,
ConfirmScreenState.PassCode -> {

View file

@ -89,20 +89,24 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
}
if (state.isTangemTwins) {
tv_erase_wallet.show()
tv_erase_wallet.text = getText(R.string.details_row_title_twins_recreate)
tv_erase_wallet.setOnClickListener {
tv_reset_to_factory.show()
tv_reset_to_factory.text = getText(R.string.details_row_title_twins_recreate)
tv_reset_to_factory.setOnClickListener {
store.dispatch(DetailsAction.ReCreateTwinsWallet(state.twinCardsState.cardNumber!!))
}
} else {
tv_erase_wallet.show()
tv_erase_wallet.text = getText(R.string.details_row_title_erase_wallet)
tv_erase_wallet.setOnClickListener {
store.dispatch(DetailsAction.EraseWallet.Check)
store.dispatch(DetailsAction.EraseWallet.Proceed)
tv_reset_to_factory.show()
tv_reset_to_factory.text = getText(R.string.details_row_title_reset_factory_settings)
tv_reset_to_factory.setOnClickListener {
store.dispatch(DetailsAction.ResetToFactory.Check)
store.dispatch(DetailsAction.ResetToFactory.Proceed)
}
}
tv_create_backup.show(state.createBackupAllowed)
tv_create_backup.setOnClickListener {
store.dispatch(DetailsAction.CreateBackup)
}
tv_app_currency.text = state.appCurrencyState.fiatCurrencyName

View file

@ -15,6 +15,7 @@ import com.tangem.tap.domain.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.features.send.redux.states.ButtonState
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
@ -26,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/"
}
}
@ -39,6 +39,7 @@ private val homeMiddleware: Middleware<AppState> = { dispatch, state ->
store.dispatch(GlobalAction.RestoreAppCurrency)
store.dispatch(GlobalAction.GetMoonPayStatus)
store.dispatch(HomeAction.SetTermsOfUseState(preferencesStorage.wasDisclaimerAccepted()))
store.dispatch(BackupAction.CheckForUnfinishedBackup)
}
is HomeAction.ShouldScanCardOnResume -> {
if (action.shouldScanCard) {
@ -59,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

@ -1,42 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet
import android.os.Bundle
import android.view.View
import androidx.transition.TransitionInflater
import com.tangem.tap.features.BaseStoreFragment
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
import com.tangem.tap.store
import com.tangem.wallet.R
import org.rekotlin.StoreSubscriber
/**
[REDACTED_AUTHOR]
*/
class OnboardingWalletFragment : BaseStoreFragment(R.layout.fragment_onboarding_wallet), StoreSubscriber<OnboardingWalletState> {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.fade)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
override fun subscribeToStore() {
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.onboardingWalletState == newState.onboardingWalletState
}.select { it.onboardingWalletState }
}
storeSubscribersList.add(this)
}
override fun newState(state: OnboardingWalletState) {
if (activity == null) return
}
}

View file

@ -1,19 +1,67 @@
package com.tangem.tap.features.onboarding.products.wallet.redux
import android.graphics.Bitmap
import com.tangem.tap.features.wallet.redux.Artwork
import org.rekotlin.Action
/**
[REDACTED_AUTHOR]
*/
sealed class OnboardingWalletAction : Action {
object Init : OnboardingWalletAction()
object GetToCreateWalletStep : OnboardingWalletAction()
object CreateWallet : OnboardingWalletAction()
object TopUp : OnboardingWalletAction()
object Done : OnboardingWalletAction()
object FinishOnboarding : OnboardingWalletAction()
object ProceedBackup : OnboardingWalletAction()
sealed class Backup : Action {
object LoadArtwork : OnboardingWalletAction()
class SetArtworkUrl(val artwork: Artwork) : OnboardingWalletAction()
object OnBackPressed : OnboardingWalletAction()
}
sealed class BackupAction : Action {
object DetermineBackupStep : BackupAction()
object IntroduceBackup : BackupAction()
object StartBackup : BackupAction()
object DismissBackup : BackupAction()
object ScanPrimaryCard : BackupAction()
object CheckForUnfinishedBackup : BackupAction()
object StartAddingBackupCards : BackupAction()
object AddBackupCard : BackupAction() {
object Success : BackupAction()
}
data class LoadBackupCardArtwork(
val cardId: CardId,
val cardPublicKey: ByteArray,
) : BackupAction() {
data class Success(val cardId: CardId, val artwork: Bitmap)
}
object GoToShop : BackupAction()
object FinishAddingBackupCards : BackupAction()
object ShowAccessCodeInfoScreen : BackupAction()
object ShowEnterAccessCodeScreen : BackupAction()
data class CheckAccessCode(val accessCode: String) : BackupAction()
data class SetAccessCodeError(val error: AccessCodeError?) : BackupAction()
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()
object PrepareToWritePrimaryCard : BackupAction()
object WritePrimaryCard: BackupAction()
object FinishBackup : BackupAction()
object DiscardBackup : BackupAction()
object DiscardSavedBackup : BackupAction()
object ResumeBackup : BackupAction()
}

View file

@ -1,18 +1,282 @@
package com.tangem.tap.features.onboarding.products.wallet.redux
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.operations.backup.BackupService
import com.tangem.tap.*
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.extensions.withMainContext
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.extensions.hasWallets
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.features.home.redux.HomeAction
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
class OnboardingWalletMiddleware {
companion object {
val handler = onboardingWalletMiddleware
const val BUY_WALLET_URL = "https://wallet.tangem.com/"
}
}
private val onboardingWalletMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
handleWalletAction(action)
next(action)
}
}
}
private fun handleWalletAction(action: Action) {
if (action !is OnboardingWalletAction) return
val globalState = store.state.globalState
val onboardingManager = globalState.onboardingState.onboardingManager
val scanResponse = onboardingManager?.scanResponse
val card = scanResponse?.card
val walletState = store.state.onboardingWalletState
when (action) {
OnboardingWalletAction.Init -> {
val action = when {
card == null -> {
OnboardingWalletAction.ProceedBackup
}
card.hasWallets() && card.backupStatus == Card.BackupStatus.NoBackup ->
OnboardingWalletAction.ProceedBackup
card.hasWallets() && card.backupStatus?.isActive == true ->
BackupAction.FinishBackup
else -> OnboardingWalletAction.GetToCreateWalletStep
}
store.dispatch(action)
}
is OnboardingNoteAction.LoadCardArtwork -> {
// scope.launch {
// val artwork = onboardingManager.loadArtwork()
// store.dispatchOnMain(OnboardingWalletAction.SetArtworkUrl(artwork))
// }
}
is OnboardingWalletAction.CreateWallet -> {
scope.launch {
scanResponse ?: return@launch
val result = tangemSdkManager.createProductWallet(scanResponse)
withMainContext {
when (result) {
is CompletionResult.Success -> {
//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)
}
is CompletionResult.Failure -> {
// do nothing
}
}
}
}
}
OnboardingWalletAction.FinishOnboarding -> {
store.dispatch(GlobalAction.Onboarding.Stop)
if (scanResponse == null) {
store.dispatch(NavigationAction.PopBackTo())
store.dispatch(HomeAction.ReadCard)
} else {
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.FinalizingPrimaryCard -> BackupAction.PrepareToWritePrimaryCard
is BackupService.State.FinalizingBackupCard ->
BackupAction.PrepareToWriteBackupCard(backupState.index)
else -> BackupAction.IntroduceBackup
}
store.dispatch(newAction)
}
OnboardingWalletAction.OnBackPressed -> {
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 ->
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

@ -1,20 +1,134 @@
package com.tangem.tap.features.onboarding.products.wallet.redux
import com.tangem.tap.backupService
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import org.rekotlin.Action
class OnboardingWalletReducer {
companion object {
fun reduce(action: Action, state: AppState): OnboardingWalletState = internalReduce(action, state)
fun reduce(action: Action, state: AppState): OnboardingWalletState =
internalReduce(action, state)
}
}
private fun internalReduce(action: Action, appState: AppState): OnboardingWalletState {
var state = appState.onboardingWalletState
when (action) {
val state = appState.onboardingWalletState
return when (action) {
is BackupAction -> state.copy(backupState = BackupReducer.reduce(action, state.backupState))
is GlobalAction.Onboarding.Start -> OnboardingWalletState(
backupState = state.backupState.copy(canSkipBackup = action.fromHomeScreen)
)
is OnboardingWalletAction.GetToCreateWalletStep ->
OnboardingWalletState(step = OnboardingWalletStep.CreateWallet)
is OnboardingWalletAction.ProceedBackup -> state.copy(step = OnboardingWalletStep.Backup)
is OnboardingWalletAction.SetArtworkUrl -> {
state.copy(artwork = action.artwork)
}
is OnboardingWalletAction.Done -> state.copy(step = OnboardingWalletStep.Done)
else -> state
}
}
return state
class BackupReducer {
companion object {
fun reduce(
action: BackupAction, state: BackupState,
): BackupState {
return when (action) {
BackupAction.IntroduceBackup -> BackupState(
backupStep = BackupStep.InitBackup,
canSkipBackup = state.canSkipBackup
)
BackupAction.StartBackup -> state.copy(backupStep = BackupStep.ScanOriginCard)
BackupAction.StartAddingBackupCards -> {
state.copy(backupStep = BackupStep.AddBackupCards)
}
BackupAction.AddBackupCard.Success -> {
state.copy(backupCardsNumber = state.backupCardsNumber + 1)
}
BackupAction.FinishAddingBackupCards -> {
state
}
BackupAction.ShowAccessCodeInfoScreen -> {
state.copy(backupStep = BackupStep.SetAccessCode)
}
BackupAction.ShowEnterAccessCodeScreen -> {
state.copy(backupStep = BackupStep.EnterAccessCode)
}
is BackupAction.SaveFirstAccessCode -> {
state.copy(
backupStep = BackupStep.ReenterAccessCode,
accessCode = action.accessCode
)
}
is BackupAction.SetAccessCodeError -> {
state.copy(
accessCodeError = action.error
)
}
is BackupAction.PrepareToWritePrimaryCard -> {
if (state.primaryCardId == null) {
state.copy(
primaryCardId = backupService.primaryCardId,
backupCardIds = backupService.backupCardIds,
backupCardsNumber = backupService.backupCardIds.size,
backupStep = BackupStep.WritePrimaryCard
)
} else {
state.copy(
backupStep = BackupStep.WritePrimaryCard
)
}
}
is BackupAction.PrepareToWriteBackupCard -> {
if (state.primaryCardId == null) {
state.copy(
primaryCardId = backupService.primaryCardId,
backupCardIds = backupService.backupCardIds,
backupCardsNumber = backupService.backupCardIds.size,
backupStep = BackupStep.WriteBackupCard(action.cardNumber)
)
} else {
state.copy(
backupStep = BackupStep.WriteBackupCard(action.cardNumber)
)
}
}
is BackupAction.FinishBackup -> {
state.copy(
backupStep = BackupStep.Finished
)
}
BackupAction.OnAccessCodeDialogClosed -> {
state.copy(backupStep = BackupStep.AddBackupCards)
}
BackupAction.WritePrimaryCard -> state
is BackupAction.WriteBackupCard -> state
is BackupAction.SaveAccessCodeConfirmation -> state
BackupAction.ScanPrimaryCard -> state
BackupAction.ShowEnterAccessCodeScreen -> state
BackupAction.ShowReenterAccessCodeScreen -> state
BackupAction.AddBackupCard -> state
BackupAction.DismissBackup -> state
is BackupAction.LoadBackupCardArtwork -> state
is BackupAction.CheckAccessCode -> state
BackupAction.GoToShop -> state
BackupAction.DetermineBackupStep -> state
BackupAction.CheckForUnfinishedBackup -> state
BackupAction.DiscardBackup -> state
BackupAction.ResumeBackup -> state
BackupAction.DiscardSavedBackup -> state
}
}
}
}

View file

@ -1,14 +1,81 @@
package com.tangem.tap.features.onboarding.products.wallet.redux
import android.graphics.Bitmap
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.features.wallet.redux.Artwork
import org.rekotlin.StateType
/**
[REDACTED_AUTHOR]
*/
data class OnboardingWalletState(
val any: String? = null
) : StateType
val step: OnboardingWalletStep = OnboardingWalletStep.None,
val backupState: BackupState = BackupState(),
val artwork: Artwork? = null,
val showConfetti: Boolean = false,
) : StateType {
fun getProgressStep(): Int {
return when {
step == OnboardingWalletStep.CreateWallet -> {
1
}
step == OnboardingWalletStep.Backup -> {
when (backupState.backupStep) {
BackupStep.InitBackup -> 2
BackupStep.ScanOriginCard -> 3
BackupStep.AddBackupCards -> 4
BackupStep.EnterAccessCode -> 4
BackupStep.ReenterAccessCode -> 4
BackupStep.SetAccessCode -> 4
BackupStep.WritePrimaryCard, is BackupStep.WriteBackupCard -> 5
BackupStep.Finished -> 6
}
}
step == OnboardingWalletStep.Done -> 6
else -> 1
}
}
}
enum class OnboardingWalletStep {
None, CreateWallet, TopUpWallet, Backup, Done
None, CreateWallet, Backup, Done
}
data class BackupState(
val primaryCardId: String? = null,
val backupCardsNumber: Int = 0,
val backupCardIds: List<CardId> = emptyList(),
val backupCardsArtworks: Map<CardId, Bitmap> = emptyMap(),
val accessCode: String = "",
val accessCodeError: AccessCodeError? = null,
val backupStep: BackupStep = BackupStep.InitBackup,
val maxBackupCards: Int = 2,
val canSkipBackup: Boolean = true
)
enum class AccessCodeError {
CodeTooShort, CodesDoNotMatch
}
typealias CardId = String
sealed class BackupStep {
object InitBackup : BackupStep()
object ScanOriginCard : BackupStep()
object AddBackupCards : BackupStep()
object SetAccessCode : BackupStep()
object EnterAccessCode : BackupStep()
object ReenterAccessCode : BackupStep()
object WritePrimaryCard : BackupStep()
data class WriteBackupCard(val cardNumber: Int) : BackupStep()
object Finished : BackupStep()
}
sealed class BackupDialog: StateDialog {
object AddMoreBackupCards : StateDialog
object BackupInProgress : StateDialog
object UnfinishedBackupFound : StateDialog
object ConfirmDiscardingBackup : StateDialog
}

View file

@ -0,0 +1,234 @@
package com.tangem.tap.features.onboarding.products.wallet.ui
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.animation.PropertyValuesHolder
import android.view.View
import android.widget.ImageView
import androidx.core.animation.doOnEnd
import com.tangem.tap.common.leapfrogWidget.LeapView
import com.tangem.tap.common.leapfrogWidget.LeapViewState
import com.tangem.tap.common.leapfrogWidget.LeapfrogWidget
class BackupCardsWidget(
val leapfrogWidget: LeapfrogWidget,
val getTopOfAnchorViewForActivateState: () -> Float,
) {
fun toWelcome(animate: Boolean = true, onEnd: () -> Unit = {}) {
val animator = createAnimator(animate, onEnd)
animator.playTogether(
createAnimator(BackupCardType.ORIGIN, createWelcomeProperties(BackupCardType.ORIGIN)),
createAnimator(BackupCardType.FIRST_BACKUP, createWelcomeProperties(BackupCardType.FIRST_BACKUP)),
createAnimator(BackupCardType.SECOND_BACKUP, createWelcomeProperties(BackupCardType.SECOND_BACKUP))
)
leapfrogWidget.fold { animator.start() }
}
fun toFolded(animate: Boolean = true, onEnd: () -> Unit = {}) {
val animator = createAnimator(animate, onEnd)
animator.playTogether(
createAnimator(BackupCardType.ORIGIN, createLeapfrogProperties(BackupCardType.ORIGIN)),
createAnimator(BackupCardType.FIRST_BACKUP, createLeapfrogProperties(BackupCardType.FIRST_BACKUP)),
createAnimator(BackupCardType.SECOND_BACKUP, createLeapfrogProperties(BackupCardType.SECOND_BACKUP))
)
leapfrogWidget.fold { animator.start() }
}
fun toFan(animate: Boolean = true, onEnd: () -> Unit = {}) {
val animator = createAnimator(animate, onEnd)
animator.playTogether(
createAnimator(BackupCardType.ORIGIN, createFanProperties(BackupCardType.ORIGIN)),
createAnimator(BackupCardType.FIRST_BACKUP, createFanProperties(BackupCardType.FIRST_BACKUP)),
createAnimator(BackupCardType.SECOND_BACKUP, createFanProperties(BackupCardType.SECOND_BACKUP))
)
leapfrogWidget.fold { animator.start() }
}
fun toLeapfrog(animate: Boolean = true, onEnd: () -> Unit = {}) {
val animator = createAnimator(animate, onEnd)
animator.playTogether(
createAnimator(BackupCardType.ORIGIN, createLeapfrogProperties(BackupCardType.ORIGIN)),
createAnimator(BackupCardType.FIRST_BACKUP, createLeapfrogProperties(BackupCardType.FIRST_BACKUP)),
createAnimator(BackupCardType.SECOND_BACKUP, createLeapfrogProperties(BackupCardType.SECOND_BACKUP))
)
leapfrogWidget.fold {
animator.doOnEnd {
leapfrogWidget.initViews()
leapfrogWidget.unfold()
}
animator.start()
}
}
//
// fun toActivate(animate: Boolean = true, onEnd: () -> Unit = {}) {
// val animator = createAnimator(animate, onEnd)
// animator.playTogether(
// createAnimator(TwinCardNumber.First, createActivateProperties(TwinCardNumber.First)),
// createAnimator(TwinCardNumber.Second, createActivateProperties(TwinCardNumber.Second))
// )
// animator.start()
// }
private fun createAnimator(animate: Boolean, onEnd: () -> Unit): AnimatorSet {
return AnimatorSet().apply {
duration = if (animate) 400 else 0
doOnEnd { onEnd() }
}
}
private fun createAnimator(
cardType: BackupCardType,
properties: CardProperties,
): ObjectAnimator {
val view = getLeapViewByCardNumber(cardType).view
val animator = ObjectAnimator.ofPropertyValuesHolder(view,
*properties.createValuesHolders().toTypedArray())
view.elevation = properties.elevation
return animator
}
private fun createWelcomeProperties(cardType: BackupCardType): CardProperties {
return when (cardType) {
BackupCardType.ORIGIN -> CardProperties(
xTranslation = 440f,
yTranslation = 0f,
rotation = 75f,
elevation = 2f,
scale = .9f,
)
BackupCardType.FIRST_BACKUP -> CardProperties(
xTranslation = 10f,
yTranslation = -100f,
rotation = 100f,
elevation = 1f,
scale = .9f,
)
BackupCardType.SECOND_BACKUP -> CardProperties(
xTranslation = -440f,
yTranslation = -100f,
rotation = 70f,
elevation = 0f,
scale = .9f,
)
}
}
private fun createFanProperties(cardType: BackupCardType): CardProperties {
return when (cardType) {
BackupCardType.ORIGIN -> CardProperties(
xTranslation = 0f,
yTranslation = 0f,
rotation = 5f,
elevation = 2f,
scale = 1f,
)
BackupCardType.FIRST_BACKUP -> CardProperties(
xTranslation = 0f,
yTranslation = -80f,
rotation = -5f,
elevation = 1f,
scale = 0.9f,
)
BackupCardType.SECOND_BACKUP -> CardProperties(
xTranslation = 10f,
yTranslation = -170f,
rotation = -20f,
elevation = 0f,
scale = 0.8f,
)
}
}
private fun createLeapfrogProperties(cardType: BackupCardType): CardProperties {
return when (cardType) {
BackupCardType.ORIGIN -> CardProperties(
xTranslation = 0f,
yTranslation = 0f,
rotation = 0f,
elevation = 2f,
scale = 1f,
)
BackupCardType.FIRST_BACKUP -> CardProperties(
xTranslation = 0f,
yTranslation = 0f,
rotation = 0f,
elevation = 1f,
scale = 0.9f,
)
BackupCardType.SECOND_BACKUP -> CardProperties(
xTranslation = 0f,
yTranslation = 0f,
rotation = 0f,
elevation = 0f,
scale = 0.8f,
)
}
}
private fun createActivateProperties(cardType: BackupCardType): CardProperties {
val topOfAnchorView = getTopOfAnchorViewForActivateState()
val twinProperties = CardProperties.from(getLeapViewByCardNumber(cardType).state)
return twinProperties.copy(
xTranslation = twinProperties.xTranslation,
yTranslation = twinProperties.yTranslation - topOfAnchorView,
rotation = 0f,
scale = twinProperties.scale - 0.4f,
)
}
private fun getLeapViewByCardNumber(cardType: BackupCardType): LeapView {
return when (cardType) {
BackupCardType.ORIGIN -> leapfrogWidget.getViewByPosition(0)
BackupCardType.FIRST_BACKUP -> leapfrogWidget.getViewByPosition(1)
BackupCardType.SECOND_BACKUP -> leapfrogWidget.getViewByPosition(2)
}
}
fun getOriginCardView(): ImageView {
return getLeapViewByCardNumber(BackupCardType.ORIGIN).view as ImageView
}
fun getFirstBackupCardView(): ImageView {
return getLeapViewByCardNumber(BackupCardType.FIRST_BACKUP).view as ImageView
}
fun getSecondBackupCardView(): ImageView {
return getLeapViewByCardNumber(BackupCardType.SECOND_BACKUP).view as ImageView
}
}
enum class BackupCardType { ORIGIN, FIRST_BACKUP, SECOND_BACKUP }
private data class CardProperties(
val xTranslation: Float = 0f,
val yTranslation: Float = 0f,
val rotation: Float = 0f,
val elevation: Float = 0f,
val scale: Float = 1.1f,
) {
fun createValuesHolders(): List<PropertyValuesHolder> {
return listOf(
PropertyValuesHolder.ofFloat(View.TRANSLATION_X, xTranslation),
PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, yTranslation),
PropertyValuesHolder.ofFloat(View.ROTATION, rotation),
PropertyValuesHolder.ofFloat(View.SCALE_X, scale),
PropertyValuesHolder.ofFloat(View.SCALE_Y, scale),
)
}
companion object {
fun from(leapViewState: LeapViewState): CardProperties {
val leapViewProperties = leapViewState.properties
return CardProperties(
yTranslation = leapViewProperties.yTranslation,
elevation = leapViewProperties.elevationEnd,
scale = leapViewProperties.scale,
)
}
}
}

View file

@ -0,0 +1,54 @@
package com.tangem.tap.features.onboarding.products.wallet.ui
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.StringRes
import androidx.recyclerview.widget.RecyclerView
import com.tangem.tap.common.extensions.getString
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.item_backup_info_adapter.view.*
class BackupInfoAdapter : RecyclerView.Adapter<BackupInfoViewHolder>() {
private val data = backupInfoSnippets
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BackupInfoViewHolder =
BackupInfoViewHolder(LayoutInflater.from(parent.context)
.inflate(R.layout.item_backup_info_adapter, parent, false))
override fun getItemCount(): Int = data.size
override fun onBindViewHolder(holder: BackupInfoViewHolder, position: Int) =
holder.itemView.run {
tv_header.text = getString(data[position].header)
tv_body.text = getString(data[position].body)
}
}
private class BackupInfo(
@StringRes val header: Int,
@StringRes val body: Int,
)
private val backupInfoSnippets = listOf(
BackupInfo(
R.string.onboarding_wallet_info_title_first,
R.string.onboarding_wallet_info_subtitle_first
),
BackupInfo(
R.string.onboarding_wallet_info_title_second,
R.string.onboarding_wallet_info_subtitle_second
),
BackupInfo(
R.string.onboarding_wallet_info_title_third,
R.string.onboarding_wallet_info_subtitle_third
),
BackupInfo(
R.string.onboarding_wallet_info_title_fourth,
R.string.onboarding_wallet_info_subtitle_fourth
),
)
class BackupInfoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)

View file

@ -0,0 +1,385 @@
package com.tangem.tap.features.onboarding.products.wallet.ui
import android.os.Bundle
import android.view.*
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import androidx.transition.TransitionManager
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.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.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.layout_onboarding_buttons_add_cards.*
import kotlinx.android.synthetic.main.layout_onboarding_buttons_common.*
import kotlinx.android.synthetic.main.view_confetti.*
import kotlinx.android.synthetic.main.view_onboarding_progress.*
import org.rekotlin.StoreSubscriber
class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
StoreSubscriber<OnboardingWalletState>, FragmentOnBackPressedHandler {
private var accessCodeDialog: AccessCodeDialog? = null
private lateinit var cardsWidget: BackupCardsWidget
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
postponeEnterTransition()
setHasOptionsMenu(true)
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.fade)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val leapfrog = LeapfrogWidget(fl_cards_container)
cardsWidget = BackupCardsWidget(leapfrog) { 200f }
startPostponedEnterTransition()
view_pager_backup_info.adapter = BackupInfoAdapter()
TabLayoutMediator(tab_layout_backup_info, view_pager_backup_info) { tab, position ->
//Some implementation
}.attach()
(activity as? AppCompatActivity)?.setSupportActionBar(toolbar)
store.dispatch(OnboardingWalletAction.Init)
toolbar.setNavigationOnClickListener { activity?.onBackPressed() }
addBackPressHandler(this)
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.onboardingWalletState == newState.onboardingWalletState
}.select { it.onboardingWalletState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun newState(state: OnboardingWalletState) {
if (activity == null) return
requireActivity().invalidateOptionsMenu()
state.artwork?.artwork?.let { imv_front_card.setImageBitmap(it) }
pb_state.max = 6
pb_state.progress = state.getProgressStep()
when (state.step) {
OnboardingWalletStep.None -> {
}
OnboardingWalletStep.CreateWallet -> setupCreateWalletState()
OnboardingWalletStep.Backup -> setBackupState(state.backupState)
OnboardingWalletStep.Done -> {
}
}
}
private fun setupCreateWalletState() {
btn_main_action.setText(R.string.onboarding_create_wallet_button_create_wallet)
btn_main_action.setOnClickListener { store.dispatch(OnboardingWalletAction.CreateWallet) }
btn_alternative_action.hide()
toolbar.title = getText(R.string.onboarding_getting_started)
tv_header.setText(R.string.onboarding_create_wallet_header)
tv_body.setText(R.string.onboarding_create_wallet_body)
cardsWidget.toFolded()
startPostponedEnterTransition()
}
private fun setBackupState(state: BackupState) {
when (state.backupStep) {
BackupStep.InitBackup -> showBackupIntro(state)
BackupStep.ScanOriginCard -> showScanOriginCard(state)
BackupStep.AddBackupCards -> showAddBackupCards(state)
BackupStep.SetAccessCode -> showSetAccessCode()
BackupStep.EnterAccessCode -> showEnterAccessCode(state)
BackupStep.ReenterAccessCode -> showReenterAccessCode(state)
is BackupStep.WritePrimaryCard -> showWritePrimaryCard(state)
is BackupStep.WriteBackupCard -> showWriteBackupCard(state)
BackupStep.Finished -> showSuccess(state)
}
}
private fun showBackupIntro(state: BackupState) {
imv_first_backup_card.show()
imv_second_backup_card.show()
cardsWidget.toWelcome()
tv_header.hide()
tv_body.hide()
view_pager_backup_info.show()
tab_layout_backup_info.show()
btn_main_action.text = getText(R.string.onboarding_button_backup_now)
btn_alternative_action.text = getText(R.string.onboarding_button_skip_backup)
btn_alternative_action.show(state.canSkipBackup)
btn_main_action.setOnClickListener { store.dispatch(BackupAction.StartBackup) }
btn_alternative_action.setOnClickListener { store.dispatch(BackupAction.DismissBackup) }
startPostponedEnterTransition()
}
private fun showScanOriginCard(state: BackupState) {
toolbar.title = getText(R.string.onboarding_navbar_title_creating_backup)
cardsWidget.toFolded()
tv_header.show()
tv_body.show()
view_pager_backup_info.hide()
tab_layout_backup_info.hide()
imv_first_backup_card.show()
imv_second_backup_card.show()
tv_header.text = getText(R.string.onboarding_title_scan_origin_card)
tv_body.text = getString(
R.string.onboarding_subtitle_scan_origin_card,
)
btn_main_action.text = getString(R.string.onboarding_button_scan_origin_card)
btn_alternative_action.hide()
btn_main_action.setOnClickListener { store.dispatch(BackupAction.ScanPrimaryCard) }
}
private fun showAddBackupCards(state: BackupState) {
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) {
btn_add_card.text = getText(R.string.onboarding_button_add_backup_card)
btn_add_card.setOnClickListener { store.dispatch(BackupAction.AddBackupCard) }
} else {
btn_add_card.text = "You hit the maximum"
btn_add_card.isEnabled = false
}
when (state.backupCardsNumber) {
0 -> {
cardsWidget.toFan()
tv_header.text = getText(R.string.onboarding_title_no_backup_cards)
tv_body.text = getText(R.string.onboarding_subtitle_no_backup_cards)
cardsWidget.getFirstBackupCardView().alpha = 0.6f
cardsWidget.getSecondBackupCardView().alpha = 0.2f
}
1 -> {
tv_header.text = getText(R.string.onboarding_title_one_backup_card)
tv_body.text = getText(R.string.onboarding_subtitle_one_backup_card)
cardsWidget.getFirstBackupCardView().animate().alpha(1f).setDuration(400)
cardsWidget.getSecondBackupCardView().alpha = 0.2f
}
2 -> {
tv_header.text = getText(R.string.onboarding_title_two_backup_cards)
tv_body.text = getText(R.string.onboarding_subtitle_two_backup_cards)
cardsWidget.getFirstBackupCardView().alpha = 1f
cardsWidget.getSecondBackupCardView().animate().alpha(1f).setDuration(400)
}
}
btn_continue.text = getText(R.string.onboarding_button_finalize_backup)
btn_continue.setOnClickListener { store.dispatch(BackupAction.FinishAddingBackupCards) }
btn_continue.isEnabled = state.backupCardsNumber != 0
}
private fun showSetAccessCode() {
accessCodeDialog = AccessCodeDialog(requireContext()).apply {
dismissWithAnimation = true
create()
setOnCancelListener {
store.dispatch(BackupAction.OnAccessCodeDialogClosed)
}
show()
showInfoScreen()
val view =
delegate.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet)
(view?.parent as? ViewGroup)?.let { TransitionManager.beginDelayedTransition(it) }
behavior.state = BottomSheetBehavior.STATE_EXPANDED
window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
view?.let { it.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT }
}
}
private fun showEnterAccessCode(state: BackupState) {
accessCodeDialog?.showEnterAccessCode()
accessCodeDialog?.showError(state.accessCodeError)
postUi(2000) { store.dispatch(BackupAction.SetAccessCodeError(null)) }
}
private fun showReenterAccessCode(state: BackupState) {
accessCodeDialog?.showReenterAccessCode()
accessCodeDialog?.showError(state.accessCodeError)
postUi(2000) { store.dispatch(BackupAction.SetAccessCodeError(null)) }
}
private fun showWritePrimaryCard(state: BackupState) {
accessCodeDialog?.dismiss()
prepareViewForFinalizeStep()
cardsWidget.getSecondBackupCardView().show(state.backupCardsNumber == 2)
cardsWidget.toLeapfrog() {
cardsWidget.getFirstBackupCardView().alpha = 0.6f
cardsWidget.getSecondBackupCardView().alpha = 0.2f
}
val cardIdFormatter = CardIdFormatter(CardIdDisplayFormat.LastMasked(4))
tv_header.text = getText(R.string.onboarding_title_prepare_origin)
tv_body.text = getString(
R.string.onboarding_subtitle_scan_primary_card_format,
state.primaryCardId?.let { cardIdFormatter.getFormattedCardId(it) }
)
btn_main_action.text = getText(R.string.onboarding_button_backup_origin)
btn_main_action.setOnClickListener { store.dispatch(BackupAction.WritePrimaryCard) }
}
private fun prepareViewForFinalizeStep() {
layout_buttons_add_cards.hide()
layout_buttons_common.show()
imv_first_backup_card.show()
imv_second_backup_card.show()
toolbar.title = getText(R.string.onboarding_button_finalize_backup)
imv_card_background.hide()
cardsWidget.toLeapfrog()
// imv_front_card.alpha = 1f
// imv_first_backup_card.alpha = 1f
// imv_second_backup_card.alpha = 1f
btn_alternative_action.hide()
}
private fun showWriteBackupCard(state: BackupState) {
prepareViewForFinalizeStep()
cardsWidget.getSecondBackupCardView().show(state.backupCardsNumber == 2)
val cardNumber = (state.backupStep as? BackupStep.WriteBackupCard)?.cardNumber ?: 1
when (cardNumber) {
1 -> cardsWidget.leapfrogWidget.leap() {
cardsWidget.getOriginCardView().alpha = 0.4f
cardsWidget.getFirstBackupCardView().alpha = 0.2f
cardsWidget.getSecondBackupCardView().alpha = 1f
}
2 -> cardsWidget.leapfrogWidget.leap() {
cardsWidget.getOriginCardView().alpha = 0.4f
cardsWidget.getFirstBackupCardView().alpha = 0.2f
cardsWidget.getSecondBackupCardView().alpha = 1f
}
}
val cardIdFormatter = CardIdFormatter(CardIdDisplayFormat.LastMasked(4))
tv_header.text =
getString(R.string.onboarding_title_backup_card_format, cardNumber)
tv_body.text = getString(
R.string.onboarding_subtitle_scan_primary_card_format,
cardIdFormatter.getFormattedCardId(state.backupCardIds[cardNumber - 1])
)
btn_main_action.text = getString(
R.string.onboarding_button_backup_card_format,
cardNumber
)
btn_main_action.setOnClickListener { store.dispatch(BackupAction.WriteBackupCard(cardNumber)) }
}
private fun showSuccess(state: BackupState) {
tv_header.text = getText(R.string.onboarding_done_header)
val text = when (state.backupCardsNumber) {
1 -> getText(R.string.onboarding_subtitle_success_backup_one_card)
2 -> getText(R.string.onboarding_subtitle_success_backup)
else -> getText(R.string.onboarding_subtitle_success_tangem_wallet_onboarding)
}
tv_header.show()
tv_body.show()
view_pager_backup_info.hide()
tab_layout_backup_info.hide()
tv_body.text = text
btn_main_action.text = getText(R.string.common_continue)
btn_alternative_action.hide()
btn_main_action.setOnClickListener {
lav_confetti.cancelAnimation()
lav_confetti.hide()
store.dispatch(OnboardingWalletAction.FinishOnboarding)
}
cardsWidget.leapfrogWidget.fold {
fl_cards_container?.hide()
imv_card_background?.hide()
lav_confetti?.show()
lav_confetti?.playAnimation()
imv_success?.alpha = 0f
imv_success?.show()
imv_success?.animate()
?.alpha(1f)
?.setDuration(400)
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.shop_menu -> {
store.dispatchOpenUrl(BUY_WALLET_URL)
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.shop, menu)
val backupStep = store.state.onboardingWalletState.backupState.backupStep
val shopMenuShouldBeVisible =
backupStep == BackupStep.ScanOriginCard || backupStep == BackupStep.AddBackupCards
menu.getItem(0).isVisible = shopMenuShouldBeVisible
}
override fun handleOnBackPressed() {
store.dispatch(OnboardingWalletAction.OnBackPressed)
}
}

View file

@ -0,0 +1,74 @@
package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.features.onboarding.products.wallet.redux.AccessCodeError
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.layout_backup_access_code.*
import kotlinx.android.synthetic.main.layout_backup_access_code_info.*
import kotlinx.android.synthetic.main.layout_backup_access_code_submit.*
class AccessCodeDialog(context: Context) : BottomSheetDialog(context) {
init {
behavior.state = BottomSheetBehavior.STATE_EXPANDED
val dialogView = LayoutInflater.from(context).inflate(R.layout.layout_backup_access_code, null)
setContentView(dialogView)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
override fun setContentView(view: View) {
super.setContentView(view)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
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 {
store.dispatch(BackupAction.ShowEnterAccessCodeScreen)
}
}
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()))
}
}
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 {
store.dispatch(BackupAction.SaveAccessCodeConfirmation(et_access_code.text.toString()))
}
}
fun showError(error: AccessCodeError?) {
when (error) {
AccessCodeError.CodeTooShort ->
til_access_code.error = context.getString(R.string.onboarding_access_code_too_short)
AccessCodeError.CodesDoNotMatch ->
til_access_code.error = context.getString(R.string.onboarding_access_codes_doesnt_match)
null -> til_access_code.error = null
}
}
}

View file

@ -0,0 +1,28 @@
package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.store
import com.tangem.wallet.R
class AddMoreBackupCardsDialog {
companion object {
fun create(context: Context): AlertDialog {
return AlertDialog.Builder(context).apply {
setTitle(R.string.alert_title)
setMessage(R.string.onboarding_alert_message_not_max_backup_cards_added)
setPositiveButton(R.string.common_continue) { _, _ ->
store.dispatch(BackupAction.ShowAccessCodeInfoScreen)
}
setNegativeButton(R.string.onboarding_button_add_more_cards) { _, _ ->
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.store
import com.tangem.wallet.R
class BackupInProgressDialog {
companion object {
fun create(context: Context): AlertDialog {
return AlertDialog.Builder(context).apply {
setTitle(R.string.alert_title)
setMessage(R.string.onboarding_backup_exit_warning)
setPositiveButton(R.string.warning_button_ok) { _, _ ->
store.dispatch(GlobalAction.HideDialog)
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}
}
}

View file

@ -0,0 +1,29 @@
package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.store
import com.tangem.wallet.R
class ConfirmDiscardingBackupDialog {
companion object {
fun create(context: Context): AlertDialog {
return AlertDialog.Builder(context).apply {
setTitle(R.string.welcome_interrupted_backup_discard_title)
setMessage(R.string.welcome_interrupted_backup_discard_message)
setPositiveButton(R.string.welcome_interrupted_backup_discard_resume) { _, _ ->
store.dispatch(BackupAction.ResumeBackup)
}
setNegativeButton(R.string.welcome_interrupted_backup_discard_discard) { _, _ ->
store.dispatch(BackupAction.DiscardSavedBackup)
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
setCancelable(false)
}.create()
}
}
}

View file

@ -0,0 +1,29 @@
package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog
import com.tangem.tap.store
import com.tangem.wallet.R
class UnfinishedBackupFoundDialog {
companion object {
fun create(context: Context): AlertDialog {
return AlertDialog.Builder(context).apply {
setTitle(R.string.alert_title)
setMessage(R.string.welcome_interrupted_backup_alert_message)
setPositiveButton(R.string.welcome_interrupted_backup_alert_resume) { _, _ ->
store.dispatch(GlobalAction.HideDialog)
store.dispatch(BackupAction.ResumeBackup)
}
setNegativeButton(R.string.welcome_interrupted_backup_alert_discard) { _, _ ->
store.dispatch(GlobalAction.HideDialog)
store.dispatch(GlobalAction.ShowDialog(BackupDialog.ConfirmDiscardingBackup))
}
setCancelable(false)
}.create()
}
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.tap.features.onboarding.service
//class OnboardingWalletService(
// override val fromScreen: AppScreen,
// override var scanResponse: ScanResponse,
// cardInfoStorage: UsedCardsPrefStorage,
// walletManagerFactory: WalletManagerFactory,
//) : ProductOnboardingService(cardInfoStorage, walletManagerFactory) {
//
// override fun navigateToScreen(): AppScreen {
// return AppScreen.OnboardingWallet
// }
//}

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

@ -285,8 +285,12 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
}
}
override fun onDestroy() {
override fun onDestroyView() {
keyboardObserver.unregisterListener()
super.onDestroyView()
}
override fun onDestroy() {
store.dispatch(ReleaseSendState)
super.onDestroy()
}

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

@ -2,6 +2,7 @@ package com.tangem.tap.features.wallet.ui.wallet
import android.app.Dialog
import androidx.recyclerview.widget.LinearLayoutManager
import com.tangem.common.card.Card
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.StateDialog
@ -49,8 +50,22 @@ class MultiWalletView : WalletView {
btn_scan_multiwallet?.show()
rv_multiwallet.show()
btn_add_token.show()
setupWalletCardNumber(fragment)
}
private fun setupWalletCardNumber(fragment: WalletFragment) = with(fragment) {
val card = store.state.globalState.scanResponse?.card
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 =
getString(R.string.wallet_twins_chip_format, 1, cardCount)
} else {
tv_twin_card_number.hide()
}
}
private fun setupButtons(fragment: WalletFragment) = with(fragment) {
btn_scan_multiwallet?.setOnClickListener { store.dispatch(WalletAction.Scan) }
}

View file

@ -96,11 +96,11 @@ class SingleWalletView : WalletView {
twinCardsState?.cardNumber?.let { cardNumber ->
tv_twin_card_number.show()
val number = when (cardNumber) {
TwinCardNumber.First -> "1"
TwinCardNumber.Second -> "2"
TwinCardNumber.First -> 1
TwinCardNumber.Second -> 2
}
tv_twin_card_number.text =
this.getString(R.string.wallet_twins_chip_format, number)
this.getString(R.string.wallet_twins_chip_format, number, 2)
}
if (twinCardsState?.cardNumber == null) {
tv_twin_card_number.hide()

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

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="#ABABAB" android:state_enabled="false" />
<item android:color="#060606" />
</selector>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="#1ACE80" android:state_focused="true"/>
<item android:color="#1ACE80" android:state_hovered="true"/>
<item android:color="#E0E0E0" android:state_enabled="false"/>
<item android:color="#E0E0E0"/> <!-- unfocused -->
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 837 KiB

View file

@ -0,0 +1,24 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="119dp"
android:height="119dp"
android:viewportWidth="119"
android:viewportHeight="119">
<path
android:pathData="M111.664,30.74H7.336C3.291,30.74 0,34.03 0,38.075V60.431C0,64.476 3.291,67.767 7.336,67.767H72.541C73.506,67.767 74.288,66.985 74.288,66.021C74.288,65.056 73.506,64.274 72.541,64.274H7.336C5.217,64.274 3.493,62.55 3.493,60.431V38.075C3.493,35.957 5.217,34.233 7.336,34.233H111.664C113.783,34.233 115.507,35.957 115.507,38.075V54.842C115.507,55.807 116.289,56.589 117.253,56.589C118.218,56.589 119,55.807 119,54.842V38.075C119,34.03 115.709,30.74 111.664,30.74Z"
android:fillColor="#060606"/>
<path
android:pathData="M111.548,58.972V56.706C111.548,48.552 104.914,41.918 96.76,41.918C88.606,41.918 81.973,48.552 81.973,56.706V58.972C79.809,59.703 78.247,61.751 78.247,64.158V82.788C78.247,85.805 80.702,88.26 83.719,88.26H109.801C112.819,88.26 115.274,85.805 115.274,82.788V64.158C115.274,61.751 113.711,59.703 111.548,58.972ZM96.76,45.411C102.988,45.411 108.055,50.478 108.055,56.706V58.685H85.466V56.706C85.466,50.478 90.533,45.411 96.76,45.411ZM111.781,82.788C111.781,83.879 110.893,84.767 109.801,84.767H83.719C82.628,84.767 81.74,83.879 81.74,82.788V64.158C81.74,63.066 82.628,62.178 83.719,62.178H109.801C110.893,62.178 111.781,63.066 111.781,64.158V82.788Z"
android:fillColor="#060606"/>
<path
android:pathData="M28.343,44.654C27.861,43.819 26.793,43.533 25.957,44.015L22.123,46.228V41.801C22.123,40.837 21.341,40.055 20.377,40.055C19.412,40.055 18.63,40.837 18.63,41.801V46.228L14.796,44.015C13.961,43.532 12.893,43.819 12.411,44.654C11.928,45.489 12.215,46.558 13.05,47.04L16.884,49.253L13.05,51.467C12.215,51.949 11.928,53.017 12.411,53.853C12.734,54.413 13.321,54.726 13.925,54.726C14.221,54.726 14.521,54.651 14.796,54.492L18.63,52.278V56.706C18.63,57.67 19.412,58.452 20.377,58.452C21.341,58.452 22.123,57.67 22.123,56.706V52.278L25.957,54.492C26.232,54.651 26.533,54.726 26.829,54.726C27.433,54.726 28.02,54.413 28.343,53.853C28.825,53.017 28.539,51.949 27.704,51.467L23.87,49.253L27.704,47.04C28.539,46.558 28.825,45.489 28.343,44.654Z"
android:fillColor="#060606"/>
<path
android:pathData="M50.699,44.654C50.217,43.819 49.149,43.533 48.313,44.015L44.48,46.228V41.801C44.48,40.837 43.698,40.055 42.733,40.055C41.768,40.055 40.986,40.837 40.986,41.801V46.228L37.153,44.015C36.317,43.532 35.249,43.819 34.767,44.654C34.285,45.489 34.571,46.558 35.406,47.04L39.24,49.253L35.406,51.467C34.571,51.949 34.285,53.017 34.767,53.853C35.09,54.413 35.677,54.726 36.281,54.726C36.577,54.726 36.877,54.651 37.153,54.492L40.986,52.278V56.706C40.986,57.67 41.768,58.452 42.733,58.452C43.698,58.452 44.48,57.67 44.48,56.706V52.278L48.313,54.492C48.588,54.651 48.889,54.726 49.185,54.726C49.789,54.726 50.376,54.413 50.699,53.853C51.181,53.017 50.895,51.949 50.06,51.467L46.226,49.253L50.06,47.04C50.895,46.558 51.181,45.489 50.699,44.654Z"
android:fillColor="#060606"/>
<path
android:pathData="M73.055,44.654C72.573,43.819 71.505,43.533 70.67,44.015L66.836,46.228V41.801C66.836,40.837 66.054,40.055 65.089,40.055C64.125,40.055 63.343,40.837 63.343,41.801V46.228L59.509,44.015C58.673,43.532 57.605,43.819 57.123,44.654C56.641,45.489 56.927,46.558 57.762,47.04L61.596,49.253L57.762,51.467C56.927,51.949 56.641,53.017 57.123,53.853C57.446,54.413 58.034,54.726 58.637,54.726C58.933,54.726 59.234,54.651 59.509,54.492L63.343,52.278V56.706C63.343,57.67 64.125,58.452 65.089,58.452C66.054,58.452 66.836,57.67 66.836,56.706V52.278L70.67,54.492C70.945,54.651 71.245,54.726 71.541,54.726C72.145,54.726 72.732,54.413 73.055,53.853C73.538,53.017 73.252,51.949 72.416,51.467L68.582,49.253L72.416,47.04C73.252,46.558 73.538,45.489 73.055,44.654Z"
android:fillColor="#060606"/>
<path
android:pathData="M96.76,66.137C93.743,66.137 91.288,68.592 91.288,71.61C91.288,74.017 92.85,76.064 95.014,76.795V79.062C95.014,80.026 95.796,80.808 96.76,80.808C97.725,80.808 98.507,80.026 98.507,79.062V76.795C100.67,76.064 102.233,74.017 102.233,71.61C102.233,68.592 99.778,66.137 96.76,66.137ZM96.76,73.589C95.669,73.589 94.781,72.701 94.781,71.61C94.781,70.518 95.669,69.63 96.76,69.63C97.852,69.63 98.74,70.518 98.74,71.61C98.74,72.701 97.852,73.589 96.76,73.589Z"
android:fillColor="#060606"/>
</vector>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape
android:innerRadius="0dp"
android:shape="ring"
android:thickness="4.5dp"
android:useLevel="false">
<solid
android:color="#80686868"
android:fillAlpha="0.5"
/>
</shape>
</item>
</layer-list>

View file

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="4.5dp"
android:height="4.5dp"
android:viewportWidth="7"
android:viewportHeight="7">
<path
android:pathData="M3.5,7C5.433,7 7,5.433 7,3.5C7,1.567 5.433,0 3.5,0C1.567,0 0,1.567 0,3.5C0,5.433 1.567,7 3.5,7Z"
android:fillColor="#686868"
android:fillAlpha="0.5"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,22 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="25dp"
android:height="25dp"
android:viewportWidth="25"
android:viewportHeight="25">
<path
android:pathData="M3,11H11C12.6569,11 14,12.3431 14,14V22C14,23.6569 12.6569,25 11,25H3C1.3432,25 0,23.6569 0,22V14C0,12.3431 1.3432,11 3,11Z"
android:fillColor="#1ACE80"
android:fillType="evenOdd"/>
<path
android:pathData="M18.7143,11H23.2857C24.2325,11 25,11.7675 25,12.7143V17.2857C25,18.2325 24.2325,19 23.2857,19H18.7143C17.7675,19 17,18.2325 17,17.2857V12.7143C17,11.7675 17.7675,11 18.7143,11Z"
android:strokeAlpha="0.15"
android:fillColor="#1ACE80"
android:fillType="evenOdd"
android:fillAlpha="0.15"/>
<path
android:pathData="M7.7143,0H19.2857C20.2325,0 21,0.7675 21,1.7143V6.2857C21,7.2325 20.2325,8 19.2857,8H7.7143C6.7675,8 6,7.2325 6,6.2857V1.7143C6,0.7675 6.7675,0 7.7143,0Z"
android:strokeAlpha="0.5"
android:fillColor="#1ACE80"
android:fillType="evenOdd"
android:fillAlpha="0.5"/>
</vector>

View file

@ -0,0 +1,22 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="25dp"
android:height="25dp"
android:viewportWidth="25"
android:viewportHeight="25">
<path
android:pathData="M18,25C21.866,25 25,21.866 25,18C25,14.134 21.866,11 18,11C14.134,11 11,14.134 11,18C11,21.866 14.134,25 18,25Z"
android:fillColor="#1ACE80"
android:fillType="evenOdd"/>
<path
android:pathData="M9,10C11.7614,10 14,7.7614 14,5C14,2.2386 11.7614,0 9,0C6.2386,0 4,2.2386 4,5C4,7.7614 6.2386,10 9,10Z"
android:strokeAlpha="0.5"
android:fillColor="#1ACE80"
android:fillType="evenOdd"
android:fillAlpha="0.5"/>
<path
android:pathData="M4,21C6.2091,21 8,19.2091 8,17C8,14.7909 6.2091,13 4,13C1.7909,13 0,14.7909 0,17C0,19.2091 1.7909,21 4,21Z"
android:strokeAlpha="0.15"
android:fillColor="#1ACE80"
android:fillType="evenOdd"
android:fillAlpha="0.15"/>
</vector>

View file

@ -0,0 +1,22 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="26dp"
android:height="22dp"
android:viewportWidth="26"
android:viewportHeight="22">
<path
android:pathData="M19.3989,4.1971L13.3558,4.1971C12.3137,4.1512 11.3955,4.9319 11.3048,5.9408C11.2723,6.3027 11.3515,6.6613 11.5324,6.9712L14.7623,12.5027C15.2667,13.3667 16.4185,13.6429 17.3349,13.1197C17.6423,12.9442 17.8966,12.6917 18.0696,12.3902L21.041,6.9712C21.5566,6.0725 21.1048,4.9181 20.1946,4.4571C19.9583,4.3374 19.6695,4.209 19.3989,4.1971Z"
android:strokeAlpha="0.5"
android:fillColor="#1ACE80"
android:fillType="evenOdd"
android:fillAlpha="0.5"/>
<path
android:pathData="M20.1304,14.9339L17.4396,19.051C16.9906,19.738 17.1702,20.6677 17.8409,21.1276C18.0815,21.2926 18.3644,21.3806 18.6539,21.3806H23.8216C24.6287,21.3806 25.2829,20.7105 25.2829,19.8837C25.2829,19.6064 25.2077,19.3345 25.0657,19.0984L22.5889,14.9813C22.1655,14.2775 21.2652,14.0586 20.5781,14.4923C20.3997,14.6049 20.247,14.7555 20.1304,14.9339Z"
android:strokeAlpha="0.15"
android:fillColor="#1ACE80"
android:fillType="evenOdd"
android:fillAlpha="0.15"/>
<path
android:pathData="M5.1416,10.4031L0.4325,17.4279C-0.3532,18.6 -0.0387,20.1864 1.1349,20.9711C1.5559,21.2526 2.051,21.4029 2.5576,21.4029H11.6011C13.0135,21.4029 14.1585,20.2594 14.1585,18.8488C14.1585,18.3756 14.0269,17.9117 13.7783,17.5089L9.4439,10.4841C8.7029,9.2832 7.1275,8.9096 5.925,9.6496C5.6128,9.8417 5.3456,10.0987 5.1416,10.4031Z"
android:fillColor="#1ACE80"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,18.1876C11.4712,18.1865 10.9435,18.1373 10.4236,18.0405C10.2623,18.0147 10.1155,17.9323 10.0094,17.8081C9.9033,17.6839 9.845,17.526 9.8447,17.3626C9.8464,17.2619 9.8699,17.1627 9.9136,17.0719C9.9572,16.9811 10.02,16.9008 10.0976,16.8366C10.1751,16.7723 10.2657,16.7256 10.3631,16.6996C10.4604,16.6737 10.5622,16.669 10.6615,16.6861C11.1028,16.7692 11.5509,16.8115 12,16.8126C16.0728,16.8126 19.61,13.2465 20.7224,12.0001C19.9811,11.1704 19.1666,10.4092 18.2886,9.7259C18.2172,9.6706 18.1574,9.6017 18.1125,9.5233C18.0677,9.4449 18.0388,9.3584 18.0274,9.2688C18.0044,9.0879 18.0542,8.9052 18.1659,8.761C18.2776,8.6167 18.442,8.5228 18.6229,8.4998C18.7125,8.4884 18.8035,8.4947 18.8906,8.5185C18.9778,8.5422 19.0594,8.5829 19.1308,8.6382C20.2501,9.5006 21.2685,10.4866 22.1668,11.5773C22.2615,11.6983 22.313,11.8475 22.313,12.0011C22.313,12.1548 22.2615,12.304 22.1668,12.425C21.9819,12.6594 17.5853,18.1876 12,18.1876Z"
android:fillColor="#A4A4A4"/>
<path
android:pathData="M19.3608,4.6384C19.2319,4.5095 19.0571,4.4371 18.8748,4.4371C18.6925,4.4371 18.5176,4.5095 18.3887,4.6384L16.2024,6.8247C15.3747,6.4258 14.4989,6.1356 13.5968,5.9612C13.0702,5.8622 12.5356,5.8122 11.9998,5.812C6.4145,5.812 2.0179,11.3402 1.8337,11.5753C1.739,11.6963 1.6875,11.8455 1.6875,11.9991C1.6875,12.1528 1.739,12.302 1.8337,12.423C2.7311,13.5128 3.7486,14.4978 4.8669,15.3593C5.396,15.7739 5.9503,16.1553 6.5266,16.5012L4.6387,18.3884C4.573,18.4518 4.5207,18.5277 4.4846,18.6116C4.4486,18.6955 4.4296,18.7857 4.4288,18.877C4.428,18.9682 4.4454,19.0588 4.48,19.1433C4.5146,19.2278 4.5656,19.3045 4.6302,19.3691C4.6947,19.4336 4.7715,19.4847 4.856,19.5192C4.9405,19.5538 5.031,19.5712 5.1223,19.5704C5.2136,19.5696 5.3038,19.5507 5.3877,19.5146C5.4715,19.4786 5.5474,19.4262 5.6108,19.3606L19.3608,5.6105C19.4897,5.4816 19.5621,5.3068 19.5621,5.1245C19.5621,4.9422 19.4897,4.7674 19.3608,4.6384ZM5.7112,14.2744C4.8332,13.5908 4.0186,12.8294 3.2774,11.9995C4.3898,10.7537 7.927,7.187 11.9998,7.187C12.4521,7.1873 12.9034,7.2297 13.3479,7.3135C13.9703,7.4319 14.5783,7.6163 15.1616,7.8635L13.8952,9.1299C13.2342,8.6931 12.4427,8.4981 11.6544,8.5778C10.8662,8.6575 10.1297,9.007 9.5695,9.5672C9.0093,10.1274 8.6598,10.8639 8.5801,11.6521C8.5004,12.4403 8.6954,13.2319 9.1322,13.8929L7.531,15.4968C6.8943,15.1361 6.2859,14.7274 5.7112,14.2744ZM10.1435,12.8822C10.0095,12.6071 9.939,12.3055 9.9373,11.9995C9.9374,11.4541 10.1528,10.9308 10.5368,10.5434C10.8428,10.2523 11.229,10.0594 11.6455,9.9896C12.0621,9.9198 12.49,9.9761 12.8743,10.1515L10.1435,12.8822Z"
android:fillColor="#A4A4A4"/>
</vector>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape
android:innerRadius="0dp"
android:shape="ring"
android:thickness="4.5dp"
android:useLevel="false">
<solid android:color="#060606"/>
</shape>
</item>
</layer-list>

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12.0003,18.1875C6.4136,18.1875 2.0185,12.6593 1.8342,12.4242C1.7393,12.3032 1.6877,12.1538 1.6877,12C1.6877,11.8462 1.7393,11.6968 1.8342,11.5758C2.0185,11.3407 6.4136,5.8125 12.0003,5.8125C17.5869,5.8125 21.9821,11.3407 22.1663,11.5758C22.2612,11.6968 22.3128,11.8462 22.3128,12C22.3128,12.1538 22.2612,12.3032 22.1663,12.4242C21.9821,12.6593 17.5869,18.1875 12.0003,18.1875ZM3.278,12C4.391,13.2471 7.9268,16.8125 12.0003,16.8125C16.0737,16.8125 19.6095,13.2471 20.7226,12C19.6095,10.7529 16.0737,7.1875 12.0003,7.1875C7.9268,7.1875 4.391,10.7529 3.278,12Z"
android:fillColor="#A4A4A4"/>
<path
android:pathData="M12,15.4375C11.3201,15.4375 10.6555,15.2359 10.0902,14.8582C9.5249,14.4804 9.0843,13.9436 8.8242,13.3155C8.564,12.6873 8.4959,11.9962 8.6285,11.3294C8.7612,10.6626 9.0886,10.0501 9.5693,9.5693C10.0501,9.0886 10.6626,8.7612 11.3294,8.6285C11.9962,8.4959 12.6873,8.564 13.3155,8.8242C13.9436,9.0843 14.4804,9.5249 14.8582,10.0902C15.2359,10.6555 15.4375,11.3201 15.4375,12C15.4364,12.9113 15.0739,13.785 14.4295,14.4295C13.785,15.0739 12.9113,15.4364 12,15.4375ZM12,9.9375C11.5921,9.9375 11.1933,10.0585 10.8541,10.2851C10.515,10.5117 10.2506,10.8338 10.0945,11.2107C9.9384,11.5876 9.8975,12.0023 9.9771,12.4024C10.0567,12.8025 10.2531,13.17 10.5416,13.4584C10.83,13.7468 11.1975,13.9433 11.5976,14.0229C11.9977,14.1024 12.4124,14.0616 12.7893,13.9055C13.1661,13.7494 13.4883,13.485 13.7149,13.1459C13.9415,12.8067 14.0625,12.4079 14.0625,12C14.0625,11.453 13.8452,10.9284 13.4584,10.5416C13.0716,10.1548 12.547,9.9375 12,9.9375Z"
android:fillColor="#A4A4A4"/>
</vector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_hide_password" android:state_checked="true"/>
<item android:drawable="@drawable/ic_show_password"/>
</selector>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_selected_dot"
android:state_selected="true"/>
<item android:drawable="@drawable/ic_dot"/>
</selector>

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

@ -160,15 +160,14 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_signed_hashes" />
<TextView
android:id="@+id/tv_erase_wallet"
android:id="@+id/tv_create_backup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawablePadding="15dp"
android:paddingTop="7dp"
android:paddingBottom="7dp"
android:text="@string/details_row_title_erase_wallet"
android:text="@string/details_row_title_create_backup"
android:textColor="@color/darkGray6"
android:textSize="16sp"
app:drawableEndCompat="@drawable/ic_angle_bracket_right"
@ -176,6 +175,21 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_security_title" />
<TextView
android:id="@+id/tv_reset_to_factory"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawablePadding="15dp"
android:paddingTop="7dp"
android:paddingBottom="7dp"
android:text="@string/details_row_title_reset_factory_settings"
android:textColor="@color/darkGray6"
android:textSize="16sp"
app:drawableEndCompat="@drawable/ic_angle_bracket_right"
app:drawableTint="@color/darkGray1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_create_backup" />
<TextView
android:id="@+id/tv_settings_title"
android:layout_width="wrap_content"
@ -187,7 +201,7 @@
android:textColor="@color/colorSecondary"
android:textSize="13sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_erase_wallet" />
app:layout_constraintTop_toBottomOf="@id/tv_reset_to_factory" />
<View
android:layout_width="match_parent"

View file

@ -1,6 +1,220 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/coordinator_details_confirm"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:background="@color/backgroundLightGray"
android:clipChildren="false"
android:clipToPadding="false"
android:orientation="vertical">
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
style="@style/ThemeOverlay.MyTheme.Toolbar.AccentColorMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/backgroundLightGray"
android:fitsSystemWindows="true"
app:liftOnScroll="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationIcon="@drawable/ic_baseline_arrow_back_24"
app:title="@string/onboarding_getting_started"
app:menu="@menu/shop"/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/onboarding_main_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<include
layout="@layout/view_onboarding_progress"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="32dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_card_background"
android:layout_width="236dp"
android:layout_height="236dp"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:background="@drawable/shape_circle"
android:backgroundTint="@color/lightGray0"
android:elevation="0dp"
android:scaleType="fitCenter"
app:layout_constraintStart_toStartOf="@id/fl_cards_container"
app:layout_constraintEnd_toEndOf="@id/fl_cards_container"
app:layout_constraintTop_toTopOf="@id/fl_cards_container"
app:layout_constraintBottom_toBottomOf="@id/fl_cards_container"
android:layout_marginBottom="16dp"
/>
<FrameLayout
android:id="@+id/fl_cards_container"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="16dp"
app:layout_constraintBottom_toTopOf="@id/guideline2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.495"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/pb_state">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_second_backup_card"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/card_placeholder_new"
android:visibility="gone"
/>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_first_backup_card"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/card_placeholder_new"
android:visibility="gone"/>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_front_card"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/card_placeholder_new"
android:transitionName="imv_front_card"
/>
</FrameLayout>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_success"
android:layout_width="142dp"
android:layout_height="142dp"
android:src="@drawable/ic_activation_success"
app:layout_constraintBottom_toTopOf="@id/guideline2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.495"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/pb_state"
android:visibility="gone"/>
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/ll_header_body_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="72dp"
android:layout_marginEnd="32dp"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/guideline2">
</androidx.appcompat.widget.LinearLayoutCompat>
<TextView
android:id="@+id/tv_header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:fontFamily="sans-serif-medium"
tools:text="Header"
android:textColor="#060606"
android:textSize="24sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.504"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/guideline2" />
<TextView
android:id="@+id/tv_body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="64dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="64dp"
tools:text="Body"
android:textAlignment="center"
android:textColor="#686868"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_header" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager_backup_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintHorizontal_bias="0.504"
app:flow_verticalBias="0"
app:layout_constraintTop_toTopOf="@+id/guideline2"
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="10dp"
android:background="@color/backgroundLightGray"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/view_pager_backup_info"
app:layout_constraintBottom_toTopOf="@id/layout_buttons_common"
app:tabBackground="@drawable/selector_tabs_dots"
app:tabIndicator="@null"
app:tabPaddingStart="8dp"
app:tabPaddingEnd="8dp"
app:tabGravity="center"/>
<include
android:id="@+id/layout_buttons_common"
layout="@layout/layout_onboarding_buttons_common"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
/>
<include
android:id="@+id/layout_buttons_add_cards"
layout="@layout/layout_onboarding_buttons_add_cards"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintGuide_percent="0.60"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<include
layout="@layout/view_confetti"
android:visibility="gone" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView
android:id="@+id/tv_header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:fontFamily="sans-serif-medium"
android:text="Header"
android:textColor="#060606"
android:textSize="24sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.504"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="64dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="64dp"
android:text="Body"
android:textAlignment="center"
android:textColor="#686868"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_header" />
</androidx.constraintlayout.widget.ConstraintLayout>

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

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/access_code_title"
style="@style/heading"
android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="100dp"
android:text="@string/onboarding_access_code_intro_title"
android:textAlignment="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<include
android:id="@+id/layout_backup_access_code_info"
layout="@layout/layout_backup_access_code_info"
android:layout_width="match_parent"
android:layout_height="0dp"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/access_code_title" />
<include
android:id="@+id/layout_backup_access_code_submit"
layout="@layout/layout_backup_access_code_submit"
android:layout_width="match_parent"
android:layout_height="0dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/access_code_title" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>

View file

@ -0,0 +1,132 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/iv_access_code_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="25dp"
android:src="@drawable/ic_access_code"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:layout_marginTop="20dp"
android:id="@+id/tv_feature_1_title"
style="@style/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/onboarding_access_code_feature_1_title"
android:textAlignment="center"
app:layout_constraintStart_toStartOf="@id/tv_feature_1_description"
app:layout_constraintTop_toBottomOf="@id/iv_access_code_icon" />
<ImageView
android:id="@+id/iv_feature_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="46dp"
android:layout_marginTop="8dp"
android:src="@drawable/ic_feature_1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_feature_1_title" />
<TextView
android:id="@+id/tv_feature_1_description"
style="@style/description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginEnd="46dp"
android:text="@string/onboarding_access_code_feature_1_description"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/iv_feature_1"
app:layout_constraintTop_toBottomOf="@id/tv_feature_1_title" />
<TextView
android:id="@+id/tv_feature_2_title"
style="@style/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="@string/onboarding_access_code_feature_2_title"
android:textAlignment="center"
app:layout_constraintStart_toStartOf="@id/tv_feature_1_description"
app:layout_constraintTop_toBottomOf="@id/tv_feature_1_description" />
<ImageView
android:id="@+id/iv_feature_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="46dp"
android:layout_marginTop="8dp"
android:src="@drawable/ic_feature_2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_feature_2_title" />
<TextView
android:id="@+id/tv_feature_2_description"
style="@style/description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginEnd="46dp"
android:text="@string/onboarding_access_code_feature_2_description"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/iv_feature_2"
app:layout_constraintTop_toBottomOf="@id/tv_feature_2_title" />
<TextView
android:id="@+id/tv_feature_3_title"
style="@style/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="@string/onboarding_access_code_feature_3_title"
android:textAlignment="center"
app:layout_constraintStart_toStartOf="@id/tv_feature_3_description"
app:layout_constraintTop_toBottomOf="@id/tv_feature_2_description" />
<ImageView
android:id="@+id/iv_feature_3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="46dp"
android:layout_marginTop="8dp"
android:src="@drawable/ic_feature_3"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_feature_3_title" />
<TextView
android:id="@+id/tv_feature_3_description"
style="@style/description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginEnd="46dp"
android:text="@string/onboarding_access_code_feature_3_description"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/iv_feature_3"
app:layout_constraintTop_toBottomOf="@id/tv_feature_3_title" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_access_code_create"
style="@style/TapButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="78dp"
android:layout_marginEnd="78dp"
android:layout_marginBottom="92dp"
android:text="@string/common_create"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<TextView
android:id="@+id/tv_access_code_enter_description"
style="DescriptionBig"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:paddingStart="32dp"
android:paddingEnd="32dp"
android:textAlignment="center"
android:text="@string/onboarding_access_code_hint"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/til_access_code"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="44dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="40dp"
android:hint="Access code"
app:boxStrokeColor="@color/selector_edit_text"
app:boxStrokeWidth="1dp"
app:endIconDrawable="@drawable/selector_password_toggle"
app:endIconMode="password_toggle"
app:hintTextColor="@color/accent"
android:theme="@style/EditTextThemeOverlay"
app:layout_constraintTop_toBottomOf="@id/tv_access_code_enter_description">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_access_code"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textVisiblePassword"
android:textSize="16sp" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_access_code_submit"
style="@style/TapButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="78dp"
android:layout_marginEnd="78dp"
android:layout_marginBottom="92dp"
android:text="@string/common_create"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_add_card"
style="@style/OnboardingButton"
android:backgroundTint="@color/gray_button"
android:textColor="@color/selector_btn_gray_text_color"
android:elevation="0dp"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="92dp"
tools:text="+ Add backup card"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/btn_continue"
app:layout_constraintStart_toStartOf="@id/btn_continue" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_continue"
style="@style/OnboardingButton"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
tools:text="Main Action"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_main_action"
style="@style/OnboardingButton"
android:layout_width="204dp"
android:layout_height="wrap_content"
android:layout_marginBottom="92dp"
tools:text="Main Action"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_alternative_action"
style="@style/Widget.AppCompat.Button.Borderless"
android:textSize="16sp"
android:layout_width="204dp"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
tools:text="Alternative action"
android:textAllCaps="false"
android:textColor="@color/tapButtonColorBlack"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

Some files were not shown because too many files have changed in this diff Show more