Updated on 2026-08-14

This commit is contained in:
Tangem 2021-12-02 21:00:18 +03:00
commit a2bbab30ad
70 changed files with 2456 additions and 144 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.tangem-sdk-kotlin:core:develop-99'
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-99'
// 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)
@ -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

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

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

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

@ -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,225 @@
<?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="296dp"
android:layout_height="186dp"
android:scaleType="fitXY"
android:layout_gravity="center"
android:src="@drawable/card_placeholder_black"
android:visibility="gone"
/>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_first_backup_card"
android:layout_width="296dp"
android:layout_height="186dp"
android:scaleType="fitXY"
android:layout_gravity="center"
android:src="@drawable/card_placeholder_black"
android:visibility="gone"/>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_front_card"
android:layout_width="296dp"
android:layout_height="186dp"
android:scaleType="fitXY"
android:layout_gravity="center"
android:src="@drawable/card_placeholder_black"
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:layout_constraintBottom_toTopOf="@id/layout_buttons_common"
app:flow_verticalBias="0"
app:layout_constraintTop_toTopOf="@+id/guideline2"
android:layout_marginBottom="12dp"/>
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout_backup_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
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

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

View file

@ -19,4 +19,10 @@
android:layout_height="wrap_content"
android:src="@drawable/card_placeholder_black" />
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tint="@color/warning"
android:src="@drawable/card_placeholder_black" />
</FrameLayout>

View file

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<merge>
<ProgressBar xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/pb_state"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/shop_menu"
android:title="@string/home_button_shop"
app:showAsAction="always" />
</menu>

View file

@ -55,4 +55,7 @@
<color name="warning_warning">#FFB71B</color>
<color name="warning_critical">#CA0F03</color>
<color name="gray_button">#EFEFF0</color>
<color name="menu_accent_color">#18C077</color>
</resources>

View file

@ -16,7 +16,7 @@
<string name="twins_onboarding_subtitle" translatable="false">One wallet. Two cards.</string>
<string name="twins_onboarding_description_format" translatable="false">This one that you are holding in your hands and the other one with number #%s.</string>
<string name="wallet_twins_chip_format" translatable="false">Card %s of 2</string>
<string name="wallet_twins_chip_format" translatable="false">Card %1d of %2d</string>
<string name="details_row_title_twins_recreate" translatable="false">Re-create twin wallet</string>
<string name="details_row_title_card_tou" translatable="false">Card terms of use</string>
@ -208,4 +208,89 @@
<string name="warning_button_ok" translatable="false">Ok, Got it!</string>
<string name="onboarding_success" translatable="false">Success!</string>
<string name="onboarding_getting_started" translatable="false">Getting started</string>
<string name="onboarding_button_backup_now" translatable="false">Backup now</string>
<string name="onboarding_button_get_backup_card" translatable="false">Get a backup card</string>
<string name="onboarding_button_skip_backup" translatable="false">Skip for later</string>
<string name="onboarding_button_finalize_backup" translatable="false">Finalize the backup process</string>
<string name="onboarding_button_add_backup_card" translatable="false">+ Add backup card</string>
<string name="onboarding_button_backup_card_format" translatable="false">Scan the card #%d</string>
<string name="onboarding_button_scan_origin_card" translatable="false">Scan primary card</string>
<string name="onboarding_button_buy_more_cards" translatable="false">Buy more cards</string>
<string name="onboarding_title_backup_card" translatable="false">Backup your wallet</string>
<string name="onboarding_title_no_backup_cards" translatable="false">No backup cards</string>
<string name="onboarding_title_scan_origin_card" translatable="false">Prepare your card</string>
<string name="onboarding_title_one_backup_card" translatable="false">One backup card added</string>
<string name="onboarding_title_two_backup_cards" translatable="false">Two backup cards added</string>
<string name="onboarding_title_backup_card_format" translatable="false">Backup card #%d</string>
<string name="onboarding_subtitle_backup_card" translatable="false">You can backup your keys up to two other blank wallet cards</string>
<string name="onboarding_subtitle_no_backup_cards" translatable="false">To start the backup process add up to two cards</string>
<string name="onboarding_subtitle_scan_origin_card_format" translatable="false">Prepare the primary card with number %s</string>
<string name="onboarding_subtitle_scan_origin_card" translatable="false">Prepare the primary card</string>
<string name="onboarding_subtitle_one_backup_card" translatable="false">You can add one more card or finalize the backup process</string>
<string name="onboarding_subtitle_two_backup_cards" translatable="false">Max number of cards added. Finalize the backup process</string>
<string name="onboarding_subtitle_backup_warning" translatable="false">If the process of backup gets interrupted in any way youll have to start over</string>
<string name="onboarding_alert_message_not_max_backup_cards_added" translatable="false">Youve added one backup card. When backup process is finished you cant add more backup cards. If you have one more card, add it in backup, otherwise you can buy it in shop. Do you like to continue backup process?</string>
<string name="onboarding_access_code_intro_title" translatable="false">Create Access Code</string>
<string name="onboarding_access_code_repeat_code_title" translatable="false">Re-enter your Access Code</string>
<string name="onboarding_access_code_feature_1_title" translatable="false">Protect</string>
<string name="onboarding_access_code_feature_2_title" translatable="false">Personalize</string>
<string name="onboarding_access_code_feature_3_title" translatable="false">Restore</string>
<string name="onboarding_access_code_feature_1_description" translatable="false">You have to setup a single access code to protect all yor wallets.</string>
<string name="onboarding_access_code_feature_2_description" translatable="false">You can set an individual access code on each card later.</string>
<string name="onboarding_access_code_feature_3_description" translatable="false">The access code can be restored with a linked card, dont keep all cards at one place.</string>
<string name="onboarding_access_code_hint" translatable="false">Choose any word, phrase, or number you want as your access code.</string>
<string name="onboarding_access_code_too_short" translatable="false">Access code must be at least 4 characters long</string>
<string name="onboarding_access_codes_doesnt_match" translatable="false">Entered access code didn\'t match initial access code</string>
<string name="common_create" translatable="false">Create</string>
<string name="common_submit" translatable="false">Submit</string>
<string name="onboarding_subtitle_success_backup" translatable="false">You have created 2 backup cards, and now these cards are ready for use.</string>
<string name="onboarding_navbar_title_creating_backup" translatable="false">Creating a backup</string>
<string name="onboarding_backup_exit_warning" translatable="false">The backup process is partly complete. You cant exit it now.</string>
<string name="onboarding_wallet_info_title_first" translatable="false">Backup wallet</string>
<string name="onboarding_wallet_info_title_second" translatable="false">Identical cards</string>
<string name="onboarding_wallet_info_title_third" translatable="false">Access code</string>
<string name="onboarding_wallet_info_title_fourth" translatable="false">Access code restore</string>
<string name="onboarding_wallet_info_subtitle_first" translatable="false">You can backup your keys up to two other blank Tangem Wallet cards.</string>
<string name="onboarding_wallet_info_subtitle_second" translatable="false">All the backup cards can be used as full-functoinal with the identical keys.</string>
<string name="onboarding_wallet_info_subtitle_third" translatable="false">You will be able to set an access code to protect your wallets.</string>
<string name="onboarding_wallet_info_subtitle_fourth" translatable="false">Access code can be restored with one of backup cards.</string>
<string name="onboarding_subtitle_success_tangem_wallet_onboarding" translatable="false">Your Tangem Wallet card is configured and ready for use.</string>
<string name="onboarding_subtitle_success_backup_one_card" translatable="false">You have created 1 backup card, and now this card are ready for use.</string>
<string name="common_origin_card" translatable="false">Primary Card</string>
<string name="onboarding_alert_i_understand" translatable="false">I understand</string>
<string name="details_row_title_backup_status" translatable="false">Backup status</string>
<string name="details_backup_status_format_active" translatable="false">Active. Backup cards count: %d</string>
<string name="details_backup_status_format_linked" translatable="false">Linked. Backup cards count: %d</string>
<string name="details_backup_status_no_backup" translatable="false">No backup</string>
<string name="details_row_title_create_backup" translatable="false">Create backup</string>
<string name="welcome_interrupted_backup_alert_message" translatable="false">You have an interrupted backup. Do you want to resume?</string>
<string name="welcome_interrupted_backup_alert_resume" translatable="false">Yes, resume</string>
<string name="welcome_interrupted_backup_alert_discard" translatable="false">Discard</string>
<string name="welcome_interrupted_backup_discard_title" translatable="false">This is an irreversible action</string>
<string name="welcome_interrupted_backup_discard_message" translatable="false">If you will discard the backup now, then you will have to reset the cards to factory settings to start over again</string>
<string name="welcome_interrupted_backup_discard_resume" translatable="false">Resume backup</string>
<string name="welcome_interrupted_backup_discard_discard" translatable="false">Discard</string>
<string name="onboarding_title_prepare_origin" translatable="false">Primary card</string>
<string name="onboarding_button_backup_origin" translatable="false">Scan the primary card</string>
<string name="onboarding_button_continue_wallet" translatable="false">Continue to my wallet</string>
<string name="card_label_number_format" translatable="false">Card %1s of %2s</string>
<string name="send_title_currency_format" translatable="false">Sending %s</string>
<string name="onboarding_subtitle_scan_backup_card_format" translatable="false">Prepare the backup card with number %s</string>
<string name="onboarding_subtitle_scan_primary_card_format" translatable="false">Prepare the primary card with number %s</string>
<string name="details_row_title_reset_factory_settings" translatable="false">Reset to factory settings</string>
<string name="details_row_title_reset_factory_settings_warning" translatable="false">This action is irreversible. If, after resetting the card, someone sends funds to it, then you will not be able to withdraw them.</string>
</resources>

View file

@ -9,6 +9,8 @@
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:windowActionBarOverlay">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="actionMenuTextColor">@color/menu_accent_color</item>
</style>
<style name="AppTheme" parent="BaseAppTheme" />
@ -28,11 +30,20 @@
<item name="actionMenuTextColor">@color/menu_item_color</item>
</style>
<style name="ThemeOverlay.MyTheme.Toolbar.AccentColorMenu" parent="ThemeOverlay.MaterialComponents.Toolbar.Surface">
<item name="actionMenuTextColor">@color/accent</item>
</style>
<!-- EditText -->
<style name="StyledEditText" parent="TextAppearance.MaterialComponents.Body1">
<item name="colorControlNormal">#e34b1c</item>
</style>
<style name="EditTextThemeOverlay" parent="ThemeOverlay.AppCompat">
<item name="colorAccent">@color/accent</item>
<item name="colorPrimary">@color/accent</item>
</style>
<style name="BaseTapButton">
<item name="android:insetTop">0dp</item>
<item name="android:insetBottom">0dp</item>
@ -54,7 +65,8 @@
</style>
<style name="OnboardingButton" parent="BaseTapButton">
<item name="android:layout_width">204dp</item>
<item name="android:minWidth">204dp</item>
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">48dp</item>
<item name="android:textSize">16sp</item>
<item name="android:fontFamily">sans-serif-medium</item>
@ -107,6 +119,34 @@
<item name="android:textSize">16sp</item>
<item name="android:lineSpacingExtra">4sp</item>
</style>
<style name="heading">
<item name="android:textSize">24sp</item>
<item name="android:textColor">#060606</item>
</style>
<style name="title">
<item name="android:textSize">16sp</item>
<item name="android:textColor">#060606</item>
<item name="android:textStyle">bold</item>
</style>
<style name="description">
<item name="android:textSize">14sp</item>
<item name="android:textColor">#686868</item>
</style>
<style name="DescriptionBig">
<item name="android:textSize">16sp</item>
<item name="android:textColor">#686868</item>
</style>
<style name="DialogStyle" parent="Theme.Design.Light.BottomSheetDialog">
<item name="android:windowIsFloating">false</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:windowSoftInputMode">adjustResize</item>
</style>
</resources>
<!-- android:fontFamily="sans-serif" // roboto regular -->