Updated on 2026-08-14

This commit is contained in:
Tangem 2021-10-17 22:40:55 +03:00
parent 1182364325
commit 7e821a934c
34 changed files with 443 additions and 615 deletions

View file

@ -75,7 +75,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
val backStackIsEmpty = supportFragmentManager.backStackEntryCount == 0
val isScannedBefore = store.state.globalState.scanResponse != null
val isOnboardingServiceActive = store.state.globalState.onboardingService != null
val isOnboardingServiceActive = store.state.globalState.onboardingManager != null
if (backStackIsEmpty || (!isOnboardingServiceActive && !isScannedBefore)) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Home))
}

View file

@ -0,0 +1,24 @@
package com.tangem.tap.common.extensions
import androidx.transition.Transition
/**
[REDACTED_AUTHOR]
*/
inline fun Transition.addListener(
crossinline onStart: (animator: Transition) -> Unit = {},
crossinline onEnd: (animator: Transition) -> Unit = {},
crossinline onCancel: (animator: Transition) -> Unit = {},
crossinline onPause: (animator: Transition) -> Unit = {},
crossinline onRepeat: (animator: Transition) -> Unit = {}
): Transition.TransitionListener {
val listener = object : Transition.TransitionListener {
override fun onTransitionStart(transition: Transition) = onStart(transition)
override fun onTransitionEnd(transition: Transition) = onEnd(transition)
override fun onTransitionCancel(transition: Transition) = onCancel(transition)
override fun onTransitionPause(transition: Transition) = onPause(transition)
override fun onTransitionResume(transition: Transition) = onRepeat(transition)
}
addListener(listener)
return listener
}

View file

@ -7,7 +7,11 @@ import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.amountToCreateAccount
import com.tangem.tap.domain.extensions.isNoAccountError
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.domain.topup.TradeCryptoHelper
import com.tangem.tap.features.wallet.redux.AddressData
import com.tangem.tap.features.wallet.redux.reducers.createAddressesData
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.store
/**
[REDACTED_AUTHOR]
@ -29,4 +33,27 @@ suspend fun WalletManager.safeUpdate(): Result<Wallet> = try {
Result.Failure(TapError.WalletManagerUpdate.InternalError(message))
}
}
}
fun WalletManager?.getToUpUrl(): String? {
val wallet = this?.wallet ?: return null
val config = store.state.globalState.configManager?.config ?: return null
val defaultAddress = wallet.address
return TradeCryptoHelper.getUrl(
TradeCryptoHelper.Action.Buy,
wallet.blockchain,
wallet.blockchain.currency,
defaultAddress,
config.moonPayApiKey,
config.moonPayApiSecretKey
)
}
fun WalletManager?.getAddressData(): AddressData? {
val wallet = this?.wallet ?: return null
val addressDataList = wallet.createAddressesData()
return if (addressDataList.isEmpty()) null
else addressDataList[0]
}

View file

@ -4,6 +4,7 @@ import com.tangem.blockchain.common.WalletManager
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError
import com.tangem.tap.common.redux.*
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
@ -12,7 +13,6 @@ import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.features.feedback.EmailData
import com.tangem.tap.features.feedback.FeedbackManager
import com.tangem.tap.features.onboarding.service.ProductOnboardingService
import com.tangem.tap.network.moonpay.MoonPayUserStatus
import org.rekotlin.Action
@ -29,11 +29,11 @@ sealed class GlobalAction : Action {
object HideDialog : GlobalAction()
sealed class Onboarding {
data class Activate(val onboardingService: ProductOnboardingService) : GlobalAction()
object Deactivate : GlobalAction()
data class Start(val scanResponse: ScanResponse, val fromScreen: AppScreen) : GlobalAction()
object Stop : GlobalAction()
}
data class ReadCard(
data class ScanCard(
val onSuccess: ((ScanResponse) -> Unit)? = null,
val onFailure: ((TangemError) -> Unit)? = null,
val messageResId: Int? = null,

View file

@ -87,7 +87,7 @@ private val globalMiddlewareHandler: Middleware<AppState> = { dispatch, appState
}
}
}
is GlobalAction.ReadCard -> {
is GlobalAction.ScanCard -> {
scope.launch {
val result = tangemSdkManager.scanProduct(FirebaseAnalyticsHandler, action.messageResId)
store.dispatch(GlobalAction.ScanFailsCounter.ChooseBehavior(result))

View file

@ -1,6 +1,8 @@
package com.tangem.tap.common.redux.global
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.onboarding.OnboardingManager
import com.tangem.tap.preferencesStorage
import org.rekotlin.Action
fun globalReducer(action: Action, state: AppState): GlobalState {
@ -10,11 +12,13 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
val globalState = state.globalState
return when (action) {
is GlobalAction.Onboarding.Activate -> {
globalState.copy(onboardingService = action.onboardingService)
is GlobalAction.Onboarding.Start -> {
val usedCardsPrefStorage = preferencesStorage.usedCardsPrefStorage
val onboardingState = OnboardingManager(action.scanResponse, action.fromScreen, usedCardsPrefStorage)
globalState.copy(onboardingManager = onboardingState)
}
is GlobalAction.Onboarding.Deactivate -> {
globalState.copy(onboardingService = null)
is GlobalAction.Onboarding.Stop -> {
globalState.copy(onboardingManager = null)
}
is GlobalAction.ScanFailsCounter.Increment -> {
globalState.copy(scanCardFailsCounter = globalState.scanCardFailsCounter + 1)
@ -36,11 +40,11 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
is GlobalAction.SetWarningManager -> globalState.copy(warningManager = action.warningManager)
is GlobalAction.UpdateWalletSignedHashes -> {
val wallet = globalState.scanResponse?.card
?.wallet(action.walletPublicKey)
?.copy(
totalSignedHashes = action.walletSignedHashes,
remainingSignatures = action.remainingSignatures
)
?.wallet(action.walletPublicKey)
?.copy(
totalSignedHashes = action.walletSignedHashes,
remainingSignatures = action.remainingSignatures
)
val card = globalState.scanResponse?.card
wallet?.let { globalState.scanResponse.card.updateWallet(wallet) }

View file

@ -8,13 +8,14 @@ import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.features.feedback.FeedbackManager
import com.tangem.tap.features.onboarding.service.ProductOnboardingService
import com.tangem.tap.features.onboarding.OnboardingManager
import com.tangem.tap.network.coinmarketcap.CoinMarketCapService
import com.tangem.tap.network.moonpay.MoonPayUserStatus
import org.rekotlin.StateType
data class GlobalState(
val scanResponse: ScanResponse? = null,
val onboardingManager: OnboardingManager? = null,
val cardVerifiedOnline: Boolean = false,
val tapWalletManager: TapWalletManager = TapWalletManager(),
val payIdManager: PayIdManager = PayIdManager(),
@ -26,7 +27,6 @@ data class GlobalState(
val scanCardFailsCounter: Int = 0,
val dialog: StateDialog? = null,
val moonPayUserStatus: MoonPayUserStatus? = null,
val onboardingService: ProductOnboardingService? = null,
) : StateType
typealias CryptoCurrencyName = String

View file

@ -17,7 +17,6 @@ import com.tangem.tap.domain.extensions.*
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.domain.tokens.CardCurrencies
import com.tangem.tap.domain.twins.getTwinCardNumber
import com.tangem.tap.domain.twins.getTwinsCardId
import com.tangem.tap.domain.twins.isTangemTwin
import com.tangem.tap.features.tokens.redux.TokensAction
import com.tangem.tap.features.wallet.redux.Currency
@ -91,12 +90,8 @@ class TapWalletManager {
store.dispatch(WalletAction.SetIfTestnetCard(data.card.isTestCard))
store.dispatch(WalletAction.MultiWallet.SetIsMultiwalletAllowed(data.card.isMultiwalletAllowed))
if (data.card.isTangemTwin()) {
val secondCardId = data.card.getTwinsCardId()
val cardNumber = data.card.getTwinCardNumber()
if (secondCardId != null && cardNumber != null) {
store.dispatch(WalletAction.TwinsAction.SetTwinCard(
secondCardId, cardNumber, isCreatingTwinCardsAllowed = true
))
data.card.getTwinCardNumber()?.let {
store.dispatch(WalletAction.TwinsAction.SetTwinCard(null, it, true))
}
}

View file

@ -3,7 +3,11 @@ package com.tangem.tap.domain.extensions
import com.tangem.common.card.Card
import com.tangem.common.card.CardWallet
import com.tangem.common.extensions.toHexString
import com.tangem.common.services.Result
import com.tangem.operations.attestation.CardVerifyAndGetInfo
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.domain.twins.getTwinCardNumber
import com.tangem.tap.features.wallet.redux.Artwork
fun Card.getSingleWallet(): CardWallet? {
@ -24,6 +28,35 @@ fun Card.signedHashesCount(): Int {
return wallets.map { it.totalSignedHashes ?: 0 }.sum()
}
suspend fun Card.getOrLoadCardArtworkUrl(cardInfo: Result<CardVerifyAndGetInfo.Response.Item>? = null): String {
fun ifAnyError(): String {
return when {
cardId.startsWith(Artwork.SERGIO_CARD_ID) -> Artwork.SERGIO_CARD_URL
cardId.startsWith(Artwork.MARTA_CARD_ID) -> Artwork.MARTA_CARD_URL
else -> {
when (getTwinCardNumber()) {
TwinCardNumber.First -> Artwork.TWIN_CARD_1
TwinCardNumber.Second -> Artwork.TWIN_CARD_2
else -> Artwork.DEFAULT_IMG_URL
}
}
}
}
val cardInfoResult = cardInfo ?: OnlineCardVerifier().getCardInfo(cardId, cardPublicKey)
return when (cardInfoResult) {
is Result.Success -> {
val artworkId = cardInfoResult.data.artwork?.id
if (artworkId == null || artworkId.isEmpty()) {
ifAnyError()
} else {
OnlineCardVerifier.getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId)
}
}
is Result.Failure -> ifAnyError()
}
}
fun Card.getArtworkUrl(artworkId: String?): String? {
return when {
artworkId != null -> {

View file

@ -21,7 +21,8 @@ import com.tangem.tap.tangemSdkManager
class TwinCardsManager(private val scanResponse: ScanResponse, context: Context) {
private val currentCardId: String = scanResponse.card.cardId
private val secondCardId: String? = TwinsHelper.getTwinsCardId(currentCardId)
// private val secondCardId: String? = TwinsHelper.getTwinsCardId(currentCardId)
private val secondCardId: String? = null
private var currentCardPublicKey: String? = null
private var secondCardPublicKey: String? = null

View file

@ -1,7 +1,6 @@
package com.tangem.tap.domain.twins
import com.tangem.common.card.Card
import com.tangem.tap.common.extensions.isEven
class TwinsHelper {
companion object {
@ -21,22 +20,6 @@ class TwinsHelper {
}
}
fun getTwinsCardId(cardId: String): String? {
val cardIdWithNewSeries = when (getTwinCardNumber(cardId) ?: return null) {
TwinCardNumber.First -> {
val index = if (cardId.startsWith(firstCardSeries[0])) 0 else 1
cardId.replace(firstCardSeries[index], secondCardSeries[index])
}
TwinCardNumber.Second -> {
val index = if (cardId.startsWith(secondCardSeries[0])) 0 else 1
cardId.replace(secondCardSeries[index], firstCardSeries[index])
}
}
val cardIdWithoutChecksum = cardIdWithNewSeries.dropLast(1)
val checkSum = cardIdWithoutChecksum.calculateLuhn()
return cardIdWithoutChecksum + checkSum
}
fun getTwinCardIdForUser(cardId: String): String {
if (cardId.length < 16) return cardId
@ -50,21 +33,6 @@ class TwinsHelper {
}
}
private fun String.calculateLuhn(): Int {
val checksum = this.reversed()
.mapIndexed { index, c ->
val digit = if (c in '0'..'9') c - '0' else c - 'A'
if (!index.isEven()) {
digit
} else {
val newDigit = digit * 2
if (newDigit >= 10) newDigit - 9 else newDigit
}
}.sum()
.rem(10)
return (10 - checksum) % 10
}
enum class TwinCardNumber(val number: Int) {
First(1), Second(2);
@ -82,10 +50,6 @@ fun Card.getTwinCardNumber(): TwinCardNumber? {
return TwinsHelper.getTwinCardNumber(this.cardId)
}
fun Card.getTwinsCardId(): String? {
return TwinsHelper.getTwinsCardId(this.cardId)
}
fun Card.getTwinCardIdForUser(): String {
return TwinsHelper.getTwinCardIdForUser(this.cardId)
}

View file

@ -1,24 +1,19 @@
package com.tangem.tap.features.home.redux
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.common.extensions.dispatchErrorNotification
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.extensions.withMainContext
import com.tangem.tap.common.post
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.FragmentShareTransition
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.domain.TapWalletManager
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.features.onboarding.service.OnboardingHelper
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.send.redux.states.ButtonState
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.Middleware
@ -52,55 +47,30 @@ private fun handleReadCard() {
if (!preferencesStorage.wasDisclaimerAccepted()) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer))
} else {
fun changeButtonState(state: ButtonState) {
val btnState = IndeterminateProgressButton(state)
store.dispatch(HomeAction.ChangeScanCardButtonState(btnState))
}
fun proceedToWalletScreen(tapWalletManager: TapWalletManager, scanResponse: ScanResponse) {
scope.launch {
tapWalletManager.onCardScanned(scanResponse)
delay(DELAY_SDK_DIALOG_CLOSE)
withMainContext { changeButtonState(ButtonState.ENABLED) }
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}
}
changeButtonState(ButtonState.PROGRESS)
store.dispatch(GlobalAction.ReadCard({ scanResponse ->
val tapWalletManager = store.state.globalState.tapWalletManager
if (!OnboardingHelper.isOnboardingCase(scanResponse)) {
proceedToWalletScreen(tapWalletManager, scanResponse)
return@ReadCard
store.dispatch(GlobalAction.ScanCard({ scanResponse ->
if (OnboardingHelper.isOnboardingCase(scanResponse)) {
val navigateTo = OnboardingHelper.whereToNavigate(scanResponse)
store.dispatch(GlobalAction.Onboarding.Start(scanResponse, AppScreen.Home))
navigateTo(navigateTo, store.state.homeState.shareTransition)
} else {
scope.launch { store.state.globalState.tapWalletManager.onCardScanned(scanResponse) }
navigateTo(AppScreen.Wallet, null)
}
val service = OnboardingHelper.createService(
AppScreen.Home,
scanResponse,
tapWalletManager.walletManagerFactory
)
if (service == null) {
proceedToWalletScreen(tapWalletManager, scanResponse)
return@ReadCard
}
service.onInitializationProgress = {
when (it) {
ProgressState.Loading -> changeButtonState(ButtonState.PROGRESS)
ProgressState.Done -> changeButtonState(ButtonState.ENABLED)
ProgressState.Error -> changeButtonState(ButtonState.ENABLED)
}
}
service.onInitialized = {
store.dispatch(GlobalAction.Onboarding.Activate(service))
store.dispatch(NavigationAction.NavigateTo(it, store.state.homeState.shareTransition))
}
service.onError = {
store.dispatchErrorNotification(it)
}
post(DELAY_SDK_DIALOG_CLOSE) { service.initializeOnboarding() }
}, {
changeButtonState(ButtonState.ENABLED)
}))
}
}
private fun changeButtonState(state: ButtonState) {
val btnState = IndeterminateProgressButton(state)
store.dispatch(HomeAction.ChangeScanCardButtonState(btnState))
}
private fun navigateTo(screen: AppScreen, transition: FragmentShareTransition?) {
post(DELAY_SDK_DIALOG_CLOSE) {
changeButtonState(ButtonState.ENABLED)
store.dispatch(NavigationAction.NavigateTo(screen, transition))
}
}

View file

@ -0,0 +1,27 @@
package com.tangem.tap.features.onboarding
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.domain.ProductType
import com.tangem.tap.domain.extensions.hasWallets
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.preferencesStorage
/**
[REDACTED_AUTHOR]
*/
class OnboardingHelper {
companion object {
fun isOnboardingCase(response: ScanResponse): Boolean = if (response.card.hasWallets())
preferencesStorage.usedCardsPrefStorage.activationIsStarted(response.card.cardId) else true
fun whereToNavigate(scanResponse: ScanResponse): AppScreen {
return when (scanResponse.productType) {
ProductType.Note -> AppScreen.OnboardingNote
ProductType.Wallet -> AppScreen.OnboardingWallet
ProductType.Twin -> AppScreen.OnboardingTwins
ProductType.Other -> AppScreen.OnboardingOther
}
}
}
}

View file

@ -0,0 +1,129 @@
package com.tangem.tap.features.onboarding
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.isZero
import com.tangem.common.services.Result
import com.tangem.operations.attestation.CardVerifyAndGetInfo
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.common.extensions.isPositive
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.UrlBitmapLoader
import com.tangem.tap.domain.extensions.getOrLoadCardArtworkUrl
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.persistence.UsedCardsPrefStorage
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.math.BigDecimal
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
/**
[REDACTED_AUTHOR]
*/
class OnboardingManager(
var scanResponse: ScanResponse,
val fromScreen: AppScreen,
val usedCardsPrefStorage: UsedCardsPrefStorage,
) {
var cardInfo: Result<CardVerifyAndGetInfo.Response.Item>? = null
private set
suspend fun loadArtwork(): Artwork {
cardInfo = OnlineCardVerifier().getCardInfo(scanResponse.card.cardId, scanResponse.card.cardPublicKey)
return loadArtwork(scanResponse.card.getOrLoadCardArtworkUrl(cardInfo))
}
suspend fun loadArtwork(url: String): Artwork {
return withContext(Dispatchers.Main) {
suspendCoroutine { continuation ->
UrlBitmapLoader().loadBitmap(url) {
val result = when (it) {
is Result.Success -> Artwork(url, it.data)
is Result.Failure -> Artwork(url, null)
}
continuation.resume(result)
}
}
}
}
suspend fun updateBalance(walletManager: WalletManager): OnboardingWalletBalance {
return when (val result = walletManager.safeUpdate()) {
is Result.Success -> {
val wallet = walletManager.wallet
val valueOfAmount = wallet.amounts[AmountType.Coin]?.value
if (valueOfAmount == null) {
val customError = TapError.CustomError("Amount is NULL")
OnboardingWalletBalance.criticalError(customError)
} else {
val currency = Currency.Blockchain(walletManager.wallet.blockchain)
val balance = if (valueOfAmount.isZero()) {
OnboardingWalletBalance.done(valueOfAmount, wallet.recentTransactions.isNotEmpty(), currency)
} else {
OnboardingWalletBalance.done(valueOfAmount, false, currency)
}
balance
}
}
is Result.Failure -> {
val error = (result.error as? TapError) ?: TapError.UnknownError
when (error) {
is TapError.WalletManagerUpdate.NoAccountError -> OnboardingWalletBalance.error(error)
// NoInternetConnection, WalletManagerUpdate.InternalError
else -> OnboardingWalletBalance.criticalError(error)
}
}
}
}
fun activationStarted(cardId: String) {
usedCardsPrefStorage.activationStarted(cardId)
}
fun activationFinished(cardId: String) {
usedCardsPrefStorage.activationFinished(cardId)
}
}
data class OnboardingWalletBalance(
val value: BigDecimal = BigDecimal.ZERO,
val currency: Currency.Blockchain = Currency.Blockchain(Blockchain.Unknown),
val hasIncomingTransaction: Boolean = false,
val state: ProgressState,
val error: TapError? = null,
val criticalError: TapError? = null,
) {
fun balanceIsToppedUp(): Boolean = value.isPositive() || hasIncomingTransaction
val amountToCreateAccount: String?
get() = if (error is TapError.WalletManagerUpdate.NoAccountError) error.customMessage else null
companion object {
fun error(error: TapError): OnboardingWalletBalance = OnboardingWalletBalance(
state = ProgressState.Error,
error = error
)
fun criticalError(error: TapError): OnboardingWalletBalance = OnboardingWalletBalance(
state = ProgressState.Error,
criticalError = error
)
fun loading(value: BigDecimal = BigDecimal.ZERO): OnboardingWalletBalance = OnboardingWalletBalance(
value,
state = ProgressState.Loading
)
fun done(value: BigDecimal, hasTransactions: Boolean, currency: Currency.Blockchain): OnboardingWalletBalance =
OnboardingWalletBalance(value, currency, hasTransactions, ProgressState.Done)
}
}

View file

@ -24,7 +24,7 @@ abstract class BaseOnboardingFragment<T> : BaseStoreFragment(R.layout.fragment_o
protected var fromScreen: AppScreen? = null
override fun onCreate(savedInstanceState: Bundle?) {
fromScreen = store.state.globalState.onboardingService?.fromScreen
fromScreen = store.state.globalState.onboardingManager?.fromScreen
super.onCreate(savedInstanceState)
}

View file

@ -1,13 +1,18 @@
package com.tangem.tap.features.onboarding.products.note
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.os.Bundle
import android.view.View
import android.view.animation.OvershootInterpolator
import android.widget.ImageView
import androidx.annotation.LayoutRes
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.isVisible
import androidx.transition.TransitionManager
import com.tangem.blockchain.common.Blockchain
import com.tangem.tangem_sdk_new.extensions.fadeIn
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
@ -28,6 +33,7 @@ import kotlinx.android.synthetic.main.view_onboarding_tv_balance.*
class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
private lateinit var btnRefreshBalanceWidget: RefreshBalanceWidget
private var currentStep: OnboardingNoteStep = OnboardingNoteStep.None
override fun getOnboardingTopContainerId(): Int = R.layout.layout_onboarding_note
@ -36,11 +42,12 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
btnRefreshBalanceWidget = RefreshBalanceWidget(onboarding_main_container)
val resources = AndroidResources(
OnboardingStringResources(
R.string.copy_toast_msg
)
OnboardingStringResources(
R.string.copy_toast_msg
)
)
store.dispatch(OnboardingNoteAction.LoadCardArtwork)
store.dispatch(OnboardingNoteAction.SetResources(resources))
store.dispatch(OnboardingNoteAction.DetermineStepOfScreen)
}
@ -57,7 +64,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
override fun newState(state: OnboardingNoteState) {
if (activity == null) return
state.artworkBitmap?.let { imv_front_card.setImageBitmap(it) }
state.cardArtwork?.let { imv_front_card.swapToBitmapDrawable(it.artwork) }
pb_state.max = state.steps.size - 1
pb_state.progress = state.progress
@ -69,13 +76,14 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
}
setBalance(state)
showConfetti(state.showConfetti)
currentStep = state.currentStep
}
private fun setBalance(state: OnboardingNoteState) {
if (state.balanceCurrency.blockchain == Blockchain.Unknown) return
if (state.walletBalance.currency.blockchain == Blockchain.Unknown) return
val balanceValue = state.balanceValue.stripZeroPlainString()
val currency = state.balanceCurrency.currencySymbol
val balanceValue = state.walletBalance.value.stripZeroPlainString()
val currency = state.walletBalance.currency.currencySymbol
tv_balance_value.text = "$balanceValue $currency"
}
@ -104,7 +112,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
tv_body.setText(R.string.onboarding_create_wallet_body)
imv_card_background.setBackgroundDrawable(requireContext().getDrawableCompat(R.drawable.shape_circle))
updateConstraints(R.layout.lp_onboarding_create_wallet)
updateConstraints(state.currentStep, R.layout.lp_onboarding_create_wallet)
}
private fun setupTopUpWalletState(state: OnboardingNoteState) {
@ -121,7 +129,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
tv_header.setText(R.string.onboarding_top_up_header)
tv_body.setText(R.string.onboarding_top_up_body)
btnRefreshBalanceWidget.changeState(state.balanceState)
btnRefreshBalanceWidget.changeState(state.walletBalance.state)
if (btnRefreshBalanceWidget.isShowing != true) {
postUi(300) {
btnRefreshBalanceWidget.mainView.setOnClickListener {
@ -130,7 +138,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
}
}
imv_card_background.setBackgroundDrawable(requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8))
updateConstraints(R.layout.lp_onboarding_topup_wallet)
updateConstraints(state.currentStep, R.layout.lp_onboarding_topup_wallet)
}
private fun setupDoneState(state: OnboardingNoteState) {
@ -149,10 +157,12 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
tv_body.setText(R.string.onboarding_done_body)
imv_card_background.setBackgroundDrawable(requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8))
updateConstraints(R.layout.lp_onboarding_done_activation)
updateConstraints(state.currentStep, R.layout.lp_onboarding_done_activation)
}
private fun updateConstraints(@LayoutRes layoutId: Int) {
private fun updateConstraints(currentStep: OnboardingNoteStep, @LayoutRes layoutId: Int) {
if (this.currentStep == currentStep) return
val constraintSet = ConstraintSet()
constraintSet.clone(requireContext(), layoutId)
constraintSet.applyTo(onboarding_main_container)
@ -160,4 +170,13 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
transition.interpolator = OvershootInterpolator()
TransitionManager.beginDelayedTransition(onboarding_main_container, transition)
}
}
fun ImageView.swapToBitmapDrawable(bitmap: Bitmap?, duration: Long = 200) {
if (drawable is BitmapDrawable && (drawable as BitmapDrawable).bitmap == bitmap) return
fadeOut(duration) {
setImageBitmap(bitmap)
fadeIn(duration)
}
}

View file

@ -1,6 +1,8 @@
package com.tangem.tap.features.onboarding.products.note.redux
import com.tangem.tap.features.onboarding.service.OnboardingWalletBalance
import com.tangem.blockchain.common.WalletManager
import com.tangem.tap.features.onboarding.OnboardingWalletBalance
import com.tangem.tap.features.wallet.redux.Artwork
import org.rekotlin.Action
/**
@ -9,11 +11,14 @@ import org.rekotlin.Action
sealed class OnboardingNoteAction : Action {
// from user, ui
data class SetResources(val resources: AndroidResources) : OnboardingNoteAction()
object LoadCardArtwork : OnboardingNoteAction()
object CreateWallet : OnboardingNoteAction()
object TopUp : OnboardingNoteAction()
object ShowAddressInfoDialog : OnboardingNoteAction()
// from redux
class SetArtworkUrl(val artwork: Artwork) : OnboardingNoteAction()
data class SetWalletManager(val walletManager: WalletManager) : OnboardingNoteAction()
object DetermineStepOfScreen : OnboardingNoteAction()
object Done : OnboardingNoteAction()

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.onboarding.products.note.redux
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
@ -8,13 +9,16 @@ 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.features.onboarding.service.balanceIsToppedUp
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.ProgressState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
import org.rekotlin.Middleware
class OnboardingNoteMiddleware {
@ -26,44 +30,57 @@ class OnboardingNoteMiddleware {
private val onboardingNoteMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
handleNoteAction(action)
handleNoteAction(action, dispatch)
next(action)
}
}
}
private fun handleNoteAction(action: Action) {
private fun handleNoteAction(action: Action, dispatch: DispatchFunction) {
if (action !is OnboardingNoteAction) return
val service = store.state.onboardingNoteState.onboardingService ?: return
val globalState = store.state.globalState
val onboardingManager = globalState.onboardingManager ?: return
val scanResponse = onboardingManager.scanResponse
val card = onboardingManager.scanResponse.card
val noteState = store.state.onboardingNoteState
when (action) {
is OnboardingNoteAction.LoadCardArtwork -> {
scope.launch {
val artwork = onboardingManager.loadArtwork()
withMainContext { store.dispatch(OnboardingNoteAction.SetArtworkUrl(artwork)) }
}
}
is OnboardingNoteAction.DetermineStepOfScreen -> {
val step = when {
service.walletManager == null -> OnboardingNoteStep.CreateWallet
service.balanceIsToppedUp() -> OnboardingNoteStep.Done
!card.hasWallets() -> OnboardingNoteStep.CreateWallet
noteState.walletBalance.balanceIsToppedUp() -> OnboardingNoteStep.Done
else -> OnboardingNoteStep.TopUpWallet
}
store.dispatch((OnboardingNoteAction.SetStepOfScreen(step)))
}
is OnboardingNoteAction.SetStepOfScreen -> {
if (action.step == OnboardingNoteStep.Done) {
service.activationFinished()
postUi(500) { store.dispatch(OnboardingNoteAction.Confetti.Show) }
when (action.step) {
OnboardingNoteStep.TopUpWallet -> {
store.dispatch(OnboardingNoteAction.Balance.Update)
}
OnboardingNoteStep.Done -> {
onboardingManager.activationFinished(card.cardId)
postUi(500) { store.dispatch(OnboardingNoteAction.Confetti.Show) }
}
}
}
is OnboardingNoteAction.CreateWallet -> {
scope.launch {
val result = tangemSdkManager.createProductWallet(service.scanResponse)
val result = tangemSdkManager.createProductWallet(scanResponse)
withMainContext {
when (result) {
is CompletionResult.Success -> {
val updatedResponse = service.scanResponse.copy(card = result.data)
service.scanResponse = updatedResponse
service.activationStarted()
val updatedResponse = scanResponse.copy(card = result.data)
onboardingManager.scanResponse = updatedResponse
onboardingManager.activationStarted(updatedResponse.card.cardId)
store.dispatch(OnboardingNoteAction.SetStepOfScreen(OnboardingNoteStep.TopUpWallet))
store.dispatch(OnboardingNoteAction.Balance.Update)
}
is CompletionResult.Failure -> {
// do nothing
@ -73,44 +90,57 @@ private fun handleNoteAction(action: Action) {
}
}
is OnboardingNoteAction.Balance.Update -> {
val loadingBalance = service.getBalance().copy(
state = ProgressState.Loading,
error = null,
criticalError = null
val walletManager = if (noteState.walletManager != null) {
noteState.walletManager
} else {
val wmFactory = globalState.tapWalletManager.walletManagerFactory
val walletManager = wmFactory.makePrimaryWalletManager(scanResponse).guard {
val message = "Loading cancelled. Cause: wallet manager didn't created"
val customError = TapError.CustomError(message)
store.dispatchErrorNotification(customError)
return
}
dispatch(OnboardingNoteAction.SetWalletManager(walletManager))
walletManager
}
val balanceIsLoading = noteState.walletBalance.copy(
state = ProgressState.Loading,
error = null,
criticalError = null
)
store.dispatch(OnboardingNoteAction.Balance.Set(loadingBalance))
store.dispatch(OnboardingNoteAction.Balance.Set(balanceIsLoading))
scope.launch {
val loadedBalance = service.updateBalance()
if (loadedBalance.criticalError != null) {
store.dispatchErrorNotification(loadedBalance.criticalError)
}
val loadedBalance = onboardingManager.updateBalance(walletManager)
loadedBalance.criticalError?.let { store.dispatchErrorNotification(it) }
withMainContext { store.dispatch(OnboardingNoteAction.Balance.Set(loadedBalance)) }
}
}
is OnboardingNoteAction.Balance.Set -> {
if (service.balanceIsToppedUp()) {
if (action.balance.balanceIsToppedUp()) {
store.dispatch(OnboardingNoteAction.SetStepOfScreen(OnboardingNoteStep.Done))
}
}
is OnboardingNoteAction.ShowAddressInfoDialog -> {
val addressData = noteState.onboardingService?.getAddressData() ?: return
val addressData = noteState.walletManager?.getAddressData() ?: return
val addressWasCopied = noteState.resources.strings.addressWasCopied
val appDialog = AppDialog.AddressInfoDialog(
addressData,
onCopyAddress = { store.dispatchToastNotification(addressWasCopied) },
onExploreAddress = { store.dispatchOpenUrl(addressData.exploreUrl) }
addressData,
onCopyAddress = { store.dispatchToastNotification(addressWasCopied) },
onExploreAddress = { store.dispatchOpenUrl(addressData.exploreUrl) }
)
store.dispatchDialogShow(appDialog)
}
is OnboardingNoteAction.TopUp -> {
val topUpUrl = noteState.onboardingService?.getToUpUrl() ?: return
val topUpUrl = noteState.walletManager?.getToUpUrl() ?: return
store.dispatchOpenUrl(topUpUrl)
}
OnboardingNoteAction.Done -> {
store.dispatch(GlobalAction.Onboarding.Deactivate)
store.dispatch(GlobalAction.Onboarding.Stop)
scope.launch {
store.state.globalState.tapWalletManager.onCardScanned(service.scanResponse)
globalState.tapWalletManager.onCardScanned(scanResponse)
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}
}

View file

@ -2,7 +2,6 @@ package com.tangem.tap.features.onboarding.products.note.redux
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.onboarding.service.OnboardingNoteService
import org.rekotlin.Action
class OnboardingNoteReducer {
@ -15,34 +14,20 @@ private fun internalReduce(action: Action, appState: AppState): OnboardingNoteSt
var state = appState.onboardingNoteState
when (action) {
is GlobalAction.Onboarding.Activate -> {
if (action.onboardingService is OnboardingNoteService) {
val service = action.onboardingService
val balance = service.getBalance()
state = state.copy(
onboardingService = service,
artworkBitmap = service.getArtwork().value?.artwork,
balanceValue = balance.value,
balanceCurrency = balance.currency,
balanceState = balance.state,
amountToCreateAccount = balance.amountToCreateAccount,
showConfetti = false
)
}
}
GlobalAction.Onboarding.Deactivate -> {
is GlobalAction.Onboarding.Start -> {
state = OnboardingNoteState()
}
is OnboardingNoteAction.SetArtworkUrl -> {
state = state.copy(cardArtwork = action.artwork)
}
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(
balanceValue = action.balance.value,
balanceCurrency = action.balance.currency,
balanceState = action.balance.state,
amountToCreateAccount = action.balance.amountToCreateAccount,
)
state = state.copy(walletBalance = action.balance)
}
is OnboardingNoteAction.SetStepOfScreen -> {
if (action.step != state.currentStep && state.steps.contains(action.step)) {

View file

@ -1,28 +1,22 @@
package com.tangem.tap.features.onboarding.products.note.redux
import android.graphics.Bitmap
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.features.onboarding.service.OnboardingNoteService
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.blockchain.common.WalletManager
import com.tangem.tap.features.onboarding.OnboardingWalletBalance
import com.tangem.tap.features.wallet.redux.Artwork
import org.rekotlin.StateType
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
data class OnboardingNoteState(
val onboardingService: OnboardingNoteService? = null,
val walletManager: WalletManager? = null,
val resources: AndroidResources = AndroidResources(),
// UI
val showConfetti: Boolean = false,
val artworkBitmap: Bitmap? = null,
val balanceValue: BigDecimal = BigDecimal.ZERO,
val balanceCurrency: Currency = Currency.Blockchain(Blockchain.Unknown),
val balanceState: ProgressState = ProgressState.Done,
val amountToCreateAccount: String? = null,
// UI
val cardArtwork: Artwork? = null,
val walletBalance: OnboardingWalletBalance = OnboardingWalletBalance.loading(),
val currentStep: OnboardingNoteStep = OnboardingNoteStep.None,
val steps: List<OnboardingNoteStep> = OnboardingNoteStep.values().toList(),
val showConfetti: Boolean = false,
) : StateType {
val progress: Int

View file

@ -10,6 +10,7 @@ import androidx.transition.TransitionManager
import com.tangem.tap.common.extensions.getDrawableCompat
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
@ -29,6 +30,7 @@ class OnboardingOtherCardsFragment : BaseOnboardingFragment<OnboardingOtherCards
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
store.dispatch(OnboardingOtherCardsAction.LoadCardArtwork)
store.dispatch(OnboardingOtherCardsAction.DetermineStepOfScreen)
}
@ -45,7 +47,7 @@ class OnboardingOtherCardsFragment : BaseOnboardingFragment<OnboardingOtherCards
if (activity == null) return
if (state.currentStep == OnboardingOtherCardsStep.None) return
state.artworkBitmap?.let { imv_front_card.setImageBitmap(it) }
state.cardArtwork?.let { imv_front_card.swapToBitmapDrawable(it.artwork) }
pb_state.max = state.steps.size - 1
pb_state.progress = state.progress

View file

@ -1,5 +1,6 @@
package com.tangem.tap.features.onboarding.products.otherCards.redux
import com.tangem.tap.features.wallet.redux.Artwork
import org.rekotlin.Action
/**
@ -7,9 +8,11 @@ import org.rekotlin.Action
*/
sealed class OnboardingOtherCardsAction : Action {
// from user, ui
object LoadCardArtwork : OnboardingOtherCardsAction()
object CreateWallet : OnboardingOtherCardsAction()
// from redux
class SetArtworkUrl(val artwork: Artwork) : OnboardingOtherCardsAction()
object DetermineStepOfScreen : OnboardingOtherCardsAction()
object Done : OnboardingOtherCardsAction()

View file

@ -8,12 +8,14 @@ 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.extensions.hasWallets
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
import org.rekotlin.Middleware
class OnboardingOtherCardsMiddleware {
@ -25,40 +27,48 @@ class OnboardingOtherCardsMiddleware {
private val onboardingOtherCardsMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
handleOtherCardsAction(action)
handleOtherCardsAction(action, dispatch)
next(action)
}
}
}
private fun handleOtherCardsAction(action: Action) {
private fun handleOtherCardsAction(action: Action, dispatch: DispatchFunction) {
if (action !is OnboardingOtherCardsAction) return
val service = store.state.onboardingOtherCardsState.onboardingService ?: return
val noteState = store.state.onboardingOtherCardsState
val globalState = store.state.globalState
val onboardingManager = globalState.onboardingManager ?: return
val card = onboardingManager.scanResponse.card
when (action) {
is OnboardingOtherCardsAction.LoadCardArtwork -> {
scope.launch {
val artwork = onboardingManager.loadArtwork()
withMainContext { store.dispatch(OnboardingOtherCardsAction.SetArtworkUrl(artwork)) }
}
}
is OnboardingOtherCardsAction.DetermineStepOfScreen -> {
val step = when {
service.walletManager == null -> OnboardingOtherCardsStep.CreateWallet
!card.hasWallets() -> OnboardingOtherCardsStep.CreateWallet
else -> OnboardingOtherCardsStep.Done
}
store.dispatch((OnboardingOtherCardsAction.SetStepOfScreen(step)))
}
is OnboardingOtherCardsAction.SetStepOfScreen -> {
if (action.step == OnboardingOtherCardsStep.Done) {
service.activationFinished()
onboardingManager.activationFinished(card.cardId)
postUi(500) { store.dispatch(OnboardingOtherCardsAction.Confetti.Show) }
}
}
is OnboardingOtherCardsAction.CreateWallet -> {
scope.launch {
val result = tangemSdkManager.createProductWallet(service.scanResponse)
val result = tangemSdkManager.createProductWallet(onboardingManager.scanResponse)
withMainContext {
when (result) {
is CompletionResult.Success -> {
val updatedResponse = service.scanResponse.copy(card = result.data)
service.scanResponse = updatedResponse
service.activationStarted()
val updatedResponse = onboardingManager.scanResponse.copy(card = result.data)
onboardingManager.scanResponse = updatedResponse
onboardingManager.activationStarted(updatedResponse.card.cardId)
delay(DELAY_SDK_DIALOG_CLOSE)
store.dispatch(OnboardingOtherCardsAction.SetStepOfScreen(OnboardingOtherCardsStep.Done))
@ -71,9 +81,9 @@ private fun handleOtherCardsAction(action: Action) {
}
}
OnboardingOtherCardsAction.Done -> {
store.dispatch(GlobalAction.Onboarding.Deactivate)
store.dispatch(GlobalAction.Onboarding.Stop)
scope.launch {
store.state.globalState.tapWalletManager.onCardScanned(service.scanResponse)
globalState.tapWalletManager.onCardScanned(onboardingManager.scanResponse)
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}
}

View file

@ -2,7 +2,6 @@ package com.tangem.tap.features.onboarding.products.otherCards.redux
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.onboarding.service.OnboardingOtherCardsService
import org.rekotlin.Action
class OnboardingOtherCardsReducer {
@ -15,19 +14,12 @@ private fun internalReduce(action: Action, appState: AppState): OnboardingOtherC
var state = appState.onboardingOtherCardsState
when (action) {
is GlobalAction.Onboarding.Activate -> {
if (action.onboardingService is OnboardingOtherCardsService) {
val service = action.onboardingService
state = state.copy(
onboardingService = service,
artworkBitmap = service.getArtwork().value?.artwork,
showConfetti = false
)
}
}
GlobalAction.Onboarding.Deactivate -> {
is GlobalAction.Onboarding.Start -> {
state = OnboardingOtherCardsState()
}
is OnboardingOtherCardsAction.SetArtworkUrl -> {
state = state.copy(cardArtwork = action.artwork)
}
is OnboardingOtherCardsAction.SetStepOfScreen -> {
if (action.step != state.currentStep && state.steps.contains(action.step)) {
state = state.copy(currentStep = action.step)

View file

@ -1,17 +1,15 @@
package com.tangem.tap.features.onboarding.products.otherCards.redux
import android.graphics.Bitmap
import com.tangem.tap.features.onboarding.service.OnboardingOtherCardsService
import com.tangem.tap.features.wallet.redux.Artwork
import org.rekotlin.StateType
/**
[REDACTED_AUTHOR]
*/
data class OnboardingOtherCardsState(
val onboardingService: OnboardingOtherCardsService? = null,
// UI
// UI
val cardArtwork: Artwork? = null,
val showConfetti: Boolean = false,
val artworkBitmap: Bitmap? = null,
val currentStep: OnboardingOtherCardsStep = OnboardingOtherCardsStep.None,
val steps: List<OnboardingOtherCardsStep> = OnboardingOtherCardsStep.values().toList(),
) : StateType {

View file

@ -1,33 +0,0 @@
package com.tangem.tap.features.onboarding.service
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.domain.ProductType
import com.tangem.tap.domain.extensions.hasWallets
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.preferencesStorage
/**
[REDACTED_AUTHOR]
*/
class OnboardingHelper {
companion object {
fun isOnboardingCase(response: ScanResponse): Boolean = if (response.card.hasWallets())
preferencesStorage.usedCardsPrefStorage.activationIsStarted(response.card.cardId) else true
fun createService(
fromScreen: AppScreen,
scanResponse: ScanResponse,
walletManagerFactory: WalletManagerFactory,
): ProductOnboardingService? {
val cardInfoStorage = preferencesStorage.usedCardsPrefStorage
return when(scanResponse.productType) {
ProductType.Note -> OnboardingNoteService(fromScreen, scanResponse, cardInfoStorage, walletManagerFactory)
ProductType.Wallet -> null
ProductType.Twin -> null
ProductType.Other -> OnboardingOtherCardsService(fromScreen, scanResponse, cardInfoStorage, walletManagerFactory)
}
}
}
}

View file

@ -1,21 +0,0 @@
package com.tangem.tap.features.onboarding.service
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.persistence.UsedCardsPrefStorage
/**
[REDACTED_AUTHOR]
*/
class OnboardingNoteService(
override val fromScreen: AppScreen,
override var scanResponse: ScanResponse,
cardInfoStorage: UsedCardsPrefStorage,
walletManagerFactory: WalletManagerFactory,
) : ProductOnboardingService(cardInfoStorage, walletManagerFactory) {
override fun navigateToScreen(): AppScreen {
return AppScreen.OnboardingNote
}
}

View file

@ -1,39 +0,0 @@
package com.tangem.tap.features.onboarding.service
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.tap.common.extensions.withMainContext
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.features.wallet.redux.AddressData
import com.tangem.tap.persistence.UsedCardsPrefStorage
/**
[REDACTED_AUTHOR]
*/
class OnboardingOtherCardsService(
override val fromScreen: AppScreen,
override var scanResponse: ScanResponse,
cardInfoStorage: UsedCardsPrefStorage,
walletManagerFactory: WalletManagerFactory,
) : ProductOnboardingService(cardInfoStorage, walletManagerFactory) {
override fun navigateToScreen(): AppScreen {
return AppScreen.OnboardingOther
}
// Return any value and any state. The balance of the card is not important in this service
override suspend fun updateBalance(): OnboardingWalletBalance {
val customError = TapError.CustomError("Loading cancelled. Cause: wallet manager didn't created")
withMainContext { loadedBalance = OnboardingWalletBalance.error(customError) }
return loadedBalance
}
override fun getToUpUrl(): String? {
throw UnsupportedOperationException()
}
override fun getAddressData(): AddressData? {
throw UnsupportedOperationException()
}
}

View file

@ -1,291 +0,0 @@
package com.tangem.tap.features.onboarding.service
import android.util.Log
import com.tangem.blockchain.common.*
import com.tangem.common.card.Card
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.isZero
import com.tangem.common.services.Result
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.isPositive
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.extensions.withMainContext
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.UrlBitmapLoader
import com.tangem.tap.domain.extensions.getArtworkUrl
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.domain.topup.TradeCryptoHelper
import com.tangem.tap.features.wallet.redux.AddressData
import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.reducers.createAddressesData
import com.tangem.tap.persistence.UsedCardsPrefStorage
import com.tangem.tap.store
import kotlinx.coroutines.*
import java.io.PrintWriter
import java.io.StringWriter
import java.math.BigDecimal
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
/**
[REDACTED_AUTHOR]
*/
interface OnboardingService {
val fromScreen: AppScreen
val scanResponse: ScanResponse
// val cardInfoStorage: UsedCardsPrefStorage
var walletManager: WalletManager?
var onInitializationProgress: ((ProgressState) -> Unit)?
var onInitialized: ((AppScreen) -> Unit)?
var onError: ((TapError) -> Unit)?
fun initializeOnboarding()
fun getArtwork(): OnboardingArtwork
fun getBalance(): OnboardingWalletBalance
suspend fun updateBalance(): OnboardingWalletBalance
fun getToUpUrl(): String?
fun getAddressData(): AddressData?
fun activationStarted()
fun activationFinished()
}
abstract class ProductOnboardingService(
private val cardInfoStorage: UsedCardsPrefStorage,
private val walletManagerFactory: WalletManagerFactory,
) : OnboardingService {
override var walletManager: WalletManager? = null
override var onInitializationProgress: ((ProgressState) -> Unit)? = null
override var onInitialized: ((AppScreen) -> Unit)? = null
override var onError: ((TapError) -> Unit)? = null
protected var isProceeded = false
protected var loadedCardArtwork: OnboardingArtwork = OnboardingArtwork.loading()
set(value) {
field = value
tryToProceed()
}
protected var loadedBalance: OnboardingWalletBalance = OnboardingWalletBalance.loading()
set(value) {
field = value
tryToProceed()
}
protected val internalScope = CoroutineScope(Job() + Dispatchers.IO + coroutineExceptionHandler())
override fun initializeOnboarding() {
val card = scanResponse.card
onInitializationProgress?.invoke(ProgressState.Loading)
internalScope.launch {
val artwork = loadCardArtwork(card)
withMainContext {
loadedCardArtwork = if (artwork == null) OnboardingArtwork.error() else OnboardingArtwork.done(artwork)
}
}
internalScope.launch { updateBalance() }
}
override fun getArtwork(): OnboardingArtwork = loadedCardArtwork
override fun getBalance(): OnboardingWalletBalance = loadedBalance
override fun getToUpUrl(): String? {
val wallet = walletManager?.wallet ?: return null
val config = store.state.globalState.configManager?.config ?: return null
val defaultAddress = wallet.address
return TradeCryptoHelper.getUrl(
TradeCryptoHelper.Action.Buy,
wallet.blockchain,
wallet.blockchain.currency,
defaultAddress,
config.moonPayApiKey,
config.moonPayApiSecretKey
)
}
override fun getAddressData(): AddressData? {
val wallet = walletManager?.wallet ?: return null
val addressDataList = wallet.createAddressesData()
return if (addressDataList.isEmpty()) null
else addressDataList[0]
}
protected suspend fun loadCardArtwork(card: Card): Artwork? {
return when (val cardInfoResult = OnlineCardVerifier().getCardInfo(card.cardId, card.cardPublicKey)) {
is Result.Success -> {
val artworkUrl = card.getArtworkUrl(cardInfoResult.data.artwork?.id) ?: Artwork.DEFAULT_IMG_URL
loadBitmap(artworkUrl)
}
is Result.Failure -> {
loadBitmap(Artwork.DEFAULT_IMG_URL)
}
}
}
override suspend fun updateBalance(): OnboardingWalletBalance {
withMainContext { loadedBalance = OnboardingWalletBalance.loading(loadedBalance.value) }
val walletManager = walletManager ?: walletManagerFactory.makePrimaryWalletManager(scanResponse).guard {
val customError = TapError.CustomError("Loading cancelled. Cause: wallet manager didn't created")
withMainContext { loadedBalance = OnboardingWalletBalance.error(customError) }
return loadedBalance
}
this.walletManager = walletManager
val currency = Currency.Blockchain(walletManager.wallet.blockchain)
val updatedBalance = when (val result = walletManager.safeUpdate()) {
is Result.Success -> {
val wallet = walletManager.wallet
val valueOfAmount = wallet.amounts[AmountType.Coin]?.value
if (valueOfAmount == null) {
val customError = TapError.CustomError("Amount is NULL")
OnboardingWalletBalance.criticalError(customError)
} else {
val balance = if (valueOfAmount.isZero()) {
OnboardingWalletBalance.done(valueOfAmount, hasForIncomingTransactions(wallet), currency)
} else {
OnboardingWalletBalance.done(valueOfAmount, false, currency)
}
balance
}
}
is Result.Failure -> {
val error = (result.error as? TapError) ?: TapError.UnknownError
when (error) {
is TapError.WalletManagerUpdate.NoAccountError -> OnboardingWalletBalance.error(error)
// NoInternetConnection, WalletManagerUpdate.InternalError
else -> OnboardingWalletBalance.criticalError(error)
}
}
}
withMainContext { loadedBalance = updatedBalance }
return loadedBalance
}
override fun activationStarted() {
cardInfoStorage.activationStarted(scanResponse.card.cardId)
}
override fun activationFinished() {
cardInfoStorage.activationFinished(scanResponse.card.cardId)
}
protected open suspend fun hasForIncomingTransactions(wallet: Wallet): Boolean {
return wallet.recentTransactions.isNotEmpty()
}
protected open fun tryToProceed() {
if (isProceeded) return
loadedBalance.criticalError?.let {
sendError(it)
return
}
if (!isReadyToProceed()) return
isProceeded = true
onInitializationProgress?.invoke(ProgressState.Done)
onInitialized?.invoke(navigateToScreen())
}
protected open fun isReadyToProceed(): Boolean {
return loadedBalance.state != ProgressState.Loading && loadedCardArtwork.state != ProgressState.Loading
}
protected fun sendError(error: TapError) {
onInitializationProgress?.invoke(ProgressState.Error)
onError?.invoke(error)
}
protected abstract fun navigateToScreen(): AppScreen
private suspend fun loadBitmap(url: String): Artwork? {
return withContext(Dispatchers.Main) {
suspendCoroutine { continuation ->
UrlBitmapLoader().loadBitmap(url) {
val result = when (it) {
is Result.Success -> Artwork(url, it.data)
is Result.Failure -> null
}
continuation.resume(result)
}
}
}
}
private fun coroutineExceptionHandler(): CoroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
StringWriter().apply {
throwable.printStackTrace(PrintWriter(this))
Log.e("Coroutine", this.toString())
store.dispatchDebugErrorNotification(this.toString())
}
}
}
data class OnboardingArtwork(
val value: Artwork? = null,
val state: ProgressState
) {
companion object {
// balance was not loaded at all when any error occurred
fun error(): OnboardingArtwork = OnboardingArtwork(state = ProgressState.Error)
// balance is loading
fun loading(): OnboardingArtwork = OnboardingArtwork(state = ProgressState.Loading)
// balance was loaded
fun done(artwork: Artwork): OnboardingArtwork = OnboardingArtwork(artwork, ProgressState.Done)
}
}
data class OnboardingWalletBalance(
val value: BigDecimal = BigDecimal.ZERO,
val currency: Currency.Blockchain = Currency.Blockchain(Blockchain.Unknown),
val hasIncomingTransaction: Boolean = false,
val state: ProgressState,
val error: TapError? = null,
val criticalError: TapError? = null,
) {
val amountToCreateAccount: String?
get() = if (error is TapError.WalletManagerUpdate.NoAccountError) error.customMessage else null
companion object {
fun error(error: TapError): OnboardingWalletBalance = OnboardingWalletBalance(
state = ProgressState.Error,
error = error
)
fun criticalError(error: TapError): OnboardingWalletBalance = OnboardingWalletBalance(
state = ProgressState.Error,
criticalError = error
)
fun loading(value: BigDecimal = BigDecimal.ZERO): OnboardingWalletBalance = OnboardingWalletBalance(
value,
state = ProgressState.Loading
)
fun done(value: BigDecimal, hasTransactions: Boolean, currency: Currency.Blockchain): OnboardingWalletBalance =
OnboardingWalletBalance(value, currency, hasTransactions, ProgressState.Done)
}
}
fun ProductOnboardingService.balanceIsToppedUp(): Boolean {
val balance = getBalance()
return balance.value.isPositive() || balance.hasIncomingTransaction
}

View file

@ -151,7 +151,7 @@ sealed class WalletAction : Action {
object ShowOnboarding : TwinsAction()
object SetOnboardingShown : TwinsAction()
data class SetTwinCard(
val secondCardId: String, val number: TwinCardNumber,
val secondCardId: String?, val number: TwinCardNumber,
val isCreatingTwinCardsAllowed: Boolean,
) : TwinsAction()
}