Updated on 2026-08-14

This commit is contained in:
Tangem 2021-10-27 23:21:34 +03:00
parent c02e5b93f6
commit 6dcce60fb4
80 changed files with 1946 additions and 1738 deletions

View file

@ -13,6 +13,8 @@ import com.tangem.tap.common.DialogManager
import com.tangem.tap.common.IntentHandler
import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.common.redux.NotificationsHandler
import com.tangem.tap.common.redux.global.AndroidResources
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.TangemSdkManager
@ -66,9 +68,19 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
tangemSdk = TangemSdk.init(this, TangemSdkManager.config)
tangemSdkManager = TangemSdkManager(tangemSdk, this)
store.dispatch(GlobalAction.SetResources(getAndroidResources()))
store.dispatch(WalletConnectAction.RestoreSessions)
}
private fun getAndroidResources(): AndroidResources {
return AndroidResources(
AndroidResources.RString(
R.string.copy_toast_msg,
R.string.details_notification_erase_wallet_not_possible
)
)
}
private fun systemActions() {
// makes the status bar text dark
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR

View file

@ -7,8 +7,8 @@ import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.*
import com.tangem.tap.features.onboarding.AddressInfoBottomSheetDialog
import com.tangem.tap.features.twins.redux.TwinCardsAction
import com.tangem.tap.features.twins.ui.dialog.CreateWalletInterruptDialog
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.wallet.ui.dialogs.ScanFailsDialog
import com.tangem.tap.store
import com.tangem.wallet.R
@ -45,7 +45,7 @@ class DialogManager : StoreSubscriber<GlobalState> {
dialog = when (state.dialog) {
is AppDialog.ScanFailsDialog -> ScanFailsDialog.create(context)
is AppDialog.AddressInfoDialog -> AddressInfoBottomSheetDialog(state.dialog, context)
is TwinCardsAction.Wallet.InterruptDialog -> CreateWalletInterruptDialog.create(state.dialog, context)
is TwinCardsAction.Wallet.ShowInterruptDialog -> CreateWalletInterruptDialog.create(state.dialog, context)
is WalletConnectDialog.UnsupportedCard ->
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect,

View file

@ -16,12 +16,10 @@ import com.tangem.tap.features.disclaimer.ui.DisclaimerFragment
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.OnboardingTwinsFragment
import com.tangem.tap.features.onboarding.products.twins.ui.TwinsCardsFragment
import com.tangem.tap.features.onboarding.products.wallet.OnboardingWalletFragment
import com.tangem.tap.features.send.ui.SendFragment
import com.tangem.tap.features.tokens.ui.AddTokensFragment
import com.tangem.tap.features.twins.ui.CreateTwinWalletFragment
import com.tangem.tap.features.twins.ui.TwinWalletWarningFragment
import com.tangem.tap.features.wallet.ui.WalletDetailsFragment
import com.tangem.tap.features.wallet.ui.WalletFragment
import com.tangem.wallet.R
@ -39,7 +37,7 @@ fun FragmentActivity.openFragment(
transaction.setReorderingAllowed(true)
shareElements.forEach { shareElement ->
shareElement.wView.get()?.let { view ->
transaction.addSharedElement(view, shareElement.name)
transaction.addSharedElement(view, shareElement.elementName)
}
}
}
@ -70,10 +68,11 @@ fun FragmentActivity.addOnBackPressedDispatcher(
private fun fragmentFactory(screen: AppScreen): Fragment {
return when (screen) {
// AppScreen.Home -> TestLeapfrogFragment()
AppScreen.Home -> HomeFragment()
AppScreen.OnboardingNote -> OnboardingNoteFragment()
AppScreen.OnboardingWallet -> OnboardingWalletFragment()
AppScreen.OnboardingTwins -> OnboardingTwinsFragment()
AppScreen.OnboardingTwins -> TwinsCardsFragment()
AppScreen.OnboardingOther -> OnboardingOtherCardsFragment()
AppScreen.Wallet -> WalletFragment()
AppScreen.Send -> SendFragment()
@ -81,8 +80,6 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
AppScreen.DetailsConfirm -> DetailsConfirmFragment()
AppScreen.DetailsSecurity -> DetailsSecurityFragment()
AppScreen.Disclaimer -> DisclaimerFragment()
AppScreen.CreateTwinWalletWarning -> TwinWalletWarningFragment()
AppScreen.CreateTwinWallet -> CreateTwinWalletFragment()
AppScreen.AddTokens -> AddTokensFragment()
AppScreen.WalletDetails -> WalletDetailsFragment()
AppScreen.WalletConnectSessions -> WalletConnectSessionsFragment()

View file

@ -6,6 +6,7 @@ import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.FrameLayout
import androidx.core.animation.doOnEnd
import androidx.core.view.children
import com.tangem.common.extensions.VoidCallback
import com.tangem.tangem_sdk_new.extensions.dpToPx
/**
@ -15,9 +16,9 @@ class LeapfrogWidget(
private val parentContainer: FrameLayout,
private val calculator: PropertyCalculator = PropertyCalculator(),
) {
private val foldAnimationDuration = 500L
private val leapAnimationDuration = 800L
private val leapBackAnimationDuration = 800L
private val foldAnimationDuration = 300L
private val leapAnimationDuration = 500L
private val leapBackAnimationDuration = 500L
private val leapViews = mutableListOf<LeapView>()
@ -25,9 +26,6 @@ class LeapfrogWidget(
private var leapProgress: Int = 100
private var leapBackProgress: Int = 100
var isFolded: Boolean = true
private set
init {
calculator.setTranslationConverter { parentContainer.dpToPx(it) }
if (viewIsFullFledged()) {
@ -43,12 +41,11 @@ class LeapfrogWidget(
* Apply initial properties to the views
*/
fun initViews() {
isFolded = true
leapViews.forEach { it.initView() }
}
fun fold(onEnd: () -> Unit = {}) {
val animator = foldAnimator()
fun fold(animate: Boolean = true, onEnd: VoidCallback = {}) {
val animator = foldAnimator(animate)
if (animator == null) {
onEnd()
} else {
@ -57,8 +54,8 @@ class LeapfrogWidget(
}
}
fun unfold(onEnd: () -> Unit = {}) {
val animator = unfoldAnimator()
fun unfold(animate: Boolean = true, onEnd: VoidCallback = {}) {
val animator = unfoldAnimator(animate)
if (animator == null) {
onEnd()
} else {
@ -67,33 +64,31 @@ class LeapfrogWidget(
}
}
fun foldAnimator(): Animator? {
if (!canFoldUnfold() || isFolded) return null
fun foldAnimator(animate: Boolean = true): Animator? {
if (!canFoldUnfold()) return null
return createFoldUnfoldAnimator(true, foldAnimationDuration)
return createFoldUnfoldAnimator(true, animate, foldAnimationDuration)
}
fun unfoldAnimator(): Animator? {
if (!canFoldUnfold() || !isFolded) return null
fun unfoldAnimator(animate: Boolean = true): Animator? {
if (!canFoldUnfold()) return null
return createFoldUnfoldAnimator(false, foldAnimationDuration)
return createFoldUnfoldAnimator(false, animate, foldAnimationDuration)
}
private fun createFoldUnfoldAnimator(isFoldAnimation: Boolean, duration: Long): Animator {
private fun createFoldUnfoldAnimator(isFoldAnimation: Boolean, animate: Boolean, duration: Long): Animator {
val animatorsList = mutableListOf<Animator>()
val animateDuration = if (animate) duration else 0
leapViews.forEach {
if (isFoldAnimation) {
it.fold()
animatorsList.add(it.view.foldAnimation(duration, it.state.properties))
animatorsList.add(it.view.foldAnimation(animateDuration, it.state.properties))
} else {
it.unfold()
animatorsList.add(it.view.unfoldAnimation(duration, it.state.properties))
animatorsList.add(it.view.unfoldAnimation(animateDuration, it.state.properties))
}
}
animatorsList.add(createProgressListener(duration) {
foldUnfoldProgress = it
if (it == 100) isFolded = isFoldAnimation
})
animatorsList.add(createProgressListener(animateDuration) { foldUnfoldProgress = it })
val animator = AnimatorSet()
animator.interpolator = AccelerateDecelerateInterpolator()
@ -101,13 +96,13 @@ class LeapfrogWidget(
return animator
}
fun leap(onEnd: () -> Unit = {}) {
fun leap(animate: Boolean = true, onEnd: VoidCallback = {}) {
if (!canLeap()) {
onEnd()
return
}
val duration = leapAnimationDuration
val duration = if (animate) leapAnimationDuration else 0
val animatorsList = mutableListOf<Animator>()
leapViews.forEach {
when (it.leap()) {
@ -128,13 +123,13 @@ class LeapfrogWidget(
animator.start()
}
fun leapBack(onEnd: () -> Unit = {}) {
fun leapBack(animate: Boolean = true, onEnd: VoidCallback = {}) {
if (!canLeapBack()) {
onEnd()
return
}
val duration = leapBackAnimationDuration
val duration = if (animate) leapBackAnimationDuration else 0
val animatorsList = mutableListOf<Animator>()
leapViews.forEach {
when (it.leapBack()) {
@ -160,12 +155,11 @@ class LeapfrogWidget(
fun getViewByPosition(position: Int): LeapView = leapViews.first { it.state.currentPosition == position }
fun getState(): LeapfrogWidgetState = LeapfrogWidgetState(isFolded, leapViews.map { it.state })
fun getState(): LeapfrogWidgetState = LeapfrogWidgetState(leapViews.map { it.state })
fun applyState(state: LeapfrogWidgetState) {
isFolded = state.isFolded
state.leapViewStates.forEach { state ->
leapViews.firstOrNull { it.index == state.index }?.applyState(state)
state.leapViewStates.forEach { viewState ->
leapViews.firstOrNull { it.index == viewState.index }?.applyState(viewState)
}
}
@ -183,7 +177,6 @@ class LeapfrogWidget(
private fun canLeap(): Boolean {
return when {
!viewIsFullFledged() -> false
isFolded -> false
leapBackInProgress() -> false
leapProgress < 70 -> false
else -> true
@ -193,7 +186,6 @@ class LeapfrogWidget(
private fun canLeapBack(): Boolean {
return when {
!viewIsFullFledged() -> false
isFolded -> false
leapInProgress() -> false
leapBackProgress < 70 -> false
else -> true
@ -206,14 +198,13 @@ class LeapfrogWidget(
}
data class LeapfrogWidgetState(
val isFolded: Boolean,
val leapViewStates: List<LeapViewState>
)
class PropertyCalculator(
val elevationFactor: Float = 1f,
val decreaseScaleFactor: Float = 0.1f,
val yTranslationFactor: Float = 20f,
val decreaseScaleFactor: Float = 0.15f,
val yTranslationFactor: Float = 35f,
val decreaseOverLiftingFactor: Float = 0.85f,
) {
// 0 view it is view on the top of stack, but it is last in parent.children

View file

@ -8,10 +8,10 @@ import com.tangem.tap.features.disclaimer.redux.DisclaimerReducer
import com.tangem.tap.features.home.redux.HomeReducer
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteReducer
import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsReducer
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsReducer
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletReducer
import com.tangem.tap.features.send.redux.reducers.SendScreenReducer
import com.tangem.tap.features.tokens.redux.TokensReducer
import com.tangem.tap.features.twins.redux.TwinCardsReducer
import com.tangem.tap.features.wallet.redux.reducers.WalletReducer
import org.rekotlin.Action
@ -20,19 +20,19 @@ fun appReducer(action: Action, state: AppState?): AppState {
if (action is AppAction.RestoreState) return action.state
return AppState(
navigationState = NavigationReducer.reduce(action, state),
globalState = globalReducer(action, state),
homeState = HomeReducer.reduce(action, state),
onboardingNoteState = OnboardingNoteReducer.reduce(action, state),
onboardingWalletState = OnboardingWalletReducer.reduce(action, state),
onboardingOtherCardsState = OnboardingOtherCardsReducer.reduce(action, state),
walletState = WalletReducer.reduce(action, state),
twinCardsState = TwinCardsReducer.reduce(action, state),
sendState = SendScreenReducer.reduce(action, state.sendState),
detailsState = DetailsReducer.reduce(action, state),
disclaimerState = DisclaimerReducer.reduce(action, state),
tokensState = TokensReducer.reduce(action, state),
walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState)
navigationState = NavigationReducer.reduce(action, state),
globalState = globalReducer(action, state),
homeState = HomeReducer.reduce(action, state),
onboardingNoteState = OnboardingNoteReducer.reduce(action, state),
onboardingWalletState = OnboardingWalletReducer.reduce(action, state),
onboardingOtherCardsState = OnboardingOtherCardsReducer.reduce(action, state),
walletState = WalletReducer.reduce(action, state),
twinCardsState = TwinCardsReducer.reduce(action, state),
sendState = SendScreenReducer.reduce(action, state.sendState),
detailsState = DetailsReducer.reduce(action, state),
disclaimerState = DisclaimerReducer.reduce(action, state),
tokensState = TokensReducer.reduce(action, state),
walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState)
)
}

View file

@ -16,14 +16,14 @@ import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteMidd
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteState
import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsMiddleware
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.OnboardingWalletMiddleware
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
import com.tangem.tap.features.send.redux.middlewares.SendMiddleware
import com.tangem.tap.features.send.redux.states.SendState
import com.tangem.tap.features.tokens.redux.TokensMiddleware
import com.tangem.tap.features.tokens.redux.TokensState
import com.tangem.tap.features.twins.redux.TwinCardsMiddleware
import com.tangem.tap.features.twins.redux.TwinCardsState
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.middlewares.WalletMiddleware
import org.rekotlin.Middleware

View file

@ -1,5 +1,6 @@
package com.tangem.tap.common.redux
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.features.wallet.redux.AddressData
/**
@ -11,7 +12,7 @@ sealed class AppDialog : StateDialog {
object ScanFailsDialog : AppDialog()
data class AddressInfoDialog(
val addressData: AddressData,
val onCopyAddress: () -> Unit,
val onExploreAddress: () -> Unit
val onCopyAddress: VoidCallback,
val onExploreAddress: VoidCallback
) : AppDialog()
}

View file

@ -0,0 +1,35 @@
package com.tangem.tap.common.redux.navigation
import android.view.View
import androidx.transition.TransitionSet
import java.lang.ref.WeakReference
/**
[REDACTED_AUTHOR]
*/
data class FragmentShareTransition(
val shareElements: List<ShareElement>,
val enterTransitionSet: TransitionSet,
val exitTransitionSet: TransitionSet
)
/**
* For ease of use, the name is used as transitionName\name into the FragmentTransaction.addSharedElement
*/
class ShareElement(view: View, name: String? = null) {
val wView: WeakReference<View>
val elementName: String
init {
name?.let { view.transitionName = it }
elementName = view.transitionName
?: throw UnsupportedOperationException("ShareElement require the name")
wView = WeakReference(view)
}
companion object {
val imvFrontCard = "imv_front_card"
val imvBackCard = "imv_back_card"
}
}

View file

@ -1,8 +1,6 @@
package com.tangem.tap.common.redux.navigation
import android.view.View
import androidx.fragment.app.FragmentActivity
import androidx.transition.TransitionSet
import org.rekotlin.Action
import java.lang.ref.WeakReference
@ -19,12 +17,4 @@ sealed class NavigationAction : Action {
data class ActivityCreated(val activity: WeakReference<FragmentActivity>) : NavigationAction()
object ActivityDestroyed : NavigationAction()
}
data class FragmentShareTransition(
val shareElements: List<ShareElement>,
val enterTransitionSet: TransitionSet,
val exitTransitionSet: TransitionSet
)
data class ShareElement(val wView: WeakReference<View>, val name: String)
}

View file

@ -5,8 +5,8 @@ import org.rekotlin.StateType
import java.lang.ref.WeakReference
data class NavigationState(
val backStack: List<AppScreen> = listOf(AppScreen.Home),
val activity: WeakReference<FragmentActivity>? = null
val backStack: List<AppScreen> = listOf(AppScreen.Home),
val activity: WeakReference<FragmentActivity>? = null
) : StateType
enum class AppScreen {
@ -16,7 +16,6 @@ enum class AppScreen {
Wallet, WalletDetails,
Send,
Details, DetailsConfirm, DetailsSecurity,
CreateTwinWalletWarning, CreateTwinWallet,
AddTokens,
WalletConnectSessions,
QrScan

View file

@ -14,6 +14,7 @@ data class Config(
val blockchainSdkConfig: BlockchainSdkConfig = BlockchainSdkConfig(),
val isSendingToPayIdEnabled: Boolean = true,
val isTopUpEnabled: Boolean = false,
@Deprecated("Not relevant since version 3.23")
val isCreatingTwinCardsAllowed: Boolean = false
)
@ -50,10 +51,10 @@ class ConfigManager(
fun resetToDefault(name: String) {
when (name) {
isSendingToPayIdEnabled -> config =
config.copy(isSendingToPayIdEnabled = defaultConfig.isSendingToPayIdEnabled)
config.copy(isSendingToPayIdEnabled = defaultConfig.isSendingToPayIdEnabled)
isTopUpEnabled -> config = config.copy(isTopUpEnabled = defaultConfig.isTopUpEnabled)
isCreatingTwinCardsAllowed -> config =
config.copy(isCreatingTwinCardsAllowed = defaultConfig.isCreatingTwinCardsAllowed)
config.copy(isCreatingTwinCardsAllowed = defaultConfig.isCreatingTwinCardsAllowed)
}
}

View file

@ -1,124 +0,0 @@
package com.tangem.tap.domain.tasks
import com.tangem.*
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.card.FirmwareVersion
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.TangemError
import com.tangem.common.extensions.toHexString
import com.tangem.operations.ScanTask
import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand
import com.tangem.tap.domain.ProductType
import com.tangem.tap.domain.TapSdkError
import com.tangem.tap.domain.TapWorkarounds.isExcluded
import com.tangem.tap.domain.TapWorkarounds.isTangemNote
import com.tangem.tap.domain.extensions.getSingleWallet
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.domain.twins.TwinsHelper
@Deprecated("Use ScanProductTask instead")
class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanResponse> {
override fun run(
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
ScanTask().run(session) { result ->
when (result) {
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
is CompletionResult.Success -> {
val card = this.card ?: result.data
val error = getErrorIfExcludedCard(card)
if (error != null) {
callback(CompletionResult.Failure(error))
return@run
}
if (TwinsHelper.getTwinCardNumber(card.cardId) != null) {
dealWithTwinCard(card, session, callback)
} else if (!isTangemNote(card) && card.firmwareVersion >= FirmwareVersion.MultiWalletAvailable) {
createMissingWalletsIfNeeded(card, session, callback)
} else {
callback(CompletionResult.Success(
ScanResponse(card, ProductType.Other, session.environment.walletData)))
}
}
}
}
}
private fun createMissingWalletsIfNeeded(
card: Card, session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
val walletData = session.environment.walletData
if (card.wallets.isEmpty()) {
callback(CompletionResult.Success(ScanResponse(card, ProductType.Other, walletData)))
return
}
val curvesPresent = card.wallets.map { it.curve }
// val curvesToCreate = EllipticCurve.values().subtract(curvesPresent)
val curvesToCreate = card.supportedCurves.subtract(curvesPresent)
if (curvesToCreate.isEmpty()) {
callback(CompletionResult.Success(ScanResponse(card, ProductType.Other, walletData)))
return
}
CreateWalletsTask(curvesToCreate.toList()).run(session) { result ->
when (result) {
is CompletionResult.Success ->
callback(CompletionResult.Success(ScanResponse(result.data, ProductType.Other, walletData)))
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
private fun dealWithTwinCard(
card: Card, session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
ReadIssuerDataCommand().run(session) { readDataResult ->
when (readDataResult) {
is CompletionResult.Success -> {
val publicKey = card.getSingleWallet()?.publicKey
if (publicKey == null) {
callback(CompletionResult.Success(ScanResponse(card, ProductType.Other, null)))
return@run
}
val verified = TwinsHelper.verifyTwinPublicKey(
readDataResult.data.issuerData, publicKey
)
if (verified) {
val twinPublicKey = readDataResult.data.issuerData.sliceArray(0 until 65)
callback(CompletionResult.Success(
ScanResponse(
card = card,
ProductType.Other,
walletData = session.environment.walletData,
secondTwinPublicKey = twinPublicKey.toHexString())
))
return@run
} else {
callback(CompletionResult.Success(ScanResponse(card, ProductType.Other, null)))
}
}
is CompletionResult.Failure ->
callback(CompletionResult.Success(ScanResponse(card, ProductType.Other, null)))
}
}
}
private fun getErrorIfExcludedCard(card: Card): TangemError? {
if (card.isExcluded()) return TapSdkError.CardForDifferentApp
// Disable new multi-currency HD wallet cards on the old version of the app
// if (card.isMultiCurrencyWallet()) return UpdateAppToUseThisCard()
return null
}
}

View file

@ -4,44 +4,45 @@ import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Types
import com.tangem.Message
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.common.CompletionResult
import com.tangem.common.KeyPair
import com.tangem.common.card.Card
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.network.createMoshi
import com.tangem.tap.tangemSdkManager
class TwinCardsManager(private val scanResponse: ScanResponse, assetReader: AssetReader) {
class TwinCardsManager(private val card: Card, assetReader: AssetReader) {
private val currentCardId: String = scanResponse.card.cardId
private val currentCardId: String = card.cardId
private var currentCardPublicKey: String? = null
private var secondCardPublicKey: String? = null
private val issuerKeyPair: KeyPair = getIssuerKeys(assetReader, scanResponse.card.issuer.publicKey.toHexString())
private val issuerKeyPair: KeyPair = getIssuerKeys(assetReader, card.issuer.publicKey.toHexString())
suspend fun createFirstWallet(message: Message): SimpleResult {
suspend fun createFirstWallet(message: Message): CompletionResult<CreateWalletResponse> {
val response = tangemSdkManager.runTaskAsync(
CreateFirstTwinWalletTask(), currentCardId, message
)
when (response) {
is CompletionResult.Success -> {
currentCardPublicKey = response.data.wallet.publicKey.toHexString()
return SimpleResult.Success
return response
}
is CompletionResult.Failure -> {
(response.error as? TangemSdkError)?.let { error ->
FirebaseAnalyticsHandler.logCardSdkError(
error,
FirebaseAnalyticsHandler.ActionToLog.CreateWallet,
card = scanResponse.card
card = card
)
}
return SimpleResult.failure(response.error)
return response
}
}
@ -52,7 +53,7 @@ class TwinCardsManager(private val scanResponse: ScanResponse, assetReader: Asse
initialMessage: Message,
preparingMessage: Message,
creatingWalletMessage: Message,
): SimpleResult {
): CompletionResult<CreateWalletResponse> {
val task = CreateSecondTwinWalletTask(
firstPublicKey = currentCardPublicKey!!,
firstCardId = currentCardId,
@ -64,20 +65,18 @@ class TwinCardsManager(private val scanResponse: ScanResponse, assetReader: Asse
when (response) {
is CompletionResult.Success -> {
secondCardPublicKey = response.data.wallet.publicKey.toHexString()
return SimpleResult.Success
}
is CompletionResult.Failure -> {
(response.error as? TangemSdkError)?.let { error ->
FirebaseAnalyticsHandler.logCardSdkError(
error,
FirebaseAnalyticsHandler.ActionToLog.CreateWallet,
card = scanResponse.card
card = card
)
}
return SimpleResult.failure(response.error)
}
}
return response
}
suspend fun complete(message: Message): Result<ScanResponse> {
@ -92,7 +91,7 @@ class TwinCardsManager(private val scanResponse: ScanResponse, assetReader: Asse
FirebaseAnalyticsHandler.logCardSdkError(
error,
FirebaseAnalyticsHandler.ActionToLog.WriteIssuerData,
card = scanResponse.card
card = card
)
}
Result.failure(response.error)

View file

@ -5,6 +5,7 @@ import android.animation.ObjectAnimator
import android.animation.PropertyValuesHolder
import android.view.View
import androidx.core.animation.doOnEnd
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.common.leapfrogWidget.LeapView
import com.tangem.tap.common.leapfrogWidget.LeapViewState
import com.tangem.tap.common.leapfrogWidget.LeapfrogWidget
@ -17,47 +18,60 @@ class TwinsCardWidget(
val getTopOfAnchorViewForActivateState: () -> Float
) {
var state: TwinCardsWidgetState? = null
private set
private val animationDuration = 300L
init {
if (leapfrogWidget.getViewsCount() != 2) throw UnsupportedOperationException()
}
fun toWelcome(animate: Boolean = true, onEnd: () -> Unit = {}) {
val animator = createAnimator(animate, onEnd)
fun toWelcome(animate: Boolean = true, onEnd: VoidCallback = {}) {
if (state == TwinCardsWidgetState.Welcome) return
state = TwinCardsWidgetState.Welcome
val animator = createAnimator(animate)
animator.playTogether(
createAnimator(TwinCardNumber.First, createWelcomeProperties(TwinCardNumber.First)),
createAnimator(TwinCardNumber.Second, createWelcomeProperties(TwinCardNumber.Second))
)
leapfrogWidget.fold { animator.start() }
animator.doOnEnd { onEnd() }
leapfrogWidget.fold(animate) { animator.start() }
}
fun toLeapfrog(animate: Boolean = true, onEnd: () -> Unit = {}) {
val animator = createAnimator(animate, onEnd)
fun toLeapfrog(animate: Boolean = true, onEnd: VoidCallback = {}) {
if (state == TwinCardsWidgetState.Leapfrog) return
state = TwinCardsWidgetState.Leapfrog
val animator = createAnimator(animate)
animator.playTogether(
createAnimator(TwinCardNumber.First, createLeapfrogProperties(TwinCardNumber.First)),
createAnimator(TwinCardNumber.Second, createLeapfrogProperties(TwinCardNumber.Second))
)
leapfrogWidget.fold {
animator.doOnEnd {
leapfrogWidget.initViews()
leapfrogWidget.unfold()
}
animator.start()
animator.doOnEnd {
leapfrogWidget.initViews()
leapfrogWidget.unfold(animate, onEnd)
}
animator.start()
}
fun toActivate(animate: Boolean = true, onEnd: () -> Unit = {}) {
val animator = createAnimator(animate, onEnd)
fun toActivate(animate: Boolean = true, onEnd: VoidCallback = {}) {
if (state == TwinCardsWidgetState.Activate) return
state = TwinCardsWidgetState.Activate
val animator = createAnimator(animate)
animator.playTogether(
createAnimator(TwinCardNumber.First, createActivateProperties(TwinCardNumber.First)),
createAnimator(TwinCardNumber.Second, createActivateProperties(TwinCardNumber.Second))
)
animator.doOnEnd { onEnd() }
animator.start()
}
private fun createAnimator(animate: Boolean, onEnd: () -> Unit): AnimatorSet {
private fun createAnimator(animate: Boolean): AnimatorSet {
return AnimatorSet().apply {
duration = if (animate) 400 else 0
doOnEnd { onEnd() }
duration = if (animate) animationDuration else 0
}
}
@ -72,20 +86,20 @@ class TwinsCardWidget(
return when (cardNumber) {
TwinCardNumber.First -> {
TwinsCardProperties(
xTranslation = -180f,
yTranslation = -350f,
xTranslation = -120f,
yTranslation = -220f,
rotation = -3f,
elevation = 1f,
scale = 1.1f,
scale = 1f,
)
}
TwinCardNumber.Second -> {
TwinsCardProperties(
xTranslation = 180f,
yTranslation = 10f,
xTranslation = 170f,
yTranslation = 170f,
rotation = -3f,
elevation = 0f,
scale = 1.1f,
scale = 1f,
)
}
}
@ -108,7 +122,7 @@ class TwinsCardWidget(
xTranslation = twinProperties.xTranslation,
yTranslation = twinProperties.yTranslation - topOfAnchorView,
rotation = 0f,
scale = twinProperties.scale - 0.4f,
scale = twinProperties.scale - 0.5f,
)
}
@ -120,6 +134,10 @@ class TwinsCardWidget(
}
}
enum class TwinCardsWidgetState {
Welcome, Leapfrog, Activate
}
private data class TwinsCardProperties(
val xTranslation: Float = 0f,
val yTranslation: Float = 0f,

View file

@ -1,26 +1,26 @@
package com.tangem.tap.features.send
package com.tangem.tap.features
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.google.android.material.snackbar.Snackbar
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_wallet.*
import org.rekotlin.StoreSubscriber
/**
[REDACTED_AUTHOR]
*/
abstract class BaseStoreFragment(layoutId: Int) : Fragment(layoutId) {
abstract fun subscribeToStore()
abstract class BaseFragment(layoutId: Int) : Fragment(layoutId), FragmentOnBackPressedHandler {
protected lateinit var mainView: View
protected val storeSubscribersList = mutableListOf<StoreSubscriber<*>>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -40,26 +40,32 @@ abstract class BaseStoreFragment(layoutId: Int) : Fragment(layoutId) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
toolbar?.setNavigationOnClickListener { activity?.onBackPressed() }
addBackPressHandler(this)
}
override fun onStart() {
super.onStart()
subscribeToStore()
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
}
override fun onStop() {
storeSubscribersList.forEach { store.unsubscribe(it) }
storeSubscribersList.clear()
super.onStop()
}
fun showRetrySnackbar(message: String, action: () -> Unit) {
fun showRetrySnackbar(message: String, action: VoidCallback) {
val snackbar = Snackbar.make(mainView, message, Snackbar.LENGTH_INDEFINITE)
snackbar.setAction(getString(R.string.common_retry)) {
snackbar.dismiss()
action()
}.show()
}
}
interface FragmentOnBackPressedHandler {
fun handleOnBackPressed()
}
@SuppressLint("FragmentBackPressedCallback")
fun Fragment.addBackPressHandler(handler: FragmentOnBackPressedHandler) {
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
handler.handleOnBackPressed()
}
})
view?.findViewById<Toolbar>(R.id.toolbar)?.setNavigationOnClickListener { activity?.onBackPressed() }
}

View file

@ -0,0 +1,24 @@
package com.tangem.tap.features
import com.tangem.tap.store
import org.rekotlin.StoreSubscriber
/**
[REDACTED_AUTHOR]
*/
abstract class BaseStoreFragment(layoutId: Int) : BaseFragment(layoutId) {
abstract fun subscribeToStore()
protected val storeSubscribersList = mutableListOf<StoreSubscriber<*>>()
override fun onStart() {
super.onStart()
subscribeToStore()
}
override fun onStop() {
storeSubscribersList.forEach { store.unsubscribe(it) }
storeSubscribersList.clear()
super.onStop()
}
}

View file

@ -11,8 +11,8 @@ 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.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.onboarding.twins.redux.CreateTwinWalletMode
import com.tangem.tap.features.onboarding.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.wallet.models.hasSendableAmountsOrPendingTransactions
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.coinmarketcap.CoinMarketCapService

View file

@ -6,7 +6,7 @@ import com.tangem.common.card.Card
import com.tangem.tap.common.entities.Button
import com.tangem.tap.common.entities.TapCurrency.Companion.DEFAULT_FIAT_CURRENCY
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.features.onboarding.twins.redux.TwinCardsState
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.network.coinmarketcap.FiatCurrency
import com.tangem.tap.store
import org.rekotlin.StateType

View file

@ -6,7 +6,6 @@ import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
@ -16,8 +15,6 @@ import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.features.feedback.FeedbackEmail
import com.tangem.tap.features.twins.redux.CreateTwinWalletMode
import com.tangem.tap.features.twins.redux.TwinCardsAction
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_details.*
@ -91,17 +88,10 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
}
if (state.isTangemTwins) {
if (state.twinCardsState.isCreatingTwinCardsAllowed) {
tv_erase_wallet.show()
tv_erase_wallet.text = getText(R.string.details_row_title_twins_recreate)
tv_erase_wallet.setOnClickListener {
store.dispatch(TwinCardsAction.Wallet.Create(
state.twinCardsState.cardNumber!!,
CreateTwinWalletMode.RecreateWallet
))
}
} else {
tv_erase_wallet.hide()
tv_erase_wallet.show()
tv_erase_wallet.text = getText(R.string.details_row_title_twins_recreate)
tv_erase_wallet.setOnClickListener {
store.dispatch(DetailsAction.ReCreateTwinsWallet(state.twinCardsState.cardNumber!!))
}
} else {
tv_erase_wallet.show()

View file

@ -16,15 +16,14 @@ import com.tangem.tap.features.onboarding.products.BaseOnboardingFragment
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_onboarding_main.*
import kotlinx.android.synthetic.main.layout_onboarding_bottom_action_views.*
import kotlinx.android.synthetic.main.layout_onboarding_home.*
import java.lang.ref.WeakReference
import kotlinx.android.synthetic.main.layout_onboarding_container_bottom.*
import kotlinx.android.synthetic.main.layout_onboarding_home_top.*
class HomeFragment : BaseOnboardingFragment<HomeState>() {
private lateinit var btnScanCard: ViewStateWidget
override fun getOnboardingTopContainerId(): Int = R.layout.layout_onboarding_home
override fun getOnboardingTopContainerId(): Int = R.layout.layout_onboarding_home_top
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -38,7 +37,10 @@ class HomeFragment : BaseOnboardingFragment<HomeState>() {
toolbar.hide()
val shareTransition = FragmentShareTransition(
listOf(ShareElement(WeakReference(imv_front_card), imv_front_card.transitionName)),
listOf(
ShareElement(imv_front_card, ShareElement.imvFrontCard),
ShareElement(imv_back_card, ShareElement.imvBackCard)
),
FrontCardEnterTransition(),
FrontCardExitTransition()
)
@ -62,6 +64,7 @@ class HomeFragment : BaseOnboardingFragment<HomeState>() {
oldState.homeState == newState.homeState
}.select { it.homeState }
}
storeSubscribersList.add(this)
}
override fun newState(state: HomeState) {

View file

@ -2,6 +2,8 @@ package com.tangem.tap.features.home.redux
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.extensions.onCardScanned
import com.tangem.tap.common.extensions.withMainContext
import com.tangem.tap.common.post
import com.tangem.tap.common.postUi
import com.tangem.tap.common.redux.AppState
@ -10,10 +12,10 @@ import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.FragmentShareTransition
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.domain.twins.getTwinCardNumber
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.send.redux.states.ButtonState
import com.tangem.tap.features.twins.redux.TwinCardsAction
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
@ -58,21 +60,24 @@ private fun handleReadCard() {
} else {
changeButtonState(ButtonState.PROGRESS)
store.dispatch(GlobalAction.ScanCard({ scanResponse ->
if (scanResponse.isTangemTwins()) {
scanResponse.card.getTwinCardNumber()?.let {
val configManager = store.state.globalState.configManager
val isCreatingTwinsAllowed = configManager?.config?.isCreatingTwinCardsAllowed ?: false
store.dispatch(TwinCardsAction.SetTwinCard(it, null, isCreatingTwinsAllowed))
}
}
store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))
if (OnboardingHelper.isOnboardingCase(scanResponse)) {
val navigateTo = OnboardingHelper.whereToNavigate(scanResponse)
store.dispatch(GlobalAction.Onboarding.Start(scanResponse))
navigateTo(navigateTo, store.state.homeState.shareTransition)
} else {
scope.launch { store.state.globalState.tapWalletManager.onCardScanned(scanResponse) }
navigateTo(AppScreen.Wallet, null)
scope.launch {
store.onCardScanned(scanResponse)
withMainContext {
if (scanResponse.twinsIsTwinned() && !preferencesStorage.wasTwinsOnboardingShown()) {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly))
navigateTo(AppScreen.OnboardingTwins, store.state.homeState.shareTransition)
} else {
navigateTo(AppScreen.Wallet, null)
}
}
}
}
}, {
changeButtonState(ButtonState.ENABLED)

View file

@ -5,7 +5,7 @@ import android.view.View
import com.tangem.tap.common.extensions.inflate
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.transitions.HomeToOnboardingTransition
import com.tangem.tap.features.send.BaseStoreFragment
import com.tangem.tap.features.BaseStoreFragment
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_onboarding_main.*
import kotlinx.android.synthetic.main.view_confetti.*
@ -16,11 +16,6 @@ import org.rekotlin.StoreSubscriber
*/
abstract class BaseOnboardingFragment<T> : BaseStoreFragment(R.layout.fragment_onboarding_main), StoreSubscriber<T> {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
configureTransitions()
}
override fun configureTransitions() {
enterTransition = HomeToOnboardingTransition()
exitTransition = HomeToOnboardingTransition()

View file

@ -16,18 +16,18 @@ import com.tangem.tangem_sdk_new.extensions.fadeOut
import com.tangem.tap.common.extensions.getDrawableCompat
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.postUi
import com.tangem.tap.common.redux.navigation.ShareElement
import com.tangem.tap.common.toggleWidget.RefreshBalanceWidget
import com.tangem.tap.common.transitions.InternalNoteLayoutTransition
import com.tangem.tap.features.onboarding.products.BaseOnboardingFragment
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteAction
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteResources
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteState
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteStep
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_onboarding_main.*
import kotlinx.android.synthetic.main.layout_onboarding_bottom_action_views.*
import kotlinx.android.synthetic.main.layout_onboarding_note.*
import kotlinx.android.synthetic.main.layout_onboarding_container_bottom.*
import kotlinx.android.synthetic.main.layout_onboarding_container_top.*
import kotlinx.android.synthetic.main.view_onboarding_progress.*
import kotlinx.android.synthetic.main.view_onboarding_tv_balance.*
@ -36,23 +36,26 @@ import kotlinx.android.synthetic.main.view_onboarding_tv_balance.*
*/
class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
private lateinit var btnRefreshBalanceWidget: RefreshBalanceWidget
private var currentStep: OnboardingNoteStep = OnboardingNoteStep.None
private var previousStep: OnboardingNoteStep = OnboardingNoteStep.None
override fun getOnboardingTopContainerId(): Int = R.layout.layout_onboarding_note
private lateinit var btnRefreshBalanceWidget: RefreshBalanceWidget
override fun getOnboardingTopContainerId(): Int = R.layout.layout_onboarding_container_top
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
postponeEnterTransition()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
imv_front_card.transitionName = ShareElement.imvFrontCard
startPostponedEnterTransition()
toolbar.setTitle(R.string.onboarding_title)
btnRefreshBalanceWidget = RefreshBalanceWidget(onboarding_main_container)
val resources = OnboardingNoteResources(
OnboardingNoteResources.Strings(
R.string.copy_toast_msg
)
)
store.dispatch(OnboardingNoteAction.SetResources(resources))
store.dispatch(OnboardingNoteAction.LoadCardArtwork)
store.dispatch(OnboardingNoteAction.DetermineStepOfScreen)
}
@ -81,7 +84,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
}
setBalance(state)
showConfetti(state.showConfetti)
currentStep = state.currentStep
previousStep = state.currentStep
}
private fun setBalance(state: OnboardingNoteState) {
@ -166,7 +169,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
}
private fun updateConstraints(currentStep: OnboardingNoteStep, @LayoutRes layoutId: Int) {
if (this.currentStep == currentStep) return
if (this.previousStep == currentStep) return
val constraintSet = ConstraintSet()
constraintSet.clone(requireContext(), layoutId)

View file

@ -10,7 +10,6 @@ import org.rekotlin.Action
*/
sealed class OnboardingNoteAction : Action {
// from user, ui
data class SetResources(val resources: OnboardingNoteResources) : OnboardingNoteAction()
object LoadCardArtwork : OnboardingNoteAction()
object CreateWallet : OnboardingNoteAction()
object TopUp : OnboardingNoteAction()

View file

@ -12,6 +12,7 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.hasWallets
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.scope
import com.tangem.tap.store
@ -107,6 +108,7 @@ private fun handleNoteAction(action: Action, dispatch: DispatchFunction) {
val isLoadedBefore = noteState.walletBalance.state != ProgressState.Loading
val balanceIsLoading = noteState.walletBalance.copy(
currency = Currency.Blockchain(walletManager.wallet.blockchain),
state = ProgressState.Loading,
error = null,
criticalError = null
@ -128,7 +130,7 @@ private fun handleNoteAction(action: Action, dispatch: DispatchFunction) {
}
is OnboardingNoteAction.ShowAddressInfoDialog -> {
val addressData = noteState.walletManager?.getAddressData() ?: return
val addressWasCopied = noteState.resources.strings.addressWasCopied
val addressWasCopied = globalState.resources.strings.addressWasCopied
val appDialog = AppDialog.AddressInfoDialog(
addressData,
onCopyAddress = { store.dispatchToastNotification(addressWasCopied) },
@ -143,7 +145,7 @@ private fun handleNoteAction(action: Action, dispatch: DispatchFunction) {
OnboardingNoteAction.Done -> {
store.dispatch(GlobalAction.Onboarding.Stop)
scope.launch {
globalState.tapWalletManager.onCardScanned(scanResponse)
store.onCardScanned(scanResponse)
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}
}

View file

@ -23,9 +23,6 @@ private fun internalReduce(action: Action, appState: AppState): OnboardingNoteSt
is OnboardingNoteAction.SetWalletManager -> {
state = state.copy(walletManager = action.walletManager)
}
is OnboardingNoteAction.SetResources -> {
state = state.copy(resources = action.resources)
}
is OnboardingNoteAction.Balance.Set -> {
state = state.copy(walletBalance = action.balance)
}

View file

@ -10,7 +10,6 @@ import org.rekotlin.StateType
*/
data class OnboardingNoteState(
val walletManager: WalletManager? = null,
val resources: OnboardingNoteResources = OnboardingNoteResources(),
// UI
val cardArtwork: Artwork? = null,
val walletBalance: OnboardingWalletBalance = OnboardingWalletBalance.loading(),
@ -25,12 +24,4 @@ data class OnboardingNoteState(
enum class OnboardingNoteStep {
None, CreateWallet, TopUpWallet, Done
}
data class OnboardingNoteResources(
val strings: Strings = Strings()
) {
data class Strings(
val addressWasCopied: Int = -1
)
}

View file

@ -8,16 +8,18 @@ import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.isVisible
import androidx.transition.TransitionManager
import com.tangem.tap.common.extensions.getDrawableCompat
import com.tangem.tap.common.redux.navigation.ShareElement
import com.tangem.tap.common.transitions.InternalNoteLayoutTransition
import com.tangem.tap.features.onboarding.products.BaseOnboardingFragment
import com.tangem.tap.features.onboarding.products.note.swapToBitmapDrawable
import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsAction
import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsState
import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsStep
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_onboarding_main.*
import kotlinx.android.synthetic.main.layout_onboarding_bottom_action_views.*
import kotlinx.android.synthetic.main.layout_onboarding_note.*
import kotlinx.android.synthetic.main.layout_onboarding_container_bottom.*
import kotlinx.android.synthetic.main.layout_onboarding_container_top.*
import kotlinx.android.synthetic.main.view_onboarding_progress.*
/**
@ -25,11 +27,19 @@ import kotlinx.android.synthetic.main.view_onboarding_progress.*
*/
class OnboardingOtherCardsFragment : BaseOnboardingFragment<OnboardingOtherCardsState>() {
override fun getOnboardingTopContainerId(): Int = R.layout.layout_onboarding_note
override fun getOnboardingTopContainerId(): Int = R.layout.layout_onboarding_container_top
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
postponeEnterTransition()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
imv_front_card.transitionName = ShareElement.imvFrontCard
startPostponedEnterTransition()
toolbar.setTitle(R.string.onboarding_title)
store.dispatch(OnboardingOtherCardsAction.LoadCardArtwork)
store.dispatch(OnboardingOtherCardsAction.DetermineStepOfScreen)

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.onboarding.products.otherCards.redux
import com.tangem.common.CompletionResult
import com.tangem.tap.common.extensions.onCardScanned
import com.tangem.tap.common.extensions.withMainContext
import com.tangem.tap.common.postUi
import com.tangem.tap.common.redux.AppState
@ -83,7 +84,7 @@ private fun handleOtherCardsAction(action: Action, dispatch: DispatchFunction) {
OnboardingOtherCardsAction.Done -> {
store.dispatch(GlobalAction.Onboarding.Stop)
scope.launch {
globalState.tapWalletManager.onCardScanned(onboardingManager.scanResponse)
store.onCardScanned(onboardingManager.scanResponse)
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}
}

View file

@ -1,72 +0,0 @@
package com.tangem.tap.features.onboarding.products.twins
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.squareup.picasso.Picasso
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.withMainContext
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.twins.redux.TwinCardsAction
import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_twin_cards_welcome.*
import kotlinx.android.synthetic.main.layout_twin_cards.*
import kotlinx.coroutines.launch
class OnboardingTwinsFragment : Fragment(R.layout.fragment_twin_cards_welcome) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
}
})
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
store.dispatch(TwinCardsAction.SetOnboardingShown)
val secondTwinNumber = store.state.twinCardsState.cardNumber?.pairNumber()?.number ?: ""
val text = getString(R.string.twins_onboarding_description_format, secondTwinNumber)
tv_twin_cards_description_1.text = text
setOnClickListeners()
Picasso.get()
.load(Artwork.TWIN_CARD_1)
.placeholder(R.drawable.card_placeholder)
?.error(R.drawable.card_placeholder)
?.into(iv_twin_card_1)
Picasso.get()
.load(Artwork.TWIN_CARD_2)
.placeholder(R.drawable.card_placeholder)
?.error(R.drawable.card_placeholder)
?.into(iv_twin_card_2)
}
private fun setOnClickListeners() {
btn_continue.setOnClickListener {
scope.launch {
val global = store.state.globalState
val scanResponse = global.onboardingManager?.scanResponse ?: return@launch
global.tapWalletManager.onCardScanned(scanResponse)
withMainContext {
store.dispatchDebugErrorNotification("Делаем онбоардинг")
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}
}
}
}
}

View file

@ -0,0 +1,64 @@
package com.tangem.tap.features.onboarding.products.twins.redux
import com.tangem.Message
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.domain.twins.AssetReader
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.features.onboarding.OnboardingWalletBalance
import org.rekotlin.Action
/**
[REDACTED_AUTHOR]
*/
sealed class TwinCardsAction : Action {
data class IfTwinsPrepareState(val scanResponse: ScanResponse) : TwinCardsAction()
object Init : TwinCardsAction()
data class SetMode(val mode: CreateTwinWalletMode) : TwinCardsAction()
data class SetStepOfScreen(val step: TwinCardsStep) : TwinCardsAction()
data class SetUserUnderstand(val isUnderstand: Boolean) : TwinCardsAction()
sealed class CardsManager {
data class Set(val manager: TwinCardsManager) : TwinCardsAction()
object Release : TwinCardsAction()
}
sealed class Wallet : TwinCardsAction() {
data class HandleOnBackPressed(
// this is necessary for the correct animation of the return of cards to the Home screen
val shouldResetTwinCardsWidget: (should: Boolean, popAction: VoidCallback) -> Unit
) : TwinCardsAction()
data class ShowInterruptDialog(val onOk: VoidCallback) : TwinCardsAction(), StateDialog
data class LaunchFirstStep(val initialMessage: Message, val reader: AssetReader) : TwinCardsAction()
data class LaunchSecondStep(
val initialMessage: Message,
val preparingMessage: Message,
val creatingWalletMessage: Message,
) : TwinCardsAction()
data class LaunchThirdStep(val message: Message) : TwinCardsAction()
}
// for the onboarding
data class SetPairCardId(val cardId: String) : TwinCardsAction()
object TopUp : TwinCardsAction()
object ShowAddressInfoDialog : TwinCardsAction()
data class SetWalletManager(val walletManager: WalletManager) : TwinCardsAction()
object Done : TwinCardsAction()
sealed class Balance {
object Update : TwinCardsAction()
data class Set(val balance: OnboardingWalletBalance) : TwinCardsAction()
}
sealed class Confetti {
object Show : TwinCardsAction()
object Hide : TwinCardsAction()
}
}

View file

@ -0,0 +1,275 @@
package com.tangem.tap.features.onboarding.products.twins.redux
import com.tangem.blockchain.extensions.Result
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.guard
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.postUi
import com.tangem.tap.common.redux.AppDialog
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.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
import org.rekotlin.Middleware
class TwinCardsMiddleware {
companion object {
val handler = twinsWalletMiddleware
}
}
private val twinsWalletMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
handle(action, dispatch)
next(action)
}
}
}
private fun handle(action: Action, dispatch: DispatchFunction) {
val action = action as? TwinCardsAction ?: return
val globalState = store.state.globalState
val onboardingManager = globalState.onboardingManager
val twinCardsState = store.state.twinCardsState
fun getScanResponse(): ScanResponse {
return when (twinCardsState.mode) {
CreateTwinWalletMode.CreateWallet -> onboardingManager?.scanResponse
CreateTwinWalletMode.RecreateWallet -> globalState.scanResponse
} ?: throw NullPointerException("ScanResponse can't be NULL")
}
fun updateScanResponse(response: ScanResponse) {
when (twinCardsState.mode) {
CreateTwinWalletMode.CreateWallet -> onboardingManager?.scanResponse = response
CreateTwinWalletMode.RecreateWallet -> store.dispatch(GlobalAction.SaveScanNoteResponse(response))
}
}
fun startCardActivation(cardId: String) {
if (twinCardsState.mode == CreateTwinWalletMode.CreateWallet) {
onboardingManager?.activationStarted(cardId)
}
}
fun finishCardActivation() {
if (twinCardsState.mode == CreateTwinWalletMode.CreateWallet) {
onboardingManager?.activationFinished(getScanResponse().card.cardId)
twinCardsState.pairCardId?.let { onboardingManager?.activationFinished(it) }
}
}
when (action) {
is TwinCardsAction.Init -> {
if (twinCardsState.currentStep == TwinCardsStep.WelcomeOnly) return
when (twinCardsState.mode) {
CreateTwinWalletMode.CreateWallet -> {
if (preferencesStorage.wasTwinsOnboardingShown()) {
val step = when {
!getScanResponse().twinsIsTwinned() -> TwinCardsStep.CreateFirstWallet
twinCardsState.walletBalance.balanceIsToppedUp() -> TwinCardsStep.Done
else -> TwinCardsStep.TopUpWallet
}
store.dispatch((TwinCardsAction.SetStepOfScreen(step)))
} else {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.Welcome))
}
}
CreateTwinWalletMode.RecreateWallet -> {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.Warning))
}
}
}
is TwinCardsAction.Wallet.HandleOnBackPressed -> {
val shouldReturnCardBack = twinCardsState.mode == CreateTwinWalletMode.CreateWallet
&& twinCardsState.currentStep != TwinCardsStep.TopUpWallet
&& twinCardsState.currentStep != TwinCardsStep.Done
if (twinCardsState.showAlert) {
val onInterruptPrompt = {
store.dispatch(TwinCardsAction.CardsManager.Release)
action.shouldResetTwinCardsWidget(shouldReturnCardBack) {
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
}
}
val stateDialog = TwinCardsAction.Wallet.ShowInterruptDialog(onInterruptPrompt)
store.dispatchDialogShow(stateDialog)
} else {
store.dispatch(TwinCardsAction.CardsManager.Release)
action.shouldResetTwinCardsWidget(shouldReturnCardBack) {
store.dispatch(NavigationAction.PopBackTo())
}
}
}
is TwinCardsAction.SetStepOfScreen -> {
when (action.step) {
TwinCardsStep.WelcomeOnly -> {
preferencesStorage.saveTwinsOnboardingShown()
}
TwinCardsStep.Welcome -> {
preferencesStorage.saveTwinsOnboardingShown()
}
TwinCardsStep.Warning -> {
}
TwinCardsStep.CreateFirstWallet -> {
}
TwinCardsStep.CreateSecondWallet -> {
}
TwinCardsStep.CreateThirdWallet -> {
}
TwinCardsStep.TopUpWallet -> {
store.dispatch(TwinCardsAction.Balance.Update)
}
TwinCardsStep.Done -> {
finishCardActivation()
postUi(500) { store.dispatch(TwinCardsAction.Confetti.Show) }
}
}
}
is TwinCardsAction.Wallet.LaunchFirstStep -> {
val manager = TwinCardsManager(getScanResponse().card, action.reader)
store.dispatch(TwinCardsAction.CardsManager.Set(manager))
scope.launch {
when (val result = manager.createFirstWallet(action.initialMessage)) {
is CompletionResult.Success -> {
startCardActivation(result.data.cardId)
delay(DELAY_SDK_DIALOG_CLOSE)
withMainContext {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.CreateSecondWallet))
}
}
is CompletionResult.Failure -> {
}
}
}
}
is TwinCardsAction.Wallet.LaunchSecondStep -> {
val manager = twinCardsState.twinCardsManager ?: return
scope.launch {
val result = manager.createSecondWallet(
action.initialMessage,
action.preparingMessage,
action.creatingWalletMessage
)
when (result) {
is CompletionResult.Success -> {
startCardActivation(result.data.cardId)
delay(DELAY_SDK_DIALOG_CLOSE)
withMainContext {
store.dispatch(TwinCardsAction.SetPairCardId(result.data.cardId))
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.CreateThirdWallet))
}
}
is CompletionResult.Failure -> {
}
}
}
}
is TwinCardsAction.Wallet.LaunchThirdStep -> {
val manager = twinCardsState.twinCardsManager ?: return
scope.launch {
when (val result = manager.complete(action.message)) {
is Result.Success -> {
updateScanResponse(result.data)
delay(DELAY_SDK_DIALOG_CLOSE)
withMainContext { store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.TopUpWallet)) }
}
is Result.Failure -> {
}
}
}
}
is TwinCardsAction.Balance.Update -> {
val walletManager = if (twinCardsState.walletManager != null) {
twinCardsState.walletManager
} else {
val wmFactory = globalState.tapWalletManager.walletManagerFactory
val walletManager = wmFactory.makePrimaryWalletManager(getScanResponse()).guard {
val message = "Loading cancelled. Cause: wallet manager didn't created"
val customError = TapError.CustomError(message)
store.dispatchErrorNotification(customError)
return
}
dispatch(TwinCardsAction.SetWalletManager(walletManager))
walletManager
}
val isLoadedBefore = twinCardsState.walletBalance.state != ProgressState.Loading
val balanceIsLoading = twinCardsState.walletBalance.copy(
currency = Currency.Blockchain(walletManager.wallet.blockchain),
state = ProgressState.Loading,
error = null,
criticalError = null
)
store.dispatch(TwinCardsAction.Balance.Set(balanceIsLoading))
scope.launch {
val loadedBalance = onboardingManager?.updateBalance(walletManager) ?: return@launch
loadedBalance.criticalError?.let { store.dispatchErrorNotification(it) }
delay(if (isLoadedBefore) 0 else 300)
withMainContext { store.dispatch(TwinCardsAction.Balance.Set(loadedBalance)) }
}
}
is TwinCardsAction.Balance.Set -> {
if (action.balance.balanceIsToppedUp()) {
scope.launch {
withMainContext { store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.Done)) }
}
}
}
is TwinCardsAction.ShowAddressInfoDialog -> {
val addressData = twinCardsState.walletManager?.getAddressData() ?: return
val addressWasCopied = globalState.resources.strings.addressWasCopied
val appDialog = AppDialog.AddressInfoDialog(
addressData,
onCopyAddress = { store.dispatchToastNotification(addressWasCopied) },
onExploreAddress = { store.dispatchOpenUrl(addressData.exploreUrl) }
)
store.dispatchDialogShow(appDialog)
}
is TwinCardsAction.TopUp -> {
val topUpUrl = twinCardsState.walletManager?.getToUpUrl() ?: return
store.dispatchOpenUrl(topUpUrl)
}
TwinCardsAction.Done -> {
scope.launch {
val scanResponse = getScanResponse()
store.onCardScanned(scanResponse)
withMainContext {
when (twinCardsState.mode) {
CreateTwinWalletMode.CreateWallet -> {
store.dispatch(GlobalAction.Onboarding.Stop)
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}
CreateTwinWalletMode.RecreateWallet -> {
store.dispatch(NavigationAction.PopBackTo())
}
}
}
}
}
}
}

View file

@ -0,0 +1,64 @@
package com.tangem.tap.features.onboarding.products.twins.redux
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.twins.getTwinCardNumber
import org.rekotlin.Action
class TwinCardsReducer {
companion object {
fun reduce(action: Action, state: AppState): TwinCardsState = internalReduce(action, state)
}
}
private fun internalReduce(action: Action, state: AppState): TwinCardsState {
if (action !is TwinCardsAction) return state.twinCardsState
var state = state.twinCardsState
when (action) {
is TwinCardsAction.IfTwinsPrepareState -> {
state = if (action.scanResponse.isTangemTwins()) {
val cardNumber = action.scanResponse.card.getTwinCardNumber() ?: return state
TwinCardsState(
cardNumber = cardNumber,
mode = CreateTwinWalletMode.CreateWallet,
)
} else {
TwinCardsState()
}
}
is TwinCardsAction.SetMode -> {
state = state.copy(mode = action.mode)
}
is TwinCardsAction.SetStepOfScreen -> {
state = state.copy(currentStep = action.step)
}
is TwinCardsAction.SetUserUnderstand -> {
state = state.copy(userWasUnderstandIfWalletRecreate = action.isUnderstand)
}
is TwinCardsAction.CardsManager.Set -> {
state = state.copy(twinCardsManager = action.manager)
}
TwinCardsAction.CardsManager.Release -> {
state = state.copy(twinCardsManager = null)
}
is TwinCardsAction.SetPairCardId -> {
state = state.copy(pairCardId = action.cardId)
}
is TwinCardsAction.SetWalletManager -> {
state = state.copy(walletManager = action.walletManager)
}
is TwinCardsAction.Balance.Set -> {
state = state.copy(walletBalance = action.balance)
}
is TwinCardsAction.Confetti.Show -> {
state = state.copy(showConfetti = true)
}
is TwinCardsAction.Confetti.Hide -> {
state = state.copy(showConfetti = false)
}
}
return state
}

View file

@ -0,0 +1,61 @@
package com.tangem.tap.features.onboarding.products.twins.redux
import com.tangem.blockchain.common.WalletManager
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.features.onboarding.OnboardingWalletBalance
import org.rekotlin.StateType
/**
[REDACTED_AUTHOR]
*/
data class TwinCardsState(
// if cardNumber = null -> the scanned card is not Twin
val cardNumber: TwinCardNumber? = null,
val mode: CreateTwinWalletMode = CreateTwinWalletMode.CreateWallet,
val currentStep: TwinCardsStep = TwinCardsStep.Welcome,
val userWasUnderstandIfWalletRecreate: Boolean = false,
// available only when creating or recreating wallets
val twinCardsManager: TwinCardsManager? = null,
// for the onboarding
val pairCardId: String? = null, // available after create\recreate wallets
val walletManager: WalletManager? = null,
val walletBalance: OnboardingWalletBalance = OnboardingWalletBalance.loading(),
val showConfetti: Boolean = false,
) : StateType {
val steps: List<TwinCardsStep>
get() = when (mode) {
CreateTwinWalletMode.CreateWallet -> listOf(
TwinCardsStep.None,
TwinCardsStep.CreateFirstWallet,
TwinCardsStep.CreateSecondWallet,
TwinCardsStep.CreateThirdWallet,
TwinCardsStep.TopUpWallet,
TwinCardsStep.Done
)
CreateTwinWalletMode.RecreateWallet -> listOf(
TwinCardsStep.None,
TwinCardsStep.CreateFirstWallet,
TwinCardsStep.CreateSecondWallet,
TwinCardsStep.CreateThirdWallet,
TwinCardsStep.Done
)
}
val progress: Int
get() = steps.indexOf(currentStep)
val showAlert: Boolean
get() = currentStep == TwinCardsStep.CreateSecondWallet || currentStep == TwinCardsStep.CreateThirdWallet
}
enum class CreateTwinWalletMode { CreateWallet, RecreateWallet }
enum class TwinCardsStep {
None, WelcomeOnly, Welcome, Warning, CreateFirstWallet, CreateSecondWallet, CreateThirdWallet,
// for the onboarding
TopUpWallet, Done
}

View file

@ -0,0 +1,366 @@
package com.tangem.tap.features.onboarding.products.twins.ui
import android.os.Bundle
import android.view.View
import android.view.animation.OvershootInterpolator
import androidx.annotation.LayoutRes
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.isVisible
import androidx.transition.TransitionInflater
import androidx.transition.TransitionManager
import com.squareup.picasso.Picasso
import com.tangem.Message
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.leapfrogWidget.LeapfrogWidget
import com.tangem.tap.common.postUi
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.common.redux.navigation.ShareElement
import com.tangem.tap.common.toggleWidget.RefreshBalanceWidget
import com.tangem.tap.common.transitions.InternalNoteLayoutTransition
import com.tangem.tap.domain.twins.AssetReader
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.domain.twins.TwinsCardWidget
import com.tangem.tap.features.onboarding.products.BaseOnboardingFragment
import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_onboarding_main.*
import kotlinx.android.synthetic.main.layout_onboarding_container_bottom.*
import kotlinx.android.synthetic.main.layout_onboarding_container_top.*
import kotlinx.android.synthetic.main.view_bg_twins_welcome.*
import kotlinx.android.synthetic.main.view_onboarding_progress.*
import kotlinx.android.synthetic.main.view_onboarding_tv_balance.*
import org.rekotlin.Action
class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
private var previousStep = TwinCardsStep.None
private lateinit var twinsWidget: TwinsCardWidget
private lateinit var btnRefreshBalanceWidget: RefreshBalanceWidget
private val assetReader: AssetReader by lazy {
object : AssetReader {
override fun readAssetAsString(name: String): String = requireContext().readAssetAsString(name)
}
}
override fun getOnboardingTopContainerId(): Int = R.layout.layout_onboarding_container_top
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
postponeEnterTransition()
store.dispatch(TwinCardsAction.Init)
}
override fun configureTransitions() {
when (store.state.twinCardsState.mode) {
CreateTwinWalletMode.CreateWallet -> super.configureTransitions()
CreateTwinWalletMode.RecreateWallet -> {
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
reconfigureLayoutForTwins()
twinsWidget = TwinsCardWidget(LeapfrogWidget(cards_container)) { 250f }
btnRefreshBalanceWidget = RefreshBalanceWidget(onboarding_main_container)
toolbar.title = getText(R.string.twins_recreate_toolbar)
Picasso.get()
.load(Artwork.TWIN_CARD_1)
.placeholder(R.drawable.card_placeholder_black)
?.into(imv_twin_front_card)
Picasso.get()
.load(Artwork.TWIN_CARD_2)
.placeholder(R.drawable.card_placeholder_white)
?.into(imv_twin_back_card)
}
private fun reconfigureLayoutForTwins() {
imv_front_card.hide()
imv_card_background.hide()
cards_container.show()
imv_twin_front_card.transitionName = ShareElement.imvFrontCard
imv_twin_back_card.transitionName = ShareElement.imvBackCard
}
override fun subscribeToStore() {
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.twinCardsState == newState.twinCardsState
}.select { it.twinCardsState }
}
storeSubscribersList.add(this)
}
override fun newState(state: TwinCardsState) {
if (activity == null) return
// toolbar.title = when (state.mode) {
// CreateTwinWalletMode.CreateWallet -> getText(R.string.wallet_button_create_wallet)
// CreateTwinWalletMode.RecreateWallet -> getText(R.string.twins_recreate_toolbar)
// }
pb_state.max = state.steps.size - 1
pb_state.progress = state.progress
when (state.currentStep) {
TwinCardsStep.WelcomeOnly -> setupWelcomeOnlyState(state)
TwinCardsStep.Welcome -> setupWelcomeState(state)
TwinCardsStep.Warning -> setupWarningState(state)
TwinCardsStep.CreateFirstWallet -> setupCreateFirstWalletState(state)
TwinCardsStep.CreateSecondWallet -> setupCreateSecondWalletState(state)
TwinCardsStep.CreateThirdWallet -> setupCreateThirdWalletState(state)
TwinCardsStep.TopUpWallet -> setupTopUpWalletState(state)
TwinCardsStep.Done -> setupDoneState(state)
}
setBalance(state)
showConfetti(state.showConfetti)
previousStep = state.currentStep
}
private fun setBalance(state: TwinCardsState) {
if (state.walletBalance.currency.blockchain == Blockchain.Unknown) return
val balanceValue = state.walletBalance.value.stripZeroPlainString()
val currency = state.walletBalance.currency.currencySymbol
tv_balance_value.text = "$balanceValue $currency"
}
private fun setupWelcomeOnlyState(state: TwinCardsState) {
setupWelcomeState(state, NavigationAction.NavigateTo(AppScreen.Wallet))
}
private fun setupWelcomeState(state: TwinCardsState) {
setupWelcomeState(state, TwinCardsAction.SetStepOfScreen(TwinCardsStep.CreateFirstWallet))
}
private fun setupWelcomeState(state: TwinCardsState, mainAction: Action) {
twinsWidget.toWelcome(false) { startPostponedEnterTransition() }
pb_state.hide()
tv_header.setText(R.string.twins_onboarding_subtitle)
tv_body.text = getString(R.string.twins_onboarding_description_format, state.cardNumber?.pairIndexNumber())
btn_main_action.setText(R.string.common_continue)
btn_main_action.setOnClickListener { store.dispatch(mainAction) }
}
private fun setupWarningState(state: TwinCardsState) {
twinsWidget.toWelcome(false) { startPostponedEnterTransition() }
pb_state.hide()
chb_understand.show()
tv_header.setText(R.string.common_warning)
tv_body.setText(R.string.twins_recreate_warning)
chb_understand.setOnCheckedChangeListener { buttonView, isChecked ->
store.dispatch(TwinCardsAction.SetUserUnderstand(isChecked))
}
btn_main_action.isEnabled = state.userWasUnderstandIfWalletRecreate
btn_main_action.setText(R.string.common_continue)
btn_main_action.setOnClickListener {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.CreateFirstWallet))
}
}
private fun setupCreateFirstWalletState(state: TwinCardsState) {
bg_circle_large.hide()
bg_circle_medium.hide()
bg_circle_min.hide()
chb_understand.hide()
pb_state.show()
onboarding_main_container.beginDelayedTransition()
when (previousStep) {
TwinCardsStep.None -> {
twinsWidget.toLeapfrog(false) {
when (state.cardNumber) {
TwinCardNumber.First -> startPostponedEnterTransition()
TwinCardNumber.Second -> {
switchToCard(state.cardNumber, false) { startPostponedEnterTransition() }
}
}
}
}
TwinCardsStep.Welcome, TwinCardsStep.Warning -> {
twinsWidget.toLeapfrog(onEnd = { switchToCard(state.cardNumber) })
}
}
val twinIndexNumber = state.cardNumber?.indexNumber()
tv_header.text = getString(R.string.twins_recreate_title_format, twinIndexNumber)
tv_body.setText(R.string.onboarding_twins_interrupt_warning)
btn_main_action.text = getString(R.string.twins_recreate_button_format, twinIndexNumber)
btn_main_action.setOnClickListener {
store.dispatch(TwinCardsAction.Wallet.LaunchFirstStep(
Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)),
assetReader
))
}
btn_alternative_action.isVisible = false
btn_alternative_action.isClickable = false
}
private fun setupCreateSecondWalletState(state: TwinCardsState) {
switchToCard(state.cardNumber?.pairNumber())
val twinPairIndexNumber = state.cardNumber?.pairIndexNumber()
tv_header.text = getString(R.string.twins_recreate_title_format, twinPairIndexNumber)
tv_body.setText(R.string.onboarding_twins_interrupt_warning)
btn_main_action.text = getString(R.string.twins_recreate_button_format, twinPairIndexNumber)
btn_main_action.setOnClickListener {
store.dispatch(TwinCardsAction.Wallet.LaunchSecondStep(
Message(getString(R.string.twins_recreate_title_format, twinPairIndexNumber)),
Message(getString(R.string.twins_recreate_title_preparing)),
Message(getString(R.string.twins_recreate_title_creating_wallet)),
))
}
}
private fun setupCreateThirdWalletState(state: TwinCardsState) {
switchToCard(state.cardNumber)
val twinIndexNumber = state.cardNumber?.indexNumber()
tv_header.text = getString(R.string.twins_recreate_title_format, twinIndexNumber)
tv_body.setText(R.string.onboarding_twins_interrupt_warning)
btn_main_action.text = getString(R.string.twins_recreate_button_format, twinIndexNumber)
btn_main_action.setOnClickListener {
store.dispatch(TwinCardsAction.Wallet.LaunchThirdStep(
Message(getString(R.string.twins_recreate_title_format, twinIndexNumber))
))
}
}
private fun setupTopUpWalletState(state: TwinCardsState) {
when (previousStep) {
TwinCardsStep.None -> {
when (state.cardNumber) {
TwinCardNumber.First -> {
twinsWidget.leapfrogWidget.unfold(false) {
twinsWidget.toActivate(false) {
startPostponedEnterTransition()
}
}
}
TwinCardNumber.Second -> {
switchToCard(state.cardNumber, false) {
twinsWidget.toActivate(false)
startPostponedEnterTransition()
}
}
}
}
TwinCardsStep.CreateThirdWallet -> {
twinsWidget.toActivate()
}
}
btn_main_action.setText(R.string.onboarding_top_up_button_but_crypto)
btn_main_action.setOnClickListener {
store.dispatch(TwinCardsAction.TopUp)
}
btn_alternative_action.setText(R.string.onboarding_top_up_button_show_wallet_address)
btn_alternative_action.setOnClickListener {
store.dispatch(TwinCardsAction.ShowAddressInfoDialog)
}
tv_header.setText(R.string.onboarding_top_up_header)
tv_body.setText(R.string.onboarding_top_up_body)
btnRefreshBalanceWidget.changeState(state.walletBalance.state)
if (btnRefreshBalanceWidget.isShowing != true) {
postUi(300) {
btnRefreshBalanceWidget.mainView.setOnClickListener {
store.dispatch(TwinCardsAction.Balance.Update)
}
}
}
imv_card_background.setBackgroundDrawable(requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8))
updateConstraints(state.currentStep, R.layout.lp_onboarding_topup_wallet_twins)
}
private fun setupDoneState(state: TwinCardsState) {
btn_main_action.setText(R.string.onboarding_done_button_continue)
btn_main_action.setOnClickListener {
store.dispatch(TwinCardsAction.Confetti.Hide)
store.dispatch(TwinCardsAction.Done)
}
btn_alternative_action.isVisible = false
btn_alternative_action.isClickable = false
tv_header.setText(R.string.onboarding_done_header)
tv_body.setText(R.string.onboarding_done_body)
val layout = when (state.mode) {
CreateTwinWalletMode.CreateWallet -> R.layout.lp_onboarding_done_activation_twins
CreateTwinWalletMode.RecreateWallet -> R.layout.lp_onboarding_done
}
updateConstraints(state.currentStep, layout)
}
private fun updateConstraints(currentStep: TwinCardsStep, @LayoutRes layoutId: Int) {
if (this.previousStep == currentStep) return
val constraintSet = ConstraintSet()
constraintSet.clone(requireContext(), layoutId)
constraintSet.applyTo(onboarding_main_container)
val transition = InternalNoteLayoutTransition()
transition.interpolator = OvershootInterpolator()
TransitionManager.beginDelayedTransition(onboarding_main_container, transition)
}
private fun switchToCard(cardNumber: TwinCardNumber?, animate: Boolean = true, onEnd: VoidCallback = {}) {
val position = cardNumber?.number ?: return
if (twinsWidget.leapfrogWidget.getViewPositionByIndex(1) != position - 1) {
twinsWidget.leapfrogWidget.leap(animate, onEnd)
} else {
onEnd()
}
}
override fun handleOnBackPressed() {
store.dispatch(TwinCardsAction.Wallet.HandleOnBackPressed { should, popAction ->
store.dispatch(TwinCardsAction.Confetti.Hide)
showConfetti(false)
if (should) switchToCard(TwinCardNumber.First, true, popAction)
else popAction()
})
}
}
private fun TwinCardNumber?.indexNumber(): String {
return this?.number?.toString() ?: ""
}
private fun TwinCardNumber?.pairIndexNumber(): String {
return this?.pairNumber()?.number?.toString() ?: ""
}

View file

@ -1,10 +1,10 @@
package com.tangem.tap.features.twins.ui.dialog
package com.tangem.tap.features.onboarding.products.twins.ui.dialog
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.features.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.store
import com.tangem.wallet.R
@ -13,10 +13,10 @@ import com.tangem.wallet.R
*/
class CreateWalletInterruptDialog {
companion object {
fun create(state: TwinCardsAction.Wallet.InterruptDialog, context: Context): AlertDialog {
fun create(state: TwinCardsAction.Wallet.ShowInterruptDialog, context: Context): AlertDialog {
return MaterialAlertDialogBuilder(context)
.setMessage(R.string.details_twins_recreate_alert)
.setPositiveButton(R.string.common_ok) { _, _ -> state.interrupt() }
.setMessage(R.string.twins_recreate_alert)
.setPositiveButton(R.string.common_ok) { _, _ -> state.onOk() }
.setNegativeButton(R.string.common_cancel) { _, _ -> }
.setOnDismissListener { store.dispatchDialogHide() }
.create()

View file

@ -3,8 +3,8 @@ 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.features.send.BaseStoreFragment
import com.tangem.tap.store
import com.tangem.wallet.R
import org.rekotlin.StoreSubscriber

View file

@ -7,7 +7,6 @@ import android.text.method.DigitsKeyListener
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import androidx.activity.OnBackPressedCallback
import androidx.core.view.postDelayed
import androidx.core.widget.addTextChangedListener
import androidx.recyclerview.widget.LinearLayoutManager
@ -25,7 +24,7 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.common.snackBar.MaxAmountSnackbar
import com.tangem.tap.common.text.truncateMiddleWith
import com.tangem.tap.common.toggleWidget.*
import com.tangem.tap.features.send.BaseStoreFragment
import com.tangem.tap.features.BaseStoreFragment
import com.tangem.tap.features.send.redux.*
import com.tangem.tap.features.send.redux.AddressPayIdActionUi.*
import com.tangem.tap.features.send.redux.AmountActionUi.*
@ -63,23 +62,6 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
private val sendSubscriber = SendStateSubscriber(this)
private lateinit var keyboardObserver: KeyboardObserver
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
val externalTransactionData = store.state.sendState.externalTransactionData
if (externalTransactionData == null) {
store.dispatch(NavigationAction.PopBackTo())
} else {
store.dispatch(
WalletAction.TradeCryptoAction.FinishSelling(externalTransactionData.transactionId)
)
}
}
})
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -97,7 +79,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
private fun initSendButtonStates() {
btnSend.setOnClickListener {
store.dispatch(SendActionUi.SendAmountToRecipient(
Message(getString(R.string.initial_message_sign_header))
Message(getString(R.string.initial_message_sign_header))
))
}
sendBtn = IndeterminateProgressButtonWidget(btnSend, progress)
@ -124,8 +106,8 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
}
imvQrCode.setOnClickListener {
startActivityForResult(
Intent(requireContext(), ScanQrCodeActivity::class.java),
ScanQrCodeActivity.SCAN_QR_REQUEST_CODE
Intent(requireContext(), ScanQrCodeActivity::class.java),
ScanQrCodeActivity.SCAN_QR_REQUEST_CODE
)
}
}
@ -281,6 +263,17 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
sp.edit().putString("mainCurrency", type.name).apply()
}
override fun handleOnBackPressed() {
val externalTransactionData = store.state.sendState.externalTransactionData
if (externalTransactionData == null) {
store.dispatch(NavigationAction.PopBackTo())
} else {
store.dispatch(
WalletAction.TradeCryptoAction.FinishSelling(externalTransactionData.transactionId)
)
}
}
override fun onDestroy() {
keyboardObserver.unregisterListener()
store.dispatch(ReleaseSendState)

View file

@ -1,6 +1,6 @@
package com.tangem.tap.features.send.ui.stateSubscribers
import com.tangem.tap.features.send.BaseStoreFragment
import com.tangem.tap.features.BaseStoreFragment
import org.rekotlin.StateType
import org.rekotlin.StoreSubscriber
import java.lang.ref.WeakReference

View file

@ -12,7 +12,7 @@ import com.tangem.tap.common.redux.getMessageString
import com.tangem.tap.common.text.DecimalDigitsInputFilter
import com.tangem.tap.domain.MultiMessageError
import com.tangem.tap.domain.assembleErrors
import com.tangem.tap.features.send.BaseStoreFragment
import com.tangem.tap.features.BaseStoreFragment
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.Error
import com.tangem.tap.features.send.redux.FeeAction
import com.tangem.tap.features.send.redux.SendAction
@ -188,15 +188,15 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
(fg as? SendFragment)?.saveMainCurrency(state.mainCurrency.type)
val balanceText = fg.getString(R.string.send_balance_subtitle_format,
state.mainCurrency.currencySymbol,
state.viewBalanceValue)
state.mainCurrency.currencySymbol,
state.viewBalanceValue)
fg.tvBalance.update(balanceText)
fg.tilAmountToSend.isEnabled = state.inputIsEnabled
val imageRes = if (state.inputIsEnabled) R.drawable.ic_arrows_up_down else 0
fg.tvAmountCurrency.setCompoundDrawablesWithIntrinsicBounds(0, 0, imageRes, 0)
val textColor = if (state.inputIsEnabled) R.color.blue else R.color.textGray
val textColor = if (state.inputIsEnabled) R.color.blue else R.color.textGray
fg.tvAmountCurrency.setTextColor(fg.getColor(textColor))
}
@ -245,7 +245,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
totalLayout.tvTotalValue.update("${roughOrEmpty(receipt.totalFiat)} ${receipt.symbols.fiat}")
val willSent = getString(R.string.send_total_subtitle_format,
receipt.willSentCrypto, receipt.symbols.crypto)
receipt.willSentCrypto, receipt.symbols.crypto)
totalLayout.tvWillBeSentValue.update(willSent)
}
@ -277,9 +277,9 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
totalLayout.tvTotalValue.update("${roughOrEmpty(receipt.totalFiat)} ${receipt.symbols.fiat}")
val willSent = getString(
R.string.send_total_subtitle_asset_format,
receipt.symbols.token ?: "", receipt.willSentToken,
receipt.symbols.crypto, receipt.willSentFeeCoin
R.string.send_total_subtitle_asset_format,
receipt.symbols.token ?: "", receipt.willSentToken,
receipt.symbols.crypto, receipt.willSentFeeCoin
)
totalLayout.tvWillBeSentValue.update(willSent)
}

View file

@ -1,62 +0,0 @@
package com.tangem.tap.features.twins.redux
import com.tangem.Message
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.domain.twins.AssetReader
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.domain.twins.TwinCardsManager
import org.rekotlin.Action
/**
[REDACTED_AUTHOR]
*/
sealed class TwinCardsAction : Action {
data class SetResources(val resources: TwinCardsResources) : TwinCardsAction()
data class SetTwinCard(
val number: TwinCardNumber,
val secondCardId: String?,
val isCreatingTwinCardsAllowed: Boolean,
) : TwinCardsAction()
sealed class CardsManager {
data class Set(val manager: TwinCardsManager) : TwinCardsAction()
object Release : TwinCardsAction()
}
object ShowOnboarding : TwinCardsAction()
object SetOnboardingShown : TwinCardsAction()
object ProceedToCreateWallet : TwinCardsAction()
sealed class Wallet : TwinCardsAction() {
object HandleOnBackPressed : TwinCardsAction()
data class Create(
val number: TwinCardNumber,
val createTwinWalletMode: CreateTwinWalletMode
) : TwinCardsAction()
data class InterruptDialog(val interrupt: () -> Unit) : TwinCardsAction(), StateDialog
data class LaunchFirstStep(
val initialMessage: Message,
val reader: AssetReader
) : TwinCardsAction() {
object Success : TwinCardsAction()
}
data class LaunchSecondStep(
val initialMessage: Message,
val preparingMessage: Message,
val creatingWalletMessage: Message,
) : TwinCardsAction() {
object Success : TwinCardsAction()
}
data class LaunchThirdStep(val message: Message) : TwinCardsAction() {
data class Success(val scanResponse: ScanResponse) : TwinCardsAction()
}
}
}

View file

@ -1,139 +0,0 @@
package com.tangem.tap.features.twins.redux
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchNotification
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.extensions.toSendableAmounts
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
import org.rekotlin.Middleware
class TwinCardsMiddleware {
companion object {
val handler = twinsWalletMiddleware
}
}
private val twinsWalletMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
handle(action, dispatch)
next(action)
}
}
}
private fun handle(action: Action, dispatch: DispatchFunction) {
val action = action as? TwinCardsAction ?: return
val twinCardsState = store.state.twinCardsState
when (action) {
is TwinCardsAction.SetTwinCard -> {
val showOnboarding = !preferencesStorage.wasTwinsOnboardingShown()
if (showOnboarding) store.dispatch(TwinCardsAction.ShowOnboarding)
}
TwinCardsAction.SetOnboardingShown -> {
preferencesStorage.saveTwinsOnboardingShown()
}
is TwinCardsAction.ProceedToCreateWallet -> {
store.dispatch(NavigationAction.NavigateTo(AppScreen.CreateTwinWallet))
}
is TwinCardsAction.Wallet.HandleOnBackPressed -> {
if (twinCardsState.createWalletState?.showAlert == true) {
val stateDialog = TwinCardsAction.Wallet.InterruptDialog {
store.dispatch(TwinCardsAction.CardsManager.Release)
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
}
store.dispatchDialogShow(stateDialog)
} else {
store.dispatch(TwinCardsAction.CardsManager.Release)
store.dispatch(NavigationAction.PopBackTo())
}
}
is TwinCardsAction.Wallet.Create -> {
val wallet = store.state.walletState.walletManagers.map { it.wallet }.firstOrNull()
if (wallet == null) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.CreateTwinWalletWarning))
return
}
val notEmpty = wallet.recentTransactions.isNotEmpty() || wallet.amounts.toSendableAmounts().isNotEmpty()
if (notEmpty) {
store.dispatchNotification(twinCardsState.resources.strings.walletIsNotEmpty)
} else {
store.dispatch(NavigationAction.NavigateTo(AppScreen.CreateTwinWalletWarning))
}
}
is TwinCardsAction.Wallet.LaunchFirstStep -> {
val scanResponse = store.state.globalState.scanResponse ?: return
val manager = TwinCardsManager(scanResponse, action.reader)
store.dispatch(TwinCardsAction.CardsManager.Set(manager))
scope.launch {
val result = manager.createFirstWallet(action.initialMessage)
withContext(Dispatchers.Main) {
when (result) {
SimpleResult.Success -> {
store.dispatch(TwinCardsAction.Wallet.LaunchFirstStep.Success)
}
is SimpleResult.Failure -> {
}
}
}
}
}
is TwinCardsAction.Wallet.LaunchSecondStep -> {
val manager = twinCardsState.twinCardsManager ?: return
scope.launch {
val result = manager.createSecondWallet(action.initialMessage,
action.preparingMessage, action.creatingWalletMessage)
withContext(Dispatchers.Main) {
when (result) {
SimpleResult.Success -> {
store.dispatch(TwinCardsAction.Wallet.LaunchSecondStep.Success)
}
is SimpleResult.Failure -> {
}
}
}
}
}
is TwinCardsAction.Wallet.LaunchThirdStep -> {
val manager = twinCardsState.twinCardsManager ?: return
scope.launch {
val result = manager.complete(action.message)
withContext(Dispatchers.Main) {
when (result) {
is Result.Success -> {
store.dispatch(TwinCardsAction.Wallet.LaunchThirdStep.Success(result.data))
}
is Result.Failure -> {
}
}
}
}
}
is TwinCardsAction.Wallet.LaunchThirdStep.Success -> {
scope.launch {
store.state.globalState.tapWalletManager.onCardScanned(action.scanResponse)
}
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}
}
}

View file

@ -1,60 +0,0 @@
package com.tangem.tap.features.twins.redux
import com.tangem.tap.common.redux.AppState
import org.rekotlin.Action
class TwinCardsReducer {
companion object {
fun reduce(action: Action, state: AppState): TwinCardsState = internalReduce(action, state)
}
}
private fun internalReduce(action: Action, state: AppState): TwinCardsState {
if (action !is TwinCardsAction) return state.twinCardsState
val state = state.twinCardsState
return when (action) {
is TwinCardsAction.SetResources -> {
state.copy(resources = action.resources)
}
is TwinCardsAction.SetTwinCard -> {
state.copy(
cardNumber = action.number,
secondCardId = action.secondCardId,
showTwinOnboarding = state.showTwinOnboarding,
isCreatingTwinCardsAllowed = action.isCreatingTwinCardsAllowed,
)
}
is TwinCardsAction.CardsManager.Set -> {
state.copy(twinCardsManager = action.manager)
}
TwinCardsAction.CardsManager.Release -> {
state.copy(twinCardsManager = null)
}
is TwinCardsAction.ShowOnboarding -> {
state.copy(showTwinOnboarding = true)
}
is TwinCardsAction.SetOnboardingShown -> {
state.copy(showTwinOnboarding = false)
}
is TwinCardsAction.Wallet.Create -> {
val prevState = state.createWalletState
state.copy(createWalletState = CreateTwinWalletState(
prevState?.scanResponse,
prevState?.number ?: action.number,
prevState?.mode ?: action.createTwinWalletMode,
))
}
TwinCardsAction.Wallet.LaunchFirstStep.Success -> {
state.copy(createWalletState = state.createWalletState?.copy(
step = CreateTwinWalletStep.SecondStep
))
}
TwinCardsAction.Wallet.LaunchSecondStep.Success ->
state.copy(createWalletState = state.createWalletState?.copy(
step = CreateTwinWalletStep.ThirdStep
))
else -> state
}
}

View file

@ -1,43 +0,0 @@
package com.tangem.tap.features.twins.redux
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.domain.twins.TwinCardsManager
import org.rekotlin.StateType
/**
[REDACTED_AUTHOR]
*/
data class TwinCardsState(
val twinCardsManager: TwinCardsManager? = null,
// if cardNumber is not set -> the scanned card is not Twin
val cardNumber: TwinCardNumber? = null,
val secondCardId: String? = null,
val showTwinOnboarding: Boolean = false,
val isCreatingTwinCardsAllowed: Boolean = false,
val createWalletState: CreateTwinWalletState? = null,
val resources: TwinCardsResources = TwinCardsResources(),
) : StateType
data class CreateTwinWalletState(
val scanResponse: ScanResponse?,
val number: TwinCardNumber = TwinCardNumber.First,
val mode: CreateTwinWalletMode = CreateTwinWalletMode.CreateWallet,
val step: CreateTwinWalletStep = CreateTwinWalletStep.FirstStep,
) {
val showAlert: Boolean
get() = step != CreateTwinWalletStep.FirstStep
}
enum class CreateTwinWalletStep { FirstStep, SecondStep, ThirdStep }
enum class CreateTwinWalletMode { CreateWallet, RecreateWallet }
data class TwinCardsResources(
val strings: Strings = Strings()
) {
//TODO: create global structure for StringRes. Set it from the MainActivity.
data class Strings(
val walletIsNotEmpty: Int = -1
)
}

View file

@ -1,162 +0,0 @@
package com.tangem.tap.features.twins.ui
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.core.content.ContextCompat.getColor
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.squareup.picasso.Picasso
import com.tangem.Message
import com.tangem.tap.common.extensions.readAssetAsString
import com.tangem.tap.domain.twins.AssetReader
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.features.twins.redux.*
import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_twin_cards_create.*
import org.rekotlin.StoreSubscriber
class CreateTwinWalletFragment : Fragment(R.layout.fragment_twin_cards_create),
StoreSubscriber<TwinCardsState> {
private val assetReader: AssetReader by lazy {
object : AssetReader {
override fun readAssetAsString(name: String): String = requireContext().readAssetAsString(name)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(TwinCardsAction.Wallet.HandleOnBackPressed)
}
})
initResources()
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
private fun initResources() {
val resources = TwinCardsResources(
TwinCardsResources.Strings(
R.string.details_notification_erase_wallet_not_possible
)
)
store.dispatch(TwinCardsAction.SetResources(resources))
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.twinCardsState == newState.twinCardsState
}.select { it.twinCardsState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
toolbar.setNavigationOnClickListener { store.dispatch(TwinCardsAction.Wallet.HandleOnBackPressed) }
Picasso.get()
.load(Artwork.TWIN_CARD_1)
.placeholder(R.drawable.card_placeholder)
?.error(R.drawable.card_placeholder)
?.into(iv_card_1)
Picasso.get()
.load(Artwork.TWIN_CARD_2)
.placeholder(R.drawable.card_placeholder)
?.error(R.drawable.card_placeholder)
?.into(iv_card_2)
}
override fun newState(state: TwinCardsState) {
if (activity == null) return
toolbar.title = when (state.createWalletState?.mode) {
CreateTwinWalletMode.CreateWallet -> getText(R.string.wallet_button_create_wallet)
CreateTwinWalletMode.RecreateWallet, null -> getText(R.string.details_twins_recreate_toolbar)
}
val selectedColor = getColor(requireContext(), R.color.colorSecondary)
val defaultColor = getColor(requireContext(), R.color.blue_pale)
val twinCardNumber = state.createWalletState?.number ?: TwinCardNumber.First
val cardNumber = when (state.createWalletState?.step) {
CreateTwinWalletStep.FirstStep -> {
val twinCardNumberString = twinCardNumber.number.toString()
tv_step_number.text =
getString(R.string.details_twins_recreate_step_format, "1")
v_step_1.setBackgroundColor(selectedColor)
v_step_2.setBackgroundColor(defaultColor)
v_step_3.setBackgroundColor(defaultColor)
btn_tap.setOnClickListener {
store.dispatch(TwinCardsAction.Wallet.LaunchFirstStep(
Message(getString(
R.string.details_twins_recreate_title_format,
twinCardNumberString
)),
assetReader
))
}
btn_tap.text = getString(R.string.details_twins_recreate_button_format,
twinCardNumber.number.toString())
twinCardNumberString
}
CreateTwinWalletStep.SecondStep -> {
val twinCardNumberString = twinCardNumber.pairNumber().number.toString()
tv_step_number.text =
getString(R.string.details_twins_recreate_step_format, "2")
v_step_1.setBackgroundColor(selectedColor)
v_step_2.setBackgroundColor(selectedColor)
v_step_3.setBackgroundColor(defaultColor)
btn_tap.setOnClickListener {
store.dispatch(TwinCardsAction.Wallet.LaunchSecondStep(
Message(getString(R.string.details_twins_recreate_title_format, twinCardNumberString)),
Message(getString(R.string.details_twins_recreate_title_preparing)),
Message(getString(R.string.details_twins_recreate_title_creating_wallet)),
))
}
btn_tap.text = getString(R.string.details_twins_recreate_button_format, "2")
twinCardNumberString
}
CreateTwinWalletStep.ThirdStep -> {
val twinCardNumberString = twinCardNumber.number.toString()
tv_step_number.text =
getString(R.string.details_twins_recreate_step_format, "3")
v_step_1.setBackgroundColor(selectedColor)
v_step_2.setBackgroundColor(selectedColor)
v_step_3.setBackgroundColor(selectedColor)
btn_tap.setOnClickListener {
store.dispatch(TwinCardsAction.Wallet.LaunchThirdStep(
Message(getString(
R.string.details_twins_recreate_title_format, twinCardNumberString)
)
))
}
btn_tap.text = getString(R.string.details_twins_recreate_button_format, twinCardNumberString)
twinCardNumberString
}
else -> null
}
btn_tap.text = getString(R.string.details_twins_recreate_button_format, cardNumber)
tv_twin_title.text = getString(R.string.details_twins_recreate_title_format, cardNumber)
}
}

View file

@ -1,57 +0,0 @@
package com.tangem.tap.features.twins.ui
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.squareup.picasso.Picasso
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.twins.redux.CreateTwinWalletMode
import com.tangem.tap.features.twins.redux.TwinCardsAction
import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_twin_cards_warning.*
import kotlinx.android.synthetic.main.layout_twin_cards_orange.*
class TwinWalletWarningFragment : Fragment(R.layout.fragment_twin_cards_warning) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
}
})
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val createTwinWallet = store.state.twinCardsState.createWalletState?.mode
if (createTwinWallet == CreateTwinWalletMode.CreateWallet) {
tv_twin_cards_description.text = getText(R.string.details_twins_recreate_subtitle)
} else if (createTwinWallet == CreateTwinWalletMode.RecreateWallet) {
tv_twin_cards_description.text = getText(R.string.details_twins_recreate_warning)
}
btn_cancel.setOnClickListener { store.dispatch(NavigationAction.PopBackTo()) }
btn_start.setOnClickListener { store.dispatch(TwinCardsAction.ProceedToCreateWallet) }
Picasso.get()
.load(Artwork.TWIN_CARD_1)
.placeholder(R.drawable.card_placeholder)
?.error(R.drawable.card_placeholder)
?.into(iv_twin_card_1)
Picasso.get()
.load(Artwork.TWIN_CARD_2)
.placeholder(R.drawable.card_placeholder)
?.error(R.drawable.card_placeholder)
?.into(iv_twin_card_2)
}
}

View file

@ -5,12 +5,13 @@ import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.TransactionStatus
import com.tangem.blockchain.common.Wallet
import com.tangem.tap.common.extensions.toFormattedString
import com.tangem.tap.domain.extensions.toSendableAmounts
data class PendingTransaction(
val address: String?,
val amount: String?,
val currency: String,
val type: PendingTransactionType
val address: String?,
val amount: String?,
val currency: String,
val type: PendingTransactionType
)
enum class PendingTransactionType { Incoming, Outgoing, Unknown }
@ -36,10 +37,10 @@ fun TransactionData.toPendingTransaction(walletAddress: String): PendingTransact
}
return PendingTransaction(
if (address == "unknown") null else address,
this.amount.value?.toFormattedString(amount.decimals),
this.amount.currencySymbol,
type
if (address == "unknown") null else address,
this.amount.value?.toFormattedString(amount.decimals),
this.amount.currencySymbol,
type
)
}
@ -66,4 +67,8 @@ fun Wallet.getPendingTransactions(): List<PendingTransaction> {
fun Wallet.hasPendingTransactions(): Boolean {
return getPendingTransactions().isNotEmpty()
}
fun Wallet.hasSendableAmountsOrPendingTransactions(): Boolean {
return hasPendingTransactions() || amounts.toSendableAmounts().isNotEmpty()
}

View file

@ -12,7 +12,7 @@ import com.tangem.tap.common.toggleWidget.WidgetState
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.extensions.toSendableAmounts
import com.tangem.tap.domain.topup.TradeCryptoHelper
import com.tangem.tap.features.twins.redux.TwinCardsState
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.models.toPendingTransactions
import com.tangem.tap.features.wallet.models.toPendingTransactionsForToken

View file

@ -19,8 +19,6 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.extensions.toSendableAmounts
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.send.redux.PrepareSendScreen
import com.tangem.tap.features.twins.redux.CreateTwinWalletMode
import com.tangem.tap.features.twins.redux.TwinCardsAction
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.network.NetworkStateChanged
import kotlinx.coroutines.launch
@ -96,31 +94,22 @@ class WalletMiddleware {
}
}
is WalletAction.CreateWallet -> {
if (walletState.isTangemTwins) {
store.dispatch(TwinCardsAction.Wallet.Create(
walletState.twinCardsState.cardNumber!!,
CreateTwinWalletMode.CreateWallet
))
} else {
scope.launch {
val result = tangemSdkManager.createWallet(
globalState.scanResponse?.card?.cardId
)
when (result) {
is CompletionResult.Success -> {
val scanNoteResponse = globalState.scanResponse?.copy(card = result.data)
scanNoteResponse?.let {
globalState.tapWalletManager.onCardScanned(scanNoteResponse)
}
}
is CompletionResult.Failure -> {
(result.error as? TangemSdkError)?.let { error ->
FirebaseAnalyticsHandler.logCardSdkError(
error,
FirebaseAnalyticsHandler.ActionToLog.CreateWallet,
card = store.state.detailsState.card
)
}
scope.launch {
val result = tangemSdkManager.createWallet(
globalState.scanResponse?.card?.cardId
)
when (result) {
is CompletionResult.Success -> {
val scanNoteResponse = globalState.scanResponse?.copy(card = result.data)
scanNoteResponse?.let { store.onCardScanned(scanNoteResponse) }
}
is CompletionResult.Failure -> {
(result.error as? TangemSdkError)?.let { error ->
FirebaseAnalyticsHandler.logCardSdkError(
error,
FirebaseAnalyticsHandler.ActionToLog.CreateWallet,
card = store.state.detailsState.card
)
}
}
}

View file

@ -38,13 +38,12 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
is WalletAction.ResetState -> newState = WalletState()
is WalletAction.SetIfTestnetCard -> newState = newState.copy(isTestnet = action.isTestnet)
is WalletAction.EmptyWallet -> {
val creatingWalletAllowed = state.twinCardsState.isCreatingTwinCardsAllowed
newState = newState.copy(
state = ProgressState.Done,
wallets = listOf(
WalletData(
currencyData = BalanceWidgetData(BalanceStatus.EmptyCard),
mainButton = WalletMainButton.CreateWalletButton(creatingWalletAllowed),
mainButton = WalletMainButton.CreateWalletButton(true),
)
)
)

View file

@ -150,8 +150,8 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
private fun setupCardImage(cardImage: Artwork?) {
Picasso.get()
.load(cardImage?.artworkId)
.placeholder(R.drawable.card_placeholder)
?.error(R.drawable.card_placeholder)
.placeholder(R.drawable.card_placeholder_black)
?.error(R.drawable.card_placeholder_black)
?.into(iv_card)
}

View file

@ -7,10 +7,8 @@ import android.widget.Button
import androidx.recyclerview.widget.LinearLayoutManager
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.features.twins.redux.TwinCardsState
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.ui.BalanceWidget
@ -93,7 +91,7 @@ class SingleWalletView : WalletView {
}
private fun setupTwinCards(
twinCardsState: TwinCardsState?, fragment: WalletFragment
twinCardsState: TwinCardsState?, fragment: WalletFragment
) = with(fragment) {
twinCardsState?.cardNumber?.let { cardNumber ->
tv_twin_card_number.show()
@ -107,11 +105,6 @@ class SingleWalletView : WalletView {
if (twinCardsState?.cardNumber == null) {
tv_twin_card_number.hide()
}
if (twinCardsState?.showTwinOnboarding == true) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingTwins))
}
}
private fun setupButtons(