Updated on 2026-08-14

This commit is contained in:
Tangem 2021-12-02 18:18:50 +03:00
parent f5cd710825
commit d68da48f0f
69 changed files with 2452 additions and 144 deletions

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)
@ -94,7 +97,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
val backStackIsEmpty = supportFragmentManager.backStackEntryCount == 0
val isScannedBefore = store.state.globalState.scanResponse != null
val isOnboardingServiceActive = store.state.globalState.onboardingManager != null
if (backStackIsEmpty || (!isOnboardingServiceActive && !isScannedBefore)) {
if (backStackIsEmpty && (!isOnboardingServiceActive && !isScannedBefore)) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Home))
}
intentHandler.handleIntent(intent)

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.BackupInProgressDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.BuyMoreBackupCardsDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.ConfirmDiscardingBackupDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.UnfinishedBackupFoundDialog
import com.tangem.tap.features.wallet.ui.dialogs.ScanFailsDialog
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.BuyMoreBackupCards -> BuyMoreBackupCardsDialog.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,37 @@ 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()
}
}

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

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,7 +31,7 @@ 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()
}

View file

@ -16,11 +16,11 @@ import com.tangem.operations.CommandResponse
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.wallet.R
@ -66,11 +66,11 @@ 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 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> {

View file

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

View file

@ -30,7 +30,9 @@ class CreateProductWalletAndRescanTask(
}
CreateProductWalletTask(card, type).run(session) { result ->
when (result) {
is CompletionResult.Success -> PreflightReadTask(PreflightReadMode.FullCardRead).run(session, callback)
is CompletionResult.Success -> PreflightReadTask(PreflightReadMode.FullCardRead).run(
session,
callback)
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
@ -42,7 +44,10 @@ private class CreateProductWalletTask(
private val type: ProductType,
) : CardSessionRunnable<CreateWalletResponse> {
override fun run(session: CardSession, callback: (result: CompletionResult<CreateWalletResponse>) -> Unit) {
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")
@ -57,7 +62,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()))
@ -89,9 +94,17 @@ private class CreateWalletTangemWallet : ProductCommandProcessor<CreateWalletRes
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()
CreateProductWalletsTask(curves).run(session) { result ->
when (result) {
is CompletionResult.Success ->
callback(CompletionResult.Success(result.data.createWalletResponses[0]))
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
}
@ -100,7 +113,7 @@ private class CreateWalletOtherCards : ProductCommandProcessor<CreateWalletRespo
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) {

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

@ -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

@ -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,7 +182,7 @@ private fun handleSecurityAction(
}
private fun prepareAllowedSecurityOptions(
card: Card?, currentSecurityOption: SecurityOption?
card: Card?, currentSecurityOption: SecurityOption?,
): EnumSet<SecurityOption> {
val prohibitDefaultPin = card?.settings?.isResettingUserCodesAllowed != true

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

@ -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
@ -39,6 +40,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) {

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,65 @@
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()
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 ResumeBackup : BackupAction()
}

View file

@ -1,6 +1,22 @@
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.features.home.redux.HomeAction
import com.tangem.tap.features.home.redux.HomeMiddleware
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteAction
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
class OnboardingWalletMiddleware {
@ -12,7 +28,215 @@ class OnboardingWalletMiddleware {
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.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 -> {
val updatedResponse = 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)
(listOf(walletState.backupState.primaryCardId) +
walletState.backupState.backupCardIds + scanResponse?.card?.cardId)
.distinct().filterNotNull()
.forEach { cardId ->
preferencesStorage.usedCardsPrefStorage.activationFinished(cardId)
}
if (scanResponse == null) {
store.dispatch(NavigationAction.PopBackTo())
store.dispatch(HomeAction.ReadCard)
} else {
scope.launch { globalState.tapWalletManager.onCardScanned(scanResponse) }
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
}
}
OnboardingWalletAction.ProceedBackup -> {
val newAction = when (val backupState = backupService.currentState) {
BackupService.State.Preparing -> BackupAction.IntroduceBackup
BackupService.State.FinalizingPrimaryCard -> BackupAction.PrepareToWritePrimaryCard
is BackupService.State.FinalizingBackupCard ->
BackupAction.PrepareToWriteBackupCard(backupState.index)
BackupService.State.Finished -> BackupAction.FinishBackup
}
store.dispatch(newAction)
}
OnboardingWalletAction.OnBackPressed -> {
if (walletState.backupState.backupStep is BackupStep.WriteBackupCard ||
walletState.backupState.backupStep is BackupStep.WritePrimaryCard) {
store.dispatch(GlobalAction.ShowDialog(BackupDialog.BackupInProgress))
} else {
store.dispatch(NavigationAction.PopBackTo())
}
}
}
}
class BackupMiddleware {
val backupMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
val backupState = state()?.onboardingWalletState?.backupState
when (action) {
is BackupAction.StartBackup -> {
backupService.discardSavedBackup()
}
is BackupAction.ScanPrimaryCard -> {
backupService.readPrimaryCard { result ->
when (result) {
is CompletionResult.Success -> {
store.dispatchOnMain(BackupAction.StartAddingBackupCards)
}
is CompletionResult.Failure -> {
}
}
}
}
is BackupAction.AddBackupCard -> {
backupService.addBackupCard { result ->
when (result) {
is CompletionResult.Success -> {
store.dispatchOnMain(BackupAction.AddBackupCard.Success)
}
is CompletionResult.Failure -> {
}
}
}
}
is BackupAction.GoToShop -> {
store.dispatchOpenUrl(HomeMiddleware.CARD_SHOP_URI)
}
is BackupAction.FinishAddingBackupCards -> {
if (backupService.addedBackupCardsCount == 1) {
store.dispatchOnMain(GlobalAction.ShowDialog(BackupDialog.BuyMoreBackupCards))
}
if (backupService.addedBackupCardsCount == 2) {
store.dispatchOnMain(BackupAction.ShowAccessCodeInfoScreen)
}
}
is BackupAction.CheckAccessCode -> {
if (action.accessCode.length < 4) {
store.dispatch(BackupAction.SetAccessCodeError(
AccessCodeError.CodeTooShort
))
} else {
store.dispatch(BackupAction.SaveFirstAccessCode(action.accessCode))
}
}
is BackupAction.SaveAccessCodeConfirmation -> {
if (action.accessCodeConfirmation == backupState?.accessCode) {
backupService.setAccessCode(action.accessCodeConfirmation)
store.dispatch(BackupAction.PrepareToWritePrimaryCard)
} else {
store.dispatch(BackupAction.SetAccessCodeError(
AccessCodeError.CodesDoNotMatch
))
}
}
is BackupAction.WritePrimaryCard -> {
backupService.proceedBackup { result ->
when (result) {
is CompletionResult.Success -> {
store.dispatchOnMain(
BackupAction.PrepareToWriteBackupCard(1)
)
}
is CompletionResult.Failure -> {
}
}
}
}
is BackupAction.WriteBackupCard -> {
backupService.proceedBackup { result ->
when (result) {
is CompletionResult.Success -> {
if (backupService.currentState == BackupService.State.Finished) {
store.dispatchOnMain(BackupAction.FinishBackup)
} else {
store.dispatchOnMain(
BackupAction.PrepareToWriteBackupCard(action.cardNumber + 1)
)
}
}
is CompletionResult.Failure -> {
}
}
}
}
is BackupAction.FinishBackup -> {
// store.dispatch(OnboardingWalletAction.Done)
}
is BackupAction.DiscardBackup -> {
backupService.discardSavedBackup()
}
is BackupAction.CheckForUnfinishedBackup -> {
if (backupService.hasIncompletedBackup) {
store.dispatch(GlobalAction.ShowDialog(BackupDialog.UnfinishedBackupFound))
}
}
is BackupAction.ResumeBackup -> {
store.dispatch(OnboardingWalletAction.ProceedBackup)
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
}
is BackupAction.DismissBackup -> {
store.dispatch(BackupAction.FinishBackup)
}
}
next(action)
}
}
}
}

View file

@ -1,20 +1,126 @@
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 -> state.copy(backupStep = BackupStep.InitBackup)
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.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
}
}
}
}

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 BuyMoreBackupCards : 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,384 @@
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.addOnBackPressedDispatcher
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.leapfrogWidget.LeapfrogWidget
import com.tangem.tap.common.postUi
import com.tangem.tap.features.home.redux.HomeMiddleware
import com.tangem.tap.features.onboarding.products.wallet.redux.*
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.AccessCodeDialog
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_onboarding_wallet.*
import kotlinx.android.synthetic.main.fragment_onboarding_wallet.toolbar
import kotlinx.android.synthetic.main.fragment_wallet_details.*
import kotlinx.android.synthetic.main.layout_onboarding_buttons_add_cards.*
import kotlinx.android.synthetic.main.layout_onboarding_buttons_common.*
import kotlinx.android.synthetic.main.view_confetti.*
import kotlinx.android.synthetic.main.view_onboarding_progress.*
import org.rekotlin.StoreSubscriber
class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
StoreSubscriber<OnboardingWalletState> {
private var accessCodeDialog: AccessCodeDialog? = null
private lateinit var cardsWidget: BackupCardsWidget
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
postponeEnterTransition()
setHasOptionsMenu(true)
activity?.addOnBackPressedDispatcher {
store.dispatch(OnboardingWalletAction.OnBackPressed)
}
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.fade)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
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() }
// store.dispatch(OnboardingWalletAction.LoadArtwork)
}
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()
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 {
}
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(HomeMiddleware.CARD_SHOP_URI)
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
}
}

View file

@ -0,0 +1,71 @@
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
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()
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()
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,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,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 BuyMoreBackupCardsDialog {
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_buy_more_cards) { _, _ ->
store.dispatch(BackupAction.GoToShop)
}
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.DiscardBackup)
}
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

@ -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?.isActive == true) {
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

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